<?php

namespace Velis\Model\DataObject;

use LogicException;
use Velis\App;
use Velis\Arrays;
use Velis\Model\Cacheable;
use Velis\Model\DataObject;
use Velis\Model\ItemCacheable;

/**
 * Cache functionality trait
 * @author Olek Procki <olo@velis.pl>
 */
trait CacheTrait
{
    /**
     * Cache buffer
     * @var array
     */
    protected static $_cacheBuffer = [];


    /**
     * Returns cached object list (Must implement Cacheable interface)
     *
     * @param mixed $objectId
     * @return static[]|void
     * @throws LogicException
     */
    public static function listCached($objectId = null)
    {
        $class = get_called_class();
        $instance = new static();

        if (!$instance instanceof Cacheable) {
            throw new LogicException($class . ' is not instance of \Velis\Model\Cacheable!');
        }

        $cacheName = $instance->_getListCacheName();

        if (!array_key_exists($cacheName, self::$_cacheBuffer)) {
            self::$_cacheBuffer[$cacheName] = App::$cache[$cacheName];

            if (self::$_cacheBuffer[$cacheName] === null) {
                self::$_cacheBuffer[$cacheName] = static::listForCache();
                App::$cache[$cacheName] = self::$_cacheBuffer[$cacheName];
            }
        }

        if (isset($objectId) && !is_array($objectId)) {
            if (static::$_listAssocKey === false) {
                $object = Arrays::find(self::$_cacheBuffer[$cacheName], $instance->_getPrimaryKeyField(), $objectId);
            } else {
                $object = self::$_cacheBuffer[$cacheName][$objectId] ?? null;
            }
            if (!empty($object)) {
                return is_object($object) && get_class($object) == $class ? $object : new $class($object);
            }
        } else {
            $list = [];

            if (!is_iterable(self::$_cacheBuffer[$cacheName])) {
                return $list;
            }

            foreach (self::$_cacheBuffer[$cacheName] as $key => $item) {
                $list[$key] = is_object($item) && get_class($item) == $class ? $item : new $class($item);
            }
            return $list;
        }
    }


    /**
     * Return list for listCached
     * @return static[]
     */
    protected static function listForCache()
    {
        return static::listAll();
    }


    /**
     * Unsets cache of objects list (Must implement Cacheable)
     * @throws LogicException
     */
    public static function unsetListCache()
    {
        $instance = new static();
        if (!$instance instanceof Cacheable) {
            throw new LogicException(static::class . ' is not instance of ' . Cacheable::class . '!');
        }

        $cacheName = $instance->_getListCacheName();

        unset(App::$cache[$cacheName]);
    }


    /**
     * Unsets cache buffer
     */
    public static function unsetCacheBuffer()
    {
        $instance = new static();
        $instance->unsetEntityCacheBuffer();
    }

    public function unsetEntityCacheBuffer()
    {
        if ($this instanceof ItemCacheable) {
            unset(self::$_cacheBuffer[$this->_getItemCacheName()]);
        }

        unset(self::$_cacheBuffer[$this->_getListCacheName()]);
    }

    public function unsetCache()
    {
        $this->unsetEntityCacheBuffer();

        if ($this instanceof ItemCacheable) {
            unset(App::$cache[$this->_getItemCacheName()]);
        }

        unset(App::$cache[$this->_getListCacheName()]);
    }

