<?php
/**
 * EGroupware API: Caching data
 *
 * @link http://www.egroupware.org
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
 * @package api
 * @subpackage cache
 * @author Ralf Becker <RalfBecker-AT-outdoor-training.de>
 * @copyright (c) 2009-16 by Ralf Becker <RalfBecker-AT-outdoor-training.de>
 * @version $Id$
 */

namespace EGroupware\Api;

/**
 * Class to manage caching in eGroupware.
 *
 * It allows to cache on 4 levels:
 * a) tree:     for all instances/domains runining on a certain source path
 * b) instance: for all sessions on a given instance
 * c) session:  for all requests of a session, same as deprecated egw_session::appsession()
 * d) request:  just for this request (same as using a static variable)
 *
 * There's a get, a set and a unset method for each level: eg. getTree() or setInstance(),
 * as well as a variant allowing to specify the level as first parameter: eg. unsetCache()
 *
 * getXXX($app,$location,$callback=null,array $callback_params,$expiration=0)
 * has three optional parameters allowing to specify:
 * 3. a callback if requested data is not yes stored. In that case the callback is called
 *    and it's value is stored in the cache AND returned
 * 4. parameters to pass to the callback as array, see call_user_func_array
 * 5. an expiration time in seconds to specify how long data should be cached,
 *    default 0 means infinite (this time is not supported for request level!)
 *
 * Data is stored under an application name and a location, like egw_session::appsession().
 * In fact data stored at cache level Api\Cache::SESSION, is stored in the same way as
 * egw_session::appsession() so both methods can be used with each other.
 *
 * The $app parameter should be either the app or the class name, which both are unique.
 *
 * The tree and instance wide cache uses a certain provider class, to store the data
 * eg. in memcached or if there's nothing else configured in the filesystem (eGW's temp_dir).
 *
 * "Admin >> clear cache and register hooks" always only clears instance level cache of
 * calling instance. It never clears tree level cache, which makes it important to set
 * reasonable expiry times or think about an other means of clearing that particular item.
 * (Not clearing of tree-level cache is important, as regenerating it is an expensive
 * operation for a huge scale EGroupware hosting operation.)
 *
 * Apps needing to talk to multiple EGroupware instances (eg. EGroupware Managementserver)
 * can use install_id of instance as $level parameter to (set|get|unset)Cache method.
 *
 * If a callback is used to calculate the data, in case it's not found in the cache, e.g.:
 * $data = Api\Cache::getCache(Api\Cache::(TREE|INSTANCE), $app, $location, function($params)
 * {
 * 		// code to calculate / query $data
 * 		return $data;
 * }, $params, $expiration);
 * then we lock the $app:$location by storing some specific data, so only one callback is used
 * to calculate the data, and other processes sleep until the data is calculate to avoid races.
 */
class Cache
{
	/**
	 * tree-wide storage
	 */
	const TREE = 'Tree';
	/**
	 * instance-wide storage
	 */
	const INSTANCE = 'Instance';
	/**
	 * session-wide storage
	 */
	const SESSION = 'Session';
	/**
	 * request-wide storage
	 */
	const REQUEST = 'Request';

	/**
	 * Default provider for tree and instance data
	 *
	 * Can be specified eg. in the header.inc.php by setting:
	 * $GLOBALS['egw_info']['server']['cache_provider_instance'] and optional
	 * $GLOBALS['egw_info']['server']['cache_provider_tree'] (defaults to instance)
	 *
	 * Default is set (if not set here) after class definition to Cache\Apc or Cache\Files,
	 * depending on function 'apc_fetch' exists or not
	 *
	 * @var array
	 */
	static $default_provider;	// = array('EGroupware\Api\Cache\Files');// array('EGroupware\Api\Cache\Memcache','localhost');

	/**
	 * Maximum expiration time, if set unlimited expiration (=0) or bigger expiration times are replaced with that time
	 *
	 * @var int
	 */
	static $max_expiration;

