<?php

namespace xmvc\cmd;

use stdClass;
use xmvc\DB;
use xmvc\Encoder;
use xmvc\MVC;
use Symfony\Component\Yaml\Yaml;

class MetaObject
{
    public DB $db;
    public function __construct()
    {
        if (!isset(MVC::$cfg['database'])) {
            MVC::$cfg['database'] = loadConfig('database');
        }
        if (!isset(MVC::$cfg['database']['xmeta'])) {
            MVC::warningExit('没有配置 xmeta 数据库');
        }
        $this->db = new DB('xmeta');
    }

    public function execute(string $op): string
    {
        switch ($op) {
            case '':
            case 'tables':
                $tables = I('selectTables', null, ['type' => 'json']) ?: [];
                if (is_string($tables)) {
                    $tables = json_decode($tables);
                }
                return $this->opTables(I('tables', []), $tables);
            default:
                error('无效的网址');
        }
    }

    public function opTables(array $tables = [], array $selectTables = []): string
    {
        if ($tables) {
            //创建Meta类
            foreach ($tables as $v) {
                $this->createMetaClass($v);
            }
            header('Location: ' . url('SELF', ['selectTables' => Encoder::encode(json_encode($tables))]));
            exit();
        }
        $tables = [];
        if ($this->db->hasTable('xmeta_db')) {
            $q = $this->db->newSqler();
            $rows = $q->table('xmeta_table')
                ->join('xmeta_db', ['xmeta_table_db_name' => '@xmeta_db_name'])
                ->where('xmeta_table_classname{<>}', '')
                ->orderBy(['xmeta_table_db_name', 'xmeta_table_name'])
                ->all();
        } else {
            MVC::warningExit('尚未创建数据库元数据结构');
        }
        $formUrl = url('SELF');
        $csrfToken = getCsrfToken();
        $html = [];
        $html[] = "<h1>Database</h1>";
        $html[] = "<form action='{$formUrl}' method='post'>";
        $html[] = "<input type='hidden' name='csrfToken' value='{$csrfToken}'>";
        $html[] = '<div style="padding-left:1em"><input type="submit" value="生成元对象类文件"></div>';
        $html[] = '<div style="padding-left:1em"><input type="checkbox" id="checkall"> 全选</div>';
        $html[] = "<table class='mvc-blue-table'>";
        $html[] = "
            <thead>
                <tr>
                    <th style='width:30px'></th>
                    <th style='width:90px'>数据库</th>
                    <th style='width:260px'>表</th>
                    <th style='width:260px'>名称</th>
                    <th>类名</th>
                    <th style='width:30px'>类型</th>
                    <th>类型的规则</th>
                    <th>描述</th>
                </tr>
            <thead>
        ";
        foreach ($rows as $row) {
            $dbName = $row['xmeta_table_db_name'];
            $tableName = $row['xmeta_table_name'];
            $classname = 'app\meta\\' . $row['xmeta_db_namespace'] . '\\' . $row['xmeta_table_classname'];
            $caption = $row['xmeta_table_caption'];
            $codeFilename = $this->codeFilename($row['xmeta_db_namespace'], $row['xmeta_table_classname']);
            $created = is_file($codeFilename) ? " class='created'" : '';
            $checked = in_array($tableName, $selectTables) ? " checked='true'" : '';
            $html[] = "<tr{$created}>";
            $html[] = "<td><input type='checkbox' value='{$tableName}' name='tables[]'{$checked}></td>";
            $html[] = "<td>{$dbName}</td>";
            $html[] = "<td><span class='table-name'>{$tableName}</span></td>";
            $html[] = "<td><span class='table-name'>{$caption}</span></td>";
            $html[] = "<td><span class='table-name'>{$classname}</span></td>";
            $html[] = "<td>{$row['xmeta_table_type']}</td>";
            $html[] = "<td>{$row['xmeta_table_type_info']}</td>";
            $html[] = "<td>{$row['xmeta_table_comment']}</td>";
            $html[] = "</tr>";
        }
        $html[] = "</table>";
        $html[] = "</form>";
        return implode("\n", $html);
    }

