<?php

namespace Luticate\Utils;

use Luticate\Utils\Business\LuDocParser;
use Luticate\Utils\Dbo\LuDboConstraintException;
use Luticate\Utils\Dbo\LuDboDeserializeException;

class LuRoute {

    const REG_UINT = "[0-9]+";
    const REG_INT = "\\-?" . self::REG_UINT;
    const REG_BOOL = "true|false";

    /**
     * @var string[]
     */
    private $middleware = array('Luticate\Utils\ParametersMiddleware');

    /**
     * @var LuRouteDbo[]
     */
    private $routes = array();

    /**
     * @var LuRoute
     */
    private static $instance = null;

    private function __construct()
    {

    }

    public static function getInstance()
    {
        if (is_null(self::$instance)) {
            self::$instance = new LuRoute();
        }
        return self::$instance;
    }

    public function getRoutes()
    {
        return $this->routes;
    }

    /**
     * @param $middleware string
     */
    public function addMiddleware($middleware)
    {
        $this->middleware[] = $middleware;
    }

    private function getClassName(\ReflectionParameter $param) {
        preg_match('/\[\s\<\w+?>\s([\w]+)/s', $param->__toString(), $matches);
        return isset($matches[1]) ? $matches[1] : null;
    }

    private function getParam(\ReflectionParameter $param, $value)
    {
        $typedValue = null;
        $className = $this->getClassName($param);
        if (is_null($className)) {
            $typedValue = $value;
        }
        else {
            $class = $param->getClass();
            try
            {
                $json = json_decode($value, true);
                $typedValue = call_user_func_array(array($class->getName(), "jsonDeserialize"), array($json));
            }
            catch (LuDboDeserializeException $e)
            {
                throw $e;
            }
            catch (\Exception $e)
            {
                LuLog::log($e);
                LuBusiness::badInput("Unable to parse JSON value for '" . $param->getName() . "'");
            }
        }
        return $typedValue;
    }

    private function getOptions($httpMethod, $url, $controller, $method, $permissions, $middleware)
    {
        if (!is_array($permissions)) {
            $permissions = array($permissions);
        }
        if (!is_array($middleware)) {
            $middleware = array($middleware);
        }
        $permissions_string = implode(",", $permissions);
        $middleware_string = [];
        foreach (array_merge($this->middleware, $middleware) as $mid) {

            $middleware_string[] = $mid . (strpos($mid, ":") !== false ? "," : ":") . $permissions_string;
        }

        if (strpos($controller, "\\") === false) {
            $controller = "App\\Http\\Controller\\" . $controller . "Controller";
        }

        $route = new LuRouteDbo();
        $route->setUrl($url);
        $route->setBusinessClass($controller);
        $route->setBusinessMethod($method);
        $route->setMiddlware($middleware_string);
        $route->setPermissions($permissions);
        $route->setMethod($httpMethod);
        $this->routes[] = $route;

        $ctrl = $this;

        return [function() use($controller, $method, $ctrl)
        {
            $reflect = new \ReflectionMethod($controller, $method);
            $params = $reflect->getParameters();
            $doc = new LuDocParser($reflect->getDocComment());
            $doc->parse();

            $args = array();
            foreach ($params as $param) {

                try {
                    if ($param->isOptional()) {
                        $value = null;
                        if (LuBusiness::hasParam([$param->getName()])) {
                            $value = $ctrl->getParam($param, LuBusiness::getParam($param->getName()));
                        }
                        else {
                            $value = $param->getDefaultValue();
                        }
                        $args[$param->getName()] = $value;
                    }
                    else {
                        $args[$param->getName()] = $ctrl->getParam($param, LuBusiness::checkParam($param->getName()));
                    }
                    if (array_key_exists($param->getName(), $doc->getParams())) {
                        foreach ($doc->getParams()[$param->getName()]->getConstraints() as $constraint) {
                            call_user_func_array([$args[$param->getName()], $constraint->getMethod()],
                                $constraint->getArguments());
                        }
                    }
                }
                catch (LuDboConstraintException $e)
                {
                    $paramName = $param->getName();
                    LuBusiness::badInput("Invalid value for '${paramName}': " . $e->getMessage());
                }
            }

            $controllerInstance = new $controller();
            $result = call_user_func_array(array($controllerInstance, $method), $args);

            return LuOutputFormatter::formatSuccess($result);
        }, 'middleware' => $middleware_string];
    }

    /**
     * @param $url string
     * @param $controller string
     * @param $method string
     * @param array $permissions string|string[]
     * @param array $middleware string|string[]
     * @return mixed
     */
    public function get($url, $controller, $method, $permissions = array(), $middleware = array())
    {
        global $app;
        return $app->get($url, $this->getOptions("GET", $url, $controller, $method, $permissions, $middleware));
    }

    /**
     * @param $url string
     * @param $controller string
     * @param $method string
     * @param array $permissions string|string[]
     * @param array $middleware string|string[]
     * @return mixed
     */
    public function post($url, $controller, $method, $permissions = array(), $middleware = array())
    {
        global $app;
        return $app->post($url, $this->getOptions("POST", $url, $controller, $method, $permissions, $middleware));
    }

    /**
     * @param $url string
     * @param $controller string
     * @param $method string
     * @param array $permissions string|string[]
     * @param array $middleware string|string[]
     * @return mixed
     */
    public function put($url, $controller, $method, $permissions = array(), $middleware = array())
    {
        global $app;
        return $app->put($url, $this->getOptions("PUT", $url, $controller, $method, $permissions, $middleware));
    }

    /**
     * @param $url string
     * @param $controller string
     * @param $method string
     * @param array $permissions string|string[]
     * @param array $middleware string|string[]
     * @return mixed
     */
    public function delete($url, $controller, $method, $permissions = array(), $middleware = array())
    {
        global $app;
        return $app->delete($url, $this->getOptions("DELETE", $url, $controller, $method, $permissions, $middleware));
    }
}