	/**
	 * Used to determine keys for tree- and instance-level caches
	 *
	 * @var string
	 */
	static $egw_server_root = EGW_SERVER_ROOT;

	/**
	 * Add some data in the cache, only if the key does not yet exist
	 *
	 * @param string $level use Api\Cache::(TREE|INSTANCE)
	 * @param string $app application storing data
	 * @param string $location location name for data
	 * @param mixed $data
	 * @param int $expiration =0 expiration time in seconds, default 0 = never
	 * @return boolean true if data could be stored, false otherwise incl. key already existed
	 */
	public static function addCache($level,$app,$location,$data,$expiration=0)
	{
		//error_log(__METHOD__."('$level','$app','$location',".array2string($data).",$expiration)");
		switch($level)
		{
			case self::SESSION:
			case self::REQUEST:
				throw new Exception\WrongParameter(__METHOD__."('$level', ...) unsupported level parameter!");

			case self::INSTANCE:
			case self::TREE:
			default:
				if (!($provider = self::get_provider($level)))
				{
					return false;
				}
				// limit expiration to configured maximum time
				if (isset(self::$max_expiration) && (!$expiration || $expiration > self::$max_expiration))
				{
					$expiration = self::$max_expiration;
				}
				return $provider->add(self::keys($level,$app,$location),$data,$expiration);
		}
		throw new Exception\WrongParameter(__METHOD__."() unknown level '$level'!");
	}

	/**
	 * Set some data in the cache
	 *
	 * @param string $level use Cache::(TREE|INSTANCE|SESSION|REQUEST)
	 * @param string $app application storing data
	 * @param string $location location name for data
	 * @param mixed $data
	 * @param int $expiration =0 expiration time in seconds, default 0 = never
	 * @return boolean true if data could be stored, false otherwise
	 */
	public static function setCache($level,$app,$location,$data,$expiration=0)
	{
		//error_log(__METHOD__."('$level','$app','$location',".array2string($data).",$expiration)");
		switch($level)
		{
			case self::SESSION:
			case self::REQUEST:
				return call_user_func(array(__CLASS__,'set'.$level),$app,$location,$data,$expiration);

			case self::INSTANCE:
			case self::TREE:
			default:
				if (!($provider = self::get_provider($level)))
				{
					return false;
				}
				// limit expiration to configured maximum time
				if (isset(self::$max_expiration) && (!$expiration || $expiration > self::$max_expiration))
				{
					$expiration = self::$max_expiration;
				}
				return $provider->set(self::keys($level,$app,$location),$data,$expiration);
		}
		throw new Exception\WrongParameter(__METHOD__."() unknown level '$level'!");
	}

	/**
	 * Increment some value in the cache
	 *
	 * @param string $level use Cache::(TREE|INSTANCE), Cache::(SESSION|REQUEST) are NOT supported!
	 * @param string $app application storing data
	 * @param string $location location name for data
	 * @param int $offset =1 how much to increment by
	 * @param int $intial_value =0 value to set and return, if not in cache
	 * @param int $expiration =0
	 * @return false|int new value on success, false on error
	 */
	public static function incrementCache(string $level, string $app, string $location, int $offset=1, int $intial_value=0, int $expiration=0)
	{
		//error_log(__METHOD__."('$level', '$app', '$location', $offset, $inital_value, $expiration)");
		switch($level)
		{
			case self::SESSION:
			case self::REQUEST:
				break;

			case self::INSTANCE:
			case self::TREE:
			default:
				if (!($provider = self::get_provider($level)))
				{
					return false;
				}
				// limit expiration to configured maximum time
				if (isset(self::$max_expiration) && (!$expiration || $expiration > self::$max_expiration))
				{
					$expiration = self::$max_expiration;
				}
				return $provider->increment(self::keys($level, $app, $location), $offset, $intial_value, $expiration);
		}
		throw new Exception\WrongParameter(__METHOD__."() unsupported level '$level'!");
	}

