<?php
/**
 * Memcached Class
 * Wrapper for the Memcached library.
 *
 * @author ynori7
 * @copyright Copyright (c) 2014, halls-of-valhalla.org
 * @license http://creativecommons.org/licenses/by-sa/4.0/ Creative Commons Attribution-ShareAlike 4.0 International License. 
 *
 * Example Usage:
use Valhalla\CoreUtilities\Cache\Memcached;
Class Test{
  public function getData(array $parameters = array()){
    return array(
        'a' => 'blah',
        'b' => 'werwer',
        'c' => 'blorp',
    );
  }
  public function testCall(){
    $serverConfig = array(
        'servers' => array(
            array('localhost', 11211),
        ),
        'default_ttl' => 600,
        'max_age' => 604800,
    );
    $c = new Memcached($serverConfig);
    var_dump($c->getDataWithCaching($this, 'getData'));
  }
}
 *
 */

namespace Valhalla\CoreUtilities\Cache;

use \Memcached as MemcachedBase;

class Memcached {

    const PROJECT_CACHE_PREFIX = '';

    private $_cache;
    private $_serverConfig;
    private $_defaultTtl;
    private $_maxAge;

    /**
     * The cache config should appear as follows:
     * 'servers' => array(
     *      array('memcached.server1', 11219),
     *      array('memcached.server2', 11219),
     *  ),
     *  'default_ttl' => 600, 
     *  'max_age' => 604800, 
     * 
     * default_ttl is the default time which cache should remain valid.
     * When the cache is invalid, new data should be retrieved. If it is not
     * possible to retrieve new data, the previously cached result will be returned
     * until that cached result has reached max_age at which point the entry will
     * disappear.
     * @param array $cacheConfig
     */
    public function __construct(array $cacheConfig){
        $this->_serverConfig = $cacheConfig['servers'];
        $this->_cache = new MemcachedBase();
        $this->_cache->addServers($this->_serverConfig);
        $this->_defaultTtl = $cacheConfig['default_ttl'];
        $this->_maxAge = $cacheConfig['max_age'];
    }

    /**
     * Generates an id for inserting into cache.
     *
     * @param $classname
     * @param array $parameters
     * @return string
     */
    public function createCacheKey($classname, array $parameters = array()){
        return hash('crc32', self::PROJECT_CACHE_PREFIX . '_' . $classname . '_' . implode('_', $parameters));
    }

    /**
     * Check if an entry exists in cache with a given id.
     * 
     * @param $id
     * @return bool
     */
    public function contains($id) {
        return (false !== $this->_cache->get($id));
    }

    /**
     * Replace an existing cache entry.
     *
     * @param $id
     * @param $payload
     * @param int $lifeTime
     * @return bool
     */
    public function replace($id, $payload, $lifeTime = 0) {
        if($lifeTime == 0) {
            $lifeTime = $this->_defaultTtl;
        }
        $data = array(
            'timestamp' => gmdate('U'),
            'ttl' => $lifeTime,
            'payload' => $payload,
        );

        return $this->_cache->replace($id, serialize($data), $this->_maxAge);
    }

    /**
     * Create a new cache entry.
     * 
     * @param $id
     * @param $payload
     * @param int $lifeTime
     * @return bool
     */
    public function save($id, $payload, $lifeTime = 0) {
        if($lifeTime == 0) {
            $lifeTime = $this->_defaultTtl;
        }
        $data = array(
            'timestamp' => gmdate('U'),
            'ttl' => $lifeTime,
            'payload' => $payload,
        );

        return $this->_cache->set($id, serialize($data), $this->_maxAge);
    }


    /**
     * Get a cached entry from cache.
     *
     * @param $id
     * @return mixed
     */
    public function fetch($id){
        return unserialize($this->_cache->get($id));
    }

    /**
     * Checks if the data retrieved from cache is still within its time-to-live.
     *
     * @param $cachedData
     * @return bool
     */
    public function isStillValid($cachedData){
        return (!empty($cachedData) and (($cachedData['timestamp'] + $cachedData['ttl']) > gmdate('U')));
    }
    
    /**
     * Wrapper function for handling all the caching around data-retrieval
     *
     * @param $class The class which is requesting data
     * @param $function The function defined in $class for retreiving the desired data
     * @param array $parameters Arguments to $class->$function
     * @param int $ttl The user-defined lifetime for the cache
     * @return mixed
     */
    public function getDataWithCaching($class, $function, array $parameters = array(), $ttl = 0){
        $cacheId = $this->createCacheKey(get_class($class), $parameters);

        $cachedData = $this->contains($cacheId) ? $this->fetch($cacheId) : array();

        //If there's a cached response and it's still alive, return it
        if($this->isStillValid($cachedData)){
            return $cachedData['payload'];
        } else {
            try{
                $newData = $class->{$function}($parameters);
                if(empty($newData)){ //Return the last cached response if this data is invalid
                    return $cachedData['payload'];
                }
                if(!empty($cachedData)){ //Replace existing cache entry with updated data
                    $this->replace($cacheId, $newData, $ttl);
                } else { //Insert new entry into cache
                    $this->save($cacheId, $newData, $ttl);
                }
                return $newData;
            } catch(\Exception $e){ //If there was an error retrieving data, return previous cached data
                return $cachedData['payload'];
            }
        }
    }

}