<?php

namespace dokuwiki\Cache;

use dokuwiki\Debug\PropertyDeprecationHelper;
use dokuwiki\Extension\Event;

/**
 * Generic handling of caching
 */
class Cache
{
    use PropertyDeprecationHelper;

    public $key = '';          // primary identifier for this item
    public $ext = '';          // file ext for cache data, secondary identifier for this item
    public $cache = '';        // cache file name
    public $depends = []; // array containing cache dependency information,
    //   used by makeDefaultCacheDecision to determine cache validity

    // phpcs:disable
    /**
     * @deprecated since 2019-02-02 use the respective getters instead!
     */
    protected $_event = '';       // event to be triggered during useCache
    protected $_time;
    protected $_nocache = false;  // if set to true, cache will not be used or stored
    // phpcs:enable

    /**
     * @param string $key primary identifier
     * @param string $ext file extension
     */
    public function __construct($key, $ext)
    {
        $this->key = $key;
        $this->ext = $ext;
        $this->cache = getCacheName($key, $ext);

        /**
         * @deprecated since 2019-02-02 use the respective getters instead!
         */
        $this->deprecatePublicProperty('_event');
        $this->deprecatePublicProperty('_time');
        $this->deprecatePublicProperty('_nocache');
    }

    public function getTime()
    {
        return $this->_time;
    }

    public function getEvent()
    {
        return $this->_event;
    }

    public function setEvent($event)
    {
        $this->_event = $event;
    }

    /**
     * public method to determine whether the cache can be used
     *
     * to assist in centralisation of event triggering and calculation of cache statistics,
     * don't override this function override makeDefaultCacheDecision()
     *
     * @param  array $depends array of cache dependencies, support dependecies:
     *                            'age'   => max age of the cache in seconds
     *                            'files' => cache must be younger than mtime of each file
     *                                       (nb. dependency passes if file doesn't exist)
     *
     * @return bool    true if cache can be used, false otherwise
     */
    public function useCache($depends = [])
    {
        $this->depends = $depends;
        $this->addDependencies();

        if ($this->getEvent()) {
            return $this->stats(
                Event::createAndTrigger(
                    $this->getEvent(),
                    $this,
                    [$this, 'makeDefaultCacheDecision']
                )
            );
        }

        return $this->stats($this->makeDefaultCacheDecision());
    }

    /**
     * internal method containing cache use decision logic
     *
     * this function processes the following keys in the depends array
     *   purge - force a purge on any non empty value
     *   age   - expire cache if older than age (seconds)
     *   files - expire cache if any file in this array was updated more recently than the cache
     *
     * Note that this function needs to be public as it is used as callback for the event handler
     *
     * can be overridden
     *
     * @internal This method may only be called by the event handler! Call \dokuwiki\Cache\Cache::useCache instead!
     *
     * @return bool               see useCache()
     */
    public function makeDefaultCacheDecision()
    {
        if ($this->_nocache) {
            return false;
        }                              // caching turned off
        if (!empty($this->depends['purge'])) {
            return false;
        }              // purge requested?
        if (!($this->_time = @filemtime($this->cache))) {
            return false;
        }   // cache exists?

        // cache too old?
        if (!empty($this->depends['age']) && ((time() - $this->_time) > $this->depends['age'])) {
            return false;
        }

        if (!empty($this->depends['files'])) {
            foreach ($this->depends['files'] as $file) {
                if ($this->_time <= @filemtime($file)) {
                    return false;
                }         // cache older than files it depends on?
            }
        }

        return true;
    }

    /**
     * add dependencies to the depends array
     *
     * this method should only add dependencies,
     * it should not remove any existing dependencies and
     * it should only overwrite a dependency when the new value is more stringent than the old
     */
    protected function addDependencies()
    {
        global $INPUT;
        if ($INPUT->has('purge')) {
            $this->depends['purge'] = true;
        }   // purge requested
    }

    /**
     * retrieve the cached data
     *
     * @param   bool $clean true to clean line endings, false to leave line endings alone
     * @return  string          cache contents
     */
    public function retrieveCache($clean = true)
    {
        return io_readFile($this->cache, $clean);
    }

    /**
     * cache $data
     *
     * @param   string $data the data to be cached
     * @return  bool           true on success, false otherwise
     */
    public function storeCache($data)
    {
        if ($this->_nocache) {
            return false;
        }

        return io_saveFile($this->cache, $data);
    }

    /**
     * remove any cached data associated with this cache instance
     */
    public function removeCache()
    {
        @unlink($this->cache);
    }

    /**
     * Record cache hits statistics.
     * (Only when debugging allowed, to reduce overhead.)
     *
     * @param    bool $success result of this cache use attempt
     * @return   bool              pass-thru $success value
     */
    protected function stats($success)
    {
        global $conf;
        static $stats = null;
        static $file;

        if (!$conf['allowdebug']) {
            return $success;
        }

        if (is_null($stats)) {
            $file = $conf['cachedir'] . '/cache_stats.txt';
            $lines = explode("\n", io_readFile($file));

            foreach ($lines as $line) {
                $i = strpos($line, ',');
                $stats[substr($line, 0, $i)] = $line;
            }
        }

        if (isset($stats[$this->ext])) {
            [$ext, $count, $hits] = explode(',', $stats[$this->ext]);
        } else {
            $ext = $this->ext;
            $count = 0;
            $hits = 0;
        }

        $count++;
        if ($success) {
            $hits++;
        }
        $stats[$this->ext] = "$ext,$count,$hits";

        io_saveFile($file, implode("\n", $stats));

        return $success;
    }

    /**
     * @return bool
     */
    public function isNoCache()
    {
        return $this->_nocache;
    }
}