	/**
	 * Decrements value in cache, but never below 0
	 *
	 * If new value would be below 0, 0 will be set as new value!
	 *
	 * @param string $level use Cache::(TREE|INSTANCE), Cache::(SESSION|REQUEST) are NOT supported!
	 * @param string $app application storing data
	 * @param string $location location name for data
	 * @param int $offset =1 how much to increment by
	 * @param int $intial_value =0 value to set and return, if not in cache
	 * @param int $expiration =0
	 * @return false|int new value on success, false on error
	 */
	public static function decrementCache(string $level, string $app, string $location, int $offset=1, int $intial_value=0, int $expiration=0)
	{
		//error_log(__METHOD__."('$level', '$app', '$location', $offset, $inital_value, $expiration)");
		switch($level)
		{
			case self::SESSION:
			case self::REQUEST:
				break;

			case self::INSTANCE:
			case self::TREE:
			default:
				if (!($provider = self::get_provider($level)))
				{
					return false;
				}
				// limit expiration to configured maximum time
				if (isset(self::$max_expiration) && (!$expiration || $expiration > self::$max_expiration))
				{
					$expiration = self::$max_expiration;
				}
				return $provider->decrement(self::keys($level, $app, $location), $offset, $intial_value, $expiration);
		}
		throw new Exception\WrongParameter(__METHOD__."() unsupported level '$level'!");
	}

	/**
	 * Content stored to signal one callback is already running and calculating the data, to not run it multiple times
	 */
	const CALLBACK_LOCK = '***LOCKED***';
	/**
	 * Maximum time an item is locked, to recover from eg. a crash while the lock is set
	 */
	const CALLBACL_LOCK_TIMEOUT = 10;	// 10s
	/**
	 * Time other processes sleep to wait for one callback to calculate the data
	 */
	const CALLBACK_LOCK_USLEEP = 50000;	// 50ms

	/**
	 * Get some data from the cache
	 *
	 * @param string $level use Api\Cache::(TREE|INSTANCE|SESSION|REQUEST)
	 * @param string $app application storing data
	 * @param string|array $location location(s) name for data
	 * @param callback $callback =null callback to get/create the value, if it's not cache
	 * @param array $callback_params =array() array with parameters for the callback
	 * @param int $expiration =0 expiration time in seconds, default 0 = never
	 * @return mixed NULL if data not found in cache (and no callback specified) or
	 * 	if $location is an array: location => data pairs for existing location-data, non-existing is not returned
	 */
	public static function getCache($level,$app,$location,$callback=null,array $callback_params=array(),$expiration=0)
	{
		switch($level)
		{
			case self::SESSION:
			case self::REQUEST:
				foreach((array)$location as $l)
				{
					$data[$l] = call_user_func(array(__CLASS__,'get'.$level),$app,$l,$callback,$callback_params,$expiration);
				}
				return is_array($location) ? $data : $data[$l];

			case self::INSTANCE:
			case self::TREE:
			default:
				if (!($provider = self::get_provider($level)))
				{
					return null;
				}
				try {
					if (is_array($location))
					{
						if (!is_null($callback))
						{
							throw new Exception\WrongParameter(__METHOD__."() you can NOT use multiple locations (\$location parameter is an array) together with a callback!");
						}
						if (is_a($provider, 'EGroupware\Api\Cache\ProviderMultiple'))
						{
							$data = $provider->mget($keys=self::keys($level,$app,$location));
						}
						else	// default implementation calls get multiple times
						{
							$data = array();
							foreach($location as $l)
							{
								$data[$l] = $provider->get($keys=self::keys($level,$app,$l));
								if (!isset($data[$l])) unset($data[$l]);
							}
						}
					}
					else
					{
						// try reading cached data and, if not found AND a callback to do so given, calculate it
						// only allow one process to do the (heavy) calculation by storing some lock-data
						// other processes reading the lock-data sleep for a short time, before try reading it again
						for($n=10+self::CALLBACL_LOCK_TIMEOUT*1000000/self::CALLBACK_LOCK_USLEEP; $n >= 0; $n--)
						{
							$data = $provider->get($keys=self::keys($level,$app,$location));
							if ($data === self::CALLBACK_LOCK)
							{
								usleep(self::CALLBACK_LOCK_USLEEP);
							}
							elseif($data === null && isset($callback))
							{
								$provider->set($keys, self::CALLBACK_LOCK, self::CALLBACL_LOCK_TIMEOUT);
								try {
									$data = call_user_func_array($callback, $callback_params);
								}
								catch(\Throwable $e) {
									// remove lock, before re-throwing the exception/error
									$provider->delete($keys);
									throw $e;
								}
								// limit expiration to configured maximum time
								if (isset(self::$max_expiration) && (!$expiration || $expiration > self::$max_expiration))
								{
									$expiration = self::$max_expiration;
								}
								$provider->set($keys, $data, $expiration);
								break;
							}
							else
							{
								break;
							}
						}
					}
				}
				catch(Exception $e) {
					unset($e);
					$data = null;
				}
				return $data;
		}
		throw new Exception\WrongParameter(__METHOD__."() unknown level '$level'!");
	}

