<?php

namespace Codeages\Biz\Framework\Dao;

use Codeages\Biz\Framework\Dao\Annotation\MetadataReader;

class DaoProxy
{
    /**
     * @var ContainerInterface
     */
    protected $container;

    /**
     * @var GeneralDaoInterface
     */
    protected $dao;

    /**
     * @var SerializerInterface
     */
    protected $serializer;

    /**
     * @var CacheStrategy
     */
    protected $cacheStrategy;

    /**
     * @var ArrayStorage
     */
    protected $arrayStorage;

    /**
     * @var MetadataReader
     */
    protected $metadataReader;

    public function __construct($container, DaoInterface $dao, MetadataReader $metadataReader, SerializerInterface $serializer, ArrayStorage $arrayStorage = null)
    {
        $this->container = $container;
        $this->dao = $dao;
        $this->metadataReader = $metadataReader;
        $this->serializer = $serializer;
        $this->arrayStorage = $arrayStorage;
    }

    public function __call($method, $arguments)
    {
        $proxyMethod = $this->getProxyMethod($method);
        if ($proxyMethod) {
            return $this->$proxyMethod($method, $arguments);
        } else {
            return $this->callRealDao($method, $arguments);
        }
    }

    protected function getProxyMethod($method)
    {
        foreach (['get', 'find', 'search', 'count', 'create', 'batchCreate', 'batchUpdate', 'batchDelete', 'update', 'wave', 'delete'] as $prefix) {
            if (0 === strpos($method, $prefix)) {
                return $prefix;
            }
        }

        return null;
    }

    /**
     * 代理 get 开头的方法调用
     *
     * @param string $method    被调用的 Dao 方法名
     * @param array  $arguments 调用参数
     *
     * @return array|null
     */
    protected function get($method, $arguments)
    {
        $lastArgument = end($arguments);
        reset($arguments);

        // lock模式下，因为需要借助mysql的锁，不走cache
        if (is_array($lastArgument) && isset($lastArgument['lock']) && true === $lastArgument['lock']) {
            $row = $this->callRealDao($method, $arguments);
            $this->unserialize($row);

            return $row;
        }

        if ($this->arrayStorage) {
            $key = $this->getCacheKey($this->dao, $method, $arguments);
            if (!empty($this->arrayStorage[$key])) {
                return $this->arrayStorage[$key];
            }
        }

        $strategy = $this->buildCacheStrategy();
        if ($strategy) {
            $cache = $strategy->beforeQuery($this->dao, $method, $arguments);
            // 命中 cache, 直接返回 cache 数据
            if (false !== $cache) {
                return $cache;
            }
        }

        $row = $this->callRealDao($method, $arguments);
        $this->unserialize($row);

        // 将结果缓存至 ArrayStorage
        $this->arrayStorage && ($this->arrayStorage[$this->getCacheKey($this->dao, $method, $arguments)] = $row);

        if ($strategy) {
            $strategy->afterQuery($this->dao, $method, $arguments, $row);
        }

        return $row;
    }

    protected function find($method, $arguments)
    {
        return $this->search($method, $arguments);
    }

    protected function search($method, $arguments)
    {
        $strategy = $this->buildCacheStrategy();
        if ($strategy) {
            $cache = $strategy->beforeQuery($this->dao, $method, $arguments);
            if (false !== $cache) {
                return $cache;
            }
        }

        $rows = $this->callRealDao($method, $arguments);

        if (!empty($rows)) {
            $this->unserializes($rows);
        }

        if ($strategy) {
            $strategy->afterQuery($this->dao, $method, $arguments, $rows);
        }

        return $rows;
    }

    protected function count($method, $arguments)
    {
        $strategy = $this->buildCacheStrategy();
        if ($strategy) {
            $cache = $strategy->beforeQuery($this->dao, $method, $arguments);
            if (false !== $cache) {
                return $cache;
            }
        }

        $count = $this->callRealDao($method, $arguments);

        if ($strategy) {
            $strategy->afterQuery($this->dao, $method, $arguments, $count);
        }

        return $count;
    }