    public function mysqlTypeToPhpType(string $mysqlType, string $typeParam): string
    {
        $mysqlType = strtoupper($mysqlType);
        $params = $typeParam ? json_decode($typeParam, true) : [];
        if ($mysqlType == 'TINYINT') {
            $type = ($params && $params[0] == '1') ? 'bool' : 'int';
        } elseif (in_array($mysqlType, ['SMALLINT', 'MEDIUMINT', 'INT', 'BIGINT'])) {
            $type = 'int';
        } elseif (in_array($mysqlType, ['FLOAT', 'DOUBLE', 'DECIMAL', 'NUMERIC'])) {
            $type = 'double';
        } elseif (in_array($mysqlType, ['CHAR', 'VARCHAR', 'TINYTEXT', 'TEXT', 'MEDIUMTEXT', 'LONGTEXT', 'ENUM', 'SET', 'BINARY', 'VARBINARY', 'TINYBLOB', 'BLOB', 'MEDIUMBLOB', 'LONGBLOB'])) {
            $type = 'string';
        } elseif (in_array($mysqlType, ['DATE', 'TIME', 'DATETIME', 'TIMESTAMP', 'YEAR'])) {
            $type = 'string';
        } elseif (in_array($mysqlType, ['BINARY', 'VARBINARY', 'TINYBLOB', 'BLOB', 'MEDIUMBLOB', 'LONGBLOB'])) {
            $type = 'string';
        } elseif ($mysqlType == 'JSON') {
            $type = 'array';
        } else {
            error('未对 MYSQL 类型 ' . $mysqlType . ' 做相对应的 PHP 类型处理');
        }
        return $type;
    }

    public function mysqlTypeLengths(string $mysqlType, string $typeParam): array
    {
        $mysqlType = strtoupper($mysqlType);
        $params = $typeParam ? json_decode($typeParam, true) : [];
        if ($mysqlType == 'TINYINT') {
            $result = $params ? [$params[0], 0] : [4, 0];
        } elseif ($mysqlType == 'SMALLINT') {
            $result = $params ? [$params[0], 0] : [6, 0];
        } elseif ($mysqlType == 'MEDIUMINT') {
            $result = $params ? [$params[0], 0] : [9, 0];
        } elseif ($mysqlType == 'INT') {
            $result = $params ? [$params[0], 0] : [11, 0];
        } elseif ($mysqlType == 'BIGINT') {
            $result = $params ? [$params[0], 0] : [20, 0];
        } elseif (in_array($mysqlType, ['FLOAT', 'DOUBLE', 'NUMERIC'])) {
            $result = [$params[0], $params[1]];
        } elseif ($mysqlType == 'DECIMAL') {
            $result = $params ? [$params[0], $params[1]] : [10, 0];
        } elseif ($mysqlType == 'DATE') {
            $result = [10, 0];
        } elseif ($mysqlType == 'TIME') {
            $result = [8, 0];
        } elseif ($mysqlType == 'DATETIME') {
            $result = [19, 0];
        } elseif ($mysqlType == 'TIMESTAMP') {
            $result = [19, 0];
        } elseif ($mysqlType == 'YEAR') {
            $result = [4, 0];
        } elseif ($mysqlType == 'CHAR') {
            $result = $params ? [$params[0], 0] : [1, 0];
        } elseif ($mysqlType == 'VARCHAR') {
            $result = [$params[0], 0];
        } elseif ($mysqlType == 'VARBINARY') {
            $result = [$params[0], 0];
        } elseif ($mysqlType == 'BIT') {
            $result = $params ? [$params[0], 0] : [1, 0];
        } elseif ($mysqlType == 'BINARY') {
            $result = $params ? [$params[0], 0] : [1, 0];
        } elseif ($mysqlType == 'TINYBLOB') {
            $result = $params ? [$params[0], 0] : [255, 0];
        } elseif ($mysqlType == 'BLOB') {
            $result = $params ? [$params[0], 0] : [65535, 0];
        } elseif ($mysqlType == 'MEDIUMBLOB') {
            $result = $params ? [$params[0], 0] : [16777215, 0];
        } elseif ($mysqlType == 'LONGBLOB') {
            $result = $params ? [$params[0], 0] : [4294967295, 0];
        } elseif ($mysqlType == 'TINYTEXT') {
            $result = $params ? [$params[0], 0] : [255, 0];
        } elseif ($mysqlType == 'TEXT') {
            $result = $params ? [$params[0], 0] : [65535, 0];
        } elseif ($mysqlType == 'TEXT') {
            $result = $params ? [$params[0], 0] : [65535, 0];
        } elseif ($mysqlType == 'MEDIUMTEXT') {
            $result = $params ? [$params[0], 0] : [16777215, 0];
        } elseif ($mysqlType == 'LONGTEXT') {
            $result = $params ? [$params[0], 0] : [4294967295, 0];
        } elseif ($mysqlType == 'ENUM') {
            $result = [0, 0];
        } elseif ($mysqlType == 'SET') {
            $result = [0, 0];
        } elseif ($mysqlType == 'JSON') {
            $result = [0, 0];
        } elseif (in_array($mysqlType, ['GEOMETRY', 'POINT', 'LINESTRING', 'POLYGON', 'MULTIPOINT', 'MULTILINESTRING', 'MULTIPOLYGON', 'GEOMETRYCOLLECTION'])) {
            $result = [0, 0];
        } else {
            error('未对 MYSQL 类型 ' . $mysqlType . ' 做相对应的长度处理');
        }
        return $result;
    }

