<?php
namespace Qii\Driver\Entity;
/**
 * 通过数据表生成 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;
    }

    /**
     * 获取表的备注信息
     *
     * @param string $table
     * @return mixed
     * @throws \Exception
     */
    public function getTableComment($table) {
        $db = $this->db();

        if($db->isError()) {
            throw new \Exception($db->getError());
        }

        return $this->getTableComment($table);
    }

    /**
     * 返回表的字段
     *
     * @param string $table
     * @return mixed
     * @throws \Exception
     */
    public function getTableInfo($table){
        $db = $this->db();
        if($db->isError()) {
            throw new \Exception($db->getError());
        }
        $tableInfo = $db->getTableInfo($table);
        if(!$tableInfo || empty($tableInfo['fields'])) {
            throw new \Exception($table . ' does not have any field');
        }
        return $tableInfo;
    }
    /**
     * 通过 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 = '') {
        $tableInfo = $this->getTableInfo($table);
        $properties = [];
        $docs = [];
        $defaults = [];
        foreach ($tableInfo['fields'] as $field) {
            $comment = $tableInfo['comment'][$field] ?? '';
            $convertField = $this->convertToProperty($field);
            $comment = strlen($comment) > 0 ? " ". $comment : "";

            $doc = [];
            if(isset($tableInfo['rules']['int'][$field]) ||
                isset($tableInfo['rules']['number'][$field])
            ) {
                $doc[] = "\t/**";
                $doc[] = "\t * @var int ". $convertField . $comment;
                $doc[] = "\t */";
            } else if (isset($tableInfo['rules']['maxlength'][$field])
                || (isset($tableInfo['rules']['text']) && in_array($field, $tableInfo['rules']['text']))
            ) {
                $doc[] = "\t/**";
                $doc[] = "\t * @var string ". $convertField . $comment;
                $doc[] = "\t */";
            } else if (isset($tableInfo['rules']['timestamp']) && in_array($field, $tableInfo['rules']['timestamp'])) {
                $doc[] = "\t/**";
                $doc[] = "\t * @var datetime ". $convertField . $comment;
                $doc[] = "\t */";
            } else if(isset($tableInfo['rules']['float'][$field])) {
                $doc[] = "\t/**";
                $doc[] = "\t * @var float ". $convertField . $comment;
                $doc[] = "\t */";
            }
            if(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[] = "use Qii\Driver\Entity\Inf;";
        $classAndProperty[] = "use Qii\Driver\Entity\Base;";
        $classAndProperty[] = "use Qii\Driver\Response;";
        $classAndProperty[] = <<<DOC
/**
 * @method $class where(\$where, \$args = null)
 * @method $class setWhereHooker(\$hooker, \$args = null)
 * @method $class with(\$with, \$args = null)
 * @method $class setWith(\$hooker, \$args = null)
 * @method $class order(\$order, \$args = null)
 * @method $class setOrderHooker(\$hooker, \$args = null)
 * @method $class setHooker(\$key, \$hooker, \$args)
 * @method $class setCacheConfig(\$config)
 * @method $class cache(\$cache, \$args = null)
 * @method $class setCacheHooker(\$hooker, \$args = null)
 * @method $class join(\$join, \$args = null)
 * @method $class setJoinHooker(\$hooker, \$args = null)
 * @method $class limit(\$limit, \$args = null)
 * @method $class setLimitHooker(\$hooker, \$args = null)
 * @method $class or(\$or, \$args = null)
 * @method $class setOrHooker(\$hooker, \$args = null)
 * @method $class field(\$fields, \$args = null)
 * @method $class setQueryFieldsHooker(\$hooker, \$args = null)
 * @method $class get()
 * @method mixed|Response info()
 */
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];
            }
        }
            $classAndProperty[] = <<<DOC
        /**
         * 动态变化的日期，初始化的时候赋值
         */
DOC;
            ;
            $classAndProperty[] = "\tpublic function __construct(){";
            $classAndProperty[] = "\t\tparent::__construct();";
            $classAndProperty[] = "\t}";

            $classAndProperty[] = <<<DOC

        /**
         * 获取表名
         */
        public function getTable(){
            return '{$table}';
        }
DOC;
            $fields = join("', '", $tableInfo['fields']);
            $classAndProperty[] = <<<DOC
    /**
     * 数据表中的所有字段
     */
     public function fields(){
        return array('$fields');
     }
DOC;
            $classAndProperty[] = "\t/**";
            $classAndProperty[] = "\t * 设置默认值";
            $classAndProperty[] = "\t */";
            $classAndProperty[] = "\tpublic function defaultFieldsValue(){";
            $classAndProperty[] = "\t\t\$defaultValue = [];";
            if(count($dynamicProperty) > 0) {
                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}";
        //生成 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', '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 '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);
        $primaryKey = [];
        $autoIncrement = '';
        if(isset($tableInfo['rules']['extra']['auto_increment'])) {
            $primaryKey = $tableInfo['rules']['extra']['auto_increment'];
            $autoIncrement = $tableInfo['rules']['extra']['auto_increment'][0];
        }else if(isset($tableInfo['rules']['pri'])) {
            $primaryKey = $tableInfo['rules']['pri'];
        }
        $uniqueKey = [];
        if(isset($tableInfo['rules']['uniq'])) {
            $uniqueKey = $tableInfo['rules']['uniq'];
        }else{
            $uniqueKey = $primaryKey;
        }
        $next[] = "\t\treturn ". '$rules;';
        $next[] = "\t". '}';
        $next[] = <<<DOC
    /**
     * 自增字段
     * 
     * @return string
     */
     public function autoIncrement(){
        return '$autoIncrement';
     }
DOC;


        $next [] = <<<DOC
    /**
     * unique (unique 如果是 array('a,b') 则表示 a,b联合唯一，array('a,b', 'b,c') 表示a,b唯一 或 b,c唯一
     * 'a,b' 字符串则表示任意唯一 类似于array ('a','b')
     * @return mixed
     * @throws \Exception
     */
    public function uniqueKey(){
        if(\$this->uniqKeys !== null) return \$this->uniqKeys;
DOC;

        if($uniqueKey) {
            $next[] = "\t\treturn array('". join("','", $uniqueKey). "');";
        }else {

            $next[] = <<<DOC
        throw new \Exception('请设置`{$tableInfo['table']}`表更新唯一值');
DOC;
        }
            $next[] = <<<DOC
    }

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

        $next[] = <<<DOC
    }
    /**
     * 保存数据的时候，验证唯一需要排除的值，此处仅支持，单个或联合排除，不支持单个排除
     *
     * @return array
     */
    public function exclude(){
        if(\$this->exclude !== null) return \$this->exclude;
DOC;
    if($primaryKey) {
        $next[] = "\t\treturn array('". join("','", $primaryKey). "');";
    }else{
        $next[] = <<<DOC
    return array();
DOC;
    }
    $next[] = <<<DOC
    }
    /**
     * 添加时验证的字段，自行添加
     */
    public function validFieldsForAdd(){
        \$fields = [];
        return \$this->valid(\$fields);
    }
    
    /**
     * 添加时验证的字段，自行添加
     */
    public function validFieldsForReplace(){
        \$fields = [];
        return \$this->valid(\$fields);
    }
    /**
     * 验证更新时的字段，自行添加
     */
    public function validFieldsForUpdate() {
        \$fields = [];
        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;
    }
    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']);
    }
    /**
     * 转换为属性，属性会将下划线+小写/大写 转换成大写
     *
     * @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])/", 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;
    }
}