<?php

class TimeTableCacheObject
{
	protected static $format_version = 1;
	protected static $token_len = 8;
	
	protected $token  = "";
	
	protected $lastId = 0;
	
	protected $intervals = array();
	protected $indexes   = array();
	
	protected $isModified = false;
	
	protected $filter = null;
	
	protected $sort = null;

	function __construct() {
		$this->token  = $this->getRandomToken();
		$this->filter = new FilterCacheObject();
		$this->sort   = new SortCacheObject();
	}

	public function reset() {
		$this->lastId = 0;
		$this->isModified = false;
		$this->intervals = array();
		$this->indexes   = array();
		unset($this->filter);
		$this->filter = new FilterCacheObject();;
		unset($this->sort);
		$this->sort = new SortCacheObject();
	}
	
	public function setIsModified($isModified) {
		$this->isModified = $isModified;
	}
	
	public function addInterval($startIso, $stopIso, $isNew = false, $index = -1) {
		$interval = new IntervalCacheObject($this->lastId, count($this->intervals));
		++$this->lastId;
		$interval->setStartFromISO($startIso);
		$interval->setStopFromISO($stopIso);
		$interval->setIsNew($isNew);
		array_push($this->intervals, $interval);
		if ($index < 0)
			array_push($this->indexes, count($this->intervals) - 1);
		else
			array_splice($this->indexes, $index, 0, array(count($this->intervals) - 1));
		if ($isNew)
			$this->isModified = true;
		return $interval;
	}
	
	public function removeIntervalFromId($id) {
		for ($i = 0; $i < count($this->intervals); ++$i)
		{
			if ($this->intervals[$i]->getId() == $id)
			{
				//Remove interval
				array_splice($this->intervals, $i, 1);
				//Remove interval index if exist in indexes list
				for ($j = 0; $j < count($this->indexes); ++$j)
				{
					if ($this->indexes[$j] == $i)
					{
						array_splice($this->indexes, $j, 1);
						break;
					}
				}
				//Update indexes list
				for ($j = 0; $j < count($this->indexes); ++$j)
				{
					if ($this->indexes[$j] >=  $i)
						$this->indexes[$j]--;
				}
				$this->isModified = true;
				return true;
			}
		}
		
		return false;
	}
	
	public function modifyIntervalFromId($id, $start, $stop) {
		foreach ($this->intervals as $interval)
		{
			if ($interval->getId() == $id)
			{
				if (isset($start))
					$interval->setStartFromISO($start);
				if (isset($stop))
					$interval->setStopFromISO($stop);
				$interval->setIsModified(true);
				$this->isModified = true;
				return true;
			}
		}
		
		return false;
	}
	
	public function operationIntervals($extendTime, $shiftTime) {
		if (($extendTime == 0) && ($shiftTime == 0))
			//Nothing to do
			return true;
		
		for ($i = 0; $i < count($this->indexes); ++$i) {
			$start = $this->intervals[$this->indexes[$i]]->getStartToStamp();
			$start -= $extendTime;
			$start += $shiftTime;
			$this->intervals[$this->indexes[$i]]->setStartFromStamp($start);
			
			$stop = $this->intervals[$this->indexes[$i]]->getStopToStamp();
			$stop += $extendTime;
			$stop += $shiftTime;
			$this->intervals[$this->indexes[$i]]->setStopFromStamp($stop);
			
			$this->intervals[$this->indexes[$i]]->setIsModified(true);
			$this->isModified = true;
		}
		
		return true;
	}
	