    public function mysqlDefaultToPhpDefault(string $phpType, string|null $defaultValue): string
    {
        $result = 'null';
        if (in_array($phpType, ['int', 'float', 'double'])) {
            $result = ($defaultValue == null || $defaultValue == 'NULL') ? 'null' : trim($defaultValue, "'");
        } elseif ($phpType == 'bool') {
            $result = ($defaultValue == null || $defaultValue == 'NULL') ? 'null' : (trim($defaultValue, "'") == '1' ? 'true' : 'false');
        } elseif ($phpType == 'string') {
            if ($defaultValue == null) {
                $result = 'null';
            } elseif (substr($defaultValue, 0, 1) == "'") {
                $result = $defaultValue;
            } elseif ($defaultValue == 'NULL') {
                $result = 'null';
            } elseif ($defaultValue == 'CURRENT_TIMESTAMP') {
                $result = 'CURRENT_TIMESTAMP';
            } else {
                error('未对 PHP 类型 ' . $phpType . ' 做的默认值 ' . $defaultValue . '做相应处理');
            }
        } elseif ($phpType == 'array') {
            if ($defaultValue == null) {
                $result = 'null';
            } else {
                $result = json_decode($defaultValue, true);
                if ($result == null) {
                    $result = 'null';
                } else {
                    $result = var_export($result, true);
                }
            }
        } else {
            error('未对php 类型 ' . $phpType . ' 做相对应的默认值处理');
        }
        return $result;
    }

    /** 分析当前前缀 */
    public function parseMyPrefix(string $str, string $my): string
    {
        if (substr($str, 0, 1) == '~') {
            return $my . substr($str, 1);
        }
        return $str;
    }

    public function codeFilename(string $namespace, string $classname): string
    {
        $dir = ROOT . '/meta/' . $namespace;
        $codeFilename = $dir . '/' . $classname . '.php';
        return $codeFilename;
    }

    public function yamlFilename(string $namespace, string $classname): string
    {
        $dir = ROOT . '/metaConf/yaml/' . $namespace;
        $filename = $dir . '/' . $classname . '.yml';
        return $filename;
    }

