<?php

declare(strict_types=1);

namespace xmvc;

use stdClass;

/**
 * XMVC 框架入口
 *
 * @author    hzqghost <313143468@qq.com>
 * @copyright 2024 © yipart.com
 * @license   http://opensource.org/licenses/gpl-license.php GNU Public License
 * @version   Release: 5.0.0
 */
final class MVC
{
    /** 全局配置数组 */
    public static $cfg = [];
    /** 扩展配置数组 */
    public static $extCfg = [];
    /** 请求的URI */
    public static $uri = '';
    /** 请求的控制器 */
    public static $controller = '';
    /**  请求的控制器函数 */
    public static $function = '';
    /**
     * 请求的控制器函数的参数
     */
    public static $params = [];
    /** 当前action类对象 object */
    public static $action = null;
    /** 当前action类对象::方法, 注和 $function 不同, 这里是完整方法路径 className($action)::$function */
    public static $method = '';
    /** 带扩展请求的扩展名, 例 .json, .html */
    public static $extname = '';
    /** 提交的内容 get|post|input:// */
    public static $req = [];
    /**  POST 提交的键 */
    public static $postKeys = [];
    /** php://input 提交的键 */
    public static $phpinputKeys = [];
    /**post提交的文件内容 */
    public static $reqFiles = [];
    /**数据库连接列表 */
    public static $dbList = [];
    /**对象集 */
    public static $objectList = [];
    /**语言包 */
    public static $langData = [];
    /* 是否输出过mvc.css  此css样式仅用于 mvc 本身的一些内容显示，例 runtimes表格 等*/
    public static $isOutputMvcCss = false;
    /* 运行时间&内存统计  */
    public static $runtimeArr = null;
    /* 运行的SQL列表  */
    public static $runtimeSQLs = [];
    /* 加密键  */
    private static $encryptkey = '';
    /* 是否有 ErrorClass::errorMake() 抛出的错误, 如果有则 onShutdownDebug() 不再处理错误 */
    public static $hasErrorMake = false;

