<?php

use xmvc\MVC;
use xmvc\Dump;
use xmvc\Cache;
use xmvc\Valid;
use xmvc\Viewer;
use xmvc\Encoder;
use xmvc\Session;
use xmvc\ErrorClass;
use xmvc\ReturnClass;
use xmvc\EventManager;

/**
 * XMVC函数库
 */


/**
 * 语言文本切换
 * @return string
 */
function lg(string $str, string|int|float ...$vars): string
{
    if (LANG != MVC::$cfg['site'][SITE]['packDefault']) {
        if (MVC::$cfg['lang']['cache']['enabled']) {
            $cache = Cache::create(MVC::$cfg['lang']['cache']['type'], LANG, MVC::$cfg['lang']['cache']['dns']);
            $str = $cache->get($str) ?: $str;
        } elseif (isset(MVC::$langData[LANG][$str])) {
            $str = MVC:: $langData[LANG][$str];
        }
    }
    if (!empty($vars)) {
        foreach ($vars as $i => $var) {
            $str = str_replace('{:' . ($i + 1) . '}', (string)$var, $str);
        }
    }
    return $str;
}

/**
 * 构建网址
 * @param string $route 控制器路由, INDEX|/, SELF|~, 当前页面不带默认参数 ~METHOD|SELF-METHOD
 * @param array|string $params 自定义参数
 * @param array|null $filterKeys 要过滤的参数键 [key1,key2]
 * @param bool $isAbs 是否返回带 HTTP 协议的完全网址
 * @example ```php
 * url('INDEX'); // 当前站点的首页
 * url('SELF'); // 当前页
 * url('SELF'), ['a' => 1, 'b' => 2, 'test' => 'www']); // 自定义参数
 * url('test/method-test', 'c-3~d-4'); // 字符串参数
 * ```
 */
function url(string $route, array|string $params = null, array $filterKeys = null, bool $isAbs = false): string
{
    if ($route === '/' || $route === 'INDEX') {
        return HOST ?: '/';
    } elseif ($route === '~' || $route === 'SELF') {
        if (IS_DEFAULT_SITE) {
            $route = substr(MVC::$controller, strlen(SITE)) . '-' . MVC::$function; // 主域名省略 SITE 目录
        } else {
            $route = MVC::$controller . '-' . MVC::$function;
        }
        $params = $params ? array_merge(MVC::$params, $params) : MVC::$params;
    } elseif ($route === '~METHOD' || $route === 'SELF-METHOD') {
        if (IS_DEFAULT_SITE) {
            $route = substr(MVC::$controller, strlen(SITE)) . '-' . MVC::$function; // 主域名省略 SITE 目录
        } else {
            $route = MVC::$controller . '-' . MVC::$function;
        }
    }
    $url = ($isAbs ? HTTP_HOST : HOST) . '/' . trim($route, '/');
    if ($params) {
        if (is_array($params)) {
            if ($filterKeys) {
                // 过滤参数
                foreach ($filterKeys as $filterKey) {
                    if (key_exists($filterKey, $params)) {
                        unset($params[$filterKey]);
                    }
                }
            }
            // 组装参数
            array_walk($params, function (&$value, $key) {
                if (is_array($value)) {
                    $value = $key . '-[[' . implode(',', $value) . ']]';
                } else {
                    $value = $key . '-' . urlencode($value ?: '');
                }
            });
            $url .= '~' . implode('~', $params);
        } else {
            $url .= '~' . ltrim($params, '~');
        }
    }
    return $url;
}
/**
 * 创建一个对象
 * @param string $className 类名 action.admin.demo
 * @param ... $params 可变参数
 * @return object
 */
function C(string $className, ...$params)
{
    return MVC::createObject($className, $params);
}

/**
 * 创建一个单列对象
 * @param string $guid 前面会加上类名 $className . $guid, 如果 $guid 传空, 那么全局就只能创建一个对象
 * @param string $className 类名
 * @param ...$params 类的构造参数
 * @return object
 */
function D(string $guid, string $className, ...$params): object
{
    $guid = $className . '&' . $guid;
    if (isset(MVC::$objectList[$guid])) {
        return MVC::$objectList[$guid];
    } else {
        $obj = MVC::createObject($className, $params);
        MVC::$objectList[$guid] = $obj;
        return $obj;
    }
}

/** 对一批变量判断是否为 null, '', [] */
function nullArgs(mixed ...$args)
{
    foreach ($args as $key => $value) {
        if (is_null($value) || $value == '' || $value == []) {
            return true;
        }
    }
    return false;
}

/**
 * 抛出一个错误
 * @param string $message 错误信息
 * @param array $exts 扩展信息，例  ['sql'=>$sql]
 */
function error(string $message, array $exts = null, array $filters = [], array $customCaller = [])
{
    if ($message == '') {
        return;
    }
    ErrorClass::errorMake(errorInfo($message, $exts, $filters, $customCaller));
    if (!IS_CLI || !in_array('-d', $_SERVER['argv'])) {
        // 非守护进程运行时直接退出
        exit();
    } else {
        echo $message;
    }
}