	public function mergeIntervals() {
		$this->sort->reset();
		
		$this->sort->loadFromObject(
			array(
				(object)array("property" => "start", "direction" => "DESC")
			)	
		);
		
		$this->updateIndexes();
		
		$merged_intervals = array();
		
		for ($i = 0; $i < count($this->indexes); ++$i) {
			if (count($merged_intervals) == 0)
			{
				array_push($merged_intervals,array(
					"start" => $this->intervals[$this->indexes[$i]]->getStartToStamp(),
					"stop"  => $this->intervals[$this->indexes[$i]]->getStopToStamp(),
					"mod"   => FALSE)
				);
				continue;
			}
			if (($merged_intervals[count($merged_intervals)-1]["stop"] >= $this->intervals[$this->indexes[$i]]->getStartToStamp()) &&
				($merged_intervals[count($merged_intervals)-1]["stop"] < $this->intervals[$this->indexes[$i]]->getStopToStamp()))
			{
				$merged_intervals[count($merged_intervals)-1]["stop"] = $this->intervals[$this->indexes[$i]]->getStopToStamp();
				$merged_intervals[count($merged_intervals)-1]["mod"] = TRUE;
			}
			else
				array_push($merged_intervals,array(
					"start" => $this->intervals[$this->indexes[$i]]->getStartToStamp(),
					"stop"  => $this->intervals[$this->indexes[$i]]->getStopToStamp(),
					"mod"   => FALSE)
				);
		}
		
		$this->reset();
		
		foreach ($merged_intervals as $merged_interval) {
			$interval = new IntervalCacheObject($this->lastId, count($this->intervals));
			++$this->lastId;
			$interval->setStartFromStamp($merged_interval["start"]);
			$interval->setStopFromStamp($merged_interval["stop"]);
			$interval->setIsNew($merged_interval["mod"]);
			if ($merged_interval["mod"])
				$this->isModified = true;
			array_push($this->intervals, $interval);
			array_push($this->indexes, count($this->intervals) - 1);
		}
		
		return true;
	}
	
	public function getStatistics() {
		$minTime  = NULL;
		$maxTime  = NULL;
		$minDuration  = NULL;
		$maxDuration  = NULL;
		$indexMinDuration = -1;
		$indexMaxDuration = -1;
		
		$nbValid = 0;
		$durationTotal = 0;
		
		//Min & Max
		for ($i = 0; $i < count($this->indexes); ++$i) {
			if ($this->intervals[$this->indexes[$i]]->getDuration() <= 0)
				//Invalid interval
				continue;
			
			++$nbValid;
			$durationTotal += $this->intervals[$this->indexes[$i]]->getDuration();
			
			if (!isset($minTime) || ($minTime > $this->intervals[$this->indexes[$i]]->getStartToStamp()))
				$minTime = $this->intervals[$this->indexes[$i]]->getStartToStamp();
			
			if (!isset($maxTime) || ($maxTime < $this->intervals[$this->indexes[$i]]->getStopToStamp()))
				$maxTime = $this->intervals[$this->indexes[$i]]->getStopToStamp();
			
			if (!isset($minDuration) || ($minDuration > $this->intervals[$this->indexes[$i]]->getDuration()))
			{
				$minDuration      = $this->intervals[$this->indexes[$i]]->getDuration();
				$indexMinDuration = $i;
			}
			
			if (!isset($maxDuration) || ($maxDuration < $this->intervals[$this->indexes[$i]]->getDuration()))
			{
				$maxDuration      = $this->intervals[$this->indexes[$i]]->getDuration();
				$indexMaxDuration = $i;
			}
		}
		
		if (!isset($minTime))
			$minTime = 0;
		if (!isset($maxTime))
			$maxTime = 0;
		if (!isset($minDuration))
			$minDuration = 0;
		if (!isset($maxDuration))
			$maxDuration = 0;
		
		
		//Mean
		if ($nbValid > 0)
			$mean = $durationTotal / $nbValid;
		else
			$mean = 0;
		
		//Standard deviation
		$pow = 0;
		for ($i = 0; $i < count($this->indexes); ++$i) {
			if ($this->intervals[$this->indexes[$i]]->getDuration() <= 0)
				//Invalid interval
				continue;
			
			$pow += pow($this->intervals[$this->indexes[$i]]->getDuration()-$mean,2);
		}
		if ($nbValid > 0)
			$variance = $pow/$nbValid;
		else
			$variance = 0;
		$stdev = sqrt($variance);
		
		//Sort by duration to get median
		$this->sort->reset();
		
		$this->sort->loadFromObject(
				array(
						(object)array("property" => "durationSec", "direction" => "DESC")
				)
		);
		
		$this->updateIndexes();
		
		$durations = array();
		for ($i = 0; $i < count($this->indexes); ++$i) {
			if ($this->intervals[$this->indexes[$i]]->getDuration() <= 0)
				//Invalid interval
				continue;
			
			array_push($durations, $this->intervals[$this->indexes[$i]]->getDuration());
		}
		
		if (count($durations) > 0)
		{
			if (count($durations)%2 > 0) {
				$median = $durations[count($durations)/2-0.5];
			} else { // else the number of intervals is an even number
				$median = ($durations[count($durations)/2-1] + $durations[count($durations)/2])/2;
			}
		}
		else
			$median = 0;
		
		//Merge intervals to get density
		$this->mergeIntervals();
		
		$durationMergedTotal = 0;
		for ($i = 0; $i < count($this->indexes); ++$i) {
			if ($this->intervals[$this->indexes[$i]]->getDuration() <= 0)
				//Invalid interval
				continue;
				
			$durationMergedTotal += $this->intervals[$this->indexes[$i]]->getDuration();
		}
		
		if (($maxTime-$minTime) > 0)
			$density = (($durationMergedTotal/($maxTime-$minTime)));
		else
			$density = 0;
		
		return array(
			"minDuration"     => $minDuration,
			"minDurationIndex"=> $indexMinDuration,
			"maxDuration"     => $maxDuration,
			"maxDurationIndex"=> $indexMaxDuration,
			"mean"    => $mean,
			"stdev"   => $stdev,
			"median"  => $median,
			"density" => $density);
	}
	