	/**
	 * Unset some data in the cache
	 *
	 * @param string $level use Api\Cache::(TREE|INSTANCE|SESSION|REQUEST)
	 * @param string $app application storing data
	 * @param string $location location name for data
	 * @return boolean true if data was set, false if not (like isset())
	 */
	public static function unsetCache($level,$app,$location)
	{
		switch($level)
		{
			case self::SESSION:
			case self::REQUEST:
				return call_user_func(array(__CLASS__,'unset'.$level),$app,$location);

			case self::INSTANCE:
			case self::TREE:
			default:
				if (!($provider = self::get_provider($level, false)))
				{
					return false;
				}
				return $provider->delete(self::keys($level,$app,$location));
		}
		throw new Exception\WrongParameter(__METHOD__."() unknown level '$level'!");
	}

	/**
	 * Set some data in the cache for the whole source tree (all instances)
	 *
	 * @param string $app application storing data
	 * @param string $location location name for data
	 * @param mixed $data
	 * @param int $expiration =0 expiration time in seconds, default 0 = never
	 * @return boolean true if data could be stored, false otherwise
	 */
	public static function setTree($app,$location,$data,$expiration=0)
	{
		//error_log(__METHOD__."('$app','$location',".array2string($data).",$expiration)");
		return self::setCache(self::TREE,$app,$location,$data,$expiration);
	}

	/**
	 * Get some data from the cache for the whole source tree (all instances)
	 *
	 * @param string $app application storing data
	 * @param string $location location name for data
	 * @param callback $callback =null callback to get/create the value, if it's not cache
	 * @param array $callback_params =array() array with parameters for the callback
	 * @param int $expiration =0 expiration time in seconds, default 0 = never
	 * @return mixed NULL if data not found in cache (and no callback specified)
	 */
	public static function getTree($app,$location,$callback=null,array $callback_params=array(),$expiration=0)
	{
		return self::getCache(self::TREE,$app,$location,$callback,$callback_params,$expiration);
	}

	/**
	 * Unset some data in the cache for the whole source tree (all instances)
	 *
	 * @param string $app application storing data
	 * @param string $location location name for data
	 * @return boolean true if data was set, false if not (like isset())
	 */
	public static function unsetTree($app,$location)
	{
		return self::unsetCache(self::TREE,$app,$location);
	}

	/**
	 * Set some data in the cache for the whole source tree (all instances)
	 *
	 * @param string $app application storing data
	 * @param string $location location name for data
	 * @param mixed $data
	 * @param int $expiration =0 expiration time in seconds, default 0 = never
	 * @return boolean true if data could be stored, false otherwise
	 */
	public static function setInstance($app,$location,$data,$expiration=0)
	{
		return self::setCache(self::INSTANCE,$app,$location,$data,$expiration);
	}