    protected function create($method, $arguments)
    {
        $declares = $this->dao->declares();

        $generator = $this->getIdGenerator();
        if ($generator) {
            $id = $arguments[0]['id'] = $generator->generate();
        }

        $time = time();

        if (isset($declares['timestamps'][0])) {
            $arguments[0][$declares['timestamps'][0]] = $time;
        }

        if (isset($declares['timestamps'][1])) {
            $arguments[0][$declares['timestamps'][1]] = $time;
        }

        $this->serialize($arguments[0]);
        $row = $this->callRealDao($method, $arguments);
        $this->unserialize($row);

        $this->arrayStorage && $this->arrayStorage->flush();

        $strategy = $this->buildCacheStrategy();
        if ($strategy) {
            $this->buildCacheStrategy()->afterCreate($this->dao, $method, $arguments, $row);
        }

        return $row;
    }

    protected function batchCreate($method, $arguments)
    {
        $declares = $this->dao->declares();

        end($arguments);
        $lastKey = key($arguments);
        reset($arguments);

        if (!is_array($arguments[$lastKey])) {
            throw new DaoException('batchCreate method arguments last element must be array type');
        }

        $time = time();
        $rows = $arguments[$lastKey];

        $generator = $this->getIdGenerator();

        foreach ($rows as &$row) {
            if ($generator) {
                $row['id'] = $generator->generate();
            }

            if (isset($declares['timestamps'][0])) {
                $row[$declares['timestamps'][0]] = $time;
            }

            if (isset($declares['timestamps'][1])) {
                $row[$declares['timestamps'][1]] = $time;
            }

            $this->serialize($row);
            unset($row);
        }

        $arguments[$lastKey] = $rows;

        $result = $this->callRealDao($method, $arguments);

        $this->flushTableCache();

        return $result;
    }

    protected function batchUpdate($method, $arguments)
    {
        $declares = $this->dao->declares();

        $time = time();
        $rows = $arguments[1];

        foreach ($rows as &$row) {
            if (isset($declares['timestamps'][1])) {
                $row[$declares['timestamps'][1]] = $time;
            }

            $this->serialize($row);
        }

        $arguments[1] = $rows;

        $result = $this->callRealDao($method, $arguments);

        $this->flushTableCache();

        return $result;
    }

    protected function batchDelete($method, $arguments)
    {
        $result = $this->callRealDao($method, $arguments);

        $this->flushTableCache();

        return $result;
    }

    protected function wave($method, $arguments)
    {
        $result = $this->callRealDao($method, $arguments);

        $this->arrayStorage && $this->arrayStorage->flush();

        $strategy = $this->buildCacheStrategy();
        if ($strategy) {
            $this->buildCacheStrategy()->afterWave($this->dao, $method, $arguments, $result);
        }

        return $result;
    }

    protected function update($method, $arguments)
    {
        $declares = $this->dao->declares();

        end($arguments);
        $lastKey = key($arguments);
        reset($arguments);

        if (!is_array($arguments[$lastKey])) {
            throw new DaoException('update method arguments last element must be array type');
        }

        if (isset($declares['timestamps'][1])) {
            $arguments[$lastKey][$declares['timestamps'][1]] = time();
        }

        $this->serialize($arguments[$lastKey]);

        $row = $this->callRealDao($method, $arguments);

        if (is_array($row)) {
            $this->unserialize($row);
        }

        if (!is_array($row) && !is_numeric($row) && !is_null($row)) {
            throw new DaoException('update method return value must be array type or int type');
        }

        $this->arrayStorage && $this->arrayStorage->flush();

        $strategy = $this->buildCacheStrategy();
        if ($strategy) {
            $this->buildCacheStrategy()->afterUpdate($this->dao, $method, $arguments, $row);
        }

        return $row;
    }