/**
 * 构造一个错误信息体
 * @param string $message 错误信息
 * @param array $exts 扩展信息，例  ['sql'=>$sql]
 * @param array $filters debug_backtrace() 中要过滤的文件或类方法, 参考 getTraceCaller()
 * @param array $customCaller 自定义调用者 ['file'=>string, 'line'=>int]
 * @param bool $isAddTrace 是否添加 debug_backtrace()
 */
function errorInfo(string $message, array $exts = null, array $filters = [], array $customCaller = [], bool $isAddTrace = true)
{
    if (empty($customCaller) || $isAddTrace) {
        $backtrace = defined(DEBUG_BACKTRACE) ? debug_backtrace(DEBUG_BACKTRACE) : debug_backtrace();
    }
    if (!empty($customCaller)) {
        $caller = $customCaller;
    } else {
        $filters = array_merge([__FILE__], $filters);
        $caller = getTraceCaller($backtrace, $filters);
    }
    $error = [
        'type' => E_ERROR,
        'file' => $caller['file'],
        'line' => $caller['line'],
        'message' => $message,
        'backtrace' => $isAddTrace ? $backtrace : [],
    ];

    if (!is_null($exts)) {
        $error = array_merge($error, $exts);
    }
    return $error;
}

/**
 * 取 GET 或 POST 或 php://input 的值
 * @param string $reqKey
 * @param mixed $default 默认值
 * @param array $rules 规则, 默认 null 进行值的 htmlspecialchars 转义
 * @return mixed
 * @see xmvc\test\UrlTest::req()
 */
function I(string $reqKey, mixed $default = null, array|string $rules = null): mixed
{
    if ($reqKey == '*') {
        return MVC::$req;
    }
    MVC::$req[$reqKey] = MVC::$req[$reqKey] ?? $default;
    if (is_null($rules)) {
        return isset(MVC::$req[$reqKey]) ?  (is_string(MVC::$req[$reqKey]) ? htmlspecialchars(MVC::$req[$reqKey]) : MVC::$req[$reqKey]) : null;
    } else {
        /** @var Valid $valid */
        $valid = D('I()', Valid::class);
        $isValided = $valid->check($reqKey, MVC::$req[$reqKey], $rules);
        if ($isValided) {
            return MVC::$req[$reqKey];
        } else {
            return $default; // 验证不通过
        }
    }
}

/**
 * I() 验证失败时, 直接抛出错误
 */
function IThrow()
{
    /** @var Valid $valid */
    $valid = D('I()', Valid::class);
    $valid->throw = true;
}

/**
 * 是否验证所有规则, 默认 false 只返回第一条检测到的错误
 */
function ICheckAll(bool $isAll = true)
{
    /** @var Valid $valid */
    $valid = D('I()', Valid::class);
    $valid->backAllError = $isAll;
}

/**
 * 提交参数的验证错误信息, 如果没有错误返回 null
 *
 * @return array|null
 */
function IError(): array|null
{
    /** @var Valid $valid */
    $valid = D('I()', Valid::class);
    return $valid->errorInfo();
}

/** 提交参数的是否有 GET */
function hasGet(): bool
{
    return !empty(MVC::$params);
}

/** 提交参数的是否有 POST */
function hasPost(): bool
{
    return !empty(MVC::$postKeys);
}

/** 提交参数的是否是 php://input */
function hasPhpinput(): bool
{
    return
        !empty(MVC::$phpinputKeys);
}

/** GET 提交的所有的键 */
function getKeys(): array
{
    return array_keys(MVC::$params);
}

/** POST 提交的所有的键 */
function postKeys(): array
{
    return MVC::$postKeys;
}

/** php://input 提交的所有的键 */
function phpinputKeys(): array
{
    return MVC::$phpinputKeys;
}

/**
 * 取配置文件中的具体的项的值
 * @param string $dns 路径
 * cfg('net>db>main') 取 MVC::$cfg['net']['db']['main']的值
 * @return mixed
 */
function cfg(string $dns)
{
    return getArrayFromStrLink($dns, MVC::$cfg);
}

/**
 * a.b.c 取 数组
 *
 * @param string $dns 例 net.redis.main
 * @param array|stdClass $array
 * @return mixed
 */
function getArrayFromStrLink(string $dns, array|stdClass $array)
{
    if ($dns == '>') {
        return $array;
    }
    $arr = explode('>', $dns);
    if (is_object($array)) {
        $array = get_object_vars($array);
    }
    foreach ($arr as $v) {
        if (isset($array[$v])) {
            $array = $array[$v];
            if (is_object($array)) {
                $array = get_object_vars($array);
            }
        } elseif ($v == '*') {
            return $array;
        } else {
            return null;
        }
    }
    return $array;
}

/**
 * a.b.c 写 数组
 *
 * @param string $dns 例 net.redis.main
 * @param mixed $value
 * @param array $array
 */
function setArrayFromStrLink(string|array $dns, mixed $value, array &$array)
{
    if (!$dns) {
        return;
    }
    if (is_string($dns)) {
        $dns =  explode('>', $dns);
    }
    $key = array_shift($dns);
    // 如果当前 key 存在且还有子 key 需要处理
    if (isset($array[$key]) && $dns) {
        if (is_array($array[$key])) {
            setArrayFromStrLink($dns, $value, $array[$key]);
        } else {
            error('数组 dns 寻址错误', null, [[__FILE__, __LINE__]]);
        }
    } else {
        $array[$key] = $value;
    }
}

