<?php

declare(strict_types=1);

namespace xmvc;

/**
 * XMVC 框架入口
 * @author hzqghost <313143468@qq.com>
 * @copyright 2024 © yipart.com
 * @version 5.0.0
 * @license GPL
 * @license http://opensource.org/licenses/gpl-license.php GNU Public License
 */
final class MVC
{
    /**全局配置数组 */
    public static $cfg = [];
    /**请求的控制器 */
    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 = [];
    /**MVC库 */
    public static $libs = [];
    /**对象集 */
    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 创建
     */
    public static function create()
    {
        // 注册自动加载
        spl_autoload_register('\xmvc\MVC::autoload');
    }

    /**
     * 路由处理
     * @param string $uri  路由
     * @param array $runtimeArr  前面的运行状态列表
     */
    public static function route(string $uri, array $runtimeArr)
    {
        //self::$cfg = $config;
        self::$runtimeArr = $runtimeArr;

        // 定义相关常量
        self::defineVar($uri);

        //首次运行
        if (FIRST_RUN) {
            require DIR_MVC . '/core/firstRun.php';
        }

        list($controller, $function, $extname) = self::parseUri($uri);

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


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

        self::runtimeAdd('MVC 初始化');
    }

    /**
     * 执行 MVC 主程
     */
    public static function execute()
    {
        //通过路由创建 action 类对象
        $class = str_replace('/', '\\', self::$controller);
        if (substr(self::$controller, 0, 5) !== 'xmvc/') {
            $class = '\\action\\' . $class;
        }
        self::$method = $class . '->' . self::$function;

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

        self::$action = new $class();
        if (!method_exists(self::$action, self::$function)) {
            error(self::$function . lg('() 方法在 {:1} 类中没有被定义', $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();
                    //dd($typeName);
                    if ($typeName == 'int') {
                        if (is_numeric(self::$req[$v->name])) {
                            self::$req[$v->name] = (int)self::$req[$v->name];
                        } else {
                            error(lg('{:1}(...{:2}...)  参数 ${:3} 需要一个整型数字', 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(lg('{:1}(...{:2}...)  参数 ${:3} 需要一个浮点型数字', self::$method, $typeName . ' $' . $v->name, $v->name));
                        }
                    } elseif ($typeName == 'array') {
                        $arrValue = self::$req[$v->name];
                        if (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(
                    lg('{:1}({:2})  参数 ${:3} 没有传值', self::$method, $paramDefine, $v->name),
                    null,
                    [[__FILE__, __LINE__]],
                    ['file' => $actionClassFilename, 'line' => $actionFunctionFileline]
                );
            }
        }
        self::runtimeAdd('创建控制器');
        //执行 action
        if (ob_get_level() > 0) {
            ob_end_flush();
        }
        ob_start();
       /** @var \ReturnClass $r */
        $r = call_user_func_array([self::$action, self::$function], $params);
        if (is_null($r) || get_class($r) != 'xmvc\ReturnClass') {
            error(lg('{:1}() 返回结果必须是一个 ReturnClass对象, 可使用 return R() 返回结果', self::$method), null, [__FILE__]);
        }
        self::runtimeAdd('执行控制器方法 ' . self::$method);
        if (self::$extname == 'html') {
            if (is_null($r->header) && isset(self::$cfg['header'])) {
                $r->header(self::$cfg['header']);
            }
            view(DIR_VIEW . '/' . self::$controller . '/' . self::$function, $r->data, [
                'header' => $r->header,
                'footer' => new \stdClass(),
                'return' => ['code' => $r->code, 'msg' => $r->msg, 'ext' => $r->ext],
            ], true);
            self::runtimeAdd('执行完成: 输出视图');
        } elseif (self::$extname == 'json') {
            ob_clean();
            if (!IS_CLI && !headers_sent()) {
                header('Content-Type: application/json; charset=utf-8');
            }
            echo json_encode($r->array(), JSON_UNESCAPED_UNICODE);
            self::runtimeAdd('执行完成: 输出JSON');
        } elseif (IS_CLI) {
            dump($r->array());
        } else {
            if (!is_null($r->error)) {
                ErrorClass::errorMake($r->error);
            }
        }
        ob_end_flush();

        if (DEBUG && !is_null(self::$runtimeArr)) {
            Runtime::createRuntimeReport();
        }
    }

    /**
     * 定义常量
     */
    public static function defineVar($uri)
    {
        /** 成功标记 */
        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']['files'] ?: 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_LOG', fdir(self::$cfg['dir']['log'] ?: ROOT . '/log'));
        /** 运行时的临时文件目录 */
        define('DIR_RUNTIME', fdir(self::$cfg['dir']['runtime'] ?: ROOT . '/runtime'));


        /** 当前网址常量 */
        define('URL_SELF', $uri);
        /** 当前网址带HTTP常量 */
        define('HTTP_URL_SELF', HTTP_HOST . $uri);
        /** 静态文件网址 */
        define('URL_STATIC', self::$cfg['url']['path']['static'] ?: HOST . '/static');
        /** 上传文件网址 */
        define('URL_FILES', self:: $cfg['url']['path']['files'] ?: HOST . '/files');
        /** 缓存文件网址 */
        define('URL_CACHE', self:: $cfg['url']['path']['cache'] ?: HOST . '/cahce');
        /** 日志文件网址 */
        define('URL_LOG', self:: $cfg['url']['path']['log'] ?: HOST . '/log');
        /** 运行时文件网址 */
        define('URL_RUNTIME', self:: $cfg['url']['path']['runtime'] ?: HOST . '/runtime');

        /** debug_backtrace 填充选项 */
        define(
            'DEBUG_BACKTRACE',
            self::$cfg['error']['allBacktrace'] ? 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']);
    }

    /**
     * 路由 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 = '';
        //控制器与方法
        if ($uri == '' && self::$cfg['url']['index'] != '') {
            $uri = self::$cfg['url']['index'];
        }
        $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()
    {
        if (isset($_GET)) {
            foreach ($_GET as $k => $v) {
                self::$req[$k] = urldecode($v);
                self::$params[$k] = urldecode($v);
            }
            unset($_GET);
        }
        if (!empty($_POST)) {
            // 验证 csrf 令牌
            if (!isset($_POST['csrf-token']) || !hash_equals($_POST['csrf-token'], Session::get('csrfToken', ''))) {
                self::alert(lg('非法的') . ': ' . 'CSRF TOKEN');
                exit();
            } else {
                unset($_POST['csrf-token']);
                //Session::remove('csrfToken');
            }
            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')) {
            // 验证 csrf 令牌
            $headers = getallheaders();
            if (!isset($headers['X-CSRF-Token']) || !hash_equals($headers['X-CSRF-Token'], Session::get('csrfToken', ''))) {
                self::alert(lg('非法的') . ': ' . 'CSRF TOKEN');
                exit();
            } else {
                Session::remove('csrfToken');
            }
            $jsonData = json_decode($input, true);
            if ($jsonData === null) {
                error(lg('无效的') . ': ' . 'json data');
            }
            foreach ($jsonData as $key => $value) {
                self::$req[$key] = $value;
                self::$phpinputKeys[] = $key;
            }
        }
        foreach (self::$req as $k => $v) {
            if (substr($v, 0, 7) === 'base64:') {
                self::$req[$k] = Encoder::decode($v);
            } elseif (substr($v, 0, 12) === 'base64array:') {
                self::$req[$k] = Encoder::decodeForArray($v);
            } elseif (substr($v, 0, 13) === 'base64object:') {
                self::$req[$k] = Encoder::decodeForObject($v);
            }
            // 自定义提交参数的转义
            if (MVC::$cfg['requestEscape']['class']) {
                self::$req[$k] = call_user_func_array([MVC::$cfg['requestEscape']['class'], MVC::$cfg['requestEscape']['function']], [$k, $v]);
            }
        }
    }

    /**
     * 自动加载
     */
    public static function autoload($className)
    {
        $parts = explode('\\', $className);
        $first = $parts[0];
        if ($first == 'xmvc') {
            unset($parts[0]);
            $classFile = implode('/', $parts);
            $classFile = DIR_MVC . '/' . $classFile . '.php';
        } else {
            $classFile = str_replace('\\', '/', $className);
            $classFile = ROOT . '/' . $classFile . '.php';
        }
        if (is_file($classFile)) {
            require $classFile;
        } elseif ($first == 'action') {
            if (IS_DEVELOP) {
                error('控制器 ' . $className . ' 类没有定义, ' . $classFile . ' 不存在,', [
                    'cmd' => [
                        'title' => '创建此控制器',
                        'cmdFunction' => 'newAction',
                        'newClassFile' => $classFile,
                        'newClassName' => $className,
                        'newClassFunction' => self::$function
                    ],
                ]);
            } else {
                error(lg('控制器 {:1} 类没有定义, {:2} 不存在,', $className, $classFile));
            }
        } else {
            $found = false;
            // 自定义扩展自动加载
            if (isset(self::$cfg['autoload']['class']) && self::$cfg['autoload']['class']) {
                $found = call_user_func_array([MVC::$cfg['autoload']['class'], MVC::$cfg['autoload']['function']], [$className]);
            }
            if (!$found) {
                error($className . ' 类文件没有找到 ' . $classFile, 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::save(); // 更新 SESSION
    }

    /** 脚本终止处理函数 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 warning(string $message, string $title = '')
    {
        $h1 = $title ? '<h3 class="red">' . $title . '</h3>' : '';
        self::echo('<div class="box-warning red2">' . $h1 . '<p>' . $message . '</p></div>');
    }

    /**
     * 获取 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 模式下有效*/
    protected 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';
    }
}