    /** 获取指定表的的所有的有效外键 (classname 不为空的, is_filter = 0) */
    public function getForeignRows(string $dbName, string $tableName): array
    {
        $q = $this->db->newSqler();
        return $q->table('xmeta_foreign')
            ->join('xmeta_db', ['xmeta_db_name' => '@xmeta_foreign_db_name'])
            ->join('xmeta_db(xmeta_db2)', ['xmeta_db2.xmeta_db_name' => '@xmeta_foreign_db_name2'])
            ->join('xmeta_table', ['xmeta_table_db_name' => '@xmeta_foreign_db_name', 'xmeta_table_name' => '@xmeta_foreign_table_name'])
            ->join('xmeta_table(xmeta_table2)', ['xmeta_table2.xmeta_table_db_name' => '@xmeta_foreign_db_name2', 'xmeta_table2.xmeta_table_name' => '@xmeta_foreign_table_name2'])
            ->join('xmeta_column', ['xmeta_column.xmeta_column_db_name' => '@xmeta_foreign_db_name', 'xmeta_column.xmeta_column_table_name' => '@xmeta_foreign_table_name', 'xmeta_column.xmeta_column_name' => '@xmeta_foreign_column_name'])
            ->join('xmeta_column(xmeta_column2)', ['xmeta_column2.xmeta_column_db_name' => '@xmeta_foreign_db_name2', 'xmeta_column2.xmeta_column_table_name' => '@xmeta_foreign_table_name2', 'xmeta_column2.xmeta_column_name' => '@xmeta_foreign_column_name2'])
            ->where('xmeta_foreign_db_name', $dbName)
            ->and('xmeta_foreign_table_name', $tableName)
            ->and('xmeta_table2.xmeta_table_classname{<>}', '')
            ->and('xmeta_foreign_is_filter', 0)
            ->or([
                'xmeta_foreign_db_name{<>}' => '@xmeta_foreign_db_name2',
                'xmeta_foreign_table_name{<>}' => '@xmeta_foreign_table_name2'
            ])
            ->orderBy(['xmeta_foreign_name', 'xmeta_foreign_sort'])
            ->all(true);
    }

