<?php

namespace elanpl\L3;

class Application{
    public $config; // L3 Config object
    public $request; // build from received HTTP request or created in L3 Config object
    public $response; // response to be send
    public $router; // routing engine
    public $serialization; // serialization engine
    public $viewEngines; // view engines configuration
    public $baseDirectory; // directory root of the application
    public $controllersDirectory; // subdirectory/namespace part added to controller class 
    public $viewsDirectory; // subdirectory to search for the view files
    public $applicationDirectory; // subdirectory/namespace part of the application
    public $controller; //Active controller
    public $module; //Active module
    public $action; //Active action

    public function __construct($config){
        //set L3 application config
        $this->config = $config;
        // directory root of the application
        $this->baseDirectory = $config->getBaseDirectory();
        // subdirectory/namespace part added to controller class
        $this->controllersDirectory = $config->getControllersDirectory();
        // subdirectory to search for the view files 
        $this->viewsDirectory = $config->getViewsDirectory();
        // subdirectory/namespace part of the application
        $this->applicationDirectory = $config->getApplicationDirectory();
        //create request object
        $this->request = $this->config->getRequest();
        //create router object
        $this->router = new Router($this->config->getRouting());
        //create response objcet
        $this->response = new Response();

        //create serialization object
        $this->serialization = new Serialization(); 
        //create view engines configuretion object
        $this->viewEngines = new ViewEngine();

        //perform configuration operations
        $this->config->configure($this);
    }

    public function basePath(){
        if(isset($this->request->path))
            return strstr($_SERVER['REQUEST_URI'], $this->request->path, true);
        else
            return $_SERVER['REQUEST_URI'];
    }

    public function getServerURL(){
        return (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on' ? "https" : "http") . "://{$_SERVER['HTTP_HOST']}";
    }

    public function getURL(){
        return $this->getServerURL().$_SERVER['REQUEST_URI'];
    }

    public function relPath($file){
        return ($this->router->depth>0 ? str_repeat("../", substr($this->request->path,-1)=='/'?$this->router->depth:$this->router->depth-1) : "" ).$file; 
    }

    public function link($name, $parameters){
        return $this->router->link($name, $parameters);
    }

    /*
    //not need - PSR-4 autoload handles that
    function IncludeControler($Controller){
        include_once(_L3_CONTROLLER_PATH.$Controller.'.php');
    }*/

    public function findControllerInCallStack($search_level=5){
        //search for controller, action and module if present
        $controller = "";
        $module = "";
        $action = "";
        $match = array();

        //analize the call stack
          $call_stack = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, $search_level);
        
        for($i=0; $i<$search_level; $i++){
            if(preg_match(
                    $pattern = '#('.$this->applicationDirectory.')(\\\\(?P<module>.+))?\\\\'.$this->controllersDirectory.'\\\\(?P<controller>.+)#',
                    $call_stack[$i]['class'],
                    $match
                )
            ){
                $action = $call_stack[$i]['function'];
                $module = $match['module'];
                $controller = $match['controller'];
                break;
            }
        }

        return array(
            'module' => $module,
            'controller' => $controller,
            'action' => $action
        );
    }

    function runControllerAction($controllerAction, $parameters){
        
        //prepare object and action name
        /*
        TODO ...
        if(is_object($this->controler) && (is_a($this->controler,'L2_controler') || is_subclass_of($this->controler,'L2_controler'))){
            if(is_string($this->action)){
                $controler_object = $this->controler;
                $controler_action = $this->action;
            }
        }
        else if(is_string($this->controler)){
            if(is_string($this->action)){
                $this->includeControler($this->controler);
                $controler_object = new $this->controler();
                $controler_action = $this->action;
            }
        }*/

        $field_pattern ='#^{(?<field>[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)}$#';

        if(is_array($controllerAction)){
            //$this->IncludeControler($ControllerAction['controller']);
            if(isset($controllerAction['module'])){
                if(\preg_match($field_pattern, $controllerAction['module'], $match) ){
                    $module = $this->router->parsedParameters[$match['field']]."\\";
                }
                else{
                    $module = $controllerAction['module']."\\";
                }
            }
            else{
                $module = "";
            }

            
            if(\preg_match($field_pattern, $controllerAction['controller'], $match) ){
                $controller = $this->router->parsedParameters[$match['field']];
            }
            else{
                $controller = $controllerAction['controller'];
            }
            

            $controller_class = "\\".$this->applicationDirectory."\\"
                .$module
                .$this->controllersDirectory."\\"
                .$controller;
            
            if(class_exists($controller_class,true)){
                $controller_object = new $controller_class($this);
            }
            else{
                return $this->handle404($this->request);
            }

            if(\preg_match($field_pattern, $controllerAction['action'], $match) ){
                $controller_action = $this->router->parsedParameters[$match['field']];
            }
            else{
                $controller_action = $controllerAction['action'];
            }
            
        }

        //fire the action
        if(isset($controller_object) && isset($controller_action)){
            
            // method not found - 404
            if(!method_exists($controller_object,$controller_action)){
                return $this->handle404($this->request);
            }

            $this->module = $module;
            $this->controller = $controller;
            $this->action = $controller_action;
            // prepare args and start the action
            if(!isset($reflection)){
                $reflection = new \ReflectionMethod($controller_object, $controller_action);
            }
            if($reflection->getNumberOfParameters()){
                $fire_args=array();

                foreach($reflection->getParameters() AS $arg)
                {
                    if(isset($parameters[$arg->name]) || (is_array($controllerAction) && isset($controllerAction['defaults'][$arg->name])))
                        if(isset($parameters[$arg->name]))
                            $fire_args[$arg->name]=$parameters[$arg->name];
                        else
                            $fire_args[$arg->name]=$controllerAction['defaults'][$arg->name];
                    else
                    {
                        if($arg->isDefaultValueAvailable()){
                            $fire_args[$arg->name]=$arg->getDefaultValue();
                        }
                        else{
                            $fire_args[$arg->name]=null;
                        }
                    }
                }
                return $reflection->invokeArgs ($controller_object , $fire_args );
            }
            else{
                return $reflection->invoke($controller_object);
            }
        }
    }

