<?php

namespace xmvc\libCache;

use Redis;
use xmvc\Cache;
use xmvc\interface\ICache;

/**
* 缓存 - Redis
*/
class CacheRedis implements ICache
{
    protected Redis $redis;
    protected string $guid = '';
    protected string $timesKey = ''; //缓存时间记录键
    protected string $metaKeyPrefix = ''; //元数据记录键前缀
    protected string $groupFile = '';
    protected array $groupData = [];
    protected bool $isLoad = false; // 是否已加载过
    protected bool $isDataChanged = false;

    public function __construct(string $groupName = 'default', array $options = [])
    {
        
        $this->guid = Cache::groupNameGUID($groupName);
        $this->redis = self::connect($options);
        $this->timesKey = $this->guid . '_keyTimes';
        $this->metaKeyPrefix = $this->guid . ':';
        $this->groupFile = DIR_CACHE . '/keyValue/' . $this->guid . '.php';
        if (!$this->redis->exists($this->guid)) {
            $this->loadFromFile($this->groupFile);
        }
    }

    protected static function connect(array $options): Redis
    {
        $options = array_merge([
            'host' => '127.0.0.1',
            'port' => 6379,
        ], $options);
        $netObjectKey = md5(serialize($options));
        if (isset(Cache::$netObjectList[$netObjectKey])) {
            return Cache::$netObjectList[$netObjectKey];
        } else {
            $redis = new Redis();
            try {
                $redis->connect($options['host'], $options['port']);
                if (isset($options['password']) && $options['password'] != '') {
                    $redis->auth($options['password']);
                }
            } catch (\Exception $e) {
                error('Redis ' . $e->getMessage() . ' ' . $options['host'] . ':' . $options['port'], null, [__FILE__]);
            }
            Cache::$netObjectList[$netObjectKey] = $redis;
            return $redis;
        }
    }


    protected function error(string $message)
    {
        error($message, null, [__FILE__]);
    }

    /**
     * 数组是否为纯数字下标
     */
    public function isIntIndexArray(array $array)
    {
        $keys = array_keys($array);
        foreach ($keys as $key) {
            if (!is_int($key)) {
                return false;
            }
        }
        return true;
    }

    /**
     * 加载文件到内存
     *
     * @return self
     */
    public function loadFromFile(string $filename = ''): self
    {
        if ($filename == '') {
            $filename = $this->groupFile;
        }
        if (is_file($filename)) {
            //如果存在文件,则取文件初始化, 例 viewtimes 
            // - 用 Cache::create('file', 'viewtimes') 写
            // - 用 Cache::create('redis', 'viewtimes') 读
            $this->clear()->save(); //先清空再加载
            $fileData = require($filename);
            foreach ($fileData as $key => $value) {
                $this->set($key, $value[0], $value[1]);
            }
            $this->save();
        }
        return $this;
    }

    /**
     * 保存缓存到文件
     *
     * @return self
     */
    public function saveToFile(string $filename = ''): self
    {
        if ($filename == '') {
            $filename = $this->groupFile;
        }
        $values = $this->getAllAndTime();
        arrayToFile($filename, $values);
        return $this;
    }

    protected function validKey(string $key): self
    {
        if (is_null($key) || trim($key) == '') {
            $this->error('不能传入一个空值的key');
        }
        return $this;
    }

    /**
     * 组名是否存在
     */
    public static function groupExists(string $groupName = 'default', string $dns = ''): bool
    {
        $guid = Cache::groupNameGUID($groupName);
        return self::connect($dns ? cfg($dns) : [])->exists($guid . '_keyTimes');
    }

    public function exists(string $key): bool
    {
        $this->validKey($key);
        $metaKey = $this->metaKeyPrefix . $key;
        return (bool)$this->redis->exists($metaKey);
    }

