<?php

namespace xmvc;

use xmvc\Cache;
use MatthiasMullie\Minify;
use phpDocumentor\Reflection\PseudoTypes\LowercaseString;
use phpDocumentor\Reflection\Types\Object_;

/**
 * 视图类
 *
<tpl headfooter="../public"> 目录
<tpl headfooter="../"> 等于 ../public
<tpl header="../public/header"> 到具体文件
<tpl footer="../public/footer"> 到具体文件
<tpl vendor="dx,md5,echarts"> 加载 config/view.php 配置库
<tpl json="req, data, header, footer, ext"> 将返回的变量的 json 值插入到 javascript
<tpl json="all"> 将返回的变量的 json 值插入到 javascript
<tpl  body='class="dx-viewport"'> 修改body标签为 <body class="dx-viewport">
<tpl>
    <include ../../public/header />
    <include
        x.table loop=true id="table1" arr=$data values=$data class='w100 blue-table'
        names=[
                "id"=>"ID",
                "name"=>"姓名",
                "age"=>"年龄",
                "sex"=>"性别",
            ]
    />
    <include ../../public/footer />
</tpl>

<mount src="vendor/jquery@3.7.1/jquery.min.js" /> ./static/vendor/jquery@3.7.1/jquery.min.js
<mount src="x/test.js" /> ./static/x/js/test.js 默认放在 </head> 前
<mountsrc="demo/test.css" bind="body" />  ./static/js/demo/test.css 放在 </body>前

<style to="../">

</style>

<script>

</script>
 * @version 2.1.0
 */