    /** 创建 Meta类 文件 */
    public function createMetaClass(string $tableName)
    {
        $q = $this->db->newSqler();
        // 数据库 与 表信息
        $row = $q->table('xmeta_table')
            ->join('xmeta_db', ['xmeta_table_db_name' => '@xmeta_db_name'])
            ->where('xmeta_table_classname{<>}', '')
            ->and('xmeta_table_name', $tableName)
            ->row();
        $classtpl = $row['xmeta_table_classtpl'] ?: 'class.tpl.php';
        $r = new stdClass();
        $r->this = $this;
        $r->dbName = $row['xmeta_table_db_name'];
        $r->dbKey = $row['xmeta_db_key'];
        $r->tableName = $row['xmeta_table_name'];
        $r->namespace = $row['xmeta_db_namespace'];
        $r->classname = ucfirst($row['xmeta_table_classname']);
        $r->prefix = $row['xmeta_table_field_prefix'];
        $r->classCaption = $row['xmeta_table_caption'];
        $r->classComment = $row['xmeta_table_comment'];
        $r->fieldPrefix = $row['xmeta_table_field_prefix'];
        $r->primaryFieldNames = explode(',', $row['xmeta_table_primary']);
        $r->primaryFieldName = $r->primaryFieldNames[0];
        $r->type = $row['xmeta_table_type'];
        $r->typeInfo = $row['xmeta_table_type_info'] ? json_decode($row['xmeta_table_type_info'], true) : null;

        // 字段信息
        $columnRows = $q->table('xmeta_column')
            ->where('xmeta_column_db_name', $r->dbName)
            ->and('xmeta_column_table_name', $r->tableName)
            ->orderBy('xmeta_column_sort')
            ->all();

        // 关键属性, 只支持一个
        foreach ($columnRows as $row) {
            if ($row['xmeta_column_name'] == $r->primaryFieldName) {
                $r->primary = $row['xmeta_column_attr'];
            }
        }
        // 外键信息 - 自身表引用自身表
        $foreignSelfRows = $q->table('xmeta_foreign')
            ->join('xmeta_db', ['xmeta_db_name' => '@xmeta_foreign_db_name2'])
            ->join('xmeta_table', ['xmeta_table_db_name' => '@xmeta_foreign_db_name', 'xmeta_table_name' => '@xmeta_foreign_table_name'])
            ->join('xmeta_column', ['xmeta_column_db_name' => '@xmeta_foreign_db_name', 'xmeta_column_table_name' => '@xmeta_foreign_table_name', 'xmeta_column_name' => '@xmeta_foreign_column_name'])
            ->where('xmeta_foreign_db_name', $r->dbName)
            ->and('xmeta_foreign_table_name', $r->tableName)
            ->and('xmeta_table_classname{<>}', '')
            ->and('xmeta_foreign_is_filter', 0)
            ->and([
                'xmeta_foreign_db_name' => '@xmeta_foreign_db_name2',
                'xmeta_foreign_table_name' => '@xmeta_foreign_table_name2'
            ])
            ->orderBy(['xmeta_foreign_name', 'xmeta_foreign_sort'])
            ->all();
        if ($r->tableName == 'xmeta_user') {
            //echo \xmvc\sqlFormatter::format($this->db->last());
            //dd($foreignSelfRows);
        }

        // 外键信息 - 自身表中定义指向其他表的外键
        $foreignToRows = $q->table('xmeta_foreign')
            ->join('xmeta_db', ['xmeta_db_name' => '@xmeta_foreign_db_name2'])
            ->join('xmeta_table', ['xmeta_table_db_name' => '@xmeta_foreign_db_name2', 'xmeta_table_name' => '@xmeta_foreign_table_name2'])
            ->join('xmeta_column', ['xmeta_column_db_name' => '@xmeta_foreign_db_name', 'xmeta_column_table_name' => '@xmeta_foreign_table_name', 'xmeta_column_name' => '@xmeta_foreign_column_name'])
            ->where('xmeta_foreign_db_name', $r->dbName)
            ->and('xmeta_foreign_table_name', $r->tableName)
            ->and('xmeta_table_classname{<>}', '')
            ->and('xmeta_foreign_is_filter', 0)
            ->or([
                'xmeta_foreign_db_name{<>}' => '@xmeta_foreign_db_name2',
                'xmeta_foreign_table_name{<>}' => '@xmeta_foreign_table_name2'
            ])
            ->orderBy(['xmeta_foreign_name', 'xmeta_foreign_sort'])
            ->all();

        // 外键信息 - 来自其他表中定义的指向自身的外键
        $foreignFromRows = $q->table('xmeta_foreign')
            ->join('xmeta_db', ['xmeta_db_name' => '@xmeta_foreign_db_name'])
            ->join('xmeta_db(xmeta_db2)', ['xmeta_db2.xmeta_db_name' => '@xmeta_foreign_db_name2'])
            ->join('xmeta_table', ['xmeta_table_db_name' => '@xmeta_foreign_db_name', 'xmeta_table_name' => '@xmeta_foreign_table_name'])
            ->join('xmeta_table(xmeta_table2)', ['xmeta_table2.xmeta_table_db_name' => '@xmeta_foreign_db_name2', 'xmeta_table2.xmeta_table_name' => '@xmeta_foreign_table_name2'])
            ->join('xmeta_column', ['xmeta_column.xmeta_column_db_name' => '@xmeta_foreign_db_name', 'xmeta_column.xmeta_column_table_name' => '@xmeta_foreign_table_name', 'xmeta_column.xmeta_column_name' => '@xmeta_foreign_column_name'])
            ->join('xmeta_column(xmeta_column2)', ['xmeta_column2.xmeta_column_db_name' => '@xmeta_foreign_db_name2', 'xmeta_column2.xmeta_column_table_name' => '@xmeta_foreign_table_name2', 'xmeta_column2.xmeta_column_name' => '@xmeta_foreign_column_name2'])
            ->where('xmeta_foreign_db_name2', $r->dbName)
            ->and('xmeta_foreign_table_name2', $r->tableName)
            ->and('xmeta_table.xmeta_table_classname{<>}', '')
            ->and('xmeta_foreign_is_filter', 0)
            ->or([
                'xmeta_foreign_db_name{<>}' => '@xmeta_foreign_db_name2',
                'xmeta_foreign_table_name{<>}' => '@xmeta_foreign_table_name2'
            ])
            ->orderBy(['xmeta_foreign_name', 'xmeta_foreign_sort'])
            ->all(true)
            ;
        // 多语言信息
        $langMapRows = $q->table('xmeta_lang_map')
        ->where('xmeta_lang_map_db_name', $r->dbName)
            ->and('xmeta_lang_map_table_name', $r->tableName)
            ->all();

        // 创建 Meta类 文件
        ob_start(); //模板处理
        echo '<?php' . LF . LF;
        require(DIR_MVC . '/core/codeTpl/meta/' . $classtpl);
        $contents = ob_get_contents();
        ob_end_clean();
        $codeFilename = $this->codeFilename($r->namespace, $r->classname);
        $dir = dirname($codeFilename);
        mkdirs($dir);
        file_put_contents($codeFilename, $contents);
        file_put_contents($codeFilename . '.html', $contents);

        // 创建Yaml 配置文件 - 如果不存在的话
        $yamlFilename = $this->yamlFilename($r->namespace, $r->classname);
        ob_start(); //模板处理
        require(DIR_MVC . '/core/codeTpl/meta/yaml.tpl.php');
        $contents = ob_get_contents();
        ob_end_clean();
        $dir = dirname($yamlFilename);
        mkdirs($dir);
        file_put_contents($yamlFilename . '.tmp', $contents);

        if (file_exists($yamlFilename)) {
            copy($yamlFilename, $yamlFilename . '.bak');
            $changeData = Yaml::parse($contents);
            $data = Yaml::parseFile($yamlFilename);
            $this->mergeYamlData($data, $changeData);
            $yamlString = Yaml::dump($data, 999, 2);
            $yamlString = preg_replace('/(\s*NOT_IN_META: \'YES\')/', '$1 # 不存在于 META 中', $yamlString);
            file_put_contents($yamlFilename, $yamlString);
        } else {
            copy($yamlFilename . '.tmp', $yamlFilename);
        }
        unlink($yamlFilename . '.tmp');
    }