/**
 * 加载配置文件
 *
 * @param string $filename  配置的相对目录和文件名,不用加.php
 * @return mixed 配置
 */
function loadConfig(string $filename): array
{
    $filename .= '.php';
    if (!IS_CLI && CONFIG_USE_APCU && apcu_exists(PROJECT . ':cfg.' . $filename)) {
        return apcu_fetch(PROJECT . ':cfg.' . $filename);
    } else {
        if (IS_DEVELOP) {
            $path = DIR_CONFIG . '/developer/' . $filename;
        } else {
            $path = DIR_CONFIG . '/' . $filename;
        }
        if (! is_file($path)) {
            $msg = '不存在的配置文件' . ' ' . $path;
            if (function_exists('error')) {
                error($msg, null, [[__FILE__,__LINE__]]);
            } else {
                exit($msg);
            }
        } else {
            $cfg = require($path);
            if (CONFIG_USE_APCU && !IS_CLI) {
                apcu_store(PROJECT . ':cfg.' . $filename, $cfg, 3153600000);
            }
            return $cfg;
        }
    }
}

/**
 * 发送事件给全局监听器
 */
function trigger(string $eventName, object $event): void
{
    EventManager::getInstance()->trigger($eventName, $event);
}

/**
 * 调用配置中的方法
 */
function callConfigFunc(array $cfg, array $args = []): mixed
{
    $class = $cfg['class'];
    $function = $cfg['function'];
    $params = $cfg['params'] ?? [];
    $returnType = $cfg['return']['type'] ?? 'string';
    $returnIndex = $cfg['return']['index'] ?? false;
    $valueType = $cfg['value']['type'] ?? 'string';
    $valueKey = $cfg['value']['key'] ?? '';
    if ($args && $params) {
        foreach ($args as $i => $value) {
            $findI = $i + 1;
            $idx = array_search("@{$findI}", $params);
            if ($idx !== false) {
                $params[$idx] = $value;
            }
        }
    }
    if (isset($cfg['static']) && $cfg['static']) {
        $backValue = call_user_func_array([$class, $function], $params);
    } else {
        $backValue = call_user_func_array([new $class(), $function], $params);
    }
    if ($returnIndex !== false && is_array($backValue)) {
        if (isset($backValue[$returnIndex])) {
            $backValue = $backValue[$returnIndex];
        } else {
            return false;
        }
    }
    switch ($returnType) {
        case 'object':
            switch ($valueType) {
                case 'object':
                    $result = $backValue;
                    break;
                case 'array':
                    $result = get_object_vars($backValue);
                    break;
                case 'int':
                case 'string':
                default:
                    $result = $backValue->{$valueKey};
            }
            break;
        case 'array':
            switch ($valueType) {
                case 'object':
                    $result = (object)$backValue;
                    break;
                case 'array':
                    $result = $backValue;
                    break;
                case 'int':
                case 'string':
                default:
                    $result = $backValue[$valueKey];
            }
            break;
        default:
            $result = $backValue;
    }
    return $result;
}


/**
 * 使用指定变量格式化字符串
 *
 * qgprintf("sum({:1}) sum_{:2}",$var1,$var2);
 * qgprintf("sum({:a}) sum_{:b}",$arr); //$arr['a'=>1,'b'=>2]
 * qgprintf("sum({:a}) sum_{:b} in({:sets})",$arr); //$arr['a'=>1,'b'=>2,sets=[1,2,3,4]]
 * qgprintf("{:1} sum({:a}) sum_{:b}",HOST $arr); //HOST='http://xxx',$arr['a'=>1,'b'=>2]
 * @param mixed $var
 * @return string
 */
function qgprintf($rulestr, ...$args): string
{
    $count = count($args);
    for ($i = $count - 1; $i > -1; $i--) {
        $arg = $args[$i];
        if (is_array($arg)) {
            foreach ($arg as $k => $v) {
                $type = gettype($v);
                if ($type != 'string') {
                    if (is_object($v)) {
                        $v = get_object_vars($v);
                    }
                    if (is_array($v)) {
                        foreach ($v as $k2 => $v2) {
                            $type = gettype($v2);
                            if ($type == 'array') {
                                $v2 = 'Array';
                            } elseif ($type == 'object') {
                                $v2 = get_class($v2);
                            }
                            $v[$k2] = $v2;
                        }
                        $v = implode(',', $v);
                    } elseif (is_null($v)) {
                        $v = 'NULL';
                    }
                }
                $rulestr = str_replace('{:' . $k . '}', $v, $rulestr);
            }
        } else {
            $arg = is_null($arg) ? 'NULL' : $arg;
            $rulestr = str_replace('{:' . ($i + 1) . '}', $arg, $rulestr);
        }
    }
    return $rulestr;
}

/**
 * 将任意值转成字符串
 * @param mixed $mixedValue
 * @return string
 */
