<?php

namespace top\library;

use top\library\exception\RouteException;
use top\library\http\Request;
use top\middleware\ifs\MiddlewareIfs;
use top\traits\Instance;

/**
 * 路由类
 * @author topnuomi 2018年11月19日
 */
class Router
{

    use Instance;

    /**
     * 路由配置
     * @var array
     */
    private $config = [];

    /**
     * 请求类
     * @var Request
     */
    private $request = null;

    /**
     * 是否绑定模块
     * @var bool
     */
    private $isBindModule = false;

    /**
     * 模块
     * @var string
     */
    private $module = '';

    /**
     * 类全限定名
     * @var string
     */
    private $controllerFullName = '';

    /**
     * 类名
     * @var string
     */
    private $controller = '';

    /**
     * 方法
     * @var string
     */
    private $method = '';

    /**
     * 参数
     * @var array
     */
    private $params = [];

    /**
     * 当前加载的路由
     * @var array
     */
    private $loadRuleParameters = [];

    /**
     * Router constructor.
     * @param Request $request
     */
    private function __construct(Request $request)
    {
        $this->request = $request;
    }

    /**
     * 完整控制器名
     * @return string
     */
    public function controllerFullName()
    {
        return $this->controllerFullName;
    }

    /**
     * 控制器名
     * @return string
     */
    public function controller()
    {
        return $this->controller;
    }

    /**
     * 模块名
     * @return string
     */
    public function module()
    {
        return $this->module;
    }

    /**
     * 方法名
     * @return string
     */
    public function method()
    {
        return $this->method;
    }

    /**
     * 请求参数
     * @return array
     */
    public function params()
    {
        return $this->params;
    }

    /**
     * 是否绑定模块
     * @param bool $isBindModule
     */
    public function isBindModule($isBindModule)
    {
        $this->isBindModule = $isBindModule;
    }

    /**
     * 查找最适合的路由匹配
     * @param $rules
     * @param $uri
     * @return mixed
     */
    private function findRule($rules, $uri)
    {
        $matchUri = ($uri == '/') ? '/' : '/' . trim($uri, '/');
        if (isset($rules[$matchUri])) {
            $this->loadRuleParameters = $rules[$matchUri];
            return [
                'rule' => $matchUri,
                'parameters' => $this->loadRuleParameters,
            ];
        }
        return false;
    }

    /**
     * 解析路由规则
     * @param $requestMethod
     * @param $uri
     * @return array|bool|mixed
     */
    private function parseRouteRule($requestMethod, $uri)
    {
        // 获取所有路由配置(可能从缓存文件读取)
        $routeConfig = $this->getRouteConfig();

        $rule = [];
        if (isset($routeConfig[$requestMethod])) { // 第一次去当前请求方法中查找
            $rule = $this->findRule($routeConfig[$requestMethod], $uri);
        }
        if (empty($rule) && isset($routeConfig['any'])) { // 全部中查找
            $rule = $this->findRule($routeConfig['any'], $uri);
        }

        return (!empty($rule)) ? $rule : false;
    }

    /**
     * 普通路由处理
     * @param $uri
     * @return array
     */
    private function parseRoute($uri)
    {
        // 普通处理
        $uriArray = explode('/', trim($uri, '/'));
        // 如果没有参数,则使用默认的控制器和方法
        $uriArray[0] = (isset($uriArray[0]) && $uriArray[0]) ? $uriArray[0] : config('default_controller');
        $uriArray[1] = (isset($uriArray[1]) && $uriArray[1]) ? $uriArray[1] : config('default_method');

        $controller = ucfirst($uriArray[0]);
        $rule['class'] = APP_NS . '\\' . $this->module . '\\controller\\' . $controller;
        $rule['method'] = $uriArray[1];

        return [
            'rule' => $uriArray[0] . '/' . $uriArray[1],
            'parameters' => $rule,
        ];
    }

    /**
     * 解析请求参数
     * @param $class
     * @param $method
     * @param $prefix
     * @param $uri
     * @return array
     */
    private function parseParameters($class, $method, $prefix, $uri)
    {
        $paramsString = ltrim(substr_replace($uri, '', 0, strlen($prefix)), '/');
        $paramsArray = explode('/', $paramsString);

        $params = [];
        if (config('complete_parameter')) { // 如果开启了完全参数名
            for ($i = 0; $i < count($paramsArray); $i += 2) {
                if (isset($paramsArray[$i + 1])) {
                    $_GET[$paramsArray[$i]] = $params[$paramsArray[$i]] = $paramsArray[$i + 1];
                }
            }
        } else { // 未开启完全参数名,则利用反射得到参数名做映射
            if (!empty($paramsArray)) {
                $reflectionMethod = Application::getReflectionMethod($class, $method);
                $index = 0;
                foreach ($reflectionMethod->getParameters() as $parameter) {
                    $className = $parameter->getClass();
                    if (is_null($className)) {
                        $_GET[$parameter->name] = $params[$parameter->name] = $paramsArray[$index];
                        $index++;
                    }
                }
            }
        }

        return $params;
    }

