<?php

class CacheTools
{
	public static function iso2stamp($iso) {
		$time = new DateTime($iso);
		$stamp = $time->format('U');
		unset($time);
		return $stamp;
	}
	
	public static function stamp2iso($stamp) {
		return date('Y-m-d\TH:i:s',$stamp);
	}
}

class SortPartCacheObject
{
	public static $TYPE_UNKNOWN       = 0;
	public static $TYPE_START         = 1;
	public static $TYPE_STOP          = 2;
	public static $TYPE_DURATION_SEC  = 3;
	public static $TYPE_DURATION_MIN  = 4;
	public static $TYPE_DURATION_HOUR = 5;
	
	public static $DIRECTION_UNKNOWN = 0;
	public static $DIRECTION_ASC     = 1;
	public static $DIRECTION_DES     = 2;
	
	protected $type;
	protected $dir;
	
	function __construct() {
		$this->type = self::$TYPE_UNKNOWN;
		$this->dir  = self::$DIRECTION_UNKNOWN;
	}
	
	public function getType() {
		return $this->type;
	}
	
	public function getDir() {
		return $this->dir;
	}
	
	public function isSame($part) {
		return (($this->type == $part->getType()) && ($this->dir == $part->getDir()));
	}
	
	public function compare($interval_a, $interval_b) {
		switch ($this->type) {
			case self::$TYPE_START :
				{
					switch ($this->dir) {
						case self::$DIRECTION_ASC :
							return ($interval_b->getStartToStamp() - $interval_a->getStartToStamp());
						default :
							return ($interval_a->getStartToStamp() - $interval_b->getStartToStamp());	
					}
				}
				break;
			case self::$TYPE_STOP :
				{
					switch ($this->dir) {
						case self::$DIRECTION_ASC :
							return ($interval_b->getStopToStamp() - $interval_a->getStopToStamp());
						default :
							return ($interval_a->getStopToStamp() - $interval_b->getStopToStamp());
					}
				}
				break;
			case self::$TYPE_DURATION_SEC :
			case self::$TYPE_DURATION_MIN :
			case self::$DIRECTION_DES :
				{
					switch ($this->dir) {
						case self::$DIRECTION_ASC :
							return ($interval_b->getDuration() - $interval_a->getDuration());
						default :
							return ($interval_a->getDuration() - $interval_b->getDuration());
					}
				}
				break;
			default :
				return 0;
		}
		return 0;
	}
	
	public function loadFromObject($part_obj) {
		switch ($part_obj->property)
		{
			case 'start'        :
				$this->type = self::$TYPE_START;
				break;
			case 'stop'         :
				$this->type = self::$TYPE_STOP;
				break;
			case 'durationMin'  :
				$this->type = self::$TYPE_DURATION_MIN;
				break;
			case 'durationHour' :
				$this->type = self::$TYPE_DURATION_HOUR;
				break;
			case 'durationSec'  :
				$this->type = self::$TYPE_DURATION_SEC;
				break;
			default:
				$this->type = self::$TYPE_UNKNOWN;
		}
	
		switch ($part_obj->direction)
		{
			case 'ASC' :
				$this->dir = self::$DIRECTION_ASC;
				break;
			case 'DESC' :
				$this->dir = self::$DIRECTION_DES;
				break;
			default:
				$this->dir = self::$DIRECTION_UNKNOWN;
		}
	}
	
	public function writeBin($handle) {
		fwrite($handle,pack('L2',$this->type,$this->dir));
	}
	
	public function loadBin($handle) {
		$res = unpack('L2data',fread($handle,4*2));
		$this->type = $res['data1'];
		$this->dir   = $res['data2'];
	}
	
	public function dump() {
		echo "   => SortPartCacheObject : type = ";
		switch ($this->type)
		{
			case self::$TYPE_START :
				echo "start";
				break;
			case self::$TYPE_STOP :
				echo "stop";
				break;
			case self::$TYPE_DURATION_SEC :
				echo "duration seconde";
				break;
			case self::$TYPE_DURATION_MIN :
				echo "duration minute";
				break;
			case self::$TYPE_DURATION_HOUR :
				echo "duration hour";
				break;
			default:
				echo "unknown";
		}
		echo ", direction = ";
		switch ($this->dir)
		{
			case self::$DIRECTION_ASC :
				echo "ASC";
				break;
			case self::$DIRECTION_DES :
				echo "DESC";
				break;
			default:
				echo "unknown";
		}
		echo PHP_EOL;
	}
}

class SortCacheObject
{
	protected $parts = array();

	function __construct() {
	}