function toString($mixedValue): string
{
    if (is_string($mixedValue) || is_numeric($mixedValue)) {
        return $mixedValue;
    }
    $result = '';
    $type = gettype($mixedValue);
    switch ($type) {
        case 'NULL':
            $result = '';
            break;
        case 'array':
            $result = json_encode($mixedValue);
            break;
        case 'object':
            $result = get_class($mixedValue);
            break;
        default:
            dd($type);
    }
    return $result;
}

/**
* 生成唯一ID
* @return string 32位
*/
function guid(): string
{
    return md5(uniqid((string)rand(0, PHP_INT_MAX), true) . serialize($_SERVER));
}

/**
 * 取时间的毫秒数, 默认0 为当前时间
 * @return int
 */
function msec($microtime = 0): int
{
    if (!$microtime) {
        $microtime = microtime();
    }
    // 获取当前时间戳和微秒数
    list($usec, $sec) = explode(" ", $microtime);
    return round(((float)$usec + (float)$sec) * 1000);
}

/**
 * 将时间戳转成可阅读
 * @param int $sec 秒
 * @return array
 */
function formatTimeArr(int $sec): array
{
    $t = [];
    $t['d'] = floor($sec / (3600 * 24));
    $t['h'] = floor(($sec % (3600 * 24)) / 3600);
    $t['i'] = floor(($sec % 3600) / 60);
    $t['s'] = $sec % 60;
    return $t;
}

/**
 * 将时间戳转成可阅读
 * @param int $sec 秒
 * @return string
 */
function formatTimeString(array $t, array $lgs = null): string
{
    if (is_null($lgs)) {
        $lgs = ['d' => ' 天 ', 'h' => ' 小时 ', 'i' => ' 分钟 ', 's' => ' 秒 ', 'm' => ' 毫秒'];
    }
    foreach ($t as $k => $v) {
        if ($v == 0) {
            unset($t[$k]);
        } else {
            $t[$k] = $v . $lgs[$k];
        }
    }
    return implode('', $t);
}

/**
 * 将时间戳转成可阅读
 * @param int $sec 秒
 * @return string
 */
function formatTime(int $sec, array $lgs = null): string
{
    return formatTimeString(formatTimeArr($sec), $lgs);
}


/**
 * 将毫秒时间戳转成可阅读
 * @param int $msec 毫秒
 * @param int $precision 返回结果的小数位数
 * @return string
 */
function formatMsecTime(int $msec, array $lgs = null): string
{
    $t = formatTimeArr(floor($msec / 1000), $lgs);
    $t['m'] = $msec % 1000;
    return formatTimeString($t, $lgs);
}

/**
 * 将字节数转成 TB,GB,MB,KB 便宜阅读
 * @param int $bytes 字节数
 * @param int $precision 返回结果的小数位数
 * @return string
 */
function formatBytes(int $bytes, int $precision = 2): string
{
    $units = array('TB', 'GB', 'MB', 'KB','B');
    $unitIndex = 4;
    while ($bytes >= 1024 && $unitIndex > 0) {
        $bytes /= 1024;
        $unitIndex--;
    }
    return round($bytes, $precision) . ' ' . $units[$unitIndex];
}

/**
 * 返回安全字符串
 *
 * @return string|array
 */
function escape(string|array|\stdClass|null $variables): string|array
{
    // 避免不必要的双重编码 以及 忽略 htmlspecialchars 的 $double_encode = false 产生的错误警告
    $oldErrorReporting = error_reporting();
    error_reporting(E_ALL & ~E_WARNING);
    $oldErrorHandler = set_error_handler(function () {
        ;
    });
    $escapedStr = escapeRecursive($variables);
    set_error_handler($oldErrorHandler);
    error_reporting($oldErrorReporting);
    return $escapedStr;
}

function escapeRecursive(string|array|\stdClass|null $variables): string|array
{
    if (is_object($variables)) {
        $variables = get_object_vars($variables);
    }
    if (is_array($variables)) {
        return array_map('escapeRecursive', $variables); // 递归
    }

    if (is_numeric($variables)) {
        return (string)$variables;
    }

    if (is_null($variables)) {
        return '';
    }

    if (is_bool($variables)) {
        return $variables ? '1' : '0';
    }

    if (strtotime($variables) !== false) {
        return $variables;
    }

    // $double_encode = false 避免不必要的双重编码, 但它会对非法字符产生警告, 在上级中关闭 PHP E_WARNING
    return htmlspecialchars($variables, ENT_QUOTES, 'UTF-8', false) ?: '';
}

/** md5的63进制值 */
function md6(string $string): string
{
    return Encoder::md6($string);
}

/** 取csrf token */
function getCsrfToken(): string
{
    $csrfTokens = Session::get('csrfTokens', null);
    $key = md5(url('~') . $_SERVER['HTTP_USER_AGENT'] . ($_SERVER['HTTP_REFERER'] ?? '') . ($_SERVER['HTTP_COOKIE'] ?? ''));
    $token = $key . bin2hex(random_bytes(16));
    $csrfTokens[$key] = [$token, time()];
    Session::set('csrfTokens', $csrfTokens);
    return $token;
}

/**
 * 验证 CsrfToken
 *
 * @param string $token
 * @return boolean
 */
