<?php
namespace Qii\Driver\Entity;

use Qii\Driver\Model;
use Qii\Driver\Response;

/**
 * 通过数据表生成 entity
 * 使用方法:
 * use Qii\Driver\Entity\Entity;
 * $entity = new Entity();
 * $class = $entity->generateProperties($table);
 * file_put_contents('entity/'. $table . '.php', $class);
 *
 * $fields = $entity->generateField('\entity\'. $table);
 * print_r($fields);
 *
 * entity 生成规则：
 * 首字母、下划线+字母 -> 大写字母
 */
class Entity {
    /**
     * @var bool $ucFirst 首字母是否大写
     */
    public $ucFirst = true;
    public $namespace = "entity";
    public function __construct(){
    }

    public function db() {
        return _loadClass('\Qii\Driver\Model');
    }
    /**
     * 设置 entity 的名字空间
     * @param string $namespace 名字空间
     * @return void
     */
    public function setNamespace($namespace) {
        $this->namespace = $namespace;
    }

    /**
     * 通过 Entity 获取数据表的字段
     *
     * @param $class
     * @return array
     * @throws \ReflectionException
     */
    public function generateField($class) {
        if(!$class) {
            throw new \Exception('Class is null');
        }
        if(class_exists($class, false)) {
            throw new \Exception('Class '. $class .' does not exist');
        }
        $method = new \ReflectionClass($class);
        $properties = $method->getProperties();
        $fields = [];
        foreach($properties as $property) {
            if($property->isPublic()) {
                $fields[] = $this->convertToField($property->getName());
            }
        }
        return $fields;
    }