	public function getParts() {
		return $this->parts;
	}

	public function reset() {
		$this->parts = array();
	}

	public function isEmpty() {
		return (count($this->parts) == 0);
	}

	public function loadFromObject($sort_obj) {
		$this->reset();
		foreach ($sort_obj as $sort_part)
		{
			$part = new SortPartCacheObject();
			$part->loadFromObject($sort_part);
			array_push($this->parts, $part);
		}
	}
	
	public function isSameFromObject($sort_obj) {
		$sort = new SortCacheObject();
		$sort->loadFromObject($sort_obj);
		return $this->isSame($sort);
	}

	public function isSame($sort) {
		if (count($this->parts) != count($sort->getParts()))
			return false;

		$identique = true;
		for ($i = 0; $i < count($this->parts); ++$i)
		{
			if (!$this->parts[$i]->isSame($sort->getParts()[$i]))
			{
				return false;
			}
		}
			
		return true;
	}

	public function apply($intervals) {
		$sorted_indexes = array();
		
		global $global_parts, $global_intervals;
		$global_parts = $this->parts;
		$global_intervals = $intervals;
		
		foreach ($intervals as $interval)
			array_push($sorted_indexes, $interval->getIndex());
		
		if (count($global_parts) == 0)
			return $sorted_indexes;
		
		usort($sorted_indexes, function ($index_a, $index_b) {
			global $global_parts, $global_intervals;
			foreach ($global_parts as $part)
			{
				$res = $part->compare($global_intervals[$index_a], $global_intervals[$index_b]);
				if ($res != 0)
					return $res;
			}
			return $index_a-$index_b;
		});

		return $sorted_indexes;
	}

	public function writeBin($handle) {
		fwrite($handle,pack('L',count($this->parts)));
		foreach ($this->parts as $part)
			$part->writeBin($handle);
	}

	public function loadBin($handle) {
		$this->reset();
		$res = unpack('Lcount',fread($handle,4));
		for ($i = 0; $i < $res['count']; ++$i)
		{
			$part = new SortPartCacheObject();
			$part->loadBin($handle);
			array_push($this->parts, $part);
		}
	}

	public function dump() {
		echo " => SortCacheObject : number of parts = ".count($this->parts).PHP_EOL;
		foreach ($this->parts as $part)
			$part->dump();
	}
}

class FilterPartCacheObject
{
	public static $TYPE_UNKNOWN       = 0;
	public static $TYPE_START         = 1;
	public static $TYPE_STOP          = 2;
	public static $TYPE_DURATION_SEC  = 3;
	public static $TYPE_DURATION_MIN  = 4;
	public static $TYPE_DURATION_HOUR = 5;
	
	public static $OPERATION_UNKNOWN   = 0;
	public static $OPERATION_LT        = 1;
	public static $OPERATION_GT        = 2;
	public static $OPERATION_EQ        = 3;
	
	protected $type;
	protected $op;
	protected $value;
	
	function __construct() {
		$this->type  = self::$TYPE_UNKNOWN;
		$this->op    = self::$OPERATION_UNKNOWN;
		$this->value = 0.;
	}
	
	public function getType() {
		return $this->type;
	}
	
	public function getOp() {
		return $this->op;
	}
	
	public function getValue() {
		return $this->value;
	}
	
	public function isSame($part) {
		return (($this->type == $part->getType()) && ($this->op == $part->getOp()) && ($this->value == $part->getValue()));
	}
	
	public function toFiltered($interval) {
		switch ($this->type) {
			case self::$TYPE_START :
				{
					switch ($this->op) {
						case self::$OPERATION_LT :
							return ($interval->getStartToStamp() < $this->value);
						case self::$OPERATION_GT :
							return ($interval->getStartToStamp() > $this->value);
						case self::$OPERATION_EQ :
							return (!(($interval->getStartToStamp() >= $this->value) && ($interval->getStartToStamp() <= $this->value+86400)));
						default :
							return false;
					}
				}
				break;
			case self::$TYPE_STOP :
				{
					switch ($this->op) {
						case self::$OPERATION_LT :
							return ($interval->getStopToStamp() < $this->value);
						case self::$OPERATION_GT :
							return ($interval->getStopToStamp() > $this->value);
						case self::$OPERATION_EQ :
							return (!(($interval->getStopToStamp() >= $this->value) && ($interval->getStopToStamp() <= $this->value+86400)));
						default :
							return false;
					}
				}
				break;
			case self::$TYPE_DURATION_SEC :
			case self::$TYPE_DURATION_MIN :
			case self::$TYPE_DURATION_HOUR :
				{
					$value = $this->value;
					if ($this->type == self::$TYPE_DURATION_MIN)
						$value *= 60;
					else if ($this->type == self::$TYPE_DURATION_HOUR)
						$value *= 3600;
					switch ($this->op) {
						case self::$OPERATION_LT :
							return ($interval->getDuration() < $value);
						case self::$OPERATION_GT :
							return ($interval->getDuration() > $value);
						case self::$OPERATION_EQ :
							return ($interval->getDuration() != $value);
						default :
							return false;
					}
				}
				break;
			default:
				return false;
		}
	}
	