    protected function delete($method, $arguments)
    {
        $result = $this->callRealDao($method, $arguments);

        $this->arrayStorage && $this->arrayStorage->flush();

        $strategy = $this->buildCacheStrategy();
        if ($strategy) {
            $this->buildCacheStrategy()->afterDelete($this->dao, $method, $arguments);
        }

        return $result;
    }

    protected function callRealDao($method, $arguments)
    {
        return call_user_func_array([$this->dao, $method], $arguments);
    }

    protected function unserialize(&$row)
    {
        if (empty($row) || !is_array($row)) {
            return;
        }

        $declares = $this->dao->declares();
        $serializes = empty($declares['serializes']) ? [] : $declares['serializes'];

        foreach ($serializes as $key => $method) {
            if (!array_key_exists($key, $row)) {
                continue;
            }

            $row[$key] = $this->serializer->unserialize($method, $row[$key]);
        }
    }

    protected function unserializes(array &$rows)
    {
        foreach ($rows as &$row) {
            $this->unserialize($row);
        }
    }

    protected function serialize(&$row)
    {
        $declares = $this->dao->declares();
        $serializes = empty($declares['serializes']) ? [] : $declares['serializes'];

        foreach ($serializes as $key => $method) {
            if (!array_key_exists($key, $row)) {
                continue;
            }

            $row[$key] = $this->serializer->serialize($method, $row[$key]);
        }
    }

    protected function getIdGenerator()
    {
        $declares = $this->declares();
        if (empty($declares['id_generator'])) {
            return null;
        }

        return $this->container['dao.id_generator.'.$declares['id_generator']];
    }

    private function flushTableCache()
    {
        $this->arrayStorage && ($this->arrayStorage->flush());

        $strategy = $this->buildCacheStrategy();
        if ($strategy) {
            $this->buildCacheStrategy()->flush($this->dao);
        }
    }

    /**
     * @return CacheStrategy|null
     */
    private function buildCacheStrategy()
    {
        if (!empty($this->cacheStrategy)) {
            return $this->cacheStrategy;
        }

        if (empty($this->container['dao.cache.enabled'])) {
            return null;
        }

        if (!empty($this->container['dao.cache.annotation'])) {
            $strategy = $this->getCacheStrategyFromAnnotation($this->dao);
            if ($strategy) {
                return $strategy;
            }
        }

        $declares = $this->dao->declares();

        // 未指定 cache 策略，则使用默认策略
        if (!isset($declares['cache'])) {
            return $this->container['dao.cache.strategy.default'];
        }

        // 针对某个 Dao 关闭 Cache
        if (false === $declares['cache']) {
            return null;
        }

        // 针对某个 Dao 指定 Cache 策略
        $strategyServiceId = 'dao.cache.strategy.'.strtolower($declares['cache']);
        if (!isset($this->container[$strategyServiceId])) {
            throw new DaoException('Dao %s cache strategy is not defined, please define first in biz container use %s service id.', get_class($this->dao), $strategyServiceId);
        }

        return $this->container[$strategyServiceId];
    }

    private function getCacheStrategyFromAnnotation($dao)
    {
        $metadata = $this->metadataReader->read($dao);
        if (empty($metadata)) {
            return null;
        }

        $strategyServiceId = 'dao.cache.strategy.'.strtolower($metadata['strategy']);
        if (!isset($this->container[$strategyServiceId])) {
            throw new DaoException('Dao %s cache strategy is not defined, please define first in biz container use %s service id.', get_class($this->dao), $strategyServiceId);
        }

        return $this->container[$strategyServiceId];
    }

    private function getCacheKey(GeneralDaoInterface $dao, $method, $arguments)
    {
        $key = sprintf('dao:%s:%s:%s', $dao->table(), $method, json_encode($arguments));

        return $key;
    }
}