function validCsrfToken(string $token): bool
{
    return true;
    if (strlen($token) != 64) {
        return false;
    }
    $key = substr($token, 0, 32);
    $csrfTokens = Session::get('csrfTokens', null);
    if (empty($csrfTokens) || !isset($csrfTokens[$key])) {
        return false;
    }
    $isSame = hash_equals($token, $csrfTokens[$key][0]);
    if ((time() - $csrfTokens[$key][1]) > 86400) {
        unset($csrfTokens[$key]); // 超过 1 天删除
        Session::set('csrfTokens', $csrfTokens);
    }
    return $isSame;
}


/** 取 用户 token */
function getUserToken(): string
{
    if (Session::get(SITE_USER_LOGIN_KEY)) {
        $info = Session::get(SITE_USER_INFO_KEY);
        if (!isset($info['token']) || empty($info['token'])) {
            error(lg('未找到') . ' user token', [[__FILE__, __LINE__]]);
        } else {
            return $info['token'];
        }
    }
    return '';
}

/** 判断是否是一个 json 字符串 */
function isJson(string $string): bool
{
    json_decode($string);
    return (json_last_error() == JSON_ERROR_NONE);
}

/**
 * 重构返回结果数组
 *
 * @param array $data 源,一维或都二维数组
 * @param array|string $keyField 指定哪个/组字段做为数组的键
 * @param array|string|null $valueField 指定哪个/组字段做为数组的值, null 表示全部字段
 * @param Closure $func 回调数据处理,返回 false 则忽略, func($key,$value,$allkey,$row), 级联键 $allkey 为 一级.二级.三级
 * @return self
 * @example map('id')
 * @example map('id','name')
 * @example map('id','@','name') 不改变数组下标
 * @example map('id',['name','email'])
 * @example map(['id1','id2'],['name','email']) 多个字段做键时用 点 连接 id1.id2
 * @example map('username',['email','depa','items'=>['ck1','ck2']]) 支持嵌套
 * @example map('username',true) 第二个参数 true，表示有相同键的使用追加模式将变成三维数组 ['user001'=>[  ['a','b'],['c','d'] ], 'user002'=>...]
 */
function map(array $data, $keyField, $valueField = null, \Closure $func = null): array
{
    if (!is_array(current($data))) {
        $data = [$data];
    }
    $result = [];
    foreach ($data as $i => $row) {
        $key = '';
        if ($keyField == '@') {
            $key = $i;
        } elseif (is_string($keyField)) {
            $key = is_object($row) ? $row->{$keyField} : $row[$keyField];
        } else {
            foreach ($keyField as $field) {
                if ($key != '') {
                    $key .= '.';
                }
                $key .= is_object($row) ? $row->{$field} : $row[$field];
            }
        }
        if (is_null($valueField)) {
            if ($func && false === $func($key, $row, $key, $row)) {
                continue;
            }
            $result[$key] = $row;
        } elseif ($valueField === true) {
            if ($func && false === $func($key, $row, $key, $row)) {
                continue;
            }
            $result[$key][] = $row;
        } elseif (is_string($valueField)) {
            if (is_object($row)) {
                $aTemp = (array)$row;
                $value = $aTemp[$valueField];
            } else {
                $value = $row[$valueField];
            }
            if ($func && false === $func($valueField, $value, $key, $row)) {
                continue;
            }
            $result[$key] = $value;
        } else {
            $result[$key] = _mapRecursionRow($key, $valueField, $row, $func);
        }
    }
    return $result;
}

function _mapRecursionRow($parentKey, $fields, $row, \Closure $func = null)
{
    $data = [];
    foreach ($fields as $k => $field) {
        if (is_string($field)) {
            if (is_object($row)) {
                $aTemp = (array)$row;
                $value = $aTemp[$field];
            } else {
                $value = $row[$field];
            }
            if ($func && false === $func($field, $value, $parentKey . '.' . $field, $k, $row)) {
                continue;
            }
            if (is_numeric($k)) {
                $data[$field] = $value;
            } else {
                $data[$k] = $value;
            }
        } else {
            $data[$k] = _mapRecursionRow($parentKey . '.' . $k, $field, $row, $func);
        }
    }
    return $data;
}

/*************************************************************************************************************/
/** 目录与文件 */
/*************************************************************************************************************/

/**
 * 格式化目录
 *
 * @param string $dir
 * @return string
 */
function fdir(string $dir): string
{
    if (IS_WIN) {
        $dir = str_replace('\\', '/', $dir);
    }
    $dir = rtrim($dir, '/');
    return $dir;
}

/**
 * 取相对目录
 *
 * @param string|null $dir 目录或文件名
 * @return string
 */
function relDir($dir): string
{
    if (is_null($dir)) {
        return '';
    }
    $result = fdir($dir);
    $result = str_replace(DIR_MVC . '/', '[MVC]/', $result);
    $result = str_replace(ROOT . '/', '', $result);
    return rtrim($result, '/');
}

/**
 * 取绝对 对目录
 *
 * @param string $dir 目录或文件名
 * @return string
 */