    /**
     * 执行应用
     * @return mixed
     * @throws RouteException
     */
    public function execute()
    {
        try {
            // 绑定模块优先
            if ($this->isBindModule) {
                $this->module = BIND_MODULE;
                $uri = $this->request->uri();
            } else {
                // 如果没有直接绑定模块,则从链接获取
                $rawUri = $this->request->uri();
                if (!$rawUri) {
                    $this->module = DEFAULT_MODULE;
                    $uri = $rawUri;
                } else {
                    // 取第一部分为模块名称
                    $pos = strpos($rawUri, '/');
                    if (false !== $pos) {
                        $uri = substr($rawUri, $pos + 1);
                        $this->module = substr($rawUri, 0, $pos);
                    } else {
                        $this->module = $uri = $rawUri;
                    }
                }
            }
            define('CURRENT_MODULE', $this->module());
            Application::afterRouter();
            // 处理路由
            $this->handler($uri);
        } catch (RouteException $exception) {
            if (!DEBUG) { // 非调试模式直接404
                return \response()->code(404)->send();
            } else throw $exception;
        }

        // 路由中间件处理
        return $this->middleware(function () {
            return Application::callMethod($this->controllerFullName, $this->method, $this->params);
        });
    }

    /**
     * 中间件处理
     * @param \Closure $application
     * @return mixed
     */
    public function middleware(\Closure $application)
    {
        // 加载全局配置文件中配置的中间件
        $middleware = array_reverse(config('middleware'));

        // 配置中不执行的中间件
        $exceptMiddlewareArray = [];
        if (
            isset($this->loadRuleParameters['except_middleware'])
            && $this->loadRuleParameters['except_middleware'] != ''
        ) {
            $exceptMiddlewareArray = $this->loadRuleParameters['except_middleware'];
        }

        // 配置中新增的中间件
        if (
            isset($this->loadRuleParameters['accept_middleware'])
            && $this->loadRuleParameters['accept_middleware'] != ''
        ) {
            $acceptMiddlewareArray = $this->loadRuleParameters['accept_middleware'];
            foreach ($acceptMiddlewareArray as $acceptMiddleware) {
                if (!in_array($acceptMiddleware, $middleware)) {
                    $middleware[] = $acceptMiddleware;
                } else continue;
            }
        }

        // 应用打包在在洋葱圈最里层
        $next = $application;
        foreach ($middleware as $value) {
            if (!in_array($value, $exceptMiddlewareArray)) {
                $next = function () use ($next, $value) {
                    $middleware = new $value;
                    if ($middleware instanceof MiddlewareIfs) {
                        return $middleware->handler($this->request, $next);
                    } else throw new RouteException('无效的中间件:' . $value);
                };
            }
        }

        return $next();
    }

    /**
     * 处理URI
     * @param $uri
     * @throws RouteException
     */
    public function handler($uri)
    {
        // 检查模块是否存在
        if (!is_dir(APP_PATH . $this->module)) {
            throw new RouteException('不存在的模块:' . $this->module);
        }
        // 如果为空则默认为/
        $uri = $uri ? $uri : '/';
        $defaultMethod = config('default_method');
        $requestMethod = strtolower($this->request->requestMethod());
        // 第一次用原始uri去做匹配,第二次带上默认方法去做匹配
        if (
            false === ($rule = $this->parseRouteRule($requestMethod, $uri))
            && false === ($rule = $this->parseRouteRule($requestMethod, $uri . '/' . $defaultMethod))
        ) {
            // 如果开启强制路由,则抛异常
            if (config('compel_route') === true) {
                throw new RouteException('不支持的路由规则:' . strtoupper($requestMethod) . ' ' . $uri);
            } else {
                // 进行普通处理
                $rule = $this->parseRoute($uri);
            }
        }
        $ruleParameters = $rule['parameters'];

        $this->controllerFullName = $ruleParameters['class'];
        $this->controller = substr($this->controllerFullName, strrpos($ruleParameters['class'], '\\') + 1);
        $this->method = $ruleParameters['method'];
        // 此处还需要检查控制器和方法是否存在
        if (!class_exists($this->controllerFullName)) {
            throw new RouteException('不存在的控制器:' . $this->controllerFullName);
        }
        if (!method_exists($this->controllerFullName, $this->method)) {
            throw new RouteException('不存在的方法:' . $this->method);
        }
        $this->params = $this->parseParameters($ruleParameters['class'], $ruleParameters['method'], $rule['rule'], $uri);
    }