    public function handleActionResult($result){
        if(is_string($result)){
            $content = $result;
        }
        else if(is_object($result)){
            if($result instanceof ViewModel){
                if($serializationContentType = $this->serialization->match($this->request->acceptTypes, $result)){
                    $content = $this->serialization->serialize($serializationContentType, $result);
                    if(is_object($content)){
                        if($content instanceof response){
                            $this->response = $content;
                            unset($content);
                        }
                    }
                }
                else{
                    echo "Serializer not defined for Content-Type: ".$this->request->accept."<br>".PHP_EOL;
                    throw new \Exception("Not implemented!");
                }
            }
            else if($result instanceof View){
                $content = $result->render();
            }
            else if($result instanceof Response){
                $this->response = $result;
            }  
        }
        if(isset($content))
                $this->response->withBody($content);
        $this->response->send();
    }

    public function handle404($request){
        if(!$this->router->is_Match404() && ($action = $this->router->setMatch404($request))){
            $this->runAction($action);
        }
        else{
            http_response_code(404);
            echo "<h1>Resource not found!</h1><br>".PHP_EOL;
            echo "<pre>";
            var_dump($request);
            echo "</pre>";
            if($this->router->is_Match404())
                throw new \Exception("The 404 controller or action not found!");
            else
                throw new \Exception("The 404 (not found) route not set!");
        }
    }

    public function beforeAction(){
        if(!empty($this->router->routeInfo->beforeAction)){
            foreach($this->router->routeInfo->beforeAction as $event_handler){
                $this->router->routeInfo::eventHandlerFormatCheck($event_handler, $eh);
                if(isset($eh['class'])){
                    $object = new $eh['class'];
                    $reflection = new \ReflectionMethod($object, $eh['function']);
                    $fire_args = array();
                    $fire_args[] = $this;
                    if(isset($eh['arguments'])){
                        $this->router->routeInfo::eventHandlerArgumentsFormatCheck($eh['arguments'], $args);
                        $field_pattern ='#^{(?<field>[a-zA-Z_\x80-\xff][a-zA-Z0-9_\x80-\xff]*)}$#';
                        foreach($args as $index => $param){
                            if(\preg_match($field_pattern, $param, $match) ){
                                $args[$index] = $this->router->parsedParameters[$match['field']]."\\";
                            }
                        }
                        $fire_args = array_merge($fire_args, $args);
                    }
                    $result = $reflection->invokeArgs ($object , $fire_args );
                }
                else if (is_callable($eh['function'])){
                    throw new \Exception("Not implemented!");
                }
                if(isset($result)){
                    if(is_object($result)){
                        if($result instanceof Response){
                            $this->handleActionResult($result);
                            exit();
                        }
                    }
                }
            }
        }
    }

    public function runAction($action){
        // routing match found decide what to do next...
        // BeforeAction event
        $this->beforeAction();

        // function -> call it...
        if(is_callable($action)){
            $reflection = new \ReflectionFunction($action);
            if($reflection->getNumberOfParameters()){
                $fire_args=array();
    
                foreach($reflection->getParameters() AS $arg)
                {
                    if(isset($this->router->parsedParameters[$arg->name]))
                        $fire_args[$arg->name]=$this->router->parsedParameters[$arg->name];
                    else
                        $fire_args[$arg->name]=null;
                }
                
                $result = call_user_func_array($action, $fire_args);
            }
            else{
                $result = call_user_func($action);
            }
        }
        else if(is_object($action) && is_a($action, 'L2_controler_info')){
            // not implemented yet.
            throw new \Exception("Not implemented!");
            ///$action->runControlerAction($this->routing->param);
        }
        // array with controller, and action -> run the controller action
        else if(is_array($action) && array_key_exists('controller',$action) && array_key_exists('action', $action)){
            $result = $this->runControllerAction($action, $this->router->parsedParameters);
            
        }
        // string
        else if(is_string($action)){
            $result = $action;
        }

        // Handle the action result
        if(isset($result)){
            $this->handleActionResult($result);
        }
    }

    public function run(){
        // Check if routing path is found
        if($action = $this->router->match($this->request)){
            $this->runAction($action);
        }
        // routing path not found -> generate 404 response
        else{
            $this->handle404($this->request);
        }
 
    }
}