<?php
/**
 * @brief		Redis Cache Class
 * @since		12 November 2018
 */

namespace IPS\Output\Cache;

/* To prevent PHP errors (extending class does not exist) revealing path */
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
{
	header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
	exit;
}

/**
 * Redis Cache Class
 */
class _Redis extends \IPS\Output\Cache
{
	/**
	 * Turn on debugging which logs stats about cache hits and misses in Redis
	 *
	 * @var bool
	 */
	protected $debug = FALSE;
	
	/**
	 * Server supports this method?
	 *
	 * @return	bool
	 */
	public static function supported()
	{
		if ( class_exists('Redis') )
		{
			try
			{
				$connection = \IPS\Redis::i()->connection('read');
	
				if( !$connection )
				{
					return FALSE;
				}
				
				return TRUE;
			}
			catch( \RedisException $e )
			{ 
				return FALSE;
			}
		}
		
		return FALSE;
	}

	/**
	 * Redis key
	 */
	protected $_redisKey;
		
	/**
	 * Get random string used in the keys to identify this site compared to other sites
	 *
	 * @return  string|FALSE    Value from the _datastore; FALSE if key doesn't exist
	 */
	protected function _getRedisKey()
	{
		if ( !$this->_redisKey )
		{
			if ( !( $this->_redisKey = \IPS\Redis::i()->get( 'redisKeyOutputCache' ) ) )
			{
				$this->_redisKey = md5( mt_rand() ) . '_pg_';
				\IPS\Redis::i()->setex( 'redisKeyOutputCache', 604800, $this->_redisKey );
			}
		}
		
		return $this->_redisKey;
	}

	/**
	 * Abstract Method: Get
	 *
	 * @param   string          $key
	 * @return  array|FALSE    Value from the _datastore; FALSE if key doesn't exist
	 */
	protected function _get( $key )
	{
		try
		{
			$value = \IPS\Redis::i()->get( $this->_getRedisKey() . '_' . $key );
			
			if ( ! $value )
			{
				if ( $this->debug )
				{
					\IPS\Redis::i()->zRemRangeByScore('guest_page_failures_key_gone', 0, time() - \IPS\CACHE_PAGE_TIMEOUT );
					\IPS\Redis::i()->zAdd( 'guest_page_failures_key_gone', time(), md5(microtime()), \IPS\CACHE_PAGE_TIMEOUT );
				}
				
				return FALSE;
			}

			if ( ! $decrypted = \IPS\Text\Encrypt::fromTag( $value )->decrypt() )
			{
				if ( $this->debug )
				{
					\IPS\Redis::i()->zRemRangeByScore('guest_page_failures_decrypt', 0, time() - \IPS\CACHE_PAGE_TIMEOUT );
					\IPS\Redis::i()->zAdd( 'guest_page_failures_decrypt', time(), md5(microtime()), \IPS\CACHE_PAGE_TIMEOUT );
				}

				return FALSE;
			}

			if ( ! $unzipped = @gzdecode( $decrypted ) )
			{
				if ( $this->debug )
				{					
					\IPS\Redis::i()->zRemRangeByScore('guest_page_failures_gz', 0, time() - \IPS\CACHE_PAGE_TIMEOUT );
					\IPS\Redis::i()->zAdd( 'guest_page_failures_gz', time(), md5(microtime()), \IPS\CACHE_PAGE_TIMEOUT );
				}
				
				return FALSE;
			}
			
			if ( ! $data = json_decode( $unzipped, TRUE ) )
			{
				if ( $this->debug )
				{
					\IPS\Redis::i()->zRemRangeByScore('guest_page_failures_json_decode', 0, time() - \IPS\CACHE_PAGE_TIMEOUT );
					\IPS\Redis::i()->zAdd( 'guest_page_failures_json_decode', time(), md5(microtime()), \IPS\CACHE_PAGE_TIMEOUT );
				}
				
				return FALSE;
			}
			
			if ( $this->debug )
			{
				\IPS\Redis::i()->zRemRangeByScore('guest_page_hits', 0, time() - \IPS\CACHE_PAGE_TIMEOUT );
				\IPS\Redis::i()->zAdd( 'guest_page_hits', time(), md5(microtime()), \IPS\CACHE_PAGE_TIMEOUT );
			}
			
			return array( 'output' => $data['value'], 'meta' => $data['meta'], 'expires' => $data['expires'] );
		}
		catch( \RedisException $e )
		{
			\IPS\Redis::i()->resetConnection( $e );

			return FALSE;
		}
	}
	
	/**
	 * Store value using cache method if available or falling back to the database
	 *
	 * @param	string			$key		Key
	 * @param	mixed			$value		Value
	 * @param	mixed			$meta		Meta data (contentType, headers, etc)
	 * @param	\IPS\DateTime	$expire		Expiration if using database
	 * @return	bool
	 */
	protected function _set( $key, $value, $meta, \IPS\DateTime $expire )
	{
		try
		{
			if ( $this->debug )
			{
				\IPS\Redis::i()->zRemRangeByScore('guest_page_created', 0, time() - \IPS\CACHE_PAGE_TIMEOUT );
				\IPS\Redis::i()->zAdd( 'guest_page_created', time(), md5(microtime()), \IPS\CACHE_PAGE_TIMEOUT );
			}

			/* json_encode > gzencode > crypt */
			return (bool) \IPS\Redis::i()->setex( $this->_getRedisKey() . '_' . $key, $expire->getTimestamp() - time(), \IPS\Text\Encrypt::fromPlaintext( gzencode( json_encode( array( 'value' => $value, 'meta' => $meta, 'expires' => $expire->getTimestamp() ) ) ) )->tag() );
		}
		catch( \RedisException $e )
		{
			\IPS\Redis::i()->resetConnection( $e );

			return FALSE;
		}
	}

	/**
	 * Remove all expired caches
	 *
	 * @param	boolean	$truncate	Truncate the table
	 * @return	void
	 */
	public function deleteExpired( $truncate=FALSE )
	{
		/* Redis handles this for us automatically */
	}
	
	/**
	 * Abstract Method: Clear All Caches
	 *
	 * @return	void
	 */
	public function clearAll()
	{
		$this->_redisKey = md5( mt_rand() ) . '_pg_';
		\IPS\Redis::i()->setex( 'redisKeyOutputCache', 604800, $this->_redisKey );
	}
}