function absDir(string $dir): string
{
    $relDir = relDir($dir);
    $fix = substr($relDir, 0, 5);
    if ($fix == '[MVC]') {
        $result = DIR_MVC . '/' . substr($relDir, 6);
    } else {
        $result = ROOT . '/' . $relDir;
    }
    return $result;
}

/**
 * 文件扩展名
 *
 * @return string
 */
function extname($filename)
{
    return pathinfo($filename, PATHINFO_EXTENSION);
}

/**
 * 文件名不带扩展
 *
 * @return string
 */
function filenameNotExt($filename)
{
    return pathinfo($filename, PATHINFO_FILENAME);
}

/**
 * 递归创建目录
 * @param string $dir 要创建的目录
 * @param int $mode 权限，默认 0777
 * @return  bool
 */
function mkdirs($dir, $mode = 0777)
{
    if (is_null($dir) || $dir === "") {
        return false;
    }
    if (is_dir($dir) || $dir === "/") {
        return true;
    }
    if (mkdirs(dirname($dir), $mode)) {
        return mkdir($dir, $mode);
    }
    return false;
}

/**
 * 以锁方式写文件，带重试机制
 * @param string $filename
 * @param string $data
 * @param int $retry 最大重试次数，默认 10 次
 * @param int $wait 每次重试的等待时间（秒），默认1 秒
 * @return bool
 */
function file_put_contents_lock(string $filename, string $data, int $retry = 10, int $wait = 1): bool
{
    $fp = fopen($filename, 'c');  // 'c' 模式在文件不存在时创建文件
    if ($fp === false) {
        return false;
    }

    $result = false;
    $attempts = 0;

    while ($attempts < $retry) {
        if (flock($fp, LOCK_EX | LOCK_NB)) {  // 尝试获取独占锁（非阻塞模式）
            ftruncate($fp, 0);  // 清空文件
            if (fwrite($fp, $data) !== false) {
                $result = true;
            }
            flock($fp, LOCK_UN);  // 释放锁
            break;  // 成功获取锁并写入数据后跳出循环
        } else {
            $attempts++;
            sleep($wait);  // 等待一段时间后重试
        }
    }
    if (!$result) {
        error(lg('写操作失败') . ':' . $filename, null, [[__FILE__,__LINE__]]);
    }
    fclose($fp);
    return $result;
}

/**
 * 将数组内容写入到一个php文件中
 * @param string $filename
 * @param array $data
 * @param int $mode 权限，默认 0777
 * @return bool
 */
function arrayToFile(string $filename, array $data, $mode = 0777): bool
{
    $code = var_export($data, true);
    $code = "<?php\r\n return " . $code . ";\r\n?>";
    $back = file_put_contents_lock($filename, $code);
    if ($back !== false) {
        chmod($filename, $mode);
    }
    return $back !== false;
}

/**
 * 递归遍历目录与文件
 * @param string $dir 要遍历的目录
 * @param \Closure $func 逻辑代码 function (string $path, string $basename,string $type, int $level) $type = 'dir'|'file', 返回 false 则中断遍历
 * @param bool $isEachSubDir 是否要遍历子目录
 * @param int $maxLevel 相对当前目录, 最大遍历子目录层级深度, 默认 0 不限制
 * @param int $level 相对当前目录, 当前子目录层级深度
 */
function forEachDir(string $dir, \Closure $func, bool $isEachSubDir = true, int $maxLevel = 0, int $level = 1)
{
    if (is_dir($dir)) {
        if ($handle = opendir($dir)) {
            while (false !== ($file = readdir($handle))) {
                if ($file != "." && $file != "..") {
                    $path = $dir . "/" . $file;
                    if (is_dir($path)) {
                        if ($isEachSubDir && ($maxLevel == 0 || $level <= $maxLevel)) {
                            if (forEachDir($path, $func, $isEachSubDir, $maxLevel, $level + 1) === false) {
                                return false;
                            }
                        }
                        if ($func($path, $file, 'dir', $level) === false) {
                            return false;
                        }
                    } else {
                        if ($func($path, $file, 'file', $level) === false) {
                            return false;
                        }
                    }
                }
            }
            closedir($handle);
            return true;
        }
    }
}

/**
 * 递归删除目录
 * @param string $dir 要删除的目录
 * @param bool $deleteSelf 是否删除自已, 如果为 false 则只删除目录下的子目录及文件
 * @return  bool
 */
function rmdirs($dir, $deleteSelf = false): bool
{
    if (!is_dir($dir)) {
        return true;
    }
    $dh = opendir($dir);
    while ($file = readdir($dh)) {
        if ($file != "." && $file != "..") {
            $fullpath = $dir . "/" . $file;
            if (!is_dir($fullpath)) {
                unlink($fullpath);
            } else {
                rmdirs($fullpath, true);
            }
        }
    }
    closedir($dh);
    return $deleteSelf ? rmdir($dir) : true;
}

/**
 * 通过目录名取站点名称
 *
 * @return string
 */
function getSiteNameByDir(string $dir): string
{
    if (strpos($dir, DIR_ACTION) !== false) {
        $tmp = substr($dir, strlen(DIR_ACTION));
    } elseif (strpos($dir, DIR_VIEW) !== false) {
        $tmp = substr($dir, strlen(DIR_VIEW));
    } else {
        return '';
    }
    $pos = strpos($tmp, '/');
    return $pos ? substr($tmp, 0, $pos) : '';
}