    /**
     * MVC 创建
     * @param string $projectDir 项目目录
     * @param string $mvcDir MVC目录
     * @param array $extCfg 扩展配置
     */
    public static function create(string $projectDir, string $mvcDir, array $extCfg = [])
    {
        //记录起始内存与时间
        $useMemory = memory_get_usage() ?: 0;  //占用的内存
        $peakMemory = memory_get_peak_usage() ?: 0; //分配的内存
        list($usec, $sec) = explode(" ", microtime());
        $msec = round(((float)$usec + (float)$sec) * 1000);
        self::$runtimeArr[] = [
            'msec' => $msec,
            'class' => __CLASS__,
            'type' => '::',
            'function' => __FUNCTION__,
            'call' => __METHOD__,
            'file' => __FILE__,
            'line' => __LINE__,
            'useMemory' => $useMemory,
            'peakMemory' => $peakMemory,
            'remark' => '起始',
            'color' => '#ccc',
        ];
        self::$extCfg = $extCfg;
        /** 是否是控制台模式 */
        define('IS_CLI', strtolower(PHP_SAPI) == 'cli');
        /** 是否是Windows系统 */
        define('IS_WIN', PHP_OS == 'WINNT');
        /** 是否是开发环境 */
        define('IS_DEVELOP', isset($_SERVER['X_DEVELOP']) && $_SERVER['X_DEVELOP']);

        /** 系统配置文件是否使用 APCu 缓存, CLI 和 DEVELOP 模式下此选项恒为 false */
        define('CONFIG_USE_APCU', $extCfg && $extCfg['configIsUseAPCu'] && !IS_CLI && !IS_DEVELOP);

        /** 项目主目录 */
        define('ROOT', IS_WIN ? str_replace('\\', '/', $projectDir) : $projectDir);

        /** MVC 目录 */
        define('DIR_MVC', IS_WIN ? str_replace('\\', '/', $mvcDir) : $mvcDir);

        // 加载MVC函数库
        require(DIR_MVC . '/Function.php');

        /** 项目名称 取主目录名 */
        define('PROJECT', basename(ROOT)); //用于 Basic/Digest 认证等

        /** 配置文件目录 */
        define('DIR_CONFIG', ROOT . '/config');
        /** 控制器目录 */
        define('DIR_ACTION', ROOT . '/action');
        /** 模型目录 */
        define('DIR_MODEL', ROOT . '/model');
        /** 视图目录 */
        define('DIR_VIEW', ROOT . '/view');

        /** 换行符 */
        define('LF', "\n");

        // 加载通用配置文件,
        self::$cfg = loadConfig('common');
        // 路由处理/语言标识/站点名称/加载站点配置文件
        self::route();

        /** 调试模式 */
        define('DEBUG', self::$cfg['debug']);

        /** 缓存时间 1 分钟 */
        define('CACHE_TIME_1', 1 * 60);
        /** 缓存时间 2 分钟 */
        define('CACHE_TIME_2', 2 * 60);
        /** 缓存时间 5 分钟 */
        define('CACHE_TIME_5', 5 * 60);
        /** 缓存时间 15 分钟 */
        define('CACHE_TIME_15', 15 * 60);
        /** 缓存时间 1 小时 */
        define('CACHE_TIME_HOUR', 3600);
        /** 缓存时间 1 天 */
        define('CACHE_TIME_DAY', 86400);
        /** 返回信息 - 成功标记 */
        define('R_OK', 1);
        /** 返回信息 - 失败标记 */
        define('R_NO', 2);
        /** 返回信息 - 普通消息 */
        define('R_INFO', 4);
        /** 返回信息 - 警告消息 */
        define('R_WARNING', 8);
        /** 返回信息 - 错误消息 */
        define('R_ERROR', 16);

        /** 静态文件目录 */
        define('DIR_STATIC', fdir(self::$cfg['dir']['static'] ?: ROOT . '/static'));
        /** 用户文件目录 */
        define('DIR_FILES', fdir(self::$cfg['dir']['files'] ?: ROOT . '/files'));
        /** 数据文件目录, 例语言包 */
        define('DIR_LOCALE', fdir(self::$cfg['dir']['locale'] ?: ROOT . '/locale'));
        /** 缓存目录 */
        define('DIR_CACHE', fdir(self::$cfg['dir']['cache'] ?: ROOT . '/cache'));
        /** 备份目录 */
        define('DIR_BACKUP', fdir(self::$cfg['dir']['backup'] ?: ROOT . '/backup'));
        /** 日志目录 */
        define('DIR_LOG', fdir(self::$cfg['dir']['log'] ?: ROOT . '/log'));
        /** 运行时的临时文件目录 */
        define('DIR_RUNTIME', fdir(self::$cfg['dir']['runtime'] ?: ROOT . '/runtime'));


        /** 静态文件网址 */
        define('URL_STATIC', self::$cfg['url']['static'] ?: HOST_NOT_LG . '/static');
        /** 用户文件网址 */
        define('URL_FILES', self::$cfg['url']['files'] ?: HOST_NOT_LG . '/files');
        /** 缓存文件网址 */
        define('URL_CACHE', self::$cfg['url']['cache'] ?: HOST_NOT_LG . '/cahce');
        /** 日志文件网址 */
        define('URL_LOG', self::$cfg['url']['log'] ?: HOST_NOT_LG . '/log');
        /** 运行时文件网址 */
        define('URL_RUNTIME', self::$cfg['url']['runtime'] ?: HOST_NOT_LG . '/runtime');

        /** debug_backtrace 填充选项() */
        define(
            'DEBUG_BACKTRACE',
            (self::$cfg['error']['allBacktrace'] || IS_DEVELOP) ? DEBUG_BACKTRACE_PROVIDE_OBJECT : DEBUG_BACKTRACE_IGNORE_ARGS
        );

        //加密键做特殊处理
        self::$encryptkey = self::$cfg['encrypt']['secretKey'] ?: '';

        //清除一些过渡配置及与有安全风险的配置
        unset(self::$cfg['dir'], self::$cfg['url'], self::$cfg['encrypt']['secretKey']);


        // 加载多语言
        if (LANG != MVC::$cfg['site'][SITE]['packDefault']) {
            self::loadLanguage(LANG);
        }

        // 注册错误处理
        self::registerError();

        // 注册自动加载
        spl_autoload_register('\xmvc\MVC::autoload');

        // 记录初始化时间
        self::runtimeAdd('MVC 初始化');

        // 权限认证
        self::powerVerify();

        // 执行调用
        self::execute();
    }

