<?php 
/**
 * SS_Cache provides a bunch of static functions wrapping the Zend_Cache system in something a little more
 * easy to use with the SilverStripe config system.
 * 
 * A Zend_Cache has both a frontend (determines how to get the value to cache, and how to serialize it for storage)
 * and a backend (handles the actual storage).
 * 
 * Rather than require library code to specify the backend directly, cache consumers provide a name for the cache
 * backend they want. The end developer can then specify which backend to use for each name in their project's
 * _config.php. They can also use 'all' to provide a backend for all named caches
 * 
 * End developers provide a set of named backends, then pick the specific backend for each named cache. There is a 
 * default File cache set up as the 'default' named backend, which is assigned to 'all' named caches
 * 
 * USING A CACHE
 * 
 * $cache = SS_Cache::factory('foo') ; // foo is any name (try to be specific), and is used to get configuration &
 * storage info
 * 
 * if (!($result = $cache->load($cachekey))) {
 * 	$result = caluate some how;
 * 	$cache->save($result);
 * }
 * 
 * return $result;
 * 
 * Normally there's no need to remove things from the cache - the cache backends clear out entries based on age &
 * maximum allocated storage. If you include the version of the object in the cache key, even object changes don't
 * need any invalidation
 * 
 * DISABLING CACHING IN DEV MOVE
 * 
 * (in _config.php)
 * 
 * if (Director::isDev()) SS_Cache::set_cache_lifetime('any', -1, 100);
 * 
 * USING MEMCACHED AS STORE
 * 
 * (in _config.php)
 * 
 * SS_Cache::add_backend('primary_memcached', 'Memcached',
 * array('host' => 'localhost', 'port' => 11211, 'persistent' => true, 'weight' => 1, 'timeout' => 5,
 * 	   'retry_interval' => 15, 'status' => true, 'failure_callback' => '' )
 * );
 * 
 * SS_Cache::pick_backend('primary_memcached', 'any', 10);
 * // Aggregate needs a backend with tag support, which memcached doesn't provide
 * SS_Cache::pick_backend('default', 'aggregate', 20); 
 * </code>
 * 
 * USING APC AND FILE AS TWO LEVEL STORE
 * 
 * (in _config.php)
 * 
 * SS_Cache::add_backend('two-level', 'TwoLevels', array(
 * 	'slow_backend' => 'File',
 * 	'fast_backend' => 'Apc',
 * 	'slow_backend_options' => array('cache_dir' => TEMP_FOLDER . DIRECTORY_SEPARATOR . 'cache')
 * ));
 *
 * // No need for special backend for aggregate - TwoLevels with a File slow backend supports tags
 * SS_Cache::pick_backend('two-level', 'any', 10); 
 * 
 * @author hfried
 * @package framework
 * @subpackage core
 */
class SS_Cache {
	
	protected static $backends = array();
	protected static $backend_picks = array();

	protected static $cache_lifetime = array();
	
	/**
	 * Initialize the 'default' named cache backend
	 */
	protected static function init(){
		if (!isset(self::$backends['default'])) {
			$cachedir = TEMP_FOLDER . DIRECTORY_SEPARATOR . 'cache';
			if (!is_dir($cachedir)) mkdir($cachedir);
			self::$backends['default'] = array(
				'File', 
				array('cache_dir' => TEMP_FOLDER . DIRECTORY_SEPARATOR . 'cache')
			);
			self::$cache_lifetime['default'] = array('lifetime' => 600, 'priority' => 1);
		}
	}

	/**
	 * Add a new named cache backend
	 * 
	 * @param string $name The name of this backend as a freeform string
	 * @param string $type The Zend_Cache backend ('File' or 'Sqlite' or ...)
	 * @param array $options The Zend_Cache backend options (see http://framework.zend.com/manual/en/zend.cache.html)
	 * @return none
	 */
	public static function add_backend($name, $type, $options=array()) {
		self::init();
		self::$backends[$name] = array($type, $options);
	}
	