	public function loadFromObject($part_obj) {
		$this->value = 0.;
		switch ($part_obj->field)
		{
			case 'start'        :
				$this->value = CacheTools::iso2stamp($part_obj->value);
				$this->type = self::$TYPE_START;
				break;
			case 'stop'         :
				$this->value = CacheTools::iso2stamp($part_obj->value);
				$this->type = self::$TYPE_STOP;
				break;
			case 'durationMin'  :
				$this->value = $part_obj->value;
				$this->type = self::$TYPE_DURATION_MIN;
				break;
			case 'durationHour' :
				$this->value = $part_obj->value;
				$this->type = self::$TYPE_DURATION_HOUR;
				break;
			case 'durationSec'  :
				$this->value = $part_obj->value;
				$this->type = self::$TYPE_DURATION_SEC;
				break;
			default:
				$this->value = 0.;
				$this->type = self::$TYPE_UNKNOWN;
		}
		
		switch ($part_obj->comparison)
		{
			case 'lt' :
				$this->op = self::$OPERATION_LT;
				break;
			case 'gt' :
				$this->op = self::$OPERATION_GT;
				break;
			case 'eq' :
				$this->op = self::$OPERATION_EQ;
				break;
			default:
				$this->op = self::$OPERATION_UNKNOWN;
		}
	}
	
	public function writeBin($handle) {
		fwrite($handle,pack('L2',$this->type,$this->op));
		fwrite($handle,pack('f',$this->value));
	}
	
	public function loadBin($handle) {
		$res = unpack('L2data',fread($handle,4*2));
		$this->type = $res['data1'];
		$this->op   = $res['data2'];
		
		$res = unpack('fvalue',fread($handle,4));
		$this->value = $res['value'];
	}
	
	public function dump() {
		echo "   => FilterPartCacheObject : type = ";
		switch ($this->type)
		{
			case self::$TYPE_START :
				echo "start";
				break;
			case self::$TYPE_STOP :
				echo "stop";
				break;
			case self::$TYPE_DURATION_SEC :
				echo "duration seconde";
				break;
			case self::$TYPE_DURATION_MIN :
				echo "duration minute";
				break;
			case self::$TYPE_DURATION_HOUR :
				echo "duration hour";
				break;
			default:
				echo "unknown";
		}
		echo ", operation = ";
		switch ($this->op)
		{
			case self::$OPERATION_LT :
				echo "lt";
				break;
			case self::$OPERATION_GT :
				echo "gt";
				break;
			case self::$OPERATION_EQ :
				echo "eq";
				break;
			default:
				echo "unknown";
		}
		echo ", value = ".$this->value.PHP_EOL;
	}
}

class FilterCacheObject
{
	protected $parts = array();
	
	function __construct() {
		
	}
	
	public function getParts() {
		return $this->parts;
	}
	
	public function reset() {
		$this->parts = array();
	}
	
	public function isEmpty() {
		return (count($this->parts) == 0);
	}
	
	public function loadFromJSON($filter_json) {
		$this->reset();
		$filter_obj = json_decode($filter_json);
		
		foreach ($filter_obj as $filter_part)
		{
			$part = new FilterPartCacheObject();
			$part->loadFromObject($filter_part);
			array_push($this->parts, $part);
		}
	}
	
	public function isSame($filter) {
		if (count($this->parts) != count($filter->getParts()))
			return false;
		
		$identique = true;
		for ($i = 0; $i < count($this->parts); ++$i)
		{
			if (!$this->parts[$i]->isSame($filter->getParts()[$i]))
			{
				return false;
			}
		}
			
		return true;
	}
	
	public function isSameFromJSON($filter_json) {
		$filter = new FilterCacheObject();
		$filter->loadFromJSON($filter_json);
		return $this->isSame($filter);
	}
	
	public function toFiltered($interval) {
		foreach ($this->parts as $part)
		{
			if ($part->toFiltered($interval))
				return true;
		}
		return false;
	}
	
