<?php
namespace CodeIgniter\Cache\Handlers;
use CodeIgniter\Cache\CacheInterface;
use CodeIgniter\Cache\Exceptions\CacheException;
class FileHandler implements CacheInterface
{
	
	protected $prefix;
	
	protected $path;
	
	public function __construct($config)
	{
		$path = ! empty($config->storePath) ? $config->storePath : WRITEPATH . 'cache';
		if (! is_really_writable($path))
		{
			throw CacheException::forUnableToWrite($path);
		}
		$this->prefix = $config->prefix ?: '';
		$this->path   = rtrim($path, '/') . '/';
	}
	
	public function initialize()
	{
		}
	
	public function get(string $key)
	{
		$key = $this->prefix . $key;
		$data = $this->getItem($key);
		return is_array($data) ? $data['data'] : null;
	}
	
	public function save(string $key, $value, int $ttl = 60)
	{
		$key = $this->prefix . $key;
		$contents = [
			'time' => time(),
			'ttl'  => $ttl,
			'data' => $value,
		];
		if ($this->writeFile($this->path . $key, serialize($contents)))
		{
			chmod($this->path . $key, 0640);
			return true;
		}
		return false;
	}
	
	public function delete(string $key)
	{
		$key = $this->prefix . $key;
		return is_file($this->path . $key) ? unlink($this->path . $key) : false;
	}
	
	public function increment(string $key, int $offset = 1)
	{
		$key = $this->prefix . $key;
		$data = $this->getItem($key);
		if ($data === false)
		{
			$data = [
				'data' => 0,
				'ttl'  => 60,
			];
		}
		elseif (! is_int($data['data']))
		{
			return false;
		}
		$new_value = $data['data'] + $offset;
		return $this->save($key, $new_value, $data['ttl']) ? $new_value : false;
	}
	
	public function decrement(string $key, int $offset = 1)
	{
		$key = $this->prefix . $key;
		$data = $this->getItem($key);
		if ($data === false)
		{
			$data = [
				'data' => 0,
				'ttl'  => 60,
			];
		}
		elseif (! is_int($data['data']))
		{
			return false;
		}
		$new_value = $data['data'] - $offset;
		return $this->save($key, $new_value, $data['ttl']) ? $new_value : false;
	}
	
	public function clean()
	{
		return $this->deleteFiles($this->path, false, true);
	}
	
	public function getCacheInfo()
	{
		return $this->getDirFileInfo($this->path);
	}
	
	public function getMetaData(string $key)
	{
		$key = $this->prefix . $key;
		if (! is_file($this->path . $key))
		{
			return false;
		}
		$data = @unserialize(file_get_contents($this->path . $key));
		if (is_array($data))
		{
			$mtime = filemtime($this->path . $key);
			if (! isset($data['ttl']))
			{
				return false;
			}
			return [
				'expire' => $mtime + $data['ttl'],
				'mtime'  => $mtime,
				'data'   => $data['data'],
			];
		}
		return false;
	}
	
	public function isSupported(): bool
	{
		return is_writable($this->path);
	}
	
	protected function getItem(string $key)
	{
		if (! is_file($this->path . $key))
		{
			return false;
		}
		$data = unserialize(file_get_contents($this->path . $key));
		if ($data['ttl'] > 0 && time() > $data['time'] + $data['ttl'])
		{
			unlink($this->path . $key);
			return false;
		}
		return $data;
	}
	
	protected function writeFile($path, $data, $mode = 'wb')
	{
		if (($fp = @fopen($path, $mode)) === false)
		{
			return false;
		}
		flock($fp, LOCK_EX);
		for ($result = $written = 0, $length = strlen($data); $written < $length; $written += $result)
		{
			if (($result = fwrite($fp, substr($data, $written))) === false)
			{
				break;
			}
		}
		flock($fp, LOCK_UN);
		fclose($fp);
		return is_int($result);
	}
	
	protected function deleteFiles(string $path, bool $del_dir = false, bool $htdocs = false, int $_level = 0): bool
	{
			$path = rtrim($path, '/\\');
		if (! $current_dir = @opendir($path))
		{
			return false;
		}
		while (false !== ($filename = @readdir($current_dir)))
		{
			if ($filename !== '.' && $filename !== '..')
			{
				if (is_dir($path . DIRECTORY_SEPARATOR . $filename) && $filename[0] !== '.')
				{
					$this->deleteFiles($path . DIRECTORY_SEPARATOR . $filename, $del_dir, $htdocs, $_level + 1);
				}
				elseif ($htdocs !== true || ! preg_match('/^(\.htaccess|index\.(html|htm|php)|web\.config)$/i', $filename))
				{
					@unlink($path . DIRECTORY_SEPARATOR . $filename);
				}
			}
		}
		closedir($current_dir);
		return ($del_dir === true && $_level > 0) ? @rmdir($path) : true;
	}
	
	protected function getDirFileInfo(string $source_dir, bool $top_level_only = true, bool $_recursion = false)
	{
		static $_filedata = [];
		$relative_path    = $source_dir;
		if ($fp = @opendir($source_dir))
		{
					if ($_recursion === false)
			{
				$_filedata  = [];
				$source_dir = rtrim(realpath($source_dir), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
			}
					while (false !== ($file = readdir($fp)))
			{
				if (is_dir($source_dir . $file) && $file[0] !== '.' && $top_level_only === false)
				{
					$this->getDirFileInfo($source_dir . $file . DIRECTORY_SEPARATOR, $top_level_only, true);
				}
				elseif ($file[0] !== '.')
				{
					$_filedata[$file]                  = $this->getFileInfo($source_dir . $file);
					$_filedata[$file]['relative_path'] = $relative_path;
				}
			}
			closedir($fp);
			return $_filedata;
		}
		return false;
	}
	
	protected function getFileInfo(string $file, $returned_values = ['name', 'server_path', 'size', 'date'])
	{
		if (! is_file($file))
		{
			return false;
		}
		if (is_string($returned_values))
		{
			$returned_values = explode(',', $returned_values);
		}
		foreach ($returned_values as $key)
		{
			switch ($key)
			{
				case 'name':
					$fileInfo['name'] = basename($file);
					break;
				case 'server_path':
					$fileInfo['server_path'] = $file;
					break;
				case 'size':
					$fileInfo['size'] = filesize($file);
					break;
				case 'date':
					$fileInfo['date'] = filemtime($file);
					break;
				case 'readable':
					$fileInfo['readable'] = is_readable($file);
					break;
				case 'writable':
					$fileInfo['writable'] = is_writable($file);
					break;
				case 'executable':
					$fileInfo['executable'] = is_executable($file);
					break;
				case 'fileperms':
					$fileInfo['fileperms'] = fileperms($file);
					break;
			}
		}
		return $fileInfo;
	}
}