	/**
	 * Pick a named cache backend for a particular named cache
	 *  
	 * @param string $name The name of the backend, as passed as the first argument to add_backend
	 * @param string $for The name of the cache to pick this backend for (or 'any' for any backend)
	 * @param integer $priority The priority of this pick - the call with the highest number will be the actual
	 *                          backend picked. A backend picked for a specific cache name will always be used instead
	 *                          of 'any' if it exists, no matter the priority.
	 * @return none
	 */
	public static function pick_backend($name, $for, $priority=1) {
		self::init();

		$current = -1;
		if (isset(self::$backend_picks[$for])) $current = self::$backend_picks[$for]['priority'];

		if ($priority >= $current) self::$backend_picks[$for] = array('name' => $name, 'priority' => $priority); 
	}

	/**
	 * Return the cache lifetime for a particular named cache.
	 * @return array
	 */
	public static function get_cache_lifetime($for) {
		return (isset(self::$cache_lifetime[$for])) ? self::$cache_lifetime[$for] : false;
	}

	/**
	 * Set the cache lifetime for a particular named cache
	 *
	 * @param string $for The name of the cache to set this lifetime for (or 'any' for all backends)
	 * @param integer $lifetime The lifetime of an item of the cache, in seconds, or -1 to disable caching
	 * @param integer $priority The priority. The highest priority setting is used. Unlike backends, 'any' is not
	 *                          special in terms of priority. 
	 */
	public static function set_cache_lifetime($for, $lifetime=600, $priority=1) {
		self::init();
		
		$current = -1;
		if (isset(self::$cache_lifetime[$for])) $current = self::$cache_lifetime[$for]['priority'];
		
		if ($priority >= $current) {
			self::$cache_lifetime[$for] = array('lifetime' => $lifetime, 'priority' => $priority); 
		}
	}
	
	/**
	 * Build a cache object
	 * @param string $for The name of the cache to build the cache object for
	 * @param string $frontend (optional) The type of Zend_Cache frontend to use. Output is almost always the best
	 * @param array $frontendOptions (optional) Any frontend options to use.
	 * 
	 * @return The cache object. It has been partitioned so that actions on the object
	 *	won't affect cache objects generated with a different $for value, even those using the same Zend_Backend 
	 * 
	 * -- Cache a calculation
	 * 
	 * if (!($result = $cache->load($cachekey))) {
	 * 	$result = caluate some how;
	 * 	$cache->save($result);
	 * }
	 * 
	 * return $result;
	 * 
	 * -- Cache captured output
	 * 
	 * if (!($cache->start($cachekey))) {
	 * 
	 * 	// output everything as usual
	 * 	echo 'Hello world! ';
	 * 	echo 'This is cached ('.time().') ';
	 * 
	 * 	$cache->end(); // output buffering ends
	 * }
	 * 
	 * -- Invalidate an element
	 * 
	 * $cache->remove($cachekey);
	 * 
	 * -- Clear the cache (warning - this clears the entire backend, not just this named cache partition)
	 * 
	 * $cache->clean(Zend_Cache::CLEANING_MODE_ALL);
	 * 
	 * See the Zend_Cache documentation at http://framework.zend.com/manual/en/zend.cache.html for more
	 * 
	 */
	public static function factory($for, $frontend='Output', $frontendOptions=null) {
		self::init();
		
		$backend_name = 'default';
		$backend_priority = -1;
		$cache_lifetime = self::$cache_lifetime['default']['lifetime'];
		$lifetime_priority = -1;

		foreach (array('any', $for) as $name) {
			if (isset(self::$backend_picks[$name]) && self::$backend_picks[$name]['priority'] > $backend_priority) {
				$backend_name = self::$backend_picks[$name]['name'];
				$backend_priority = self::$backend_picks[$name]['priority'];
			}
			
			if (isset(self::$cache_lifetime[$name]) && self::$cache_lifetime[$name]['priority'] > $lifetime_priority) {
				$cache_lifetime = self::$cache_lifetime[$name]['lifetime'];
				$lifetime_priority = self::$cache_lifetime[$name]['priority'];
			}
		}
		
		$backend = self::$backends[$backend_name];

		$basicOptions = array('cache_id_prefix' => $for);
		
		if ($cache_lifetime >= 0) $basicOptions['lifetime'] = $cache_lifetime;
		else $basicOptions['caching'] = false;

		$frontendOptions = $frontendOptions ? array_merge($basicOptions, $frontendOptions) : $basicOptions;
		
		require_once 'Zend/Cache.php';
		return Zend_Cache::factory($frontend, $backend[0], $frontendOptions, $backend[1]);
	}
}