/**
 * 将配置文件中的目录内容转成真实的目录
 */
function cfgDirToTrueDir(string $cfgStr): array
{
    $dirs = explode("\n", $cfgStr);
    foreach ($dirs as $i => $dir) {
        $dir = trim($dir);
        $dir = str_replace('//', '/', $dir);
        $arr = explode('/', $dir);
        foreach ($arr as $k => $v) {
            if (substr($v, 0, 1) == '$') {
                $arr[$k] = constant(substr($v, 1));
            }
        }
        $dir = implode('/', $arr);
        $dirs[$i] = $dir;
    }
    return $dirs;
}

/*************************************************************************************************************/
/** 输出相关,视图,JSON */
/*************************************************************************************************************/
/**
 * 输出视图
 * - view(__METHOD__) //明确指定当前类当前方法命名的视图文件, 可稍稍节省一点点性能
 * - view('admin/user::login') //action类与方法
 * - view('admin/public/header') //视图路径
 * - view('admin/public/header',['a'=>'1']) //视图变量与值
 * - view('admin/public/header',['a'=>'1'],['a'=>'b']) //第二视图变量与值，相同key将覆盖第一个
 * - 如果 $variables 是一个数字下标的二维数组， 则数组会给变量 $data
 *
 * @param array|\stdClass $variables 要转成变量的数组或是 stdClass
 * @param array $variables2 要转成变量的数组2
 * @param bool  $require 是否加载, 如果为 false 就只是生成模板缓存
 */
function view(string $filename, array|\stdClass $variables = null, array $variables2 = null, bool $require = true)
{
    if (MVC::$req) {
        extract(['req' => escape(MVC::$req)]);
    }
    if ($variables) {
        $variables = escape($variables);
        reset($variables);
        if (is_numeric(key($variables))) {
            $variables = ['data' => $variables];
        }
        extract($variables);
    }
    if ($variables2) {
        extract($variables2);
    }
    if (substr($filename, -8) != '.tpl.php') {
        $filename .= '.tpl.php';
    }
    if (!is_file($filename)) {
        if (IS_DEVELOP) {
            error(relDir($filename) . ' 模板文件不存在', [
                'cmd' => [
                    'title' => '创建此模板',
                    'cmdFunction' => 'newTpl',
                    'newClassFile' => $filename,
                    'newClassName' => '',
                    'newClassFunction' => ''
                ],
            ]);
        } else {
            error(relDir($filename) . ' 模板文件不存在');
        }
    } else {
        $cacheFilename = Viewer::getViewCacheFilename($filename);
        $viewName = Viewer::getViewName($filename);
        if (! Viewer::isViewCacheValid($viewName, $cacheFilename)) {
            (new Viewer())->load($filename, true, $viewName, isset($loop) && $loop); // {{x.table loop=true}}
        }
        if ($require) {
            $tmpArr = explode('.', $viewName);
            define('VIEWGUID', implode('_', $tmpArr));
            define('METHOD', end($tmpArr));
            array_shift($tmpArr);
            define('VIEWGUID_NOSITE', implode('_', $tmpArr));
            array_pop($tmpArr);
            define('CLASSNAME', implode('/', $tmpArr));
            require($cacheFilename);
            //ob_start();
            //require($cacheFilename);
            //$html = ob_get_contents();
            //ob_end_clean();
        }
    }
}

/*************************************************************************************************************/
/**  */
/*************************************************************************************************************/

/**
 * 返回一个action值对象
 *
 * @param mixed $data
 * @param mixed ...$dataArgs 多值返回
 * @return ReturnClass
 */
function R(mixed $data = null, mixed ...$dataArgs): ReturnClass
{
    if ($dataArgs) {
        $result = [];
        $result[0] = $data;
        foreach ($dataArgs as $value) {
            $result[] = $value;
        }
    } else {
        $result = $data;
    }
    return (new ReturnClass())->data($result);
}

function dd(...$args)
{
    MVC::echo(Dump::dumpResult(true, ...$args));
    exit();
}

function ddRN(...$args)
{
    MVC::echo(Dump::dumpResult(false, ...$args));
    exit();
}

function ddSql($sql)
{
    dumpSql($sql);
    exit();
}

function dump(...$args)
{
    MVC::echo(Dump::dumpResult(true, ...$args));
}

function dumpRN(...$args)
{
    MVC::echo(Dump::dumpResult(false, ...$args));
}

function dumpSql($sql)
{
    echo \xmvc\sqlFormatter::format($sql);
}

function ddPrint(...$args)
{
    MVC::echo(Dump::printResult(true, ...$args));
    exit();
}

function ddPrintRN(...$args)
{
    MVC::echo(Dump::printResult(false, ...$args));
    exit();
}

function dumpPrint(...$args)
{
    MVC::echo(Dump::printResult(true, ...$args));
}

function dumpPrintRN(...$args)
{
    MVC::echo(Dump::printResult(false, ...$args));
}