    /**
     * 将数据表生成entity
     *
     * @param string $table 表名
     * @return string
     * @throws \Exception
     */
    public function generateProperties($table, $className = '') {
        $db = $this->db();
        $tableInfo = $db->getTableInfo($table);
        if($db->isError()) {
            throw new \Exception($db->getError());
        }
        if(!$tableInfo || empty($tableInfo['fields'])) {
            throw new \Exception($table . ' does not have any field');
        }
        $properties = [];
        $docs = [];
        $defaults = [];
        foreach ($tableInfo['fields'] as $field) {
            $comment = $tableInfo['comment'][$field] ?? '';
            $convertField = $this->convertToProperty($field);
            $comment = strlen($comment) > 0 ? " ". $comment : "";

            $doc = [];
            if(($tableInfo['rules']['int'] && isset($tableInfo['rules']['int'][$field])) ||
                ($tableInfo['rules']['number'] && isset($tableInfo['rules']['number'][$field]))
            ) {
                $doc[] = "\t/**";
                $doc[] = "\t * @var int ". $convertField . $comment;
                $doc[] = "\t */";
            } else if (($tableInfo['rules']['maxlength'] && isset($tableInfo['rules']['maxlength'][$field]))
                || ($tableInfo['rules']['text'] && in_array($field, $tableInfo['rules']['text']))
            ) {
                $doc[] = "\t/**";
                $doc[] = "\t * @var string ". $convertField . $comment;
                $doc[] = "\t */";
            } else if ($tableInfo['rules']['timestamp'] && in_array($field, $tableInfo['rules']['timestamp'])) {
                $doc[] = "\t/**";
                $doc[] = "\t * @var datetime ". $convertField . $comment;
                $doc[] = "\t */";
            } else if($tableInfo['rules']['float'] && isset($tableInfo['rules']['float'][$field])) {
                $doc[] = "\t/**";
                $doc[] = "\t * @var float ". $convertField . $comment;
                $doc[] = "\t */";
            }else if($tableInfo['rules']['decimal'] && isset($tableInfo['rules']['decimal'][$field])) {
                $doc[] = "\t/**";
                $doc[] = "\t * @var decimal ". $convertField . $comment;
                $doc[] = "\t */";
            }
            if($tableInfo['rules']['default'] && isset($tableInfo['rules']['default'][$field])) {
                $default = $tableInfo['rules']['default'][$field];
                $defaults[$convertField] = $default;
            }
            if(count($doc) > 0) {
                $docs[$convertField] = join("\n", $doc);
            }

            $properties[] = $convertField;
        }
        $class = $className != '' ? $this->convertToProperty($className) : $this->convertToProperty($table);
        $classAndProperty = [];
        $classAndProperty[] = "<?php";
        $classAndProperty[] = "namespace ". $this->namespace . ";";
        $classAndProperty[] = "\n";
        $classAndProperty[] = "use Qii\Driver\Entity\Inf;";
        $classAndProperty[] = "use Qii\Driver\Entity\Base;";
        $classAndProperty[] = "use Qii\Driver\Response;";
        $classAndProperty[] = "\n";
        $classAndProperty[] = <<<DOC
/**
 * @method \$this setWhereHooker(callable \$func)
 * @method \$this setOrderHooker(callable \$func)
 * @method \$this setLimitHooker(callable \$func)
 * @method \$this setCacheHooker(callable \$func)
 * @method \$this setQueryFieldsHooker(callable \$func)
 * @method bool exit()
 * @method \$this get()
 * @method Response info()
 * @method Response save()
 * @method Response remove()
 * @method Response update()
 * @method Response updateFields()
 * @method Response incr()
 * @method Response first()
 * @method Response last()
 * @method array lists(int \$page, int \$pageSize)
 * @method array listAll()
 * @method Object rs()
 * @method Response valids
 * @method array initPages(array \$data, int \$count, int \$page, int \$pageSize)
 */
DOC;

        $classAndProperty[] = 'class '. $class . ' extends Base implements Inf{';
        $dynamicProperty = [];
        foreach ($properties as $property) {
            if(isset($docs[$property])) {
                $classAndProperty[] = $docs[$property];
            }
            /*if(isset($defaults[$property])) {
                $default = $defaults[$property];
                if($default == 'CURRENT_TIMESTAMP') {
                    $dynamicProperty[$property] = $default;
                }
                $classAndProperty[] = "\t". 'public $'. $property . " = '". $default . "';\n";
            }else{
                $classAndProperty[] = "\t". 'public $'. $property . ";";
            }*/
            $classAndProperty[] = "\t". 'public $'. $property . ";";
            if(isset($defaults[$property])) {
                $dynamicProperty[$property] = $defaults[$property];
            }
        }
        if(count($dynamicProperty)) {
            $classAndProperty[] = <<<DOC
    /**
     * 动态变化的日期，初始化的时候赋值
     */
DOC;
            $classAndProperty[] = "\tpublic function __construct(){";
            $classAndProperty[] = "\n";
            $classAndProperty[] = "\t}";

            $classAndProperty[] = "\t/**";
            $classAndProperty[] = "\t * 设置默认值";
            $classAndProperty[] = "\t */";
            $classAndProperty[] = "\tpublic function defaultFieldsValue(){";
            $classAndProperty[] = "\t\t\$defaultValue = [];";
            foreach ($dynamicProperty as $property => $dynamic) {
                if($dynamic == 'CURRENT_TIMESTAMP') {
                    $classAndProperty[] = "\t\t" . '$defaultValue[\''. $property ."'] = date('Y-m-d H:i:s');";
                }else{
                    $classAndProperty[] = "\t\t" . '$defaultValue[\''. $property ."'] = '". str_replace("'", "\\'", $dynamic)."';";
                }
            }
            $classAndProperty[] = "\t\treturn \$defaultValue;";
            $classAndProperty[] = "\t}";
        }
        $classAndProperty[] = <<<DOC

\t/**
\t * 获取表名
\t */
\tpublic function getTable(){
\t\treturn '{$table}';
\t}
DOC;


        //生成 table rules
        $classAndProperty[] = $this->generateRules($tableInfo);
        $classAndProperty[] = '}';
        return join("\n", $classAndProperty);
    }

