<?php
namespace Qii\Library;
/**
 * JSON和PHP类互转
 * @author zjh
 *
 * 使用方法：
 *
 * $json2PHP = new \Qii\Library\Json2PHP();
 * $json2PHP->json2PHP(dirname(__FILE__, 2) . "/entity/Express", 'namespace entity\Express', 'Application', $jsonStr);
 * $class = $json2PHP->jsonMarshaller(new \entity\Express\Application(), $jsonStr);
 * print_r($class);
 */
class Json2PHP {
    public $useType = false;
    public function __construct() {

    }
    /**
     * JSON decode
     * @param string $json json数据
     * @return mixed
     */
    public static function decodeJson($json) {
        return json_decode($json);
    }
    /**
     * JSON 数据绑定 PHP 类属性上
     * @param object $class 实例化的类
     * @param object $obj json_decode({})的值，不要要指定第二个参数为true
     * @return mixed
     * @throws \Exception
     */
    public function jsonMarshaller($class, $json){
        return $this->jsonBindPHP($class, self::decodeJson($json));
    }

    /**
     * JSON 绑定到 PHP 类属性上
     * @param object $class PHP类
     * @param object $obj json object
     * @return mixed
     * @throws \Exception
     */
    protected function jsonBindPHP($class, $obj) {
        if(gettype($class) != "object") {
            throw new \Exception('第一个参数必须为类');
        }
        $classInfo = $this->classInfo($class);
        $keys = $classInfo['keys'];
        if(is_array($obj)){} {
            foreach ($obj as $key => $value) {
                $key = ucfirst($key);
                if(isset($keys[$key])) {
                    if (in_array($keys[$key]->Type, ['string', 'int', 'boolean'])) {
                        //echo "1:". $key  .'-> '. $keys[$key]->Type . "\n";
                        $class->$key = $value;
                    }else if($this->typeOf($value) == 'array' && in_array($keys[$key]->Type, ['mixed'])){} {
                        if (in_array($keys[$key]->Type, ['string', 'integer', 'boolean', "double"])) {
                            //echo "1:". $key  .'-> '. $keys[$key]->Type . "\n";
                            $class->$key = $value;
                            continue;
                        }
                        $className = $classInfo['namespace'] . '\\' . $key;
                        //echo "2:". $keys[$key]->Type ."-". $className . '='. $this->typeOf($value) . "\n";
                        if(class_exists($className, false)) {
                            if($this->typeOf($value) == 'array') {
                                foreach ($value as $v) {
                                    $class->$key[] = $this->jsonBindPHP(new $className(), $v);
                                }
                            }else{
                                $class->$key = $this->jsonBindPHP(new $className(), $value);
                            }

                        }
                    }
                }
            }
        }
        return $class;
    }

    /**
     * 获取类所有属性，类属性是public并且大写字母开头同时不能为静态名
     *
     * @return array
     * @throws ReflectionException
     */
    public function classInfo($class) {
        $keys = [];
        $method = new \ReflectionClass($class);
        $properties = $method->getproperties();
        $keys['namespace'] = $method->getNamespaceName();
        foreach ($properties as $property) {
            if($property->isPublic() && preg_match("/^[A-Z]+/", $property->getName()) && !$property->isStatic()) {
                $doc = $property->getDocComment();
                $keys['keys'][$property->getName()] = (object) ["Type" => $this->getPropertyFromDoc($doc), "Variable" => $this->getTypeFromDoc($doc), "Doc" =>  $doc];
            }
        }
        return $keys;
    }

    /**
     * 获取变量类型
     *
     * @param string $doc 注释
     * @return string
     */
    public function getTypeFromDoc($doc) {
        if(preg_match("/[$](.*)=(.*)/", $doc, $match)) {
            return $match[2];
        }
        return '';
    }
    /**
     * 获取变量的类型
     *
     * @param string $doc 文档
     * @return string
     */
    public function getPropertyFromDoc($doc) {
        if(preg_match("/@var\s+(.*)\s+(.*)/U", $doc, $match)) {
            return $match[1];
        }
        return '';
    }