    /**
     * Returns cached instance(s) (Must implement Cacheable)
     *
     * @param mixed $objectId
     * @return static|array<static>|void
     * @throws LogicException
     */
    public static function get($objectId)
    {
        $instance = new static();
        if (!$instance instanceof Cacheable) {
            throw new LogicException(static::class . ' is not instance ' . Cacheable::class . '!');
        }

        $primaryKey = $instance->_getPrimaryKeyField();

        if ($instance instanceof ItemCacheable) {
            if (!is_array($objectId)) {
                $objectIds = [$objectId];
            } else {
                $objectIds = $objectId;
            }

            $result = [];
            $notFoundIds = [];

            foreach ($objectIds as $id) {
                $itemKey = static::getItemCacheName($id);
                if (!App::$cache->has($itemKey)) {
                    $notFoundIds[$itemKey] = $id;
                } elseif (is_array($primaryKey)) {
                    $result[] = App::$cache[$itemKey];
                } else {
                    $result[$id] = App::$cache[$itemKey];
                }
            }

            if ($notFoundIds) {
                /**
                 * @var int|string $id
                 * @var static $object
                 */
                foreach (static::bufferedInstance(array_values($notFoundIds)) as $id => $object) {
                    App::$cache[$object->_getItemCacheName()] = $object;
                    if (is_array($primaryKey)) {
                        $result[] = $object;
                    } else {
                        $result[$id] = $object;
                    }
                }
            }

            foreach ($result as &$object) {
                $object = static::getStaticClassInstance($object);
            }

            if (is_array($objectId) && (!is_array($primaryKey) || is_array(reset($objectId)))) {
                return $result;
            } else {
                return reset($result);
            }
        } else {
            $objectsList = [];

            if (!is_array($objectId)) {
                $object = self::listCached($objectId);

                if (isset($objectId) && !empty($object)) {
                    return static::getStaticClassInstance($object);
                }
            } else {
                // for multiple scalar keys it is more efficient to iterate through id list
                if (!is_array($primaryKey)) {
                    foreach ($objectId as $itemId) {
                        if (empty($itemId)) {
                            continue;
                        }
                        if ($object = self::listCached($itemId)) {
                            $objectsList[$object[$primaryKey]] = static::getStaticClassInstance($object);
                        }
                    }
                } else {
                    foreach (self::listCached() as $object) {
                        if ($object->id() == $objectId) {
                            return static::getStaticClassInstance($object);
                        }
                    }
                }
                return $objectsList;
            }
        }
    }

    /**
     * Returns value name for cached list
     * @return string
     */
    protected function _getListCacheName()
    {
        $cacheName = $this->_getListDatasource();
        if (strpos($cacheName, ' ')) {
            $cacheName = substr($cacheName, 0, strpos($cacheName, ' '));
        }

        return str_replace(['.', '_tab'], ['_', ''], $cacheName) . '_list';
    }


    /**
     * Returns cache name for single cache item
     * @return string
     */
    protected function _getItemCacheName()
    {
        return static::getItemCacheName($this->id());
    }


    /**
     * Returns cache name for single cache item by $id
     * @param array|string|int $id
     * @return string
     */
    public static function getItemCacheName($id)
    {
        $instance = new static();
        $primaryKey = $instance->_getPrimaryKeyField();

        if (is_array($primaryKey) && is_array($id)) {
            $id = array_map(function ($a) {
                return (string)$a;
            }, $id);
            $key = md5(serialize($id));
        } else {
            $key = $id;
        }

        return $instance->_getListCacheName() . '-' . $key;
    }

    /**
     * Returns value name for cached list for $entity
     *
     * @param DataObject $entity (must implement _getListCacheName())
     * @return string
     */
    public static function getListCacheName(?DataObject $entity)
    {
        if ($entity === null) {
            $entity = new static();
        }

        return $entity->_getListCacheName();
    }


    /**
     * Checks cache file existence and generates it
     * @throws LogicException
     */
    public static function regenerateCache()
    {
        $instance = new static();
        if (!($instance instanceof Cacheable)) {
            throw new LogicException(static::class . ' is not instance ' . Cacheable::class . '!');
        }

        $cacheName = $instance->_getListCacheName();

        if (!isset(App::$cache[$cacheName])) {
            $list = self::executeNestedQuery(fn () => static::listForCache());
            App::$cache[$cacheName] = $list;
        }
    }


    /**
     * @return void
     * @throws LogicException
     */
    public function updateCachedItem()
    {
        if (!($this instanceof Cacheable)) {
            throw new LogicException(static::class . ' is not instance ' . Cacheable::class . '!');
        }

        $cacheName = $this->_getListCacheName();
        if (App::$cache[$cacheName]) {
            $cacheContent = App::$cache[$cacheName];
            $cacheContent[$this->id()] = $this;
            App::$cache[$cacheName] = $cacheContent;
            self::$_cacheBuffer[$cacheName] = $cacheContent;
        }
    }
}
