<?php

/**
 * Joomla! Content Management System
 *
 * @copyright  (C) 2007 Open Source Matters, Inc. <https://www.joomla.org>
 * @license    GNU General Public License version 2 or later; see LICENSE.txt
 */

namespace Joomla\CMS\Cache;

use Joomla\CMS\Cache\Exception\UnsupportedCacheException;
use Joomla\CMS\Factory;
use Joomla\CMS\Filesystem\Path;

// phpcs:disable PSR1.Files.SideEffects
\defined('_JEXEC') or die;
// phpcs:enable PSR1.Files.SideEffects

/**
 * Abstract cache storage handler
 *
 * @since  1.7.0
 * @note   As of 4.0 this class will be abstract
 */
class CacheStorage
{
    /**
     * The raw object name
     *
     * @var    string
     * @since  1.7.0
     */
    protected $rawname;

    /**
     * Time that the cache storage handler was instantiated
     *
     * @var    integer
     * @since  1.7.0
     */
    public $_now;

    /**
     * Cache lifetime
     *
     * @var    integer
     * @since  1.7.0
     */
    public $_lifetime;

    /**
     * Flag if locking is enabled
     *
     * @var    boolean
     * @since  1.7.0
     */
    public $_locking;

    /**
     * Language code
     *
     * @var    string
     * @since  1.7.0
     */
    public $_language;

    /**
     * Application name
     *
     * @var    string
     * @since  1.7.0
     */
    public $_application;

    /**
     * Object hash
     *
     * @var    string
     * @since  1.7.0
     */
    public $_hash;

    /**
     * The threshold
     *
     * @var    integer
     * @since  4.3.0
     */
    public $_threshold;

    /**
     * Constructor
     *
     * @param   array  $options  Optional parameters
     *
     * @since   1.7.0
     */
    public function __construct($options = [])
    {
        $app = Factory::getApplication();

        $this->_hash        = md5($app->get('secret', ''));
        $this->_application = $options['application'] ?? md5(JPATH_CONFIGURATION);
        $this->_language    = $options['language'] ?? 'en-GB';
        $this->_locking     = $options['locking'] ?? true;
        $this->_lifetime    = ($options['lifetime'] ?? $app->get('cachetime')) * 60;
        $this->_now         = $options['now'] ?? time();

        // Set time threshold value.  If the lifetime is not set, default to 60 (0 is BAD)
        // _threshold is now available ONLY as a legacy (it's deprecated).  It's no longer used in the core.
        if (empty($this->_lifetime)) {
            $this->_threshold = $this->_now - 60;
            $this->_lifetime  = 60;
        } else {
            $this->_threshold = $this->_now - $this->_lifetime;
        }
    }

    /**
     * Returns a cache storage handler object.
     *
     * @param   string  $handler  The cache storage handler to instantiate
     * @param   array   $options  Array of handler options
     *
     * @return  CacheStorage
     *
     * @since   1.7.0
     * @throws  \UnexpectedValueException
     * @throws  UnsupportedCacheException
     */
    public static function getInstance($handler = null, $options = [])
    {
        static $now = null;

        if (!isset($handler)) {
            $handler = Factory::getApplication()->get('cache_handler');

            if (empty($handler)) {
                throw new \UnexpectedValueException('Cache Storage Handler not set.');
            }
        }

        if (\is_null($now)) {
            $now = time();
        }

        $options['now'] = $now;

        // We can't cache this since options may change...
        $handler = strtolower(preg_replace('/[^A-Z0-9_\.-]/i', '', $handler));

        /** @var CacheStorage $class */
        $class = __NAMESPACE__ . '\\Storage\\' . ucfirst($handler) . 'Storage';

        if (!class_exists($class)) {
            $class = 'JCacheStorage' . ucfirst($handler);
        }

        if (!class_exists($class)) {
            // Search for the class file in the JCacheStorage include paths.
            $path = Path::find(self::addIncludePath(), strtolower($handler) . '.php');

            if ($path === false) {
                throw new UnsupportedCacheException(sprintf('Unable to load Cache Storage: %s', $handler));
            }

            \JLoader::register($class, $path);

            // The class should now be loaded
            if (!class_exists($class)) {
                throw new UnsupportedCacheException(sprintf('Unable to load Cache Storage: %s', $handler));
            }
        }

        // Validate the cache storage is supported on this platform
        if (!$class::isSupported()) {
            throw new UnsupportedCacheException(sprintf('The %s Cache Storage is not supported on this platform.', $handler));
        }

        return new $class($options);
    }