	public function writeBin($handle) {
		fwrite($handle,pack('L',count($this->parts)));
		foreach ($this->parts as $part)
			$part->writeBin($handle);
	}
	
	public function loadBin($handle) {
		$this->reset();
		$res = unpack('Lcount',fread($handle,4));
		for ($i = 0; $i < $res['count']; ++$i)
		{
			$part = new FilterPartCacheObject();
			$part->loadBin($handle);
			array_push($this->parts, $part);
		}
	}
	
	public function dump() {
		echo " => FilterCacheObject : number of parts = ".count($this->parts).PHP_EOL;
		foreach ($this->parts as $part)
			$part->dump();
	}
}

 class TimeTableCacheMgr
{
	protected static $cache_file                = "cacheTT";
	
	protected $ttMgr = null;
	protected $cache = null;

	function __construct() {
		$this->ttMgr = new TimeTableMgr();
	}

	public function initTTCache() {
		//Create new cache
		$this->cache = new TimeTableCacheObject();
		
		//Save cache file
		return array('success' => $this->saveToFile(), 'token' => $this->cache->getToken(), 'status' => $this->cache->getStatus());
	}

	public function initFromTT($id, $typeTT) {
		//Create new cache
		$this->cache = new TimeTableCacheObject();

		//Load intervals from TT file and add to cache
		$intervals_res = $this->ttMgr->loadIntervalsFromTT($id,$typeTT);
		
		if (!$intervals_res['success'])
			return $intervals_res;

		foreach ($intervals_res['intervals'] as $interval)
		{
			//Add interval
			$this->cache->addInterval($interval['start'], $interval['stop']);
		}
		
		unset($intervals_res);

		//Update cache
		$this->cache->updateIndexes();

		//Save cache file
		return array('success' => $this->saveToFile(), 'token' => $this->cache->getToken(), 'status' => $this->cache->getStatus());
	}

	public function initFromTmpObject($folderId, $name) {
		//Create new cache
		$this->cache = new TimeTableCacheObject();

		//Load intervals from TmpObject file (DD_Search output)
		$intervals_res = $this->ttMgr->getTmpObject($folderId, $name);

		if (!isset($intervals_res))
			return array('success' => false, 'message' => 'Cannot get Tmp Object');

		if (array_key_exists('intervals', $intervals_res))
		{
			foreach ($intervals_res['intervals'] as $interval)
			{
				//Add interval
				$this->cache->addInterval($interval['start'], $interval['stop']);
			}
		}

		$this->cache->setIsModified(true);
		
		unset($intervals_res);
		
		//Update cache
		$this->cache->updateIndexes();

		//Save cache file
		return array('success' => $this->saveToFile(), 'token' => $this->cache->getToken(), 'status' => $this->cache->getStatus());
	}

	public function initFromUploadedFile($name, $format) {
		//Create new cache
		$this->cache = new TimeTableCacheObject();

		//Load intervals from uploaded file
		$intervals_res = $this->ttMgr->getUploadedObject($name, $format);

		if (!isset($intervals_res))
			return array('success' => false, 'message' => 'Cannot get Tmp Object');

		if (array_key_exists('intervals', $intervals_res))
		{
			foreach ($intervals_res['intervals'] as $interval)
			{
				//Add interval
				$this->cache->addInterval($interval['start'], $interval['stop']);
			}
		}

		$this->cache->setIsModified(true);
		
		unset($intervals_res);
		
		//Update cache
		$this->cache->updateIndexes();

		//Save cache file
		return array('success' => $this->saveToFile(), 'token' => $this->cache->getToken(), 'status' => $this->cache->getStatus());
	}

	public function saveInTT($id, $action, $token) {
		if (!$this->loadFromFile())
			return array('success' => false, 'message' => 'Cannot load cache file');

		if ($token != $this->cache->getToken())
			return array('success' => false, 'message' => 'Cache token check error');
		
		$this->cache->updateIndexes();
		$this->saveToFile();
		
		$intervals = $this->cache->getIntervalsArray(NULL,NULL,true);
		
		$this->cache->reset();
		
		$res_intervals = array();
		foreach ($intervals as $interval)
		{
			array_push(
			$res_intervals,
			(object)array(
			'start' => $interval['start'],
			'stop'  => $interval['stop']
			)
			);
		}

		unset($intervals);
		
		return $this->ttMgr->saveIntervals($id, $res_intervals, $action);
	}