function ddTable($rows)
{
    dumpTable($rows);
    exit();
}
function dumpTable($rows, $styles = [])
{
    if (!IS_CLI) {
        MVC::echo(dumpTableResult($rows, $styles));
    } else {
        print_r($rows);
    }
}
function dumpTableResult($rows, $styles = [])
{
    if (empty($rows)) {
        return '';
    }
    $html = '<table class="mvc-blue-table"  cellspacing="0" cellpadding="0"><thead><tr>';
    $firstRow = reset($rows);
    foreach ($firstRow as $k => $v) {
        $style = !isset($styles[$k]) ? '' : $styles[$k];
        if ($style) {
            $html .= '<th style="' . $style . '">' . $k . '</th>';
        } else {
            $html .= '<th>' . $k . '</th>';
        }
    }
    $html .= '</tr></thead><tbody>';
    foreach ($rows as $i => $row) {
        $html .= '<tr>';
        foreach ($row as $k => $v) {
            $v = toString($v);
            $title = strtr($v, ['"' => "'", '<' => '《', '>' => '》']);
            $html .= '<td title="' . $title . '">' . $v . '</td>';
        }
        $html .= '</tr>';
    }
    $html .= '</tbody></table>';
    return $html;
}

/**
 * 转义单引号，但已转义过的不重复转义
 *
 * @param string $str
 */
function addslashesOnce(string $str): string
{
    $result = '';
    $length = strlen($str);
    $escaped = false;
    for ($i = 0; $i < $length; $i++) {
        $char = $str[$i];
        if ($char === '\\') {
            if ($i + 1 < $length && $str[$i + 1] === '\'') {
                $result .= '\\\'';
                $i++;
                $escaped = false;
            } else {
                $result .= '\\';
            }
        } elseif ($char === '\'') {
            if (!$escaped) {
                $result .= '\\\'';
            } else {
                $result .= '\'';
                $escaped = false;
            }
        } else {
            $result .= $char;
            $escaped = false;
        }
    }
    return $result;
}

/**
 * 匿名函数转字符串
 * @param object $closureObject
 * @return string 匿名函数代码
 */
function closureToString($closureObject)
{
    $reflection = new \ReflectionFunction($closureObject);
    $file = new \SplFileObject($reflection->getFileName());
    $file->seek($reflection->getStartLine() - 1);
    $code = '';
    while ($file->key() < $reflection->getEndLine()) {
        $code .= $file->current();
        $file->next();
    }
    $begin = strpos($code, 'function');
    $end = strrpos($code, '}');
    $code = substr($code, $begin, $end - $begin + 1);
    return $code;
}

/**
 * 取跟踪记录中的真实调用者
 *
 * @param array $traceList
 * @param array $filters 二维数组, 下面的例子是二维里的一维值
 * - ['', 'onShutdownDebug'], //空文件来自php自己触发
 * - ['/index.php',  'execute'], //[文件名(含有.点号),方法名]
 * - ['dump',  'dumpResult'], //[类名,方法名]
 * - ['/MVC.php', 388],  //文件, 行号(整型数字)
 * - 'ErrorClass.php',  //整个文件过滤(含有.点号)
 * - 'dump',  //整类过滤
 * @return array
 */
function getTraceCaller(array &$traceList, array $filters = []): array|null
{
    $caller = null;
    if (empty($filters)) {
        $caller  = array_shift($traceList);
    } else {
        $newTraceList = [];
        foreach ($traceList as $k => $trace) {
            $isFilter = false;
            if (!isset($trace['line']) || $trace['line'] == '') {
                $isFilter = true; //定位不到文件与文件行的
            }
            $trace = array_merge(['file' => '', 'line' => '', 'class' => '', 'type' => '',
                                  'function' => '', 'object' => '', 'args' => ''], $trace);
            if (IS_WIN) {
                $trace['file'] = str_replace('\\', '/', $trace['file']);
            }
            //过滤哪些类和方法
            if (!$isFilter) {
                foreach ($filters as $filter) {
                    if (is_string($filter)) {
                        $filter = [$filter];
                    }
                    if (isset($filter[1])) {
                        if (
                            (strpos($filter[0], '.') > 0 || $filter[0] == '')
                            && strpos($trace['file'], $filter[0]) !== false
                            && $trace['function'] == $filter[1]
                        ) {
                            $isFilter = true; //文件中的方法, 或者空文件中的系统触发的方法
                            break;
                        } elseif (is_int($filter[1]) && $trace['file'] == $filter[0] && $trace['line'] == $filter[1]) {
                            $isFilter = true; //文件 + 行号
                            break;
                        } elseif ($trace['class'] == $filter[0] && $trace['function'] == $filter[1]) {
                            $isFilter = true; //类名 + 方法
                            break;
                        }
                    } elseif (strpos($filter[0], '.') > 0 && (strpos($trace['file'], $filter[0]) !== false)) {
                        $isFilter = true; //纯文件
                        break;
                    } elseif ($trace['class'] == $filter[0]) {
                        $isFilter = true; //纯类名
                        break;
                    }
                }
            }
            if ($isFilter) {
                continue;
            } else {
                if (is_null($caller)) {
                    $caller = $trace;
                }
                $newTraceList[] = $trace;
            }
        }
    }
    $traceList = $newTraceList;
    return $caller;
}