    /**
     * Check if the cache contains data stored by ID and group
     *
     * @param   string  $id     The cache data ID
     * @param   string  $group  The cache data group
     *
     * @return  boolean
     *
     * @since   3.7.0
     */
    public function contains($id, $group)
    {
        return false;
    }

    /**
     * Get cached data by ID and group
     *
     * @param   string   $id         The cache data ID
     * @param   string   $group      The cache data group
     * @param   boolean  $checkTime  True to verify cache time expiration threshold
     *
     * @return  mixed  Boolean false on failure or a cached data object
     *
     * @since   1.7.0
     */
    public function get($id, $group, $checkTime = true)
    {
        return false;
    }

    /**
     * Get all cached data
     *
     * @return  mixed  Boolean false on failure or a cached data object
     *
     * @since   1.7.0
     */
    public function getAll()
    {
        return false;
    }

    /**
     * Store the data to cache by ID and group
     *
     * @param   string  $id     The cache data ID
     * @param   string  $group  The cache data group
     * @param   string  $data   The data to store in cache
     *
     * @return  boolean
     *
     * @since   1.7.0
     */
    public function store($id, $group, $data)
    {
        return true;
    }

    /**
     * Remove a cached data entry by ID and group
     *
     * @param   string  $id     The cache data ID
     * @param   string  $group  The cache data group
     *
     * @return  boolean
     *
     * @since   1.7.0
     */
    public function remove($id, $group)
    {
        return true;
    }

    /**
     * Clean cache for a group given a mode.
     *
     * group mode    : cleans all cache in the group
     * notgroup mode : cleans all cache not in the group
     *
     * @param   string  $group  The cache data group
     * @param   string  $mode   The mode for cleaning cache [group|notgroup]
     *
     * @return  boolean
     *
     * @since   1.7.0
     */
    public function clean($group, $mode = null)
    {
        return true;
    }

    /**
     * Flush all existing items in storage.
     *
     * @return  boolean
     *
     * @since   3.6.3
     */
    public function flush()
    {
        return true;
    }

    /**
     * Garbage collect expired cache data
     *
     * @return  boolean
     *
     * @since   1.7.0
     */
    public function gc()
    {
        return true;
    }

    /**
     * Test to see if the storage handler is available.
     *
     * @return  boolean
     *
     * @since   3.0.0
     */
    public static function isSupported()
    {
        return true;
    }

    /**
     * Lock cached item
     *
     * @param   string   $id        The cache data ID
     * @param   string   $group     The cache data group
     * @param   integer  $locktime  Cached item max lock time
     *
     * @return  mixed  Boolean false if locking failed or an object containing properties lock and locklooped
     *
     * @since   1.7.0
     */
    public function lock($id, $group, $locktime)
    {
        return false;
    }

    /**
     * Unlock cached item
     *
     * @param   string  $id     The cache data ID
     * @param   string  $group  The cache data group
     *
     * @return  boolean
     *
     * @since   1.7.0
     */
    public function unlock($id, $group = null)
    {
        return false;
    }

    /**
     * Get a cache ID string from an ID/group pair
     *
     * @param   string  $id     The cache data ID
     * @param   string  $group  The cache data group
     *
     * @return  string
     *
     * @since   1.7.0
     */
    protected function _getCacheId($id, $group)
    {
        $name          = md5($this->_application . '-' . $id . '-' . $this->_language);
        $this->rawname = $this->_hash . '-' . $name;

        return Cache::getPlatformPrefix() . $this->_hash . '-cache-' . $group . '-' . $name;
    }

    /**
     * Add a directory where CacheStorage should search for handlers. You may either pass a string or an array of directories.
     *
     * @param   array|string  $path  A path to search.
     *
     * @return  array  An array with directory elements
     *
     * @since   1.7.0
     */
    public static function addIncludePath($path = '')
    {
        static $paths;

        if (!isset($paths)) {
            $paths = [];
        }

        if (!empty($path) && !\in_array($path, $paths)) {
            array_unshift($paths, Path::clean($path));
        }

        return $paths;
    }
}
