<?php
/**
 *
 * This file is part of phpFastCache.
 *
 * @license MIT License (MIT)
 *
 * For full copyright and license information, please see the docs/CREDITS.txt file.
 *
 * @author Khoa Bui (khoaofgod)  <khoaofgod@gmail.com> http://www.phpfastcache.com
 * @author Georges.L (Geolim4)  <contact@geolim4.com>
 *
 */

namespace phpFastCache\Core\Pool;

use phpFastCache\Core\Item\ExtendedCacheItemInterface;
use phpFastCache\EventManager;
use phpFastCache\Exceptions\phpFastCacheInvalidArgumentException;
use phpFastCache\Exceptions\phpFastCacheLogicException;
use Psr\Cache\CacheItemInterface;


trait ExtendedCacheItemPoolTrait
{
    use CacheItemPoolTrait;

    /**
     * @inheritdoc
     */
    public function getItemsAsJsonString(array $keys = [], $option = 0, $depth = 512)
    {
        $callback = function (CacheItemInterface $item) {
            return $item->get();
        };
        return json_encode(array_map($callback, array_values($this->getItems($keys))), $option, $depth);
    }

    /**
     * @inheritdoc
     */
    public function getItemsByTag($tagName)
    {
        if (is_string($tagName)) {
            $driverResponse = $this->getItem($this->getTagKey($tagName));
            if ($driverResponse->isHit()) {
                $items = (array)$driverResponse->get();

                /**
                 * getItems() may provides expired item(s)
                 * themselves provided by a cache of item
                 * keys based stored the tag item.
                 * Therefore we pass a filter callback
                 * to remove the expired Item(s) provided by
                 * the item keys passed through getItems()
                 *
                 * #headache
                 */
                return array_filter($this->getItems(array_unique(array_keys($items))), function (ExtendedCacheItemInterface $item) {
                    return $item->isHit();
                });
            } else {
                return [];
            }
        } else {
            throw new phpFastCacheInvalidArgumentException('$tagName must be a string');
        }
    }

    /**
     * @inheritdoc
     */
    public function getItemsByTags(array $tagNames)
    {
        $items = [];
        foreach (array_unique($tagNames) as $tagName) {
            if (is_string($tagName)) {
                $items = array_merge($items, $this->getItemsByTag($tagName));
            } else {
                throw new phpFastCacheInvalidArgumentException('$tagName must be a a string');
            }
        }

        return $items;
    }


    /**
     * @inheritdoc
     */
    public function getItemsByTagsAll(array $tagNames)
    {
        $items = $this->getItemsByTags($tagNames);

        foreach ($items as $key => $item) {
            if (array_diff($tagNames, $item->getTags())) {
                unset($items[ $key ]);
            }
        }

        return $items;
    }


    /**
     * @inheritdoc
     */
    public function getItemsByTagsAsJsonString(array $tagNames, $option = 0, $depth = 512)
    {
        $callback = function (CacheItemInterface $item) {
            return $item->get();
        };

        return json_encode(array_map($callback, array_values($this->getItemsByTags($tagNames))), $option, $depth);
    }

    /**
     * @inheritdoc
     */
    public function deleteItemsByTag($tagName)
    {
        if (is_string($tagName)) {
            $return = null;
            foreach ($this->getItemsByTag($tagName) as $item) {
                $result = $this->deleteItem($item->getKey());
                if ($return !== false) {
                    $return = $result;
                }
            }

            return $return;
        } else {
            throw new phpFastCacheInvalidArgumentException('$tagName must be a string');
        }
    }

    /**
     * @inheritdoc
     */
    public function deleteItemsByTags(array $tagNames)
    {
        $return = null;
        foreach ($tagNames as $tagName) {
            $result = $this->deleteItemsByTag($tagName);
            if ($return !== false) {
                $return = $result;
            }
        }

        return $return;
    }

    /**
     * @inheritdoc
     */
    public function deleteItemsByTagsAll(array $tagNames)
    {
        $return = null;
        $items = $this->getItemsByTagsAll($tagNames);

        foreach ($items as $key => $item) {
            $result = $this->deleteItem($item->getKey());
            if ($return !== false) {
                $return = $result;
            }
        }

        return $return;
    }