	/**
	 * Get some data from the cache for the whole source tree (all instances)
	 *
	 * @param string $app application storing data
	 * @param string $location location name for data
	 * @param callback $callback =null callback to get/create the value, if it's not cache
	 * @param array $callback_params =array() array with parameters for the callback
	 * @param int $expiration =0 expiration time in seconds, default 0 = never
	 * @return mixed NULL if data not found in cache (and no callback specified)
	 */
	public static function getInstance($app,$location,$callback=null,array $callback_params=array(),$expiration=0)
	{
		return self::getCache(self::INSTANCE,$app,$location,$callback,$callback_params,$expiration);
	}

	/**
	 * Unset some data in the cache for the whole source tree (all instances)
	 *
	 * @param string $app application storing data
	 * @param string $location location name for data
	 * @return boolean true if data was set, false if not (like isset())
	 */
	public static function unsetInstance($app,$location)
	{
		return self::unsetCache(self::INSTANCE,$app,$location);
	}

	/**
	 * Prefix for appname to store expiration time in session cache
	 */
	const SESSION_EXPIRATION_PREFIX = '*expiration*';

	/**
	 * Set some data in the cache for the whole source tree (all instances)
	 *
	 * @param string $app application storing data
	 * @param string $location location name for data
	 * @param mixed $data
	 * @param int $expiration =0 expiration time in seconds, default 0 = never
	 * @return boolean true if data could be stored, false otherwise
	 */
	public static function setSession($app,$location,$data,$expiration=0)
	{
		if (isset($_SESSION[Session::EGW_SESSION_ENCRYPTED]))
		{
			if (Session::ERROR_LOG_DEBUG) error_log(__METHOD__.' called after session was encrypted --> ignored!');
			return false;	// can no longer store something in the session, eg. because commit_session() was called
		}
		// user password is no longer stored (unencrypted) in session, but encrypted by session-class
		if ($app === 'phpgwapi' && $location === 'password')
		{
			return false;
		}
		$_SESSION[Session::EGW_APPSESSION_VAR][$app][$location] = $data;

		if ($expiration > 0)
		{
			$_SESSION[Session::EGW_APPSESSION_VAR][self::SESSION_EXPIRATION_PREFIX.$app][$location] = time()+$expiration;
		}

		return true;
	}

	/**
	 * Get some data from the cache for the whole source tree (all instances)
	 *
	 * Returns a reference to the var in the session!
	 *
	 * @param string $app application storing data
	 * @param string $location location name for data
	 * @param callback $callback =null callback to get/create the value, if it's not cache
	 * @param array $callback_params =array() array with parameters for the callback
	 * @param int $expiration =0 expiration time in seconds, default 0 = never
	 * @return mixed NULL if data not found in cache (and no callback specified)
	 */
	public static function &getSession($app, $location, $callback=null, array $callback_params=array(), $expiration=0)
	{
		if (isset($_SESSION[Session::EGW_SESSION_ENCRYPTED]))
		{
			if (Session::ERROR_LOG_DEBUG) error_log(__METHOD__.' called after session was encrypted --> ignored!');
			$ret = null;	// can no longer store something in the session, eg. because commit_session() was called
			return $ret;
		}
		// user password is no longer stored (unencrypted) in session, but encrypted by session-class
		if ($app === 'phpgwapi' && $location === 'password')
		{
			$passwd = isset($GLOBALS['egw']->session) && $GLOBALS['egw']->session->__isset('passwd') ?
				base64_encode($GLOBALS['egw']->session->__get('passwd')) : null;
			return $passwd;
		}
		// check if entry is expired and clean it up in that case
		if (isset($_SESSION[Session::EGW_APPSESSION_VAR][self::SESSION_EXPIRATION_PREFIX.$app][$location]) &&
			$_SESSION[Session::EGW_APPSESSION_VAR][self::SESSION_EXPIRATION_PREFIX.$app][$location] < time())
		{
			unset($_SESSION[Session::EGW_APPSESSION_VAR][$app][$location],
				$_SESSION[Session::EGW_APPSESSION_VAR][self::SESSION_EXPIRATION_PREFIX.$app][$location]);
		}
		if (!isset($_SESSION[Session::EGW_APPSESSION_VAR][$app][$location]) && !is_null($callback))
		{
			$_SESSION[Session::EGW_APPSESSION_VAR][$app][$location] = call_user_func_array($callback,$callback_params);
		}
		return $_SESSION[Session::EGW_APPSESSION_VAR][$app][$location];
	}