	public function getStatus() {
		$nbFiltered = count($this->intervals) - count($this->indexes);
		
		$nbModified = 0;
		$nbNew      = 0;
		$nbInvalid  = 0;
		$nbValid    = 0;
		for ($i = 0; $i < count($this->indexes); ++$i) {
			if ($this->intervals[$this->indexes[$i]]->getDuration() <= 0)
				++$nbInvalid;
			else
				++$nbValid;
			if ($this->intervals[$this->indexes[$i]]->isModified())
				++$nbModified;
			if ($this->intervals[$this->indexes[$i]]->isNew())
				++$nbNew;
		}
		
		return array(
			"nbFiltered" => $nbFiltered,
			"nbModified" => $nbModified,
			"nbNew"      => $nbNew,
			"nbInvalid"  => $nbInvalid,
			"nbValid"    => $nbValid,
			"isModified" => $this->isModified
		);
	}
	
	public function getIntervalsArray($startIndex, $limit,$skipInvalid = false) {
		$intervals = array();
		
		if (!isset($startIndex))
			$startIndex = 0;
		
		if (!isset($limit))
			$limit = count($this->indexes);
		
		for ($i = 0; $i < $limit; ++$i) {
			if ($startIndex+$i >= count($this->indexes))
				break;
			if ($skipInvalid && ($this->intervals[$this->indexes[$startIndex+$i]]->getDuration() <= 0))
				continue;
			array_push($intervals, $this->intervals[$this->indexes[$startIndex+$i]]->toArray());
		}
		return $intervals;
	}
	
	public function getLength() {
		return count($this->indexes);
	}
	
	public function getToken() {
		return $this->token;
	}
	
	public function getFilter() {
		return $this->filter;
	}
	
	public function getSort() {
		return $this->sort;
	}
	