    /**
     * @inheritdoc
     */
    public function incrementItemsByTag($tagName, $step = 1)
    {
        if (is_string($tagName) && is_int($step)) {
            foreach ($this->getItemsByTag($tagName) as $item) {
                $item->increment($step);
                $this->saveDeferred($item);
            }

            return $this->commit();
        } else {
            throw new phpFastCacheInvalidArgumentException('$tagName must be a string and $step an integer');
        }
    }

    /**
     * @inheritdoc
     */
    public function incrementItemsByTags(array $tagNames, $step = 1)
    {
        $return = null;
        foreach ($tagNames as $tagName) {
            $result = $this->incrementItemsByTag($tagName, $step);
            if ($return !== false) {
                $return = $result;
            }
        }

        return $return;
    }

    /**
     * @inheritdoc
     */
    public function incrementItemsByTagsAll(array $tagNames, $step = 1)
    {
        if (is_int($step)) {
            $items = $this->getItemsByTagsAll($tagNames);

            foreach ($items as $key => $item) {
                $item->increment($step);
                $this->saveDeferred($item);
            }
            return $this->commit();
        } else {
            throw new phpFastCacheInvalidArgumentException('$step must be an integer');
        }
    }

    /**
     * @inheritdoc
     */
    public function decrementItemsByTag($tagName, $step = 1)
    {
        if (is_string($tagName) && is_int($step)) {
            foreach ($this->getItemsByTag($tagName) as $item) {
                $item->decrement($step);
                $this->saveDeferred($item);
            }

            return $this->commit();
        } else {
            throw new phpFastCacheInvalidArgumentException('$tagName must be a string and $step an integer');
        }
    }

    /**
     * @inheritdoc
     */
    public function decrementItemsByTags(array $tagNames, $step = 1)
    {
        $return = null;
        foreach ($tagNames as $tagName) {
            $result = $this->decrementItemsByTag($tagName, $step);
            if ($return !== false) {
                $return = $result;
            }
        }

        return $return;
    }

    /**
     * @inheritdoc
     */
    public function decrementItemsByTagsAll(array $tagNames, $step = 1)
    {
        if (is_int($step)) {
            $items = $this->getItemsByTagsAll($tagNames);

            foreach ($items as $key => $item) {
                $item->decrement($step);
                $this->saveDeferred($item);
            }
            return $this->commit();
        } else {
            throw new phpFastCacheInvalidArgumentException('$step must be an integer');
        }
    }

    /**
     * @inheritdoc
     */
    public function appendItemsByTag($tagName, $data)
    {
        if (is_string($tagName)) {
            foreach ($this->getItemsByTag($tagName) as $item) {
                $item->append($data);
                $this->saveDeferred($item);
            }

            return $this->commit();
        } else {
            throw new phpFastCacheInvalidArgumentException('$tagName must be a string');
        }
    }

    /**
     * @inheritdoc
     */
    public function appendItemsByTags(array $tagNames, $data)
    {
        $return = null;
        foreach ($tagNames as $tagName) {
            $result = $this->appendItemsByTag($tagName, $data);
            if ($return !== false) {
                $return = $result;
            }
        }

        return $return;
    }

    /**
     * @inheritdoc
     */
    public function appendItemsByTagsAll(array $tagNames, $data)
    {
        if (is_scalar($data)) {
            $items = $this->getItemsByTagsAll($tagNames);

            foreach ($items as $key => $item) {
                $item->append($data);
                $this->saveDeferred($item);
            }
            return $this->commit();
        } else {
            throw new phpFastCacheInvalidArgumentException('$data must be scalar');
        }
    }

    /**
     * @inheritdoc
     */
    public function prependItemsByTag($tagName, $data)
    {
        if (is_string($tagName)) {
            foreach ($this->getItemsByTag($tagName) as $item) {
                $item->prepend($data);
                $this->saveDeferred($item);
            }

            return $this->commit();
        } else {
            throw new phpFastCacheInvalidArgumentException('$tagName must be a string');
        }
    }

    /**
     * @inheritdoc
     */
    public function prependItemsByTags(array $tagNames, $data)
    {
        $return = null;
        foreach ($tagNames as $tagName) {
            $result = $this->prependItemsByTag($tagName, $data);
            if ($return !== false) {
                $return = $result;
            }
        }

        return $return;
    }

