<?php
    namespace descartes;

    /**
	 * Cette classe gère l'appel des ressources
	 */
	class Router
    {
        /**
         * Generate an url for a page of the app
         * @param string $controller : Name of controller we want url for
         * @param string $method : Name of method we want url for
         * @param array $params : Parameters we want to transmit to controller method
         * @param array $get_params : Get parameters we want to add to url
         * @return string : the generated url
         */
        public static function url (string $controller, string $method, array $params = [], array $get_params = []) : string
        {
            $url = HTTP_PWD;

            if (!array_key_exists($controller, ROUTES))
            {
                throw new \descartes\exceptions\DescartesExceptionRouterUrlGenerationError('Try to generate url for controller ' . $controller . ' that did not exist.');
            }

            if (!array_key_exists($method, ROUTES[$controller]))
            {
                throw new \descartes\exceptions\DescartesExceptionRouterUrlGenerationError('Try to generate url for method ' . $controller . '::' . $method . ' that did not exist.');
            }

            $get_params = http_build_query($get_params);

            $routes = ROUTES[$controller][$method];

            if (!is_array($routes))
            {
                $routes = [$routes];
            }

            foreach ($routes as $route)
            {
                foreach ($params as $name => $value)
                {
                    $find_flag = mb_strpos($route, '{' . $name . '}');
                    if ($find_flag === false)
                    {
                        continue 2;
                    }

                    $route = str_replace('{' . $name . '}', $value, $route);
                }

                $remain_flag = mb_strpos($route, '{');
                if ($remain_flag)
                {
                    continue;
                }

                return $url . $route . ($get_params ? '?' . $get_params : '');
            }

            throw new \descartes\exceptions\DescartesExceptionRouterUrlGenerationError('Cannot find any route for ' . $controller . '::' . $method . ' with parameters ' . print_r($params, true));
        }



		/**
         * Clean url to remove anything before app root, and ancre, and parameters
         * @param string $url : url to clean
         * @return string : cleaned url
		 */
		protected static function clean_url (string $url)
        {
            $to_remove = parse_url(HTTP_PWD, PHP_URL_PATH);
            $url = mb_strcut($url, $to_remove ? mb_strlen($to_remove) : 0);
            $url = parse_url($url, PHP_URL_PATH);

            return $url;
		}


        /**
         * Find controller and method to call for an url
         * @param array $routes : Routes of the app
         * @param string $url : url to find route for
         * @return array|bool : An array, ['controller' => name of controller to call, 'method' => name of method to call, 'route' => 'route matching the url', 'route_regex' => 'the regex to extract data from url'], false on error
         */
        protected static function map_url (array $routes, string $url)
        {
            foreach ($routes as $controller => $controller_routes)
            {
                foreach ($controller_routes as $method => $method_routes)
                {
                    if (!is_array($method_routes))
                    {
                        $method_routes = [$method_routes];
                    }

                    foreach ($method_routes as $route)
                    {
                        $route_regex = preg_replace('#\\\{(.+)\\\}#iU', '([^/]+)', preg_quote($route, '#'));
                        $route_regex = preg_replace('#/$#', '/?', $route_regex);

                        $match = preg_match('#^' . $route_regex . '$#U', $url);
                        if (!$match)
                        {
                            continue;
                        }
                        
                        return [
                            'controller' => $controller,
                            'method' => $method,
                            'route' => $route,
                            'route_regex' => $route_regex,
                        ];
                    }
                }
            }

            return false;
        }


        /**
         * Get data from url and map it with route flags
         * @param string $url : A clean url to extract data from (see clean_url)
         * @param string $route : Route we must extract flag from
         * @param string $route_regex : Regex to extract data from url
         * @return array : An array with flagname and values, flag => value
         */
        protected static function map_params_from_url (string $url, string $route, string $route_regex)
        {
			$flags = [];
			preg_match_all('#\\\{(.+)\\\}#iU', preg_quote($route, '#'), $flags);
            $flags = $flags[1];

			$values = [];
			if (!preg_match('#^' . $route_regex . '$#U', $url, $values))
			{
				return false;
			}
			unset($values[0]);

			$values = array_map('rawurldecode', $values);

			//On retourne les valeurs associées aux flags
			return array_combine($flags, $values);
        }


        /**
         * Compute a controller name to return is real path with namespace
         * @param string $controller : The controller name
         * @return string|bool : False if controller does not exist, controller name with namespace else
         */
        protected static function compute_controller (string $controller)
        {
            $controller = str_replace('/', '\\', PWD_CONTROLLER . '/publics/') . $controller;
            $controller = mb_strcut($controller, mb_strlen(PWD));
            
            if (!class_exists($controller))
            {
                return false;

            }

            return $controller;
        }


        /**
         * Compute a method to find his real name and check its available
         * @param string $controller : Full namespace of controller to call
         * @param string $method : The method to call
         * @return string | bool : False if method unavaible, its realname else
         */
        protected static function compute_method (string $controller, string $method)
        {
            if (is_subclass_of($controller, 'descartes\ApiController'))
            {
				//On va choisir le type à employer
				$http_method = $_SERVER['REQUEST_METHOD'];
				switch (mb_convert_case($http_method, MB_CASE_LOWER))
				{
					case 'delete' :
						$prefix_method = 'delete_';
						break;
					case 'patch' :
						$prefix_method = 'patch_';
						break;
					case 'post' :
						$prefix_method = 'post_';
						break;
					case 'put' :
						$prefix_method = 'put_';
						break;
					default :
						$prefix_method = 'get_';
                }

                //If we dont match prefix with request method, return error
                if (mb_substr($method, 0, mb_strlen($prefix_method)) != $prefix_method)
                {
                    return false;
                }
            }

            if (!method_exists($controller, $method))
            {
                return false;
            }

            if (!is_callable($controller, $method))
            {
                return false;
            }

            return $method;
        }


        /**
         * Type, order and check params we must pass to method
         * @param string $controller : Full namespace of controller
         * @param string $method : Name of method
         * @param array $params : Parameters to compute, format name => value
         * @return array : Array ['success' => false, 'message' => error message] on error, and ['success' => true, 'method_arguments' => array of method arguments key=>val] on success
         */
        protected static function compute_params (string $controller, string $method, array $params) : array
        {
            $reflection = new \ReflectionMethod($controller, $method);
            $method_arguments = [];

			foreach ($reflection->getParameters() as $parameter)
			{
				if (!array_key_exists($parameter->getName(), $params) && !$parameter->isDefaultValueAvailable())
                {
                    return ['success' => false, 'message' => 'Try to call ' . $controller . '::' . $method . ' but ' . $parameter->getName() . ' is missing.'];
				}

				if ($parameter->isDefaultValueAvailable())
				{
					$method_arguments[$parameter->getName()] = $parameter->getDefaultValue();
				}

				if (!array_key_exists($parameter->getName(), $params))
				{
					continue;
                }


                $type = $parameter->getType();
                $type = $type ?? false;
                $type = ($type instanceof \ReflectionNamedType) ? $type->getName() : $type;

                if ($type)
                {
                    switch ($type)
                    {
                        case 'bool' :
                            $params[$parameter->getName()] = (bool) $params[$parameter->getName()];
                            break;

                        case 'int' :
                            $params[$parameter->getName()] = (int) $params[$parameter->getName()];
                            break;

                        case 'float' :
                            $params[$parameter->getName()] = (float) $params[$parameter->getName()];
                            break;

                        case 'string' :
                            $params[$parameter->getName()] = (string) $params[$parameter->getName()];
                            break;

                        default :
                            return ['success' => false, 'message' => 'Method ' . $controller . '::' . $method . ' use an invalid type for param ' . $parameter->getName() . '. Only bool, int float and string are supported.']; 
                            break;
                    }
                }
                
                $method_arguments[$parameter->getName()] = $params[$parameter->getName()];
            }

            return ['success' => true, 'method_arguments' => $method_arguments];
        }


        /**
         * Throw a 404 exception
         */
        public static function error_404 () : void
        {
            throw new \descartes\exceptions\DescartesException404();
        }


        /**
         * Route a query
         * @param array $routes : Routes of app
         * @param string $url : Url call
         * @param mixed $args : Args we want to pass to Controller constructor
         */
        public static function route (array $routes, string $url, ...$args) : void
        {
            $url = static::clean_url($url);

            $computed_url = static::map_url($routes, $url);
            if (!$computed_url)
            {
                static::error_404();
            }


            $params = static::map_params_from_url($url, $computed_url['route'], $computed_url['route_regex']);

            $controller = static::compute_controller($computed_url['controller']);
            if (!$controller)
            {
                throw new \descartes\exceptions\DescartesExceptionRouterInvocationError('Try to call controller ' . $computed_url['controller'] . ' that did not exists.');
            }

            $method = static::compute_method($controller, $computed_url['method']);
            if (!$method)
            {
                throw new \descartes\exceptions\DescartesExceptionRouterInvocationError('Try to call the method ' . $computed_url['method'] . ' that did not exists from controller ' . $controller . '.');
            }

            $compute_params_result = static::compute_params($controller, $method, $params);
            if (!$compute_params_result['success'])
            {
                throw new \descartes\exceptions\DescartesExceptionRouterInvocationError($compute_params_result['message']);
            }

            $method_arguments = $compute_params_result['method_arguments'];

            $controller = new $controller(...$args);
            call_user_func_array([$controller, $method], $method_arguments);
        }
	} 