    /**
     * 路由处理
     */
    public static function route()
    {
        // 站点名根网址与路由请求串
        if (isset(self::$extCfg['host'])) {
            $host = self::$extCfg['host'];
        } else {
            $host = $_SERVER['PHP_SELF'];
            if (strpos($host, 'index.php') !== false) {
                $host = substr($host, 0, strpos($host, 'index.php'));
            } else {
                $host = '/';
            }
        }
        $uri = '';
        if (IS_CLI) {
            if (!isset($_SERVER['argv'][1])) {
                throw new \Exception('控制台命令必须传递路由串');
            }
            $uri = $_SERVER['argv'][1];
            /** 加载控制台配置 */
            self::$cfg = array_merge(self::$cfg, loadConfig('cli'));
        } else {
            $uri = $_SERVER['REQUEST_URI'];
            if ($uri != '/') {
                $pos = strpos($uri, $host);
                if ($pos !== false) {
                    $uri = substr($uri, strlen($host) - 1);
                    if (strpos($uri, '/?') !== false || strpos($uri, 'index.php?') !== false) {
                        // xxx/xxx/index.php?xx/xx-xx 模式
                        $uri = explode('?', $uri)[1];
                        $host .= 'index.php?';
                        /** 网址类型
                         * - PATH 纯路径模式
                         * - QUERY_STRING ?号传路径 // index.php?xx/xx-xx 模式
                         */
                        define('URL_TYPE', 'QUERY_STRING');
                    }
                }
            }
        }
        !defined('URL_TYPE') && define('URL_TYPE', 'PATH');

        $host = rtrim($host, '/');
        $uri = trim($uri, '/');
        $uri = $uri ?: 'index-index';
        // 网址路由跳转规则
        if (strpos($uri, 'global.js') !== false) {
            $uri = 'js-global';
        } else {
            if (self::$cfg['url']['route']) {
                $tmpArr = explode("\n", self:: $cfg['url']['route']);
                foreach ($tmpArr as $rule) {
                    if (trim($rule) != '') {
                        list($pattern, $replacement) =  explode('=>', $rule . '=>');
                        $pattern = trim($pattern);
                        $replacement = trim($replacement);
                        if ($pattern && $replacement && preg_match($pattern, $uri)) {
                            $uri = preg_replace($pattern, $replacement, $uri);
                            break; // 找到任意一条就退出
                        }
                    }
                }
                unset($tmpArr);
            }
        }
        self::$uri = $uri;
        // 如果是没有配置的域名, 使用 default 配置
        $domain = (isset($_SERVER['SERVER_NAME']) && isset(self::$cfg['domain'][$_SERVER['SERVER_NAME']]))
        ? self::$cfg['domain'][$_SERVER['SERVER_NAME']]
        : self::$cfg['domain']['default'];

        // 域名主站点
        $domainSite = $domain['site'];
        $others = $domain['others'];
        $others[] = 'xmvc'; // 添加 xmvc 管理站点

        $pos1 = strpos($uri, '/');
        $first = $pos1 !== false ?  substr($uri, 0, $pos1) : ''; // 网址中第一个目录
        $second = '';
        if ($first) {
            $pos2 = strpos($uri, '/', $pos1 + 1);
            $second = $pos2 !== false ?  substr($uri, $pos1 + 1, $pos2 - $pos1 - 1) : ''; // 网址中第二个目录
        }
        $langName = ''; // 语言名称
        $siteName = ''; // 站点名称
        if (in_array($first, self::$cfg['lang']['list'])) {
            // 第一个目录在语言列表中
            $langName = $first;
            $siteName = $second; // 站点名称取第二个目录
        } else {
            $siteName = $first; // 第一个目录是语言名称, 站点名称取第一个目录
        }
        if (in_array($siteName, $others)) {
            // 站点名在其他站点列表中
            /** 站点 */
            define('SITE', $siteName);
            /** 是否是默认站点, 默认站点在网址中需省略 */
            define('IS_DEFAULT_SITE', false);
        } else {
            define('SITE', $domainSite);
            define('IS_DEFAULT_SITE', true);
        }

        /** 站点唯一ID */
        define('SITE_GUID', 'xmvc_' . PROJECT . '_' . SITE);
        define('SITE_USER_LOGIN_KEY', SITE_GUID . '_logined');
        define('SITE_USER_INFO_KEY', SITE_GUID . '_info');

        // 加载站配置文件
        $siteCfgArr = loadConfig(SITE);
        self::$cfg['site'][SITE] = $siteCfgArr['site'];
        unset($siteCfgArr['site']);
        self::$cfg = array_merge(self::$cfg, $siteCfgArr);
        unset($siteCfgArr);

        if ($langName == '') {
            $langName = self::$cfg['site'][SITE]['defaultLang'];
            /** 当前是否是默认语言, 默认语言在网址中需省略*/
            define('IS_DEFAULT_LANG', true);
        } elseif ($langName == self::$cfg['site'][SITE]['defaultLang']) {
            self::alert('The default language [' . $langName . '] must be omitted in the url.');
            exit();
        } else {
            define('IS_DEFAULT_LANG', false);
        }

        if (!in_array($langName, self::$cfg['site'][SITE]['list'])) {
            // 检查是否是当前站点支持的语言
            self::alert('Unsupported site language name [' . $langName . '].');
            exit();
        }
        /** 当前站点的静态文件版本号 */
        define('SVER', self::$cfg['site'][SITE]['sver']);
        /** 当前语言名称 */
        define('LANG', $langName);
        /** 根网址 */
        define('HOST', IS_DEFAULT_LANG ? $host : (URL_TYPE == 'PATH' ? '/' . LANG : $host . '/' . LANG));
        /** 根网址 - 不带语言标识 */
        define('HOST_NOT_LG', $host);
        /** 当前网址 */
        define('URL_SELF', IS_CLI ? '' : $_SERVER['REQUEST_URI']);//$uri ? '/' . $uri : $uri);
        /** http|https 协议 */
        define('HTTP', ($_SERVER['REQUEST_SCHEME'] == 'https' || self::$cfg['site'][SITE]['ssl']) ? 'https://' : 'http://');
        if (isset(self::$extCfg['http_host'])) {
            /** 带http|https头域名及端口的根网址, cli 模式无此常量 */
            define('HTTP_HOST', self::$extCfg['http_host']);
            /** 带http|https的当前网址, cli 模式无此常量 */
            define('HTTP_URL_SELF', IS_CLI ? '' : self::$extCfg['http_host'] . URL_SELF);
        } else {
            /** 带http|https头域名及端口的根网址, cli 模式无此常量 */
            define('HTTP_HOST', IS_CLI ? '' : HTTP . $_SERVER['HTTP_HOST'] . HOST);
            /** 带http|https的当前网址, cli 模式无此常量 */
            define('HTTP_URL_SELF', IS_CLI ? '' : HTTP . $_SERVER['HTTP_HOST'] . URL_SELF);
        }
        // 组装省略的 URI
        if (IS_DEFAULT_LANG && IS_DEFAULT_SITE) {
            $uri = SITE . '/' . $uri; // 即是默认语言也是默认站点
        } elseif (IS_DEFAULT_SITE) {
            $uri = SITE . '/' . substr($uri, $pos1 + 1); // 仅是默认站点
        } elseif (IS_DEFAULT_LANG) {
            $uri = $uri; // 仅是默认语言
        } else {
            $uri = substr($uri, $pos1 + 1); // 不是默认语言, 也不是默认站点
        }
        list($controller, $function, $extname) = self::parseUri($uri);

        self::$controller   = $controller;
        self::$function = $function;
        self::$extname  = $extname;
    }