    /**
     * @inheritdoc
     */
    public function prependItemsByTagsAll(array $tagNames, $data)
    {
        if (is_scalar($data)) {
            $items = $this->getItemsByTagsAll($tagNames);

            foreach ($items as $key => $item) {
                $item->prepend($data);
                $this->saveDeferred($item);
            }
            return $this->commit();
        } else {
            throw new phpFastCacheInvalidArgumentException('$data must be scalar');
        }
    }

    /**
     * @param \Psr\Cache\CacheItemInterface $item
     * @return void
     */
    public function detachItem(CacheItemInterface $item)
    {
        if (isset($this->itemInstances[ $item->getKey() ])) {
            $this->deregisterItem($item->getKey());
        }
    }

    /**
     * @inheritdoc
     */
    public function detachAllItems()
    {
        foreach ($this->itemInstances as $item) {
            $this->detachItem($item);
        }
    }

    /**
     * @inheritdoc
     */
    public function attachItem(CacheItemInterface $item)
    {
        if (isset($this->itemInstances[ $item->getKey() ]) && spl_object_hash($item) !== spl_object_hash($this->itemInstances[ $item->getKey() ])) {
            throw new phpFastCacheLogicException('The item already exists and cannot be overwritten because the Spl object hash mismatches ! You probably tried to re-attach a detached item which has been already retrieved from cache.');
        } else {
            $this->itemInstances[ $item->getKey() ] = $item;
        }
    }


    /**
     * @internal This method de-register an item from $this->itemInstances
     * @param string $item
     * @throws phpFastCacheInvalidArgumentException
     */
    protected function deregisterItem($item)
    {
        if (is_string($item)) {
            unset($this->itemInstances[ $item ]);
        } else {
            throw new phpFastCacheInvalidArgumentException('Invalid type for $item variable');
        }
        if (gc_enabled()) {
            gc_collect_cycles();
        }
    }

    /**
     * @param ExtendedCacheItemInterface $item
     */
    protected function cleanItemTags(ExtendedCacheItemInterface $item)
    {
        $this->driverWriteTags($item->removeTags($item->getTags()));
    }

    /**
     * Returns true if the item exists, is attached and the Spl Hash matches
     * Returns false if the item exists, is attached and the Spl Hash mismatches
     * Returns null if the item does not exists
     *
     * @param \Psr\Cache\CacheItemInterface $item
     * @return bool|null
     */
    public function isAttached(CacheItemInterface $item)
    {
        if (isset($this->itemInstances[ $item->getKey() ])) {
            return spl_object_hash($item) === spl_object_hash($this->itemInstances[ $item->getKey() ]);
        }
        return null;
    }

    /**
     * Set the EventManager instance
     *
     * @param EventManager $em
     */
    public function setEventManager(EventManager $em)
    {
        $this->eventManager = $em;
    }

    /**
     * @inheritdoc
     */
    public function saveMultiple(...$items)
    {
        if (isset($items[ 0 ]) && is_array($items[ 0 ])) {
            foreach ($items[ 0 ] as $item) {
                $this->save($item);
            }
            return true;
        } else if (is_array($items)) {
            foreach ($items as $item) {
                $this->save($item);
            }
            return true;
        }
        return false;
    }

    /**
     * @return string
     */
    public function getHelp()
    {
        return '';
    }

    /**
     * Driver-related methods
     */

    /**
     * @param \Psr\Cache\CacheItemInterface $item
     * @return null|array [
     *      'd' => 'THE ITEM DATA'
     *      't' => 'THE ITEM DATE EXPIRATION'
     *      'g' => 'THE ITEM TAGS'
     * ]
     *
     */
    abstract protected function driverRead(CacheItemInterface $item);

    /**
     * @param \Psr\Cache\CacheItemInterface $item
     * @return mixed
     */
    abstract protected function driverWrite(CacheItemInterface $item);

    /**
     * @return bool
     */
    abstract protected function driverClear();

    /**
     * @return bool
     */
    abstract protected function driverConnect();

    /**
     * @param \Psr\Cache\CacheItemInterface $item
     * @return bool
     */
    abstract protected function driverDelete(CacheItemInterface $item);
}