	public function getIntervals($start,$limit,$sort_obj,$filter_json) {
		if (!$this->loadFromFile())
			return array('success' => false, 'message' => 'Cannot load cache file');

		$needToUpdate = false;
		if (isset($filter_json))
		{
			if (!$this->cache->getFilter()->isSameFromJSON($filter_json))
			{
				$needToUpdate = true;
				$this->cache->getFilter()->loadFromJSON($filter_json);
			}
		}
		else
		{
			if (!$this->cache->getFilter()->isEmpty())
			{
				$needToUpdate = true;
				$this->cache->getFilter()->reset();
			}
		}
		
		if (isset($sort_obj))
		{
			if (!$this->cache->getSort()->isSameFromObject($sort_obj))
			{
				$needToUpdate = true;
				$this->cache->getSort()->loadFromObject($sort_obj);
			}
		}
		else
		{
			if (!$this->cache->getSort()->isEmpty())
			{
				$needToUpdate = true;
				$this->cache->getSort()->reset();
			}
		}
		
		if ($needToUpdate)
		{
			$this->cache->updateIndexes();
			$this->saveToFile();
		}

		return array(
				'token'      => $this->cache->getToken(),
				'totalCount' => $this->cache->getLength(),
				'intervals'  => $this->cache->getIntervalsArray($start, $limit),
				'start' => isset($start) ? $start : 0,
				'limit' => isset($limit) ? $limit : 0,
				'status' => $this->cache->getStatus(),
				'success'    => true
		);
	}

	public function addInterval($index, $start, $stop) {
		if (!$this->loadFromFile())
			return array('success' => false, 'message' => 'Cannot load cache file');

		if (!isset($index))
			$index = 0;

		if (!isset($start))
			$start = date('Y-m-d\TH:i:s');

		if (!isset($stop))
			$stop = date('Y-m-d\TH:i:s');
		
		$this->cache->addInterval($start, $stop, true, $index);
		
		//$this->cache->updateIndexes();
		
		$this->saveToFile();
		
		return array('success' => true, 'index' => $index, 'status' => $this->cache->getStatus());
	}

	public function removeIntervalFromId($id) {
		if (!$this->loadFromFile())
			return array('success' => false, 'message' => 'Cannot load cache file');

		$this->cache->removeIntervalFromId($id);
		
		$this->cache->updateIndexes();

		$this->saveToFile();

		return array('success' => true, 'status' => $this->cache->getStatus());
	}

	public function modifyIntervalFromId($id, $start, $stop) {
		if (!$this->loadFromFile())
			return array('success' => false, 'message' => 'Cannot load cache file');

		$this->cache->modifyIntervalFromId($id, $start, $stop);
		
		$this->saveToFile();
		
		return array('success' => true, 'status' => $this->cache->getStatus());
	}
	
	public function operationIntervals($extendTime, $shiftTime) {
		if (!$this->loadFromFile())
			return array('success' => false, 'message' => 'Cannot load cache file');
		
		$this->cache->operationIntervals($extendTime, $shiftTime);
		
		$this->saveToFile();
		
		return array('success' => true, 'status' => $this->cache->getStatus());
	}
	
	public function mergeIntervals() {
		if (!$this->loadFromFile())
			return array('success' => false, 'message' => 'Cannot load cache file');
		
		$this->cache->mergeIntervals();
		
		$this->saveToFile();
		
		return array('success' => true, 'status' => $this->cache->getStatus());
	}
	
	public function getStatistics() {
		if (!$this->loadFromFile())
			return array('success' => false, 'message' => 'Cannot load cache file');
		
		return array('success' => true, "result" => $this->cache->getStatistics(), 'status' => $this->cache->getStatus());
	}

	public function dump() {
		if (!$this->loadFromFile())
		{
			echo "ERROR to load cache file : ".$this->getCacheFilePath().PHP_EOL;
			return;
		}
		$this->cache->dump();
	}
	
	protected function getCacheFilePath() {
		return USERTTDIR.(self::$cache_file);
	}
	
	protected function saveToFile() {
		if (!isset($this->cache))
			return false;
		$handle = fopen($this->getCacheFilePath(), 'wb');
		$result = false;
		if (flock($handle, LOCK_EX))
		{
			$this->cache->writeBin($handle);
			flock( $handle, LOCK_UN );
			$result = true;
		}
		fclose($handle);
		return $result;
	}
	
	protected function loadFromFile() {
		if (!file_exists($this->getCacheFilePath()))
			return false;
		$this->cache = new TimeTableCacheObject();
		$handle = fopen($this->getCacheFilePath(), 'rb');
		$result = false;
		if (flock($handle, LOCK_SH))
		{
			$this->cache->loadBin($handle);
			flock( $handle, LOCK_UN );
			$result = true;
		}
		fclose($handle);
		return $result;
	}
}

?>