    /**
     * 加载语言包
     */
    public static function loadLanguage(string $tag)
    {
        if (MVC::$cfg['lang']['cache']['enabled'] == false) {
            $xmvcLangFilename = DIR_LOCALE . '/lang/_xmvc_' . $tag . '.php';
            $projectLangFilename = DIR_LOCALE . '/lang/' . $tag . '.php';
            self::$langData[$tag] =  require($projectLangFilename);
            self::$langData[$tag] += require($xmvcLangFilename);
        }
    }

    /** 权限认证 */
    public static function powerVerify()
    {
        if (!IS_CLI) {
            if (!isset(self::$cfg['site'][SITE]['power'])) {
                throw new \Exception("必须在 config/" . SITE . ".php 中配置站点 [ " . SITE . " ] 的权限认证 ['site']['power']");
            }
            if (self::$cfg['site'][SITE]['power']) {
                /** @var xmvc\Login $power */
                $power = new self::$cfg['site'][SITE]['power']();
                if (!$power->verify()) {
                    header($_SERVER['SERVER_PROTOCOL'] . ' 403 Forbidden', true, 403);
                    self::echo('<div class="box-alert"><h1>403</h1><p>Operations without this permission</p></div>');
                    exit();
                }
                self::runtimeAdd('权限认证');
            }
        }
    }

