<?php
declare(strict_types=1);

/**
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link          https://cakephp.org CakePHP(tm) Project
 * @since         3.0.0
 * @license       https://opensource.org/licenses/mit-license.php MIT License
 */
namespace Cake\Datasource;

use Cake\Cache\Cache;
use Closure;
use Psr\SimpleCache\CacheInterface;
use RuntimeException;
use Traversable;

/**
 * Handles caching queries and loading results from the cache.
 *
 * Used by {@link \Cake\Datasource\QueryTrait} internally.
 *
 * @internal
 * @see \Cake\Datasource\QueryTrait::cache() for the public interface.
 */
class QueryCacher
{
    /**
     * The key or function to generate a key.
     *
     * @var \Closure|string
     */
    protected $_key;

    /**
     * Config for cache engine.
     *
     * @var \Psr\SimpleCache\CacheInterface|string
     */
    protected $_config;

    /**
     * Constructor.
     *
     * @param \Closure|string $key The key or function to generate a key.
     * @param \Psr\SimpleCache\CacheInterface|string $config The cache config name or cache engine instance.
     * @throws \RuntimeException
     */
    public function __construct($key, $config)
    {
        if (!is_string($key) && !($key instanceof Closure)) {
            throw new RuntimeException('Cache keys must be strings or callables.');
        }
        $this->_key = $key;

        if (!is_string($config) && !($config instanceof CacheInterface)) {
            throw new RuntimeException('Cache configs must be strings or \Psr\SimpleCache\CacheInterface instances.');
        }
        $this->_config = $config;
    }

    /**
     * Load the cached results from the cache or run the query.
     *
     * @param object $query The query the cache read is for.
     * @return mixed|null Either the cached results or null.
     */
    public function fetch(object $query)
    {
        $key = $this->_resolveKey($query);
        $storage = $this->_resolveCacher();
        $result = $storage->get($key);
        if (empty($result)) {
            return null;
        }

        return $result;
    }

    /**
     * Store the result set into the cache.
     *
     * @param object $query The query the cache read is for.
     * @param \Traversable $results The result set to store.
     * @return bool True if the data was successfully cached, false on failure
     */
    public function store(object $query, Traversable $results): bool
    {
        $key = $this->_resolveKey($query);
        $storage = $this->_resolveCacher();

        return $storage->set($key, $results);
    }

    /**
     * Get/generate the cache key.
     *
     * @param object $query The query to generate a key for.
     * @return string
     * @throws \RuntimeException
     */
    protected function _resolveKey(object $query): string
    {
        if (is_string($this->_key)) {
            return $this->_key;
        }
        $func = $this->_key;
        $key = $func($query);
        if (!is_string($key)) {
            $msg = sprintf('Cache key functions must return a string. Got %s.', var_export($key, true));
            throw new RuntimeException($msg);
        }

        return $key;
    }

    /**
     * Get the cache engine.
     *
     * @return \Psr\SimpleCache\CacheInterface
     */
    protected function _resolveCacher()
    {
        if (is_string($this->_config)) {
            return Cache::pool($this->_config);
        }

        return $this->_config;
    }
}