	/**
	 * Unset some data in the cache for the whole source tree (all instances)
	 *
	 * @param string $app application storing data
	 * @param string $location location name for data
	 * @return boolean true if data was set, false if not (like isset())
	 */
	public static function unsetSession($app,$location)
	{
		if (isset($_SESSION[Session::EGW_SESSION_ENCRYPTED]))
		{
			if (Session::ERROR_LOG_DEBUG) error_log(__METHOD__.' called after session was encrypted --> ignored!');
			return false;	// can no longer store something in the session, eg. because commit_session() was called
		}
		// check if entry is expired and clean it up in that case
		if (isset($_SESSION[Session::EGW_APPSESSION_VAR][self::SESSION_EXPIRATION_PREFIX.$app][$location]) &&
			$_SESSION[Session::EGW_APPSESSION_VAR][self::SESSION_EXPIRATION_PREFIX.$app][$location] < time())
		{
			unset($_SESSION[Session::EGW_APPSESSION_VAR][$app][$location],
				$_SESSION[Session::EGW_APPSESSION_VAR][self::SESSION_EXPIRATION_PREFIX.$app][$location]);
		}
		if (!isset($_SESSION[Session::EGW_APPSESSION_VAR][$app][$location]))
		{
			return false;
		}
		unset($_SESSION[Session::EGW_APPSESSION_VAR][$app][$location]);

		return true;
	}

	/**
	 * Static varible to cache request wide
	 *
	 * @var array
	 */
	private static $request_cache = array();

	/**
	 * Set some data in the cache for the whole source tree (all instances)
	 *
	 * @param string $app application storing data
	 * @param string $location location name for data
	 * @param mixed $data
	 * @param int $expiration =0 expiration time is NOT used for REQUEST!
	 * @return boolean true if data could be stored, false otherwise
	 */
	public static function setRequest($app,$location,$data,$expiration=0)
	{
		unset($expiration);	// not used, but required by function signature
		self::$request_cache[$app][$location] = $data;

		return true;
	}

	/**
	 * Get some data from the cache for the whole source tree (all instances)
	 *
	 * @param string $app application storing data
	 * @param string $location location name for data
	 * @param callback $callback =null callback to get/create the value, if it's not cache
	 * @param array $callback_params =array() array with parameters for the callback
	 * @param int $expiration =0 expiration time is NOT used for REQUEST!
	 * @return mixed NULL if data not found in cache (and no callback specified)
	 */
	public static function getRequest($app,$location,$callback=null,array $callback_params=array(),$expiration=0)
	{
		unset($expiration);	// not used, but required by function signature
		if (!isset(self::$request_cache[$app][$location]) && !is_null($callback))
		{
			self::$request_cache[$app][$location] = call_user_func_array($callback,$callback_params);
		}
		return self::$request_cache[$app][$location];
	}

	/**
	 * Unset some data in the cache for the whole source tree (all instances)
	 *
	 * @param string $app application storing data
	 * @param string $location location name for data
	 * @return boolean true if data was set, false if not (like isset())
	 */
	public static function unsetRequest($app,$location)
	{
		if (!isset(self::$request_cache[$app][$location]))
		{
			return false;
		}
		unset(self::$request_cache[$app][$location]);

		return true;
	}