    /**
     * 执行 MVC 主程
     */
    public static function execute()
    {
        // 加载 compose autoload.php
        require DIR_MVC . '/composer/vendor/autoload.php';
        if (self::$extname == 'php') {
            // 以 .php 结尾的文件, 例对守护进程 daemon 文件的调用
            $filename = self::$controller . '.php';
            if (substr($filename, 0, 5) == 'xmvc/') {
                $filename = DIR_MVC . '/' . substr($filename, 5);
            } else {
                $filename = ROOT . '/' . $filename;
            }
            require($filename);
            return;
        }
        //通过路由创建 action 类对象
        $class = str_replace('/', '\\', self::$controller);
        if (substr(self::$controller, 0, 5) !== 'xmvc/') {
            $class = '\\app\\action\\' . $class;
        }
        self::$method = $class . '->' . self::$function;

        // 处理提交传参
        self::parseRequest();

        try {
            self::$action = new $class();
        } catch (\Throwable $th) {
            if (IS_DEVELOP && strpos($th->getMessage(), 'not found') !== false) {
                $filename = str_replace('\\', '/', substr($class, 4)) . '.php';
                error($filename . ' 控制器不存在, ' . $th->getMessage(), [
                    'cmd' => [
                        'title' => '创建控制器',
                        'cmdFunction' => 'newAction',
                        'newClassFile' => ROOT . $filename,
                        'newClassName' => $class,
                        'newClassFunction' => self::$function
                    ],
                ]);
            } else {
                throw new \Exception($th->getMessage(), 1);
            }
        }
        if (!method_exists(self::$action, self::$function)) {
            error(self::$function . '() 方法在 ' . $class . ' 类中没有被定义');
        }
        $params = [];

        //反射, 配对参数对应关系,及处理默认值
        $reflector = new \ReflectionMethod(self::$action, self::$function);
        $reflectorParameters = $reflector->getParameters();
        //配对参数对应关系
        foreach ($reflectorParameters as $k => $v) {
            if ($v->name === 'req') {
                $params[$k] = self::$req;
            } elseif (isset(self::$req[$v->name])) {
                if ($v->hasType()) {
                    $typeName = $v->getType()->getName();
                    if ($typeName == 'int') {
                        if (is_numeric(self::$req[$v->name])) {
                            self::$req[$v->name] = (int)self::$req[$v->name];
                        } else {
                            error(sprintf('%s(...%s...)  参数 $%s 需要一个整型数字', self::$method, $typeName . ' $' . $v->name, $v->name));
                        }
                    } elseif ($typeName == 'float') {
                        if (is_numeric(self::$req[$v->name])) {
                            self::$req[$v->name] = (int)self::$req[$v->name];
                        } else {
                            error(sprintf('%s(...%s...)  参数 $%s 需要一个浮点型数字', self::$method, $typeName . ' $' . $v->name, $v->name));
                        }
                    } elseif ($typeName == 'array') {
                        $arrValue = self::$req[$v->name];
                        if (is_array($arrValue)) {
                            $value = $arrValue;
                        } elseif (strpos($arrValue, ',') !== false) {
                            $value = explode(',', $arrValue);
                        } elseif (base64_decode($arrValue, true)) {
                            $arrValue = json_decode(base64_decode($arrValue), true);
                        } else {
                            $arrValue = [$arrValue];
                        }
                        self::$req[$v->name] = $value;
                    }
                    $params[$k] = ($v->getType()->getName() == 'int') ? (int)self::$req[$v->name] : self::$req[$v->name];
                }
                $params[$k] = self::$req[$v->name];
            } elseif (!$v->isDefaultValueAvailable()) {
                $paramDefine = '';
                foreach ($reflectorParameters as $vDef) {
                    if ($paramDefine != '') {
                        $paramDefine .= ', ';
                    }
                    if ($vDef->getType()) {
                        $paramDefine .= $vDef->getType() . ' ';
                    }
                    $paramDefine .= '$' . $vDef->name;
                    if ($vDef->isDefaultValueAvailable()) {
                        $paramDefine .= ' = ' . $vDef->getDefaultValue();
                    }
                }
                $reflectorClass = new \ReflectionClass(self::$action);
                $actionClassFilename = $reflectorClass->getFileName();
                $actionFunctionFileline = '';
                $tokens = token_get_all(file_get_contents($actionClassFilename), TOKEN_PARSE);
                foreach ($tokens as $token) {
                    if (is_array($token) && $token[0] === T_STRING && $token[1] === self::$function) {
                        $actionFunctionFileline = $token[2];
                        break;
                    }
                }
                error(
                    sprintf('%s(%s)  参数 $%s 没有传值', self::$method, $paramDefine, $v->name),
                    null,
                    [[__FILE__, __LINE__]],
                    ['file' => $actionClassFilename, 'line' => $actionFunctionFileline]
                );
            }
        }
        self::runtimeAdd('创建控制器');
        //执行 action
        if (self::$extname == 'json') {
            if (ob_get_level() > 0) {
                ob_end_flush();
            }
            ob_start();
        }
        /** @var \ReturnClass $r */
        $r = null;
        // 验证 csrf 令牌
        if (!empty(self::$phpinputKeys)) {
            $headers = getallheaders();
        }
        if (
            (!empty(self::$postKeys) && (!isset(self::$req['csrfToken']) || !validCsrfToken(self::$req['csrfToken'])))
            ||  (!empty(self::$phpinputKeys) && (!isset($headers['X-CSRF-Token']) || !validCsrfToken($headers['X-CSRF-Token'])))
        ) {
            $errormsg = lg('非法的') . ' or ' . lg('过期的') . ': ' . 'CSRF TOKEN';
            if (self::$extname == 'json') {
                $r = (new ReturnClass())->error($errormsg);
            } else {
                self::warningExit($errormsg);
            }
        } else {
            $r = call_user_func_array([self::$action, self::$function], $params);
        }
        if (is_null($r) || get_class($r) != 'xmvc\ReturnClass') {
            self::alert(sprintf('%s() 返回结果必须是一个 ReturnClass对象, 可使用 return R() 返回结果, 当前结果为:%s', self::$method, substr(json_encode($r, JSON_UNESCAPED_UNICODE), 0, 512)));
        } else {
            self::runtimeAdd('执行控制器方法 ' . self::$method . '()');
            if (self::$extname == 'html') {
                if (isset(MVC::$cfg['site'][SITE]['header'])) {
                    if (is_null($r->header)) {
                        $r->header = new \stdClass();
                    }
                    foreach (MVC::$cfg['site'][SITE]['header'] as $key => $value) {
                        if (!isset($r->header->$key)) {
                            $r->header->$key = $value;
                        }
                    }
                }
                view(
                    DIR_VIEW . '/' . self::$controller . '/' . self::$function,
                    $r->data,
                    [
                        'header' => $r->header,
                        'footer' => $r->footer,
                        'server' => [
                            'error' => ($r->code & R_NO) > 0,
                            'type' => ($r->code & R_INFO) > 0 ? 'INFO' : (($r->code & R_WARNING) > 0 ? 'WARNING' : (($r->code & R_ERROR) > 0 ? 'ERROR' : '')),
                            'code' => $r->code,
                            'msg' => $r-> msg,
                            'ext' => $r->ext,
                            'user' => Login::user(),
                            'csrfToken' => getCsrfToken(),
                        ]
                    ],
                    true
                );
                self::runtimeAdd('执行完成: 输出视图');
            } elseif (self::$extname == 'json') {
                $r->output();
                self::runtimeAdd('执行完成: 输出JSON');
            } elseif (IS_CLI) {
                print_r($r->array());
            } else {
                if (!is_null($r->error)) {
                    ErrorClass::errorMake($r->error);
                }
            }
        }
    }

