<?php namespace ProcessWire;

/**
 * ProcessWire Markup Cache module
 *
 * A simple way to cache segments of markup in your templates. 
 * A simpler front end to ProcessWire's CacheFile class. 
 *
 * Example usage:
 * 
 * $mycache = $modules->get("MarkupCache"); 
 * if(!$data = $mycache->get("cityOptions")) {
 * 	foreach($pages->find("template=city, sort=name") as $city) {
 * 		$data .= "<option value='{$city->id}'>{$city->title}</option>";
 *	}
 *	$mycache->save($data); 
 * }
 * echo $data; 
 *
 *
 * ProcessWire 3.x, Copyright 2016 by Ryan Cramer
 * https://processwire.com
 *
 *
 * @todo add serialize/unserialize to support non-string  data in MarkupCache
 *
 */

class MarkupCache extends Wire implements Module, ConfigurableModule {

	/**
	 * getModuleInfo is a module required by all modules to tell ProcessWire about them
	 *
	 * @return array
	 *
	 */
	public static function getModuleInfo() {

		return array(
			'title' => 'Markup Cache', 
			'version' => 101, 
			'summary' => 'A simple way to cache segments of markup in your templates. ',
			'href' => 'https://processwire.com/api/modules/markupcache/',
			'singular' => true, 
			'autoload' => true, 
			);
	}

	/**
	 * Instance of CacheFile
	 * 
	 * @var CacheFile
	 *
	 */
	protected $cache = null;


	/**
	 * Boolean indicating whether we've already cleared the cache.
	 *
	 */
	protected $cleared = false; 

	/**
	 * Path to cache files, as set by the init() method. 
	 *
	 */
	protected $path = '';

	/**
	 * Non zero when caches shouldn't expire on page save
	 *
	 */
	protected $noExpire = 0; 

	/**
	 * Generate the module's path, static so it can be used by the static getModuleConfigInputfields function
	 *
	 */
	public function path() {
		return $this->wire('config')->paths->cache . 'MarkupCache' . '/';
	}

	/**
	 * Initialize the module and add a hook after Pages::save
	 *
	 */
	public function init() {
		$this->path = $this->path();
		if(!$this->noExpire) $this->pages->addHookAfter('save', $this, 'expire'); 
	}

	/**
	 * Get cached data identified by 'uniqueName' or false if cache not available
	 *
	 * @param string $uniqueName A unique string or number to identify this cache, i.e. 'citiesList' 
	 * @param int $seconds The number of seconds the cache should live. 
	 * @return string|bool Returns the cache data, or FALSE if it has expired and needs to be re-created. 
	 * @throws WireException
	 *
	 */
	public function get($uniqueName, $seconds = 3600) {
		$cache = $this->wire(new CacheFile($this->path, $uniqueName, $seconds));
		if(!$cache) throw new WireException("Unable to create cache '{$this->path}/$uniqueName'"); 
		$this->cache = $cache; 
		return $this->cache->get();
	}

	/**
	 * Save the data to the cache
	 *
	 * Must be preceded by a call to get() so that you have set the cache unique name
	 *
	 * @param string $data Data to cache
	 * @return int Number of bytes written to cache, or FALSE on failure. 
	 * @throws WireException
	 *
	 */
	public function save($data) {
		if(!$this->cache) throw new WireException("You must attempt to retrieve a cache first, before you can save it."); 	
		$result = $this->cache->save($data); 
		$this->cache = null;
		return $result; 
	}

	/**
	 * Expire the cache, automatically hooked to every $pages->save() call
	 *
	 */
	public function expire($event = null) {
		/*
		 * If already cleared during this session, don't do it again
		 * that way if we're saving 100+ pages, we aren't clearing the cache 100+ times
		 *
		 */
		if($this->cleared) return; 

		if($this->cache) $cache = $this->cache; 
			else $cache = $this->wire(new CacheFile($this->path, '', 0)); 
		$cache->expireAll(); 
		$this->cleared = true; 
	}

	/**
	 * Clears all MarkupCache files
	 *
	 * @return number of files/dirs deleted
	 *
	 */
	public function removeAll() {
		$path = $this->path();
		try {
			$num = CacheFile::removeAll($path, true);
		} catch(\Exception $e) {
			$num = 0;
		}
		return $num; 
	}

	/**
	 * For ConfigurableModule interface, even though we aren't currently using it
	 *
	 */
	public function __set($key, $value) {
		if($key == 'noExpire') $this->noExpire = (int) $value; 
		// intentionally left blank
	}

	/**
	 * Provide cache clearing capability in the module's configuration
	 * 
	 * @param array $data
	 * @return InputfieldWrapper
	 *	
 	 */
	public function getModuleConfigInputfields(array $data) {

		$inputfields = $this->wire(new InputfieldWrapper());
		$clearNow = $this->wire('input')->post->_clearCache ? true : false;
		$message = '';

		if($clearNow) {
			$numFiles = $this->removeAll();
			$message = "Cleared $numFiles MarkupCache files and dirs";
			$inputfields->message($message);
		}

		$name = "_clearCache"; // prefix with '_' tells ProcessModule not to save it in module's config data
		$f = $this->wire('modules')->get('InputfieldCheckbox');
		$f->attr('name', $name);
		$f->attr('value', 1);
		$f->attr('checked', '');
		$f->label = "Clear the MarkupCache?";
		$f->notes = $message; 
		$f->description = "This will remove all files and directories used by the MarkupCache";
		$inputfields->append($f);

		$f = $this->wire('modules')->get('InputfieldRadios');
		$f->attr('name', 'noExpire');
		$f->attr('value', empty($data['noExpire']) ? 0 : 1);
		$f->label = "Expire markup caches when pages are saved?";
		$f->description = "If you want to ensure stale data is never shown, you should choose: Yes. If you want to maximize performance, you should choose: No."; 
		$f->addOption(0, "Yes - Expire markup caches"); 
		$f->addOption(1, "No - Don't expire markup caches");
		$inputfields->append($f);

		return $inputfields;
	}

	/**
	 * Uninstall this module and remove it's files
	 *
	 */
	public function ___uninstall() {
		$numFiles = $this->removeAll();
		$this->message("Removed $numFiles MarkupCache files"); 
	}

	
}