	/**
	 * Get a caching provider for tree or instance level
	 *
	 * The returned provider already has an opened connection
	 *
	 * @param string $level Api\Cache::(TREE|INSTANCE) or install_id
	 * @param boolean $log_not_found =true false do not log if no provider found, used eg. to supress error via unsetCache during installation
	 * @return Cache\Provider|Cache\ProviderMultiple
	 */
	static protected function get_provider($level, $log_not_found=true)
	{
		static $providers = array();

		if ($level != self::TREE) $level = self::INSTANCE;

		if (!isset($providers[$level]))
		{
			$params = $GLOBALS['egw_info']['server']['cache_provider_'.strtolower($level)] ?? null;
			if (!isset($params) && $level == self::INSTANCE && isset(self::$default_provider))
			{
				$params = self::$default_provider;
			}
			if (!isset($params))
			{
				if ($level == self::TREE)	// if no tree level provider use the instance level one
				{
					$providers[$level] = self::get_provider(self::INSTANCE);
				}
				else
				{
					$providers[$level] = false;	// no provider specified
					$reason = 'no provider specified';
				}
			}
			elseif (!$params)
			{
					$providers[$level] = false;	// cache for $level disabled
					$reason = "cache for $level disabled";
			}
			else
			{
				if (!is_array($params)) $params = (array)$params;

				$class = array_shift($params);
				if (!class_exists($class))
				{
					$providers[$level] = false;	// provider class not found
					$reason = "provider $class not found";
				}
				else
				{
					try
					{
						$providers[$level] = new $class($params);
					}
					catch(Exception $e)
					{
						$providers[$level] = false;	// eg. could not open connection to backend
						$reason = "error instanciating provider $class: ".$e->getMessage();
					}
				}
			}
			if (!$providers[$level] && $log_not_found) error_log(__METHOD__."($level) no provider found ($reason)!".function_backtrace());
		}
		//error_log(__METHOD__."($level) = ".array2string($providers[$level]).', cache_provider='.array2string($GLOBALS['egw_info']['server']['cache_provider_'.strtolower($level)]));
		return $providers[$level];
	}

	/**
	 * Get class-name of caching provider
	 *
	 * @param string $level
	 * @return string class-name of provider
	 */
	public static function getProvider($level=self::INSTANCE)
	{
		$provider = self::get_provider($level);

		return get_class($provider);
	}

	/**
	 * Get a system configuration, even if in setup and it's not read
	 *
	 * @param string $name
	 * @param boolean $throw =true throw an exception, if we can't retriev the value
	 * @return string|boolean string with config or false if not found and !$throw
	 */
	public static function get_system_config($name, $throw=true)
	{
		if(!isset($GLOBALS['egw_info']['server'][$name]))
		{
			if (isset($GLOBALS['egw_setup']) && isset($GLOBALS['egw_setup']->db) || $GLOBALS['egw']->db)
			{
				$db = $GLOBALS['egw']->db ? $GLOBALS['egw']->db : $GLOBALS['egw_setup']->db;

				try {
					if (($rs = $db->select(Config::TABLE,'config_value',array(
						'config_app'	=> 'phpgwapi',
						'config_name'	=> $name,
					),__LINE__,__FILE__)))
					{
						$GLOBALS['egw_info']['server'][$name] = $rs->fetchColumn();
					}
					else
					{
						error_log(__METHOD__."('$name', $throw) config value NOT found!");//.function_backtrace());
					}
				}
				catch(Db\Exception $e)
				{
					if ($throw) error_log(__METHOD__."('$name', $throw) cound NOT query value: ".$e->getMessage());//.function_backtrace());
				}
			}
			if (!$GLOBALS['egw_info']['server'][$name] && $throw)
			{
				throw new Exception (__METHOD__."($name) \$GLOBALS['egw_info']['server']['$name'] is NOT set!");
			}
		}
		return $GLOBALS['egw_info']['server'][$name];
	}

	/**
	 * Flush (delete) whole (instance) cache or application/class specific part of it
	 *
	 * @param string $level =self::INSTANCE
	 * @param string $app =null app-name or "all" to empty complete cache
	 */
	public static function flush($level=self::INSTANCE, $app=null)
	{
		$ret = true;
		if (!($provider = self::get_provider($level)))
		{
			$ret = false;
		}
		else
		{
			if (!$provider->flush($app !== "all" ? self::keys($level, $app) : array()))
			{
				if ($level == self::INSTANCE)
				{
					self::generate_instance_key();
				}
				else
				{
					$ret = false;
				}
			}
		}
		//error_log(__METHOD__."('$level', '$app') returning ".array2string($ret));
		return $ret;
	}