// @codingStandardsIgnoreStart
class View
// @codingStandardsIgnoreEnd
{
    public Viewer $viewer;
    public bool $isMainView = false;
    public bool $isLoop = false;
    public string $name = '';
    public string $shortName = '';
    public string $filename = '';
    public string $mainNodeName = ''; //落在哪一个主节点上
    public array $tagViews = []; //子模板名称数组
    public int $time; //视图生成时间
    public string $contents = '';
    public array $tags = []; // 标签集
    public array $blocks = []; //文件块
    public string $tpl = '';
    public string $bodyTag = '';
    public array $staticData = []; //style 和 js 的内容
    public array $cssFiles = []; //css 文件
    public array $jsFiles = []; //js 文件
    public array $includeFiles = []; //所有引入的文件
    public array $viewCfg = []; //视图配置文件中的相关配置
    public string $cacheFilename = '';
    public function __construct(string $filename, string $name = '')
    {
        $this->name = $name ?: Viewer::getViewName($filename);
        $this->shortName = substr($name, strrpos($name, '.') + 1);
        $this->filename = $filename;
        $this->mainNodeName = $this->getMainNodeName($this->name);
        $this->time = time();
    }

    /**
     * 合并生成视图缓存文件
     */
    public function createCache()
    {
        $this->cacheFilename = Viewer::getViewCacheFilename($this->filename);
        if ($this->cacheFilename == $this->filename) {
            error('缓存文件路径 ' . $this->cacheFilename . ' 不能与  ' . $this->filename . '模板文件路径相同');
        }

        // 合并子模板tpl内容
        $this->mergeTagViewTpl();

        if (!$this->isLoop) {
            // 合并style,javascript
            $this->mergeTagViewScript();

            // 插入脚本的 html 标签
            $this->insertScriptHtmlTag();
        }

        // 修改 <body>
        if ($this->bodyTag) {
            $this->tpl = str_replace('<body>', $this->bodyTag, $this->tpl);
        }

        // 写 HTML
        if (!DEBUG || MVC::$cfg['cache']['useHtmlMini']) {
            $this->tpl = $this->minifyHtmlAndPhp($this->tpl); //最小化 HTML
        } else {
            $this->tpl = preg_replace('/^ {4}/m', '', $this->tpl);
        }
        mkdirs(dirname($this->cacheFilename));
        file_put_contents_lock($this->cacheFilename, trim($this->tpl));

        //记录所有使用到的文件及缓存创建时间
        if ($this->includeFiles) {
            $cache = Cache::create(cfg('cache>group>viewtimes>type'), 'viewtimes', cfg('cache>group>viewtimes>configKey'));
            $cache->set($this->name, $this->includeFiles)->save();
        }
    }

    /**
     * 插入脚本的 html 标签
     */
    public function insertScriptHtmlTag()
    {
        if ($this->cssFiles || $this->jsFiles) {
            $linkHeadTag = '';
            $scriptHeadTag = '';
            $linkBodyTag = '';
            $scriptBodyTag = '';
            $headPos = mb_strpos($this->tpl, '</head>');
            $bodyPos = mb_strpos($this->tpl, '</body>');
            foreach ($this->cssFiles as $mainNodeName => $file) {
                $xName = ($headPos === false && $bodyPos === false) ? ' x-name="' . $mainNodeName . '"' : '';
                if (mb_substr($mainNodeName, 0, 6) == 'mount.') {
                    $ver = mb_substr($mainNodeName, 0, 12) != 'mount.vendor' ? '?ver=' . SVER : '';
                } else {
                    $ver = '?ver=' . $this->time;
                }
                if (file_exists(DIR_STATIC . $file)) {
                    if (strpos($mainNodeName, '=>body') !== false) {
                        $linkBodyTag .= '<link' . $xName . ' rel="stylesheet" href="./static' . $file . $ver . '" />' . "\n";
                    } else {
                        $linkHeadTag .= '<link' . $xName . ' rel="stylesheet" href="./static' . $file . $ver . '" />' . "\n";
                    }
                }
            }
            foreach ($this->jsFiles as $mainNodeName => $file) {
                $xName = ($headPos === false && $bodyPos === false) ? ' x-name="' . $mainNodeName . '"' : '';
                if (mb_substr($mainNodeName, 0, 6) == 'mount.') {
                    $ver = mb_substr($mainNodeName, 0, 12) != 'mount.vendor' ? '?ver=' . SVER : '';
                } else {
                    $ver = '?ver=' . $this->time;
                }
                $linkBodyTag .= "// {$file}\n";
                if (file_exists(DIR_STATIC . $file)) {
                    if (strpos($mainNodeName, '=>body') !== false) {
                        $scriptBodyTag .= '<script' . $xName . ' src="./static' . $file . $ver . '"></script>' . "\n";
                    } else {
                        $scriptHeadTag .= '<script' . $xName . ' src="./static' . $file . $ver . '"></script>' . "\n";
                    }
                }
            }
            if ($headPos === false && $bodyPos === false) {
                $this->tpl =  $linkHeadTag . $linkBodyTag . $scriptHeadTag . $scriptBodyTag . $this->tpl;
            } else {
                if ($bodyPos !== false) {
                    $this->tpl = mb_substr($this->tpl, 0, $bodyPos) . $linkBodyTag . $scriptBodyTag . mb_substr($this->tpl, $bodyPos);
                }
                if ($headPos !== false) {
                    $this->tpl = mb_substr($this->tpl, 0, $headPos) . $linkHeadTag . $scriptHeadTag . mb_substr($this->tpl, $headPos);
                }
            }
        }
    }

    /**
     * 处理所有的 style 和 jsavascript 脚本内容
     */
    public function mergeTagViewScript()
    {
        /*
        $staticData = [];
        foreach ($this->viewer->list as $view) {
            $this->includeFiles[$view->filename] = $this->time;
            foreach ($view->staticData as $mainNodeName => $nodes) {
                // 将层级最少的排在前面
                uksort($nodes, function ($a, $b) {
                    return substr_count($a, '.') <=> substr_count($b, '.');
                });

                foreach ($nodes as $nodeName => $item) {
                    $staticData[$mainNodeName][$nodeName] = $item;
                }
            }
            foreach ($view->cssFiles as $mainNodeName => $filename) {
                $this->cssFiles[$mainNodeName] = $filename;
            }
            foreach ($view->jsFiles as $mainNodeName => $filename) {
                $this->jsFiles[$mainNodeName] = $filename;
            }
        }
        // 将 x 排在最前面, 将层级最少的排在前面, 也就意味着，后面的层级可以对前面的一些内容做覆盖
        uksort($staticData, function ($a, $b) {
            if ($a === "x" && $b !== "x") {
                return -1;
            } elseif ($a !== "x" && $b === "x") {
                return 1;
            } else {
                return substr_count($a, '.') <=> substr_count($b, '.');
            }
        });
        */
        foreach ($this->viewer->list as $view) {
            $this->includeFiles[$view->filename] = $this->time;
            foreach ($view->cssFiles as $mainNodeName => $filename) {
                $this->cssFiles[$mainNodeName] = $filename;
            }
            foreach ($view->jsFiles as $mainNodeName => $filename) {
                $this->jsFiles[$mainNodeName] = $filename;
            }
        }
        $item = $this->staticData[$this->mainNodeName][$this->name];
        // style
        if ($item->style) {
            $style = [];
            foreach ($item->style as $i => $arr) {
                $nodeName = $arr['nodeName'];
                if (!isset($style[$nodeName])) {
                    $style[$nodeName] = "";
                }
                $style[$nodeName] .= "/**** STYLE {$this->name} Part " . ($i + 1) . " ****/\n"
                . $arr['style']
                . "\n// End Part " . ($i + 1) . " {$this->name} \n\n";
            }
            foreach ($style as $nodeName => $style) {
                $this->cssFiles[$nodeName] = '/auto/css/' . $nodeName . '.css';
                $filename = DIR_STATIC . $this->cssFiles[$nodeName];
                if ($nodeName === $this->name) {
                    file_put_contents($filename, $style);
                } else {
                    if (is_file($filename)) {
                        $contents = file_get_contents($filename);
                        $name = str_replace('.', '\.', $this->name);
                        $partern = '/\/\*\*\*\*\sSTYLE\s' . $name . '\s(.*)' . $name . '\s*\n*/s';
                        $contents = preg_replace($partern, $style, $contents);
                    } else {
                        $contents = $style;
                        mkdirs(dirname($filename));
                    }
                    if (!IS_DEVELOP) {
                        $contents = (new Minify\CSS($contents))->minify();
                    }
                    file_put_contents($filename, $contents);
                }
            }
        }
        // javascript
        if ($item->javascript) {
            $scripts = [];
            foreach ($item->javascript as $i => $arr) {
                $nodeName = $arr['nodeName'];
                if (!isset($scripts[$nodeName])) {
                    $scripts[$nodeName] = "";
                }
                $scripts[$nodeName] .= "/**** SCRIPT {$this->name} Part " . ($i + 1) . " ****/\n"
                . $arr['script']
                . "\n// End Part " . ($i + 1) . " {$this->name} \n\n";
            }
            foreach ($scripts as $nodeName => $script) {
                $this->jsFiles[$nodeName] = '/auto/js/' . $nodeName . '.js';
                $filename = DIR_STATIC . $this->jsFiles[$nodeName];
                if ($nodeName === $this->name) {
                    file_put_contents($filename, $script);
                } else {
                    if (is_file($filename)) {
                        $contents = file_get_contents($filename);
                        $name = str_replace('.', '\.', $this->name);
                        $partern = '/\/\*\*\*\*\sSCRIPT\s' . $name . '\s(.*)' . $name . '\s*\n*/s';
                        $contents = preg_replace($partern, $script, $contents);
                    } else {
                        $contents = $script;
                        mkdirs(dirname($filename));
                    }
                    if (!IS_DEVELOP) {
                        $contents = (new Minify\JS($contents))->minify();
                    }
                    file_put_contents($filename, $contents);
                }
            }
        }
    }

    /**
     * 取 <script goto="../xxx"> <style goto="xxx"> 的节点名
     */
    public function getGotoNodeName($goto)
    {
        if (empty($goto) || $goto == './') {
            return $this->name; // 如果为空或 ./ 则是自己一个单独的脚本
        }
        if (substr($goto, 0, 2) == './') {
            $goto = substr($goto, 2);
        }
        $count = substr_count($goto, '../');
        $tmpArr = explode('.', $this->name);
        if ($count == 0) {
            Viewer::error($this, '', 'goto="" 属性必须是 ./ 或 ../');
        }
        if ($count > count($tmpArr) - 1) {
            Viewer::error($this, '', 'goto="' . $goto . '" 属性的层级超过了 ' . $this->name . '路径');
        }
        for ($i = 0; $i < $count; $i++) {
            array_pop($tmpArr);
        }
        return implode('.', $tmpArr);
    }

    /**
     * 合并子模板内容
     */
    public function mergeTagViewTpl()
    {
        if ($this->tagViews) {
            foreach ($this->tagViews as $i => $tag) {
                $subView = $this->viewer->list[$tag->name];
                // 替换文件块
                if ($subView->isLoop === true || $tag->loop === true) {
                    $this->blocks[$tag->blockIndex] = sprintf(
                        '<?php view("%s",%s); ?>',
                        $tag->filename,
                        $this->attrArrayToText($tag->params)
                    );
                } else {
                    //非循环引用,直接引入模板内容
                    $subView->mergeTagViewTpl();
                    if ($tag->params) {
                        $subView->tpl = $this->replaceSubTplAttByStatic($subView->tpl, $tag->params);
                    }
                    $this->blocks[$tag->blockIndex] = trim($subView->tpl);
                }
            }
            // 合并块为模板内容
            $this->tpl = implode("", $this->blocks);
            if (preg_match('/<tpl(.*?)>(.*?)<\/tpl>/s', $this->tpl, $m)) {
                $this->tpl =  $m[2];
            }
        }
    }

    /**
     * 静态替换子模板 $attr 属性
     */
    public function replaceSubTplAttByStatic(string $tplContents, array $attrs)
    {
        if (empty($attrs)) {
            return $tplContents;
        }
        $patterns = [];
        foreach ($attrs as $key => $value) {
            /*
            $mark = '/<\?\=\s*' . $key . '\s*(\?\?[^\?]+)?\?\>/';
            $patterns[$mark] = function ($m) use ($value) {
                $value = trim($value);
                if (substr($value, 0, 1) == '$') {
                    $value = sprintf('<?= %s ?? \'\' ?>', $value);
                }
                return $value;
            };
            */
            $mark = '/\$\b' . $key . '\b/';
            $patterns[$mark] = function ($m) use ($value) {
                $value = trim($value);
                return $value;
            };
        }
        return preg_replace_callback_array($patterns, $tplContents);
    }

    /**
     * 根据tag的名称取主节点名称
     * - 如果 cfg.cache.staticOneFile = true 则style合成一个 index.css, js 合成 一个 index.js
     * - false
     * - 如果第一级是 x , 则直接使用 x
     * - 如果是二级或二级以下(含有文件名)，则使用第一个内容, admin.sys 返回 admin
     * - 如果是三级或以上(含有文件名)，则使用第一个.第二个内容 admin.public.header 返回 admin.public
     */
    public function getMainNodeName(string $tagName): string
    {
        if (MVC::$cfg['cache']['staticOneFile']) {
            return 'index';
        }
        $arr = explode('.', $tagName);
        $count = count($arr);
        if ($arr[0] == 'x') {
            return 'x';
        } elseif ($count > 2) {
            return $arr[0] . '.' . $arr[1];
        } else {
            return $arr[0];
        }
    }

    /**
     * 分析
     */
    public function parse()
    {
        $this->time = time();
        $this->contents = file_get_contents($this->filename); //文件内容
        $this->replaceBase();
        $this->script(); // 文件脚本分析
        $this->parseTags($this->tpl); //分析子模板
        $this->block(); // 文件分块
    }

    /**
     * 分析和构建视图的所有脚本, html,css,js
     *
     * @param stdClass $view
     */
    public function script()
    {
        $vendors = ""; // config/view.php配置文件中的
        if (!$this->tagViews) {
            if (preg_match('/<tpl(.*?)>(.*?)<\/tpl>/s', $this->contents, $m)) {
                $this->tpl =  $m[2];
                if ($this->isMainView) {
                    $scriptInsertPos = 0;
                    $script = 'const SERVER = <?= json_encode($server, JSON_UNESCAPED_UNICODE) ?>;' . "\n";
                    $script .= 'const CLASSNAME = "<?= CLASSNAME ?>";' . "\n";
                    $script .= 'const METHOD = "<?= METHOD ?>";' . "\n";
                    $script .= 'const VIEWGUID = "<?= VIEWGUID ?>";' . "\n";
                    $script .= 'const VIEWGUID_NOSITE = "<?= VIEWGUID_NOSITE ?>";' . "\n";
                    if (trim($m[1])) {
                        //<tpl headfooter="目录" header="文件" footer="文件"></tpl>
                        $attrs = $this->parseAttributes($m[1]);
                        $headerTpl = '';
                        $footerTpl = '';
                        if (isset($attrs['headfooter'])) {
                            $headfooter = rtrim(trim($attrs['headfooter'], '\'"'), '/');
                            if (!preg_match('/[a-zA-Z0-9_]/', $headfooter)) {
                                $headfooter .= 'public';
                            }
                            $headerTpl = $headfooter . '/header';
                            $footerTpl = $headfooter . '/footer';
                        }
                        if (isset($attrs['header'])) {
                            $headerTpl = trim($attrs['header'], '\'"');
                        }
                        if (isset($attrs['footer'])) {
                            $footerTpl = trim($attrs['footer'], '\'"');
                        }
                        if ($headerTpl) {
                            $headerTpl = "    <include {$headerTpl} />\n";
                            $this->tpl =  $headerTpl . ltrim($this->tpl);
                            $scriptInsertPos = strlen($headerTpl);
                        }
                        if ($footerTpl) {
                            $this->tpl = rtrim($this->tpl) . "\n    <include {$footerTpl} />";
                        }

                        // vendors
                        if (isset($attrs['vendor']) && $attrs['vendor']) {
                            $vendorScript = '';
                            $files = $this->getVendorFiles(explode(',', trim($attrs['vendor'], '\'"')), $vendorScript);
                            foreach ($files as $i => $file) {
                                $files[$i] = "<mount src=\"{$file}\" />";
                            }
                            $vendors = "\n" . implode("\n", $files);
                            if ($vendorScript) {
                                $script .= $vendorScript . "\n";
                            }
                        }

                        // back json
                        if (isset($attrs['json']) && $attrs['json']) {
                            $attrs['json'] = trim($attrs['json'], '\'"');
                            $tmpArr = explode(',', $attrs['json']);
                            foreach ($tmpArr as $i => $key) {
                                $tmpArr[$i] = strtoupper(trim($key));
                            }
                            $tmpList = [
                                'REQ' => 'const REQ = <?= isset($req) ? json_encode($req, JSON_UNESCAPED_UNICODE) : "null" ?>;' . "\n",
                                'TITLE' => 'const TITLE = <?= json_encode($variables2["header"]->title, JSON_UNESCAPED_UNICODE) ?>;' . "\n",
                                'DATA' => 'var DATA = <?= json_encode($variables, JSON_UNESCAPED_UNICODE) ?>;' . "\n",
                            ];
                            if (in_array('ALL', $tmpArr)) {
                                foreach ($tmpList as $key => $value) {
                                    $script .= $value;
                                }
                            } else {
                                foreach ($tmpArr as $key) {
                                    if (isset($tmpList[$key])) {
                                        $script .= $tmpList[$key];
                                    }
                                }
                            }
                        } // attrs

                        // body
                        if (isset($attrs['body']) && $attrs['body']) {
                            $this->bodyTag = trim($attrs['body']);
                            $qt = substr($this->bodyTag, 0, 1);
                            $this->bodyTag = trim($attrs['body'], $qt);
                            $this->bodyTag = '<body ' . $this->bodyTag . '>';
                        }
                    }
                    $script = "<script>\n{$script}</script>\n";
                    $this->tpl = substr($this->tpl, 0, $scriptInsertPos) . $script . substr($this->tpl, $scriptInsertPos);
                } // isMainView
            } //tap
        } // tagView

        // mount
        $pattern = '/<mount\s+\bsrc\b\s*=[\'\"]([^\'\"]+)\.(js|css)[\'\"]([^>]*)>/is';
        if (preg_match_all($pattern, $this->contents . $vendors, $m)) {
            foreach ((array)$m[1] as $i => $v) {
                $fileType = $m[2][$i];
                $arr = explode('/', $v);
                $jsPath = '';
                $cssPath = '';
                $mountKey = 'mount.' . $v . '.' . $fileType;
                if (isset($m[3][$i])) {
                    $attrs = $this->parseAttributes($m[3][$i]);
                    if (isset($attrs['bind'])) {
                        $mountKey .= '=>' . trim($attrs['bind'], '\'"');
                    }
                }
                if ($arr[0] == 'x') {
                    $jsPath = '/x/js/';
                    $cssPath = '/x/css/';
                    unset($arr[0]);
                } elseif ($arr[0] == 'vendor') {
                    $jsPath = '/vendor/';
                    $cssPath = '/vendor/';
                    unset($arr[0]);
                } elseif ($arr[0] == 'auto') {
                    $jsPath = '/auto/';
                    $cssPath = '/auto/';
                    unset($arr[0]);
                } else {
                    $jsPath = '/js/';
                    $cssPath = '/css/';
                }
                $filename = implode('/', $arr) . '.' . $fileType;
                if ($fileType == 'js') {
                    $this->jsFiles[$mountKey] = $jsPath . $filename;
                } elseif ($fileType == 'css') {
                    $this->cssFiles[$mountKey] = $cssPath . $filename;
                }
            }
        } // mount

        // style
        $style = [];
        $javascript = [];
        if (preg_match('/@loop/', $this->contents, $m)) {
            $this->isLoop = true;
        }
        $pattern = '/(?!<tpl(.*?)>[\s\S]*?)<style(.*?)>((?:(?!<\/style>).)*)<\/style>(?![\s\S]*?<\/tpl>)/s';
        if (preg_match_all($pattern, $this->contents, $m)) {
            foreach ((array)$m[2] as $i => $attrStr) {
                $attrs = $this->parseAttributes($attrStr);
                $to = isset($attrs['to']) ? trim($attrs['to'], '\'"') : '';
                $styleStr = trim($m[3][$i]);
                if ($styleStr) {
                    $nodeName = $this->getGotoNodeName($to);
                    $this->cssFiles[$nodeName] = '/auto/css/' . $nodeName . '.js';
                    $style[] = [
                        'nodeName' => $nodeName,
                        'to' => $to,
                        'style' => $styleStr,
                    ];
                }
            }
        }

        // javascript
        $pattern = '/(?!<tpl(.*?)>[\s\S]*?)<script(.*?)>((?:(?!<\/script>).)*)<\/script>(?![\s\S]*?<\/tpl>)/s';
        if (preg_match_all($pattern, $this->contents, $m)) {
            foreach ((array)$m[2] as $i => $attrStr) {
                $attrs = $this->parseAttributes($attrStr);
                $to = isset($attrs['to']) ? trim($attrs['to'], '\'"') : '';
                $scriptStr = trim($m[3][$i]);
                if ($scriptStr) {
                    $nodeName = $this->getGotoNodeName($to);
                    $this->jsFiles[$nodeName] = '/auto/js/' . $nodeName . '.js';
                    $javascript[] = [
                        'nodeName' => $nodeName,
                        'to' => $to,
                        'script' => $scriptStr,
                    ];
                }
            }
        }
        if ($style != '' || $javascript != '') {
            $item = new \stdClass();
            $item->style = $style;
            $item->javascript = $javascript;
            $this->staticData[$this->mainNodeName][$this->name] = $item;
        }
    }

    /** 将配置文件中的 vendor 转 mount 规则字符串
     * @param array $vendors config/view.php 的 vendor 键
     * @param string &$script 初始化 javascript 代码字符串
     */
    public function getVendorFiles(array $vendors, string &$vendorScript): array
    {
        $files = [];
        if ($vendors) {
            if (empty($this->viewCfg)) {
                $this->viewCfg = loadConfig('view');
            }
            foreach ($vendors as $vendor) {
                $vendor = trim($vendor);
                if (isset($this->viewCfg['vendor'][$vendor])) {
                    $items = $this->viewCfg['vendor'][$vendor];
                    foreach ($items as $key => $value) {
                        if (is_string($value)) {
                            if (substr($value, 0, 1) == '@') {
                                $must = substr($value, 1);
                                $files = array_merge($files, $this->getVendorFiles([$must], $vendorScript));
                            } else {
                                $files[] = $value;
                            }
                        } elseif ($key == 'lang') {
                            if (isset($value[LANG])) {
                                $files = array_merge($files, $value[LANG]);
                            } elseif (isset($value['default'])) {
                                $files = array_merge($files, $value['default']);
                            }
                        } elseif ($key == 'script') {
                            $vendorScript .= implode("\n", $value);
                        }
                    }
                }
            }
        }
        return array_unique($files);
    }

    /**
     * 分析 文件块
     */
    public function block()
    {
        if ($this->blocks || !$this->tagViews) {
            return; //已处理过的不再处理, 如果没有子模板不处理
        }
        $blockOffset = 0; //文件分块位置
        $blockIndex = 0;   //文件索引
        $tagPosOffset = []; //相同tag名的起始位置累加
        foreach ($this->tagViews as $tag) {
            $offset = $tagPosOffset[$tag->name] ?? 0;
            $pos = mb_strpos($this->tpl, $tag->codeText, $offset);
            $len = mb_strlen($tag->codeText);
            $tagPosOffset[$tag->name] = $pos + $len;
            $this->blocks[$blockIndex++] = mb_substr($this->tpl, $blockOffset, $pos - $blockOffset);
            $this->blocks[$blockIndex] = mb_substr($this->tpl, $pos, $len);
            $tag->blockIndex = $blockIndex;
            $blockIndex++;
            $blockOffset = $pos + $len;
        }
        $contentLenght = mb_strlen($this->tpl);
        if ($blockOffset != $contentLenght) {
            $this->blocks[$blockIndex] = mb_substr($this->tpl, $blockOffset);
        }
    }

    /**
     * 基础语法替换
     */
    protected function replaceBase()
    {
        $replaceArr = [
            /* 移除 HTML 注释 */
            '/<!--.*?-->\s*/s' => function ($m) {
                return '';
            },
            /* 替换 <? $class ? " class='$class'" : ""?> 为 <?= (isset($class) && $class) ? " class='$class'" : "" ; ?> */
            '/\<\?\s*(\$[a-zA-Z_][^\s]*)\s*\?\s+([^:]+)\s+:\s*([^\?]+)\s*\?\>/' => function ($m) {
                return sprintf('<?= (isset(%s) && %s) ? %s : %s; ?>', $m[1], $m[1], $m[2], ltrim($m[3]));
                return sprintf('<?php !isset(%s) ? %s=%s : %s=%s; ?>', $m[1], $m[1], ltrim($m[3]), $m[1], $m[2]);
            },
            /* 替换  <?$id ?: 0?> 为 <?= (isset($id) && $id) ? $id : 0; ?> */
            '/\<\?\s*(\$[a-zA-Z_][^\s]*)\s*\?\:\s*([^\?]+)\s*\?\>/' => function ($m) {
                return sprintf('<?= (isset(%s) && %s) ? %s : %s; ?>', $m[1], $m[1], $m[1], $m[2]);
            },
            /* 替换 <?$css?> 为 <?= $css ?? ""?>*/
            '/\<\?\s*(\$)*([a-zA-Z_][a-z_A-Z0-9\-\>\'\"\[\]]*)\s*\?\>/' => function ($m) {
                if ($m[1] != '') { //变量
                    return '<?= $' . $m[2] . ' ?? "" ?>';
                } else { //常量
                    return '<?= ' . $m[2] . ' ?>';
                }
            },
            // 替换 <? 短标签为长标签 <?php
            '/<\?(?!php\s)(?!=)\s*?/' => function ($m) {
                return '<?php';
            }
        ];
        $this->contents = preg_replace_callback_array($replaceArr, $this->contents);
    }

    /**
     * 分析 <include   /> 中的内容并返回结果
     */
    protected function parseTags()
    {
        $idx = 0;
        $partern = '/\<include\s*([^\s]+)\s*([^\/]+)?\/\>/s';
        if (preg_match_all($partern, $this->tpl, $m)) {
            foreach ($m[1] as $i => $v) {
                $codeText = $m[0][$i];
                $paramText = trim($m[2][$i]);
                if (trim($v) == '') {
                    continue;
                }
                $pattern = '/^[\s]*([a-zA-Z0-9_\.\/]*)(?: |$)/';
                if (preg_match($pattern, $v, $m2)) {
                    $tag = new \stdClass();
                    $tag->name = $m2[1];
                    $tag->codeText = $codeText;
                    $tag->params = [];
                    if ($paramText != '') {
                        $tag->params = $this->parseAttributes($paramText);
                    }
                    $tag->loop = isset($tag->params['loop'])
                                    && filter_var($tag->params['loop'], FILTER_VALIDATE_BOOLEAN);
                    if (strpos($tag->name, '.') !== false) {
                        // tag 中含有.点号即子视图模板
                        $tag->type = 'view';
                        $tag->filename = Viewer::getFilenameByViewName($tag->name);
                        $tag->name = Viewer::getViewName($tag->filename);
                        $this->tagViews[] = $tag;
                    } else {
                        $this->tags[$idx] = $tag;
                        $idx++;
                    }
                }
            }
        }
    }

    /**
     * 标签的属性数组转php代码文本
     *
     * @param array $params ['attrs'=>$attrs, 'types'=>$types]
     * @return string
     */
    public static function attrArrayToText(array $params): string
    {
        if (!$params) {
            return 'null';
        }
        $texts = [];
        foreach ($params as $name => $text) {
            $texts[] = "'" . $name . "' => " . $text;
        }
        return '[' . implode(", ", $texts) . ']';
    }

    /**
     * 标签的属性文本转数组
     *
     * @param string $string
     */
    protected function parseAttributes(string $string): array
    {
        if (empty($string)) {
            return [];
        }
        $attrs = [];
        $string = trim($string);
        $currentKey = null;
        $currentValue = '';
        $inQuotes = false; //是否在引号内
        $inBrackets = false; //是否在中括号内
        $equal = false; //等号+空格块,忽略 = 号的空格
        $depth = 0;
        for ($i = 0; $i < strlen($string); $i++) {
            $char = $string[$i];
            if ($equal && ($char === ' ' || $char === "\n")) {
                //忽略 = 号后面的空格
            } elseif (($char === ' ' || $char === ',' || $char === "\n") && !$inQuotes && !$inBrackets) {
                $equal = false;
                // 遇到空格且不在引号或中括号内，可能是键和值的分隔符
                if ($currentKey !== null) {
                    $currentValue = trim($currentValue);
                    $attrs[$currentKey] = $currentValue;
                    $currentKey = null;
                    $currentValue = '';
                }
            } elseif ($char === '=' && !$inQuotes && !$inBrackets) {
                // 遇到等号且不在引号或中括号内，可能是键和值的开始
                $equal = true;
                $currentKey = $currentValue;
                $currentValue = '';
            } elseif (($char === '"' || $char === "'") && !$inBrackets) {
                $equal = false; //等号+空格结果
                // 遇到引号且不在中括号内，开始或结束一个被引号包围的值
                $inQuotes = !$inQuotes;
                $currentValue .= $char;
            } elseif ($char === '[') {
                $equal = false; //等号+空格结果
                // 遇到左中括号，开始一个被中括号包围的值
                if (!$inQuotes) {
                    $inBrackets = true;
                    $depth++;
                }
                $currentValue .= $char;
            } elseif ($char === ']') {
                // 遇到右中括号，结束一个被中括号包围的值
                if (!$inQuotes) {
                    $depth--;
                    if ($depth === 0) {
                        $inBrackets = false;
                    }
                }
                $currentValue .= $char;
            } elseif (!$inQuotes && !$inBrackets) {
                $equal = false;
                // 不在引号或中括号内，直接添加字符到当前值
                $currentValue .= $char;
            } elseif ($inQuotes) {
                $equal = false;
                $currentValue .= $char;
            } elseif ($inBrackets) {
                $equal = false;
                $currentValue .= $char;
            }
        }

        // 处理最后一个键值对
        if ($currentKey !== null) {
            $attrs[$currentKey] = $currentValue;
        }

        // 处理中括号内的内容（如果需要的话）
        /*
        foreach ($result as $key => $value) {
            if (strpos($value, '[') !== false) {
                $result[$key] = parseAttributes($value); // 递归调用解析器
            }
        }*/

        return $attrs;
    }

    /** 最小化模板代码 */
    public function minifyHtmlAndPhp($content)
    {
        // 移除 HTML 注释， 在前面已经做了
        //$content = preg_replace('/<!--.*?-->/', '', $content);

        // 移除 PHP 注释
        $content = preg_replace('/<\?php\s*\/\*(.*?)\*\/\s*\?>/s', '', $content);
        $content = preg_replace('/<\?php\s*\/\/(.*?)\s*\?>/s', '', $content);
        // 移除 HTML 标签之间的空格，但保留在 PHP 代码内的内容
        $content = preg_replace_callback('/(<\?php.*?\?>)|>\s+</', function ($matches) {
            if (!empty($matches[1])) {
                // PHP 代码内的内容
                return $matches[0];
            } else {
                // HTML 标签之间的空格
                return '><';
            }
        }, $content);
        // 移除多余的空格、制表符、换行符，但保留在双引号或单引号内的空格、制表符、换行符
        $parttern = '/("[^"\\\\]*(?:\\\\.[^"\\\\]*)*"|\'[^\'\\\\]*(?:\\\\.[^\'\\\\]*)*\')|(\s)+/';
        $content = preg_replace_callback($parttern, function ($matches) {
            if (!empty($matches[1])) {
                // 双引号或单引号内的内容
                return $matches[0];
            } else {
                // 非双引号或单引号内的空格、制表符、换行符
                return ' ';
            }
        }, $content);
        return $content;
    }
}