    public function mergeYamlData(&$data, $changeData)
    {
        $changeKeys = ['caption', 'fieldName', 'phpType', 'mysqlType','must','default', 'dxGridColumn'];
        foreach ($changeData as $attr => $item) {
            if (array_key_exists($attr, $data)) {
                foreach ($item as $key => $value) {
                    if (in_array($key, $changeKeys)) {
                        if ($key == 'must') {
                            if ($value === true) {
                                $data[$attr]['must'] = true;
                            }
                        } elseif ($key == 'dxGridColumn') {
                            if (isset($value['lookup'])) {
                                if (!array_key_exists('lookup', $data[$attr][$key])) {
                                    $data[$attr][$key]['lookup'] = $value['lookup'];
                                }
                            }
                        } else {
                            $data[$attr][$key] = $value;
                        }
                    }
                }
            } else {
                $data[$attr] = $item;
            }
        }
        foreach ($data as $attr => $item) {
            if (!array_key_exists($attr, $changeData)) {
                $data[$attr]['NOT_IN_META'] = 'YES';
            }
        }
    }
}

$metaObject = new MetaObject();
$html = $metaObject->execute(I('op', ''));
?>

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charSet="utf-8" />
    <title>元对象</title>
    <?php echo MVC::getMvcCssJs(); ?>
    <style>
        html,
        body {
            font-size: 14px;
            font-family: "Microsoft YaHei", monaco, Consolas, Menlo, Courier, monospace;
        }

        input[type="checkbox"] {
            transform: scale(1.5);
            margin: 5px;
        }

        a {
            color: #267FB3;
            text-decoration: none;
        }

        a:hover {
            color: #AE538D;
        }

        .table-name {
            font-size: 1rem;
            color: #333
        }

        .created .table-name {
            font-size: 1.1rem;
            color: #080;
        }
    </style>
    <script>
        let trCheckedBackgroundColor = '#EFE';
        document.addEventListener('DOMContentLoaded', (event) => {
            // 全选
            document.getElementById('checkall').addEventListener('click', function() {
                let that = this;
                let checkboxs = document.querySelectorAll('input[type="checkbox"]');
                checkboxs.forEach(function(item) {
                    if (item != that) {
                        item.checked = that.checked;
                        let tr = item.closest('tr');
                        if (item.checked) {
                            tr.style.backgroundColor = trCheckedBackgroundColor;
                        } else {
                            tr.style.backgroundColor = '';
                        }
                    }
                });
            });
            // 当前选择tr变色
            let checkboxes = document.querySelectorAll('input[name="tables[]"]');
            checkboxes.forEach(function(checkbox) {
                checkbox.addEventListener('change', function() {
                    let tr = this.closest('tr');
                    if (this.checked) {
                        tr.style.backgroundColor = trCheckedBackgroundColor;
                    } else {
                        tr.style.backgroundColor = '';
                    }
                });
            });
        });
    </script>
</head>

<body>
    <?= $html ?>
</body>

</html>