    /**
     * 真实值
     */
    protected function trueValue(string $metaKey, string $hashKey = ''): mixed
    {
        $redisType = $this->redis->type($metaKey);
        
        // 检查是否返回 \Redis 对象
        if (!is_int($redisType)) {
            if ($this->isDataChanged) {
                $this->error(lg('检测到有数据修改, Redis 不能在调用 save() 提交事务之前执行 get() 读操作'));
            } else {
                $this->error(lg('异常错误') . "\r\nredis->type(metaKey = " . $metaKey . ', hashKey = ' . $hashKey . '); return ' . var_export($redisType, true));
            }
            return null; // 返回空值以避免进一步处理
        }

        // 处理正常类型
        switch ($redisType) {
            case Redis::REDIS_STRING:
                $value = $this->redis->get($metaKey);
                $this->unSerialize($value);
                break;
            case Redis::REDIS_LIST:
                if ($hashKey == '') {
                    $value = $this->redis->lRange($metaKey, 0, -1);
                    $this->unSerialize($value);
                } else {
                    if ($hashKey === 'count') {
                        $value = $this->redis->lLen($metaKey);
                    } else {
                        if (strpos($hashKey, '-') !== false) {
                            list($start, $end) = explode('-', $hashKey);
                            $start = $start ? (int)$start : 0;
                            $end = $end ? (int)$end : -1;
                            $value = $this->redis->lRange($metaKey, $start, $end);
                        } else {
                            $value = $this->redis->lIndex($metaKey, $hashKey);
                        }
                        $this->unSerialize($value);
                    }
                }
                break;
            case Redis::REDIS_SET:
                $value = $this->redis->sMembers($metaKey);
                break;
            case Redis::REDIS_ZSET:
                $value = $this->redis->zRange($metaKey, 0, -1);
                break;
            case Redis::REDIS_HASH:
                if ($hashKey == '') {
                    $value = $this->redis->hGetAll($metaKey);
                } else {
                    $value = $this->redis->hGet($metaKey, $hashKey);
                }
                $this->unSerialize($value);
                break;
            case Redis::REDIS_NOT_FOUND:
                $value = null; // NOT FOUND
            default:
                $this->error(lg('未知的 Redis 类型: ') . $redisType);
        }
        return $value;
    }

    protected function serialize(mixed $value): string
    {
        if (is_array($value) || is_object($value)) {
            $value = 'SERIALIZE::' . serialize($value);
        }
        return $value;
    }

    protected function unSerialize(string|array|Redis &$value): void
    {
        if (is_array($value)) {
            foreach ($value as $k => $v) {
                $this->unSerialize($value[$k]);
            }
        } elseif (is_string($value) && substr($value, 0, 11) === 'SERIALIZE::') {
            $value =  unserialize(substr($value, 11));
        }
    }

    /**
     * 取值
     * - get('items.0') 取数字下标数组第一个元素
     * - get('items.count') 取数字下标数组的长度
     * - get('user.name') 取数组指定键的值
     * @param string $key
     * @return mixed
     */
    public function get(string $key): mixed
    {
        $this->validKey($key);
        $hashKey = '';
        if (strpos($key, '.') > 0) {
            list($key, $hashKey) = explode('.', $key);
        }
        if ($this->exists($key)) {
            $metaKey = $this->metaKeyPrefix . $key;
            if ($hashKey != '') {
                return $this->trueValue($metaKey, $hashKey);
            } else {
                return $this->trueValue($metaKey);
            }
        } else {
            return null;
        }
    }

    public function getTime(string $key): int
    {
        $hashKey = '';
        if (strpos($key, '.') > 0) {
            list($key, $hashKey) = explode('.', $key);
        }
        $this->validKey($key);
        if ($this->exists($key)) {
            $metaKey = $this->metaKeyPrefix . $key;
            $value = $this->redis->zScore($this->timesKey, $metaKey);
            return $value;
        } else {
            return 0;
        }
    }

    public function getBoth(string $key): array
    {
        $this->validKey($key);
        if ($this->exists($key)) {
            return [$this->get($key), $this->getTime($key)];
        } else {
            return [null, 0];
        }
    }

