<?php

namespace Gregwar\Cache;

/**
 * A cache system based on files
 *
 * @author Gregwar <g.passault@gmail.com>
 */
class Cache
{
    /**
     * Cache directory
     */
    protected $cacheDirectory;

    /**
     * Use a different directory as actual cache
     */
    protected $actualCacheDirectory = null;

    /**
     * Prefix directories size
     *
     * For instance, if the file is helloworld.txt and the prefix size is
     * 5, the cache file will be: h/e/l/l/o/helloworld.txt
     *
     * This is useful to avoid reaching a too large number of files into the 
     * cache system directories
     */
    protected $prefixSize = 5;

    /**
     * Directory mode
     *
     * Allows setting of the access mode for the directories created.
     */
    protected $directoryMode = 0755;

    /**
     * Constructs the cache system
     */
    public function __construct($cacheDirectory = 'cache')
    {
        $this->cacheDirectory = $cacheDirectory;
    }

    /**
     * Sets the cache directory
     *
     * @param $cacheDirectory the cache directory
     */
    public function setCacheDirectory($cacheDirectory)
    {
        $this->cacheDirectory = $cacheDirectory;

        return $this;
    }

    /**
     * Gets the cache directory
     *
     * @return string the cache directory
     */
    public function getCacheDirectory()
    {
        return $this->cacheDirectory;
    }

    /**
     * Sets the actual cache directory
     */
    public function setActualCacheDirectory($actualCacheDirectory = null)
    {
        $this->actualCacheDirectory = $actualCacheDirectory;

        return $this;
    }

    /**
     * Returns the actual cache directory
     */
    public function getActualCacheDirectory()
    {
        return $this->actualCacheDirectory ?: $this->cacheDirectory;
    }

    /**
     * Change the prefix size
     *
     * @param $prefixSize the size of the prefix directories
     */
    public function setPrefixSize($prefixSize)
    {
        $this->prefixSize = $prefixSize;

        return $this;
    }

    /**
     * Change the directory mode
     *
     * @param $directoryMode the directory mode to use
     */
    public function setDirectoryMode($directoryMode)
    {
        if (!$directoryMode) {
            $directoryMode = 0755;
        }
        $this->directoryMode = $directoryMode;

        return $this;
    }

    /**
     * Creates a directory
     *
     * @param $directory, the target directory
     */
    protected function mkdir($directory)
    {
        if (!is_dir($directory)) {
            @mkdir($directory, $this->directoryMode, true);
        }
    }

    /**
     * Gets the cache file name
     *
     * @param $filename, the name of the cache file
     * @param $actual get the actual file or the public file
     * @param $mkdir, a boolean to enable/disable the construction of the
     *        cache file directory
     */
    public function getCacheFile($filename, $actual = false, $mkdir = false)
    {
        $path = array();

        // Getting the length of the filename before the extension
        $parts = explode('.', $filename);
        $len = strlen($parts[0]);

        for ($i=0; $i<min($len, $this->prefixSize); $i++) {
            $path[] = $filename[$i];

        }
        $path = implode('/', $path);

        $actualDir = $this->getActualCacheDirectory() . '/' . $path;
        if ($mkdir && !is_dir($actualDir)) {
            mkdir($actualDir, $this->directoryMode, true);
        }

        $path .= '/' . $filename;

        if ($actual) {
            return $this->getActualCacheDirectory() . '/' . $path;
        } else {
            return $this->getCacheDirectory() . '/' . $path;
        }
    }

    /**
     * Checks that the cache conditions are respected
     *
     * @param $cacheFile the cache file
     * @param $conditions an array of conditions to check
     */
    protected function checkConditions($cacheFile, array $conditions = array())
    {
        // Implicit condition: the cache file should exist
        if (!file_exists($cacheFile)) {
            return false;
        }

        foreach ($conditions as $type => $value) {
            switch ($type) {
            case 'maxage':
            case 'max-age':
                // Return false if the file is older than $value
                $age = time() - filectime($cacheFile);
                if ($age > $value) {
                    return false;
                }
                break;
            case 'younger-than':
            case 'youngerthan':
                // Return false if the file is older than the file $value, or the files $value
                $check = function($filename) use ($cacheFile) {
                    return !file_exists($filename) || filectime($cacheFile) < filectime($filename);
                };

                if (!is_array($value)) {
                    if (!$this->isRemote($value) && $check($value)) {
                        return false;
                    }
                } else {
                    foreach ($value as $file) {
                        if (!$this->isRemote($file) && $check($file)) {
                            return false;
                        }
                    }
                }
                break;
            default:
                throw new \Exception('Cache condition '.$type.' not supported');
            }
        }

        return true;
    }

    /**
     * Checks if the targt filename exists in the cache and if the conditions
     * are respected
     *
     * @param $filename the filename 
     * @param $conditions the conditions to respect
     */
    public function exists($filename, array $conditions = array())
    {
        $cacheFile = $this->getCacheFile($filename, true);

        return $this->checkConditions($cacheFile, $conditions);
    }

    /**
     * Alias for exists
     */
    public function check($filename, array $conditions = array())
    {
        return $this->exists($filename, $conditions);
    }

    /**
     * Write data in the cache
     */
    public function set($filename, $contents = '')
    {
        $cacheFile = $this->getCacheFile($filename, true, true);

        file_put_contents($cacheFile, $contents);

        return $this;
    }

    /**
     * Alias for set()
     */
    public function write($filename, $contents = '')
    {
        return $this->set($filename, $contents);
    }

    /**
     * Get data from the cache
     */
    public function get($filename, array $conditions = array())
    {
        if ($this->exists($filename, $conditions)) {
            return file_get_contents($this->getCacheFile($filename, true));
        } else {
            return null;
        }
    }

    /**
     * Is this URL remote?
     */
    protected function isRemote($file)
    {
        return preg_match('/^http(s{0,1}):\/\//', $file);
    }

    /**
     * Get or create the cache entry
     *
     * @param $filename the cache file name
     * @param $conditions an array of conditions about expiration
     * @param $function the closure to call if the file does not exists
     * @param $file returns the cache file or the file contents
     * @param $actual returns the actual cache file
     */
    public function getOrCreate($filename, array $conditions = array(), $function, $file = false, $actual = false)
    {
        if (!is_callable($function)) {
            throw new InvalidArgumentException('The argument $function should be callable');
        }

        $cacheFile = $this->getCacheFile($filename, true, true);
        $data = null;

        if ($this->check($filename, $conditions)) {
            $data = file_get_contents($cacheFile);
        } else {
            if(file_exists($cacheFile)) {
                unlink($cacheFile);
            }

            $data = call_user_func($function, $cacheFile);

            // Test if the closure wrote the file or if it returned the data
            if (!file_exists($cacheFile)) {
                $this->set($filename, $data);
            } else {
                $data = file_get_contents($cacheFile);
            }
        }

        return $file ? $this->getCacheFile($filename, $actual) : $data;
    }

    /**
     * Alias to getOrCreate with $file = true
     */
    public function getOrCreateFile($filename, array $conditions = array(), $function, $actual = false)
    {
        return $this->getOrCreate($filename, $conditions, $function, true, $actual);
    }
}