	/**
	 * Unset instance key, so it get read again and re-read install_id from database
	 */
	public static function unset_instance_key()
	{
		self::$instance_key = null;
		$GLOBALS['egw_info']['server']['install_id'] = self::get_system_config('install_id', false);
	}

	/**
	 * Key used for instance specific data
	 *
	 * @var string
	 */
	private static $instance_key;

	/**
	 * Generate a new instance key and by doing so effectivly flushes whole instance cache
	 *
	 * @param string $install_id =null default use install_id of current instance
	 * @return string new key also stored in self::$instance_key
	 */
	public static function generate_instance_key($install_id=null)
	{
		if (!isset($install_id))
		{
			self::$instance_key = null;
			$install_id = self::get_system_config('install_id');
		}
		$instance_key = self::INSTANCE.'-'.$install_id.'-'.microtime(true);
		self::setTree(__CLASS__, $install_id, $instance_key);

		//error_log(__METHOD__."(install_id='$install_id') returning '".$instance_key."'");
		return $instance_key;
	}

	/**
	 * Get keys array from $level, $app and $location
	 *
	 * @param string $level Api\Cache::(TREE|INSTANCE) or instance_id
	 * @param string $app =null
	 * @param string $location =null
	 * @return array
	 */
	public static function keys($level, $app=null, $location=null)
	{
		static $tree_key = null;

		switch($level)
		{
			case self::TREE:
				if (!isset($tree_key))
				{
					$tree_key = $level.'-'.str_replace(array(':','/','\\'),'-', self::$egw_server_root);
					// add charset to key, if not utf-8 (as everything we store depends on charset!)
					if (($charset = self::get_system_config('system_charset',false)) && $charset != 'utf-8')
					{
						$tree_key .= '-'.$charset;
					}
				}
				$level_key = $tree_key;
				break;
			default:	// arbitrary install_id given --> check for current instance
				if ($level !== $GLOBALS['egw_info']['server']['install_id'])
				{
					$level_key = self::getTree(__CLASS__, $level);
					if (!isset($level_key)) $level_key = self::generate_instance_key($level);
					break;
				}
				// fall-through for current instance
			case self::INSTANCE:
				if (!isset(self::$instance_key))
				{
					self::$instance_key = self::getTree(__CLASS__, self::get_system_config('install_id'));
					//error_log(__METHOD__."('$level',...) instance_key read from tree-cache=".array2string(self::$instance_key));
					if (!isset(self::$instance_key)) self::$instance_key = self::generate_instance_key();
				}
				$level_key = self::$instance_key;
				break;
		}
		$keys = array($level_key);
		if (isset($app))
		{
			$keys[] = $app;
			if (isset($location)) $keys[] = $location;
		}
		return $keys;
	}

	/**
	 * Let everyone know the methods of this class should be used only statically
	 *
	 */
	function __construct()
	{
		throw new Exception\WrongParameter("All methods of class ".__CLASS__." should be called static!");
	}
}

// setting apc(u) as default provider, if apc(u)_fetch function exists AND further checks in Api\Cache\Apc(u) recommed it
if (is_null(Cache::$default_provider))
{
	Cache::$default_provider =
		PHP_SAPI === 'cli' ? 'EGroupware\Api\Cache\Files' :
			(function_exists('apcu_fetch') && Cache\Apcu::available() ? 'EGroupware\Api\Cache\Apcu' :
				(function_exists('apc_fetch') && Cache\Apc::available() ? 'EGroupware\Api\Cache\Apc' :
					'EGroupware\Api\Cache\Files'));
}

//error_log('Cache::$default_provider='.array2string(Cache::$default_provider));