<?php
namespace CoreXEngine\Cache\Backend;

use CoreXEngine\Cache\Backend\Cache;
use CoreXEngine\Cache\AccessException;
use CoreXEngine\Cache\Exception;
use \InvalidArgumentException;
use \SQLite3;

require_once __DIR__ . "/Cache.php";
require_once __DIR__ . "/../AccessException.php";

/**
 * Cache implementation wrapped around a SQLite3 database.
 * @category CoreXEngine
 * @package CoreXEngine\Cache\Backend
 * @author Levente Polyak <levente@leventepolyak.de>
 */
class SQLite implements Cache {
	/**
	 * Stores the database we use for caching.
	 * @var \SQLite3
	 */
	protected $database;
	
	/**
	 * Stores the table name we use for caching.
	 * @var string
	 */
	protected $table;

	/**
	 * Creates a new cache object by passing a SQLite3 database. It tries to create the cache
	 * table if it does not yet exists. Its important the the sqlite database is not read-only
	 * else you may get a AccessException if trying to do a write operation.
	 * @param \SQlite3 $database the SQLite database we want to store in
	 * @param string $table default 'cache' name of the table inside the SQLite database
	 * @throws \InvalidArgumentException if we add some evil arguments
	 * @throws CoreXEngine\Cache\AccessException if we want to write a read-only file
	 * @throws CoreXEngine\Cache\Exception for no special typed problem
	 */
	public function __construct( SQLite3 $database, $table = 'cache' ) {
		if ( ! (string) $table ) {
			throw new InvalidArgumentException("Argument $table is not valid table name.");
		}
		@$database->exec("CREATE TABLE IF NOT EXISTS $table (
			c_key TEXT PRIMARY KEY NOT NULL,
			c_value BLOB NOT NULL,
			c_ttl INTEGER
		)");
		$this->database = $database;
		$this->table = $table;
		$this->checkError();
	}
	
	/**
	 * Set a key value pair with ttl. Entries that are already stored under this
	 * key will be overwritten. It uses the local time() to store the ttl.
	 * @param string $key the key identifier
	 * @param mixed $value the value to store
	 * @param integer $ttl default 0 zero to not expire else ttl
	 * @return boolean TRUE on success or FALSE on failure
	 * @throws CoreXEngine\Cache\AccessException if we want to write a read-only file
	 * @throws CoreXEngine\Cache\Exception for no special typed problem
	 */
	public function set ( $key, $value, $ttl = 0 ) {
		$statement = $this->database->prepare("REPLACE INTO {$this->table} (c_key, c_value, c_ttl) VALUES (:key, :value, :ttl)");
		$statement->bindValue(':key', $key, SQLITE3_TEXT);
		$statement->bindValue(':value', serialize($value), SQLITE3_BLOB);
		$statement->bindValue(':ttl', $ttl ? time()+$ttl : null, SQLITE3_INTEGER);
		$result = @$statement->execute();
		$this->checkError();
		return true;
	}

	/**
	 * Retrieve value by key from the cache. If the entry is out-dated it will perform a
	 * delete operation to clean the table.
	 * @param string $key the key identifier
	 * @return mixed the stored value or NULL
	 * @throws CoreXEngine\Cache\AccessException if we want to write a read-only file
	 * @throws CoreXEngine\Cache\Exception for no special typed problem
	 */
	public function get ( $key ) {
		@$this->database->exec('BEGIN TRANSACTION');
		$this->checkError();
		$statement = $this->database->prepare("SELECT c_value, c_ttl FROM {$this->table} WHERE c_key = :key");
		$statement->bindValue(':key', $key, SQLITE3_TEXT);
		$result = @$statement->execute();
		$this->checkError();
		$result = $result->fetchArray(SQLITE3_ASSOC);
		if ( $result['c_ttl'] && $result['c_ttl'] <= time() ) {
			$statement = $this->database->prepare("DELETE FROM {$this->table} WHERE c_key = :key");
			$statement->bindValue(':key', $key, SQLITE3_TEXT);
			@$statement->execute();
			$this->checkError();
			$result = null;
		}
		@$this->database->exec('COMMIT TRANSACTION');
		$this->checkError();
		return $result ? unserialize($result['c_value']) : null;
	}

	/**
	 * Delete item by key from the cache.
	 * @param string $key the key identifier
	 * @return boolean TRUE on success
	 * @throws CoreXEngine\Cache\AccessException if we want to write a read-only file
	 * @throws CoreXEngine\Cache\Exception for no special typed problem
	 */
	public function delete ( $key ) {
		$statement = $this->database->prepare("DELETE FROM {$this->table} WHERE c_key = :key");
		$statement->bindValue(':key', $key, SQLITE3_TEXT);
		@$statement->execute();
		$this->checkError();
		return true;
	}

	/**
	 * Flush the whole cache storage.
	 * @return boolean TRUE on success
	 * @throws CoreXEngine\Cache\AccessException if we want to write a read-only file
	 * @throws CoreXEngine\Cache\Exception for no special typed problem
	 */
	public function flush ( ) {
		@$this->database->exec("DELETE FROM {$this->table}");
		$this->checkError();
		return true;
	}

	/**
	 * Clean up outdated entries by checking against the ttl.
	 * @return boolean TRUE on success
	 * @throws CoreXEngine\Cache\AccessException if we want to write a read-only file
	 * @throws CoreXEngine\Cache\Exception for no special typed problem
	 */
	public function clean ( ) {
		$statement = $this->database->prepare("DELETE FROM {$this->table} WHERE c_ttl IS NOT NULL AND c_ttl < :ttl");
		$statement->bindValue(':ttl', time(), SQLITE3_INTEGER);
		@$statement->execute();
		$this->checkError();
		return true;
	}

	/**
	 * Checks the last error code and decide if and what kind of exception to throw.
	 * @return true if nothing is wrong
	 * @throws CoreXEngine\Cache\AccessException if we want to write a read-only file
	 * @throws CoreXEngine\Cache\Exception for no special typed problem
	 */
	protected function checkError ( ) {
		switch ( ($code = $this->database->lastErrorCode( )) ) {
			case 8:
				throw new AccessException($this->database->lastErrorMsg(), $this->database->lastErrorCode());
				break;
			default:
				if(0 < $code) throw new Exception($this->database->lastErrorMsg(), $this->database->lastErrorCode());
		}
		return true;
	}
}