    /**
     * 创建路由配置缓存文件
     * @return array|mixed
     */
    public function getRouteConfig()
    {
        $fileName = './runtime/' . $this->module . '_route_cache.php';
        if (!DEBUG && is_file($fileName)) {
            return require $fileName;
        } else {
            $result = [];
            $controllerPath = APP_PATH . $this->module . '/controller/';
            $namespace = APP_NS . '\\' . $this->module . '\\controller';
            $files = scandir($controllerPath);
            for ($i = 2; $i < count($files); $i++) {
                $className = $namespace . '\\' . pathinfo($files[$i])['filename'];
                // 获取路由组名称
                $groupName = '';
                $clsasAnnotation = Annotation::getClassAnnotation($className);
                if (isset($clsasAnnotation['group']) && ($group = trim($clsasAnnotation['group'])) != '') {
                    $groupName = '/' . trim($group, '/');
                }
                // 路由组接受的中间件
                $groupAcceptMiddleware = [];
                if (isset($clsasAnnotation['acceptMiddleware']) && $clsasAnnotation['acceptMiddleware'] != '') {
                    $groupAcceptMiddleware = explode('|', $clsasAnnotation['acceptMiddleware']);
                }
                // 路由组拒绝的中间件
                $groupExceptMiddleware = [];
                if (isset($clsasAnnotation['exceptMiddleware']) && $clsasAnnotation['exceptMiddleware'] != '') {
                    $groupExceptMiddleware = explode('|', $clsasAnnotation['exceptMiddleware']);
                }
                $reflectionClass = Application::getReflectionClass($className);
                foreach ($reflectionClass->getMethods() as $method) {
                    if ($method->class == $className && substr($method->name, 0, 1) != '_') {
                        $annotation = Annotation::getMethodAnnotation($className, $method->name);
                        $requestMethod = (isset($annotation['requestMethod'])) ? $annotation['requestMethod'] : 'any';
                        if (isset($annotation['route']) && ($uri = trim($annotation['route'])) != '') {
                            $requestUri = '/' . trim($uri, '/');
                        } else continue;
                        $requestMethod = strtolower($requestMethod);
                        $rule = $groupName . (($groupName != '' && $requestUri == '/') ? '' : $requestUri);
                        $result[$requestMethod][$rule] = [
                            'class' => $className,
                            'method' => $method->name,
                            'accept_middleware' => [],
                            'except_middleware' => [],
                        ];
                        if (isset($annotation['acceptMiddleware']) && $annotation['acceptMiddleware'] != '') {
                            foreach (explode('|', $annotation['acceptMiddleware']) as $acceptMiddleware) {
                                $result[$requestMethod][$rule]['accept_middleware'][] = $acceptMiddleware;
                            }
                        } else {
                            $result[$requestMethod][$rule]['accept_middleware'] = $groupAcceptMiddleware;
                        }
                        if (isset($annotation['exceptMiddleware']) && $annotation['exceptMiddleware'] != '') {
                            foreach (explode('|', $annotation['exceptMiddleware']) as $exceptMiddleware) {
                                $result[$requestMethod][$rule]['except_middleware'][] = $exceptMiddleware;
                            }
                        } else {
                            $result[$requestMethod][$rule]['except_middleware'] = $groupExceptMiddleware;
                        }
                    }
                }
            }

            // 创建缓存文件
            $this->createCacheFile($fileName, $result);

            return $result;
        }
    }

    /**
     * 创建缓存文件
     * @param string $fileName
     * @param array $rules
     * @return void
     */
    private function createCacheFile($fileName, $rules)
    {
        // 加载配置文件中的路由配置
        $routeConfigFile = CONFIG_DIR . 'route.php';
        if (is_file($routeConfigFile)) {
            $routeConfig = require $routeConfigFile;
            foreach ($routeConfig as $key => $value) {
                if (isset($rules[$key])) { // 存在当前请求方法的配置就检查含有的路由配置
                    foreach ($value as $uri => $config) {
                        $uri = ($uri == '/') ? $uri : trim($uri, '/');
                        if (isset($rules[$key][$uri])) { // 如果已经存在这个路由配置,可能不完全,直接合并覆盖已有项
                            $rules[$key][$uri] = array_merge($rules[$key][$uri], $config);
                        } else {
                            $rules[$key][$uri] = $config;
                        }
                    }
                } else {
                    $rules[$key] = $value;
                }
            }
        }

        // 写入文件
        ob_start();
        var_export($rules);
        $content = ob_get_contents();
        ob_clean();
        file_put_contents($fileName, "<?php\nreturn " . $content . ';');
    }

}