    /**
     * 路由 uri
     * seo 模式 http://127.0.0.1/user-login~a-1~~1~b-2 如果参数值中有 ~ 用两个 ~ 转义
     *          http://127.0.0.1/core/cmd-  默认 index 方法,等同 /core/cmd-index
     * standard 模式 http://127.0.0.1/user/login?a=1~1&b=2
     * 不用地址重写 http://127.0.0.1/index.php/user-login~a-1~~1~b-2
     * 支持随意加文件后缀 http://127.0.0.1/product/cate-list.html
     * 控制台 由于 windows cmd 下,调用文件传参 & 有特殊意义所以不支持 ?a=1&b=2 可以使用 seo 模式
     * 控制台 php index.php user-login~a-1~~1~b-2
     * 控制台 php.ini 须开启 register_argc_argv 传参
     * 特殊用法
     * - page-1,2,3,4 如果 action函数定义参数为array类型,它将会被 explode 转成数组
     * - page-base64_encode(json_encode($array)) 如果 action函数定义参数为array类型,它将会被 json_decode(base64_decode($base64String), true) 转成数组
     * @param string $uri  自定义路由
     */
    public static function parseUri(string $uri): array
    {
        $extname = '';
        //控制器与方法
        $qtPos = strpos($uri, '?');
        if ($qtPos !== false) {
            $uri = substr($uri, 0, $qtPos);
        }
        $len = strlen($uri);
        $pos1 = strrpos($uri, '.');
        if ($pos1) {
            // 如果最后一个字符是 . 那么不是文件扩展名
            if ($pos1 !== $len - 1) {
                $pos2 = strrpos($uri, '~'); //判断是给参数传的是一个文件名, 还是就是当前的文件扩展名
                if ($pos2 === false || $pos2 < $pos1) {
                    $extname = substr($uri, $pos1 + 1);
                    $uri = substr($uri, 0, $pos1);
                }
            }
        }
        $pos1 = strpos($uri, '-') ?: $len;
        $pos2 = strpos($uri, '~') ?: $len;
        if ($pos1 > $pos2 || $pos1 == $len) {
            // 省略 index 方法的情况 /xmvc/core/cmd 或 /xmvc/core/cmd~a-1
            $pos1 = $pos2;
        } else {
            $function = substr($uri, $pos1 + 1, $pos2 - $pos1 - 1);
        }
        $function = isset($function) && $function  ? $function : 'index';
        $controller = substr($uri, 0, $pos1);
        if ($pos2 != $len) {
            $str = substr($uri, $pos2 + 1);
            if ($str) {
                $str = str_replace('~~', "\n", $str);
                $arr = explode('~', $str);
                foreach ($arr as $k => $v) {
                    $pos = strpos($v, '-');
                    if ($pos) {
                        self::$req[substr($v, 0, $pos)] = urldecode(str_replace("\n", '~', substr($v, $pos + 1)));
                    }
                }
            }
        }

        $arr = explode('/', $controller);
        //控制器类为 / 的最后一个,转首字母大写
        $endIdx = count($arr) - 1;
        $arr[$endIdx] = ucfirst($arr[$endIdx]);
        $controller = implode('/', $arr);
        self::$params = self::$req;
        return [$controller, $function, $extname];
    }