	public function updateIndexes() {
		$this->indexes = array();
		
		for ($i = 0; $i < count($this->intervals); ++$i)
			$this->intervals[$i]->setIndex($i);
		
		//Apply sort
		$sort_result = $this->sort->apply($this->intervals);
		
		//Apply filter
		for ($i = 0; $i < count($sort_result); ++$i)
		{
			if (!$this->filter->toFiltered($this->intervals[$sort_result[$i]]))
				array_push($this->indexes,$this->intervals[$sort_result[$i]]->getIndex());
		}
	}
	
	public function writeBin($handle) {
		//Magic key ("TTC")
		fwrite($handle,pack('C3',ord('T'),ord('T'),ord('C')));
		
		//Version
		fwrite($handle,pack('L',TimeTableCacheObject::$format_version));
		
		//Token
		for ($i = 0; $i < TimeTableCacheObject::$token_len; ++$i)
			fwrite($handle,pack('C',ord($this->token[$i])));
		
		//Modified
		fwrite($handle,pack('L',$this->isModified));
		
		//Filter
		$this->filter->writeBin($handle);
		
		//Sort
		$this->sort->writeBin($handle);

		//Intervals
		fwrite($handle,pack('L2',count($this->intervals), $this->lastId));
		foreach($this->intervals as $interval)
			$interval->writeBin($handle);
		
		//Indexes
		fwrite($handle,pack('L',count($this->indexes)));
		foreach($this->indexes as $index)
			fwrite($handle,pack('L',$index));
	}
	
	public function loadBin($handle) {
		//Magic key ("TTC")
		if (!$res = unpack('C3key',fread($handle,3)))
			return false;
		
		if (($res['key1'] != ord('T')) || ($res['key2'] != ord('T')) || ($res['key3'] != ord('C')))
			return false;
		
		//Version
		if (!$res = unpack('Lversion',fread($handle,4)))
			return false;
		if (($res['version'] != TimeTableCacheObject::$format_version))
			return false;

		//Token
		$token = "";
		for ($i = 0; $i < TimeTableCacheObject::$token_len; ++$i)
		{
			if (!$res = unpack('Ctoken',fread($handle,1)))
				return false;
			$token .= chr($res['token']);
		}
		$this->token = $token;
		
		//Modified
		if (!$res = unpack('Lmodified',fread($handle,4)))
			return false;
		$this->isModified = $res['modified'];
		
		//Filter
		$this->filter->loadBin($handle);
		
		//Sort
		$this->sort->loadBin($handle);
		
		//Intervals
		$res = unpack('L2data',fread($handle,2*4));
		$nbIntervals  = $res['data1'];
		$this->lastId = $res['data2'];
		for ($i = 0; $i < $nbIntervals; ++$i)
		{
			$interval = new IntervalCacheObject(-1);
			$interval->loadBin($handle);
			array_push($this->intervals, $interval);
		}
		
		//Indexes
		$res = unpack('Ldata',fread($handle,4));
		$nbIndexes  = $res['data'];
		for ($i = 0; $i < $nbIndexes; ++$i)
		{
			$res = unpack('Lindex',fread($handle,4));
			array_push($this->indexes, $res['index']);
		}
		
		return true;
	}
	
	protected function getRandomToken() {
		$letters = 'abcefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
		return substr(str_shuffle($letters), 0, TimeTableCacheObject::$token_len);
	}
	
	public function dump() {
		echo " => TimeTableCacheObject : token = ".$this->token.", nb intervals = ".count($this->intervals).", last id = ".$this->lastId.", nb indexes = ".count($this->indexes).PHP_EOL;
		echo PHP_EOL;
		
		$this->filter->dump();
		echo PHP_EOL;
		
		$this->sort->dump();
		echo PHP_EOL;
		
		foreach ($this->intervals as $interval)
			$interval->dump();
		echo PHP_EOL;
		
		echo " => Indexes list : ";
		foreach ($this->indexes as $index)
		{
			echo $index.", ";
		}
		echo PHP_EOL;
	}
}

?>