    public function generateRules($tableInfo){
        $constantJoin[] = <<<DOC

    /**
     * 字段验证数据规则
     */
    public function rules() { 
DOC;
        $constantJoin[] = "\t\t". '$rules = [];';
        $next = array_reduce(array_reverse(['required', 'int', 'minlength', 'maxlength', 'decimal', 'timestamp', 'datetime']), function($carry, $item){
            return function() use ($carry, $item){
                $tableInfo = func_get_args()[0];
                $constantJoin = func_get_args()[1];
                $currentRules = $tableInfo['rules'][$item];
                $comments = $tableInfo['comment'];
                $join = [];
                switch ($item) {
                    case 'required':
                        $join = $this->generateRequired($currentRules, $comments);
                        break;
                    case 'int':
                        $join = $this->generateInt($currentRules, $comments);
                        break;
                    case 'decimal':
                        $join = $this->generateDecimal($currentRules, $comments);
                        break;
                    case 'minlength':
                        $join = $this->generateMinLength($currentRules, $comments);
                        break;
                    case 'maxlength':
                        $join = $this->generateMaxLength($currentRules, $comments);
                        break;
                    case 'timestamp':
                    case 'datetime':
                        $join = $this->generateDatetime($currentRules, $comments);
                }
                if(!empty($join)) {
                    $constantJoin = array_merge($constantJoin, $join);
                }
                return $carry($tableInfo, $constantJoin);
            };
        }, function($tableInfo, $constantJoin){
            return $constantJoin;
        })($tableInfo, $constantJoin);

        $next[] = "\t\treturn ". '$rules;';
        $next[] = "\t". '}';
        //主键自动添加
        $primaryKey = array();
        if(isset($tableInfo['rules']['pri'])) {
            $primaryKey = $tableInfo['rules']['pri'];
        }
        //保存验证的时候使用 required 并去掉pri和default里边的内容
        $defaults = array();
        if(isset($tableInfo['rules']['default'])) {
            $defaults = array_keys($tableInfo['rules']['default']);
        }
        $validForSave = array();
        if(isset($tableInfo['rules']) && isset($tableInfo['rules']['required'])) {
            foreach ($tableInfo['rules']['required'] as $field) {
                if(!in_array($field, $primaryKey) && !in_array($field, $defaults)) {
                    $validForSave[] = $this->convertToProperty($field);
                }
            }
        }
        $uniKey = array();
        if(isset($tableInfo['rules']) && isset($tableInfo['rules']['uniq'])) {
            foreach ($tableInfo['rules']['uniq'] as $uniq) {
                $uniKey[] = $this->convertToProperties($uniq);
            }
        }

        $next[] = <<<DOC
    /**
     * unique (unique 如果是 array 则表示 联合唯一，如果是string则是任意唯一，多个单独唯一使用字符串以逗号隔开, 主键除外)
     * array(array1, array2) , array1 和array 2单独唯一
     * @return mixed
     * @throws \Exception
     */
    public function uniqueKey(){
DOC;
        if(count($uniKey) > 0) {
            $uniq = array();
            foreach ($uniKey as $val) {
                $uniq[] = "array('". join("','", $val). "')";
            }
            $next[] = "\t\treturn array(". join(", ", $uniq). ");";
        }else{
            $next[] = <<<DOC
        return array();
DOC;
        }

        $next[] = <<<DOC
    }

    /**
     * 主键
     *
     * @return mixed
     * @throws \Exception
     */
    public function primaryKey(){
DOC;
        if($primaryKey) {
            $next[] = "\t\treturn array('". join("','", $this->convertToProperties($primaryKey)). "');";
        }else{
            $next[] = <<<DOC
        throw new \Exception('请设置主键');
DOC;
        }

        $next[] = <<<DOC
    }

    /**
     * 保存数据的时候，验证唯一需要排除的值，此处仅支持，单个或联合排除，不支持单个排除
     *
     * @return array
     */
    public function exclude(){
        return array();
    }
    /**
     * 添加时验证的字段，自行添加
     */
    public function validFieldsForAdd(){
DOC;
        if(count($validForSave) > 0) {
            $next[] = "\t\$fields = array('". join("', '", $validForSave)."');";
        }else{
            $next[] = "\t\$fields = array();";
        }
        $next[] = <<<DOC
        return \$this->valid(\$fields);
    }
    
    /**
     * 更新时验证的字段，自行添加
     */
    public function validFieldsForUpdate() {
        \$fields = array();
        return \$this->valid(\$fields);
    }
DOC;
        return join("\n", $next);
    }

    /**
     * required
     * @param array $rules 规则
     * @param array $comments field comment
     * @return array
     */
    public function generateRequired($rules, $comments = array()) {
        $constantJoin = [];
        if(!$rules || !is_array($rules)) {
            return $constantJoin;
        }
        $constantJoin[] = "\t\t". '$rules["required"] = [';

        $join = [];
        foreach($rules as $key => $value) {
            $comment = '';
            if(isset($comments[$value]) && $comments[$value] != "") {
                $comment = $comments[$value];
            }
            $message = ($comment != '' ? $comment :  $value) . "不能为空";
            $join[] = <<<DOC
    \t\t["{$this->convertToProperty($value)}", "{$message}"]
DOC;
        }
        $constantJoin[] = join(",\n", $join);
        $constantJoin[] = "\t\t];";
        return $constantJoin;
    }
    public function generateMaxLength($rules, $comments = array()) {
        $constantJoin = [];
        if(!$rules || !is_array($rules)) {
            return $constantJoin;
        }
        $constantJoin[] = "\t\t". '$rules["maxLength"] = [';
        $join = [];
        foreach($rules as $key => $value) {
            $comment = '';
            if(isset($comments[$value]) && $comments[$value] != "") {
                $comment = $comments[$value];
            }
            $message = ($comment != '' ? $comment : $key) . "不能超过 ". $value ."个字符";
            $join[] = <<<DOC
    \t\t["{$this->convertToProperty($key)}", "{$message}", {$value}]
DOC;
        }
        $constantJoin[] = join(",\n", $join);
        $constantJoin[] = "\t\t];";
        return $constantJoin;
    }
    public function generateMinLength($rules, $comments = array()) {
        $constantJoin = [];
        if(!$rules || !is_array($rules)) {
            return $constantJoin;
        }
        $constantJoin[] = "\t\t". '$rules["minLength"] = [';
        $join = [];
        foreach($rules as $key => $value) {
            $comment = '';
            if(isset($comments[$value]) && $comments[$value] != "") {
                $comment = $comments[$value];
            }
            $message = ($comment != '' ? $comment : $key) . "不能少于 ". $value ."个字符";
            $join[] = <<<DOC
    \t\t["{$this->convertToProperty($key)}", "{$message}", {$value}]
DOC;
        }
        $constantJoin[] = join(",\n", $join);
        $constantJoin[] = "\t\t];";
        return $constantJoin;
    }

    /**
     * datetime 类型
     * @param array $rules
     * @param $comments
     * @return array
     */
    public function generateDatetime($rules, $comments = array()) {
        $constantJoin = [];
        if(!$rules || !is_array($rules)) {
            return $constantJoin;
        }

        $constantJoin[] = "\t\t". '$rules["date"] = [';

        $join = [];
        foreach($rules as $key => $value) {
            $comment = '';
            if(isset($comments[$value]) && $comments[$value] != "") {
                $comment = $comments[$value];
            }
            $message = $comment . "格式不正确";
            $join[] = <<<DOC
    \t\t["{$this->convertToProperty($value)}", "{$message}"]
DOC;
        }
        $constantJoin[] = join(",\n", $join);
        $constantJoin[] = "\t\t];";
        return $constantJoin;
    }

    /**
     * int 类型数据
     * @param $rules
     * @param $comments
     * @return array
     */
    public function generateInt($rules, $comments = array()) {
        $constantJoin = [];

        if(!$rules || !is_array($rules)) {
            return $constantJoin;
        }
        $intProperty['min'][] = "\t\t". '$rules["minNumber"] = [';
        $intProperty['max'][] = "\t\t". '$rules["maxNumber"] = [';
        $intProperty['number'][] = "\t\t". '$rules["number"] = [';
        $joinMin = [];
        $joinMax = [];
        $joinNumber = [];
        foreach ($rules as $key => $val) {
            $message = isset($comments[$key]) && $comments[$key] != "" ? $comments[$key] : $key;
            $min = $message . '不能小于'. $val[0];
            $max = $message . '不能大于'. $val[1];
            $number = $message . '必须是数字';

            $joinMin[] = <<<DOC
    \t\t["{$this->convertToProperty($key)}", "{$min}", {$val[0]}]
DOC;
            $joinMax[] = <<<DOC
    \t\t["{$this->convertToProperty($key)}", "{$max}", {$val[1]}]
DOC;
            $joinNumber[] = <<<DOC
    \t\t["{$this->convertToProperty($key)}", "{$number}"]
DOC;

        }
        $intProperty['min'][] = join(",\n", $joinMin);
        $intProperty['min'][] = "\t\t];";

        $intProperty['max'][] = join(",\n", $joinMax);
        $intProperty['max'][] = "\t\t];";

        $intProperty['number'][] = join(",\n", $joinNumber);
        $intProperty['number'][] = "\t\t];";

        return array_merge($constantJoin, $intProperty['min'], $intProperty['max'], $intProperty['number']);
    }
    public function generateDecimal($rules, $comments = array()) {
        $constantJoin = [];

        if(!$rules || !is_array($rules)) {
            return $constantJoin;
        }
        $constantJoin = [];
        $constantJoin[] = "\t\t". '$rules["decimal"] = [';
        $joinProperty = [];
        foreach ($rules as $key => $val) {
            $comment = '';
            if(isset($comments[$key]) && $comments[$key] != "") {
                $comment = $comments[$key];
            }

            $message = ($comment != '' ? $comment : $key) . "格式示例： ". str_repeat(6, min(1, (int)$val[0] - (int)$val[1])) . ".". str_repeat(6, (int)$val[1]);
            $joinProperty[] = <<<DOC
    \t\t["{$this->convertToProperty($key)}", "{$message}", "{$val[0]},{$val[1]}"]
DOC;
        }
        $constantJoin[] = join(",\n", $joinProperty);
        $constantJoin[] = "\t\t];";
        return $constantJoin;
    }
    /**
     * 转换为属性，属性会将下划线+小写/大写 转换成大写
     *
     * @param $field
     * @return array|string|string[]|null
     */
    public function convertToProperty($field) {
        $field = $this->ucFirst ? ucfirst($field) : $field;
        return preg_replace_callback("/\_([a-z]|[A-Z]|[0-9])/", function($match){
            return ucfirst($match[1]);
        }, $field);
    }

    /**
     * 批量转 property 名称
     *
     * @param $fields
     * @return array|string|string[]|null
     */
    public function convertToProperties($fields) {
        if(!is_array($fields)) {
            return $this->convertToProperty($fields);
        }
        $map = [];
        foreach ($fields as $val) {
            $map[] = $this->convertToProperty($val);
        }
        return $map;
    }
    /**
     * 将属性转换成字段,属性中的大写会转换成成下划线+小写
     *
     * @param $field
     * @return array|string|string[]|null
     */
    public function convertToField($field) {
        $field = $this->ucFirst ? lcfirst($field) : $field;
        return preg_replace_callback("/[A-Z]/", function($match){
            return "_". lcfirst($match[0]);
        }, $field);
    }

    /**
     * 批量转 field
     * @param array|string $fields
     * @return array|string|string[]|null
     */
    public function convertToFields($fields) {
        if(!is_array($fields)) {
            return $this->convertToField($fields);
        }
        $map = [];
        foreach ($fields as $val) {
            $map[] = $this->convertToField($val);
        }
        return $map;
    }
}