    /**
     * 处理参数
     * -  MVC初始化时会填充 _GET, _POST, _FILES, php://input
     */
    public static function parseRequest()
    {
        /* 禁用 _GET */
        if (isset($_GET)) {
            foreach ($_GET as $k => $v) {
                self::$req[$k] = is_string($v) ? urldecode($v) : $v;
            }
            unset($_GET);
        }
        if (!empty($_POST)) {
            if (isset($_POST['app_ajaxD'])) {
                $tmpArr = json_decode($_POST['app_ajaxD'], true);
                foreach ($tmpArr as $key => $value) {
                    self::$req[$key] = $value;
                    self::$postKeys[] = $key;
                }
            } else {
                foreach ($_POST as $key => $value) {
                    self::$req[$key] = $value;
                    self::$postKeys[] = $key;
                }
            }
            unset($_POST);
            if (isset($_FILES)) {
                self::$reqFiles = array_merge([], $_FILES);
                unset($_FILES);
            }
        } elseif ($input = file_get_contents('php://input')) {
            $jsonData = json_decode($input, true);
            if ($jsonData === null) {
                error(lg('无效的') . ': ' . 'json data');
            }
            foreach ($jsonData as $key => $value) {
                self::$req[$key] = $value;
                self::$phpinputKeys[] = $key;
            }
        }
        self::decodeRequest(self::$req);
    }

    /**
     * 解码参数值 及 自定义参数值转义
     *
     * @param array $arr
     * @return void
     */
    public static function decodeRequest(array &$arr)
    {
        foreach ($arr as $k => $v) {
            if (is_string($v)) {
                if (substr($v, 0, 7) === 'base64:') {
                    $arr[$k] = Encoder::decode($v);
                } elseif (substr($v, 0, 12) === 'base64array:') {
                    $arr[$k] = Encoder::decodeForArray($v);
                } elseif (substr($v, 0, 13) === 'base64object:') {
                    $arr[$k] = Encoder::decodeForObject($v);
                }
                // 自定义提交参数的转义
                if (self::$cfg['requestEscape']['class']) {
                    $arr[$k] = call_user_func_array([self::$cfg['requestEscape']['class'], self::$cfg['requestEscape']['function']], [$k, $v]);
                }
            } elseif (is_array($v)) {
                self::decodeRequest($arr[$k]);
            }
        }
    }


    /**
     * 自动加载
     */
    public static function autoload($className)
    {
        $parts = explode('\\', $className);
        $first = reset($parts);
        switch ($first) {
            case 'xmvc':
                unset($parts[0]);
                $classFile = implode('/', $parts);
                $classFile = DIR_MVC . '/' . $classFile . '.php';
                require $classFile;
                return;
            case 'app':
                unset($parts[0]);
                $second = reset($parts);
                $classFile = implode('/', $parts);
                $classFile = ROOT . '/' . $classFile . '.php';
                if (is_file($classFile)) {
                    require $classFile;
                } elseif ($second == 'action') {
                    if (IS_DEVELOP) {
                        error('控制器 ' . $className . ' 类没有定义, ' . $classFile . ' 不存在,', [
                            'cmd' => [
                                'title' => '创建此控制器',
                                'cmdFunction' => 'newAction',
                                'newClassFile' => $classFile,
                                'newClassName' => $className,
                                'newClassFunction' => self::$function
                            ],
                        ]);
                    } else {
                        error(sprintf('控制器 %s 类没有定义, %s 不存在,', $className, $classFile));
                    }
                }
                return;
        }
        $found = false;
        // 自定义扩展自动加载
        if (isset(self::$cfg['autoload']['class']) && self::$cfg['autoload']['class']) {
            $found = call_user_func_array([self::$cfg['autoload']['class'], self::$cfg['autoload']['function']], [$className]);
        }
        /*
        影响某些composer 类的加载
        例 workerman 的 Worker.php 中的 if (!\class_exists($this->protocol)) { $this->protocol = "Workerman\\Protocols\\$scheme"; }
        if (!$found) {
            error($className . ' 类没有找到 ', null, [[__FILE__, __LINE__]]);
        }
        */
    }

    /**
     * 错误处理
     */
    public static function registerError()
    {
        //错误级别
        if (self::$cfg['error']['showPhpSystem']) {
            error_reporting(self::$cfg['error']['error_levels']);
        } else {
            error_reporting(0); //使用自己的错误处理，禁用系统的
        }

        //错误句柄
        self::setErrorHandler(true);

        //异常中断
        if (IS_CLI) {
            if (extension_loaded('xdebug')) {
                ini_set('xdebug.remote_enable', 0); //控制台模式, 禁用 xdebug
            }
            register_shutdown_function('\xmvc\MVC::onShutdownCli');
        } elseif (DEBUG) {
            if (function_exists("opcache_reset")) {
                opcache_reset(); //debug模式, 每次清空 opcache 缓存用于更新代码
            }
            register_shutdown_function('\xmvc\MVC::onShutdownDebug');
        } else {
            register_shutdown_function('\xmvc\MVC::onShutdownProduct');
        }
    }

    /**
     * 开户或关闭 set_error_handler
     * @param bool $state true:开启, false:关闭
     */
    public static function setErrorHandler(bool $state)
    {
        if ($state) {
            set_error_handler("\xmvc\MVC::errorHandlerDebug", self::$cfg['error']['error_levels']);
        } else {
            restore_error_handler();
        }
    }