    /**
     * JSON 转 PHP 类
     * @param string $path 保存路径
     * @param string $namespace 名字空间
     * @param string $json json 数据
     * @param $className
     * @return void
     */
    public function json2PHP($path, $namespace, $className, $json) {
        if($className == '') {
            $className = "Application";
        }
        $className = ucfirst($className);
        $php = $this->generateJson2PHP($namespace, $className, $json);
        file_put_contents($path . "/". $className .".php", join("\n", $php));
    }
    /**
     * 生成 json 转 php类
     *
     * @param string $namespace 命名空间
     * @param string $className 类名
     * @param string $json json数据
     * @return array
     * @date
     */
    protected function generateJson2PHP($namespace, $className, $json) {
        $obj = self::decodeJson($json);
        $line = $this->createClass($className, $obj);
        $date = date('Y-m-d H:i:s');
        array_unshift($line,
            $this->comment([
                "this file auto create by generateJson2PHP, please do not edit by yourself",
                "",
                "@author zjh",
                "@date {$date}",
            ]) . "\n"
        );
        array_unshift($line, $namespace. ";");
        array_unshift($line, "<?php");
        return $line;
    }
    /**
     * @param string $name 类名
     * @param mixed $values json值
     * @param bool $isArray false 数组只使用第一个 true 使用所有的
     * @return array
     */
    protected function createClass($name, $values, $isArray = false) {
        $name = ucfirst($name);
        $line = [];
        $line[] = $this->comment([
            "class {$name}"
        ], 1);
        $line[] = "\tclass {$name} {";
        if($isArray) {
            $values = $values[0];
        }
        if(is_array($values) || is_object($values)) {
            foreach ($values as $key => $value) {
                $type = $this->typeOf($value);
                if($type == 'array' || $type == 'object') {
                    $isArray = $type == 'array';
                    $class = $this->createClass($key, $value, $type == 'array');
                    array_unshift($line, join("\n", $class));
                    $type = ucfirst($key);
                    if ($isArray) {
                        $line[] = $this->comment([
                                "\${$key} = array",
                                "@var {$type} \${$key}",
                                ""
                            ]
                            , 2);
                    }else{
                        $line[] = $this->comment([
                                "\${$key} = object",
                                "@var {$type} \${$key}",
                                ""
                            ]
                            , 2);
                    }
                }else{
                    $line[] = $this->comment([
                        "@var {$type} \${$key}",
                        ""
                    ], 2);
                }
                $line[] ="\t\t" .  $this->format('public '. $this->getType($type, $this->useType) .' $'. $key .";");
            }
        }else{
            $type = $this->typeOf($values);
            $line[] = $this->comment([
                "@var {$type} \${$values}",
                ""
            ], 2);
            $line[] = "\t\t". $this->format('public '. $this->getType($type, $this->useType) .' $'. $values .";");
        }
        $line[] = $this->comment([
            "魔术方法:使用get变量名获取变量的值"
        ], 2);
        $line[] = "\t\tpublic function __call(\$name, \$args){";
        $line[] = "\t\t\t". 'if(preg_match("/^(get).?/i", $name)){';
        $line[] = "\t\t\t\t". '$variable = substr($name, 3);';
        $line[] = "\t\t\t\t". 'if(isset($this->$variable)) {';
        $line[] = "\t\t\t\t\t". 'return $this->$variable;';
        $line[] = "\t\t\t\t". '}';
        $line[] = "\t\t\t}";
        $line[] = "\t\t\t". 'throw new \Exception("Call to undefined method " . __CLASS__ . "::" . $name . "(". var_export($args, true) .")");';
        $line[] = "\t\t}";

        $line[] = "\t}\n";
        return $line;
    }
    /**
     * 去掉两个空格
     *
     * @param $str
     * @return array|string|string[]
     */
    public function format($str) {
        return preg_replace("/\s{2}/", " ", $str);
    }
    /**
     * 注释
     * @param array $arr 行注释
     * @param int $space 占几tab个位置
     * @return string
     */
    public function comment($arr, $space = 1) {
        $line = [];
        $append = str_pad("", $space, "\t");
        $line[] = $append . "/**";
        foreach($arr as $value) {
            $line[] = $append ." * ". $value;
        }
        $line[] = $append . "*/";
        return join("\n", $line);
    }
    /**
     * 返回值的类型  (boolean, integer, double, float, string, array, object, resource)
     *
     * @param $val
     * @return string
     */
    public function typeOf($val) {
        if(is_bool($val)) {
            return 'boolean';
        }
        if(is_int($val)) {
            return 'integer';
        }
        if(is_double($val)) {
            return 'double';
        }
        if(is_float($val)) {
            return 'float';
        }
        if(is_string($val)) {
            return 'string';
        }
        if(is_array($val)) {
            return 'array';
        }
        if(is_object($val)) {
            return 'object';
        }
        if(is_resource($val)) {
            return 'resource';
        }
        return 'unknown';
    }
    /**
     * 获取强制类型的定义 int, float, bool, string, array, object
     * @param string $type 类型
     * @params bool $force 是否强制类型转换
     * @return string
     */
    public function getType($type, $userType = true) {
        if(!$userType || version_compare(PHP_VERSION, '7.4.0', '<')) {
            return '';
        }
        $type = ["boolean","integer","double","string","array","object","resource"];
        if(!in_array($type, $allow)) {
            return '';
        }
        switch ($type) {
            case 'integer':
                return 'int';
            case 'double':
                return 'float';
            case 'boolean':
                return 'bool';
        }
        return $type;
    }
}