    public function getAll(): array
    {
        $keys = $this->keys();
        if (!empty($keys)) {
            $values = [];
            foreach ($keys as  $key) {
                $values[substr($key, strlen($this->metaKeyPrefix))] = $this->trueValue($key);
            }
            return $values;
        }
        return [];
    }

    public function getAllAndTime(): array
    {
        $keys = $this->keys();
        if (!empty($keys)) {
            $result = [];
            foreach ($keys as  $metaKey) {
                $key = substr($metaKey, strlen($this->metaKeyPrefix));
                $time = $this->getTime($key);
                $result[$key] = [$this->trueValue($metaKey), $time];
            }
            return $result;
        }
        return [];
    }

    public function set(string $key, $data, int $time = null): self
    {
        $this->validKey($key);
        $this->dataChange();
        $hashKey = '';
        if (strpos($key, '.') > 0) {
            list($key, $hashKey) = explode('.', $key);
        }
        $metaKey = $this->metaKeyPrefix . $key;
        if ($hashKey == '') {
            if (is_array($data) || is_object($data)) {
                if ($this->isIntIndexArray($data)) {
                    // 纯数字下标数组存 list 结构
                    foreach ($data as $v) {
                        $v = $this->serialize($v);
                        $this->redis->rPush($metaKey, $v);
                    }
                } else {
                    // 对象 或者 是数组混合下标存 hash 结构
                    $this->hSetArray($metaKey, $data);
                }
            } else {
                $this->redis->set($metaKey, $data);
            }
        } else {
            $data = $this->serialize($data);
            if (is_numeric($hashKey)) {
                // 数字下标存 list, set('items.0')
                $this->redis->lSet($metaKey, $hashKey, $data);
            } else {
                // 非数字下标存 hash, set('user.name')
                $this->redis->hSet($metaKey, $hashKey, $data);
            }
        }
        if (is_null($time)) {
            $time = time();
        }
        $this->redis->zAdd($this->timesKey, $time, $metaKey);    // 使用有序集合记录键的写入时间     
        return $this;
    }

    protected function hSetArray(string $metaKey, $data)
    {
        $mData = [];
        foreach ($data as $key => $value) {
             $mData[$key] = $this->serialize($value);
        }
        $this->redis->hMSet($metaKey, $mData);
    }

    /**
     * 删除指定 key 的缓存单元
     *
     * @param string $key 键名
     */
    public function remove(string $key): self
    {
        $this->validKey($key);
        $hashKey = '';
        if (strpos($key, '.') > 0) {
            list($key, $hashKey) = explode('.', $key);
        }
        $metaKey = $this->metaKeyPrefix . $key;
        if ($this->exists($key)) {
            $this->dataChange();
            if ($hashKey == '') {
                $this->redis->del($metaKey);
                $this->redis->zRem($this->timesKey, $key);  // 删除有序集合中对应的成员
            } else {
                $this->redis->hDel($metaKey, $hashKey);
            }
        }
        return $this;
    }

    public function keys(): array
    {
        $this->save();
        $iterator = null; // 初始化迭代器, 游标为 0
        $pattern = $this->metaKeyPrefix . '*'; // 要匹配的模式  
        $count = 200; // 每次迭代返回的键的数量        
        do {
            $keys = $this->redis->scan($iterator, $pattern, $count);
        } while ($iterator !== 0); // 当迭代器返回 0 时，表示迭代完成 
        return $keys;
    }

    public function clear(): self
    {
        $keys = $this->keys();
        $this->dataChange();
        if (!empty($keys)) {
            $this->redis->del($keys);
        }
        $this->redis->del($this->timesKey);
        return $this;
    }

    public function dataChange()
    {
        if (!$this->isDataChanged) {
            $this->isDataChanged = true;
            $this->redis->multi(); //开启事务
        }
    }

    public function save(): self
    {
        if ($this->isDataChanged) {
            $this->redis->exec(); //执行事务
            $this->isDataChanged = false;
        }
        return $this;
    }
}