    /** 异常中断处理函数 */
    public static function errorHandlerDebug($errno, $message, $errfile, $errline)
    {
        ErrorClass::errorHandlerDebug($errno, $message, $errfile, $errline);
    }

    /** 全局脚本终止处理函数 */
    public static function onShutdown()
    {
        // 更新 SESSION
        Session::save();

        // 创建运行时记录
        if (DEBUG && !is_null(self::$runtimeArr)) {
            Runtime::createRuntimeReport();
        }
    }

    /** 脚本终止处理函数 Debug */
    public static function onShutdownDebug()
    {
        self::onShutdown();
        if (!self::$hasErrorMake) {
            ErrorClass::onShutdownDebug();
        }
    }

    /** 脚本终止处理函数 Product */
    public static function onShutdownProduct()
    {
        self::onShutdown();
        if (!self::$hasErrorMake) {
            ErrorClass::onShutdownProduct();
        }
    }

    /** 脚本终止处理函数 控制台 */
    public static function onShutdownCli()
    {
        if (!self::$hasErrorMake) {
            ErrorClass::onShutdownCli();
        }
    }

    /**
     * 向前端输出内容
     *
     * @param string $contents 内容文本
     * @param string $htmlTpl html模板
     * @param string $head 追加的头信息，须 $htmlTpl 不为空
     * @param string $body <body>标签，须 $htmlTpl 不为空
     * @param bool $return 为true 返回文本,  默认 false 直接输出到前台
     * @return string|void
     */
    public static function echo(
        string $contents,
        string $htmlTpl = '',
        string $head = '',
        string $body = '<body>',
        bool $return = false
    ) {
        $cssJs = '';
        if (self::$isOutputMvcCss == false) {
            $cssJs = self::getMvcCssJs();
            self::$isOutputMvcCss = true;
        }
        if ($htmlTpl) {
            if ($cssJs == '') {
                $cssJs = self::getMvcCssJs();
            }
            $html = '<!DOCTYPE html>' . LF . '<html>' . LF . '<head>' . LF . '<meta charSet="utf-8" />' . LF
                    . $head . $cssJs . '</head>' . LF . $body . LF . $contents . LF . "</body>" . LF . '</html>';
        } else {
            $html = $cssJs . $contents;
        }
        if ($return) {
            return $html;
        } else {
            echo $html;
        }
    }

    public static function alert(string $message, string $title = '')
    {
        $h1 = $title ? '<h3>' . $title . '</h3>' : '';
        self::echo('<div class="box-alert">' . $h1 . '<p>' . $message . '</p></div>');
    }

    public static function alertExit(string $message, string $title = '')
    {
        self::alert($message, $title);
        exit();
    }

    public static function warning(string $message, string $title = '')
    {
        $h1 = $title ? '<h3 class="red">' . $title . '</h3>' : '';
        self::echo('<div class="box-warning red2">' . $h1 . '<p>' . $message . '</p></div>');
    }

    public static function warningExit(string $message, string $title = '')
    {
        http_response_code(403);
        self::warning($message, $title);
        exit();
    }

    /**
     * 获取 mvc.css 和 mvc.js 内容
     *
     * @param boolean $backContents 是否返回样式内容， 默认 false 直接输出
     * @return void|string
     */
    public static function getMvcCssJs()
    {
        return '<style>' . LF . file_get_contents(DIR_MVC . '/core/static/mvc.css') . LF . '</style>' . LF
             . '<script>' . LF . file_get_contents(DIR_MVC . '/core/static/mvc.js') . LF . '</script>' . LF;
    }

    /**添加运行时间统计, 只在 DEBUG 模式下有效*/
    public static function runtimeAdd($remark)
    {
        if (DEBUG) {
            Runtime::add($remark, '#333');
        }
    }

    /**
     * 反射创建对象
     * @param string $className 类名
     * @param array $params 参数,可带参数名称
     * @return object
     */
    public static function createObject(string $className, array $params = null)
    {
        $class = new \ReflectionClass($className);
        $args = [];
        if ($params) {
            $constructor = $class->getConstructor();
            if ($constructor) {
                foreach ($constructor->getParameters() as $k => $v) {
                    if (isset($params[$v->name])) {
                        $args[$k] = $params[$v->name];
                    } elseif (isset($params[$k])) {
                        $args[$k] = $params[$k];
                    } elseif ($v->isDefaultValueAvailable()) {
                        $args[$k] = $v->getDefaultValue();
                    } else {
                        error($className . '::__construct' . ' 参数 $' . $v->name . ' 没有传值');
                    }
                }
            }
        }
        return $class->newInstanceArgs($args);
    }

    /**
     * 取加密键
     * @return string
     */
    public static function getEncryptkey(): string
    {
        return '3De*pAy%zK82' . md5(self::$encryptkey) . '1L96.nsSit';
    }
}
