<?php

/* Timezones
 * - Feeds (table `feeds`):
 *   - last_update: timestamp in site time (see Settings::TIMEZONE)
 * - Spot Messages (table `messages`): 
 *   - unix_time: UNIX (int) in UTC 
 *   - site_time: timestamp in site time (see Settings::TIMEZONE)
 *   - iso_time: raw ISO 8601 in local timezone
 * - Medias (table `medias`):
 *   - posted_on: timestamp in site time (see Settings::TIMEZONE)
 *   - taken_on: timestamp in site time (see Settings::TIMEZONE)
 * - Posts (table `posts`):
 *	 - site_time: timestamp in site time (see Settings::TIMEZONE)
 */

class Spot extends Main
{
	//Database
	const POST_TABLE = 'posts';
	
	const FEED_CHUNK_SIZE = 15;
	
	const DEFAULT_LANG = 'en';
	
	/**
	 * Active Project
	 * @var Project
	 */
	private $oProject;
	
	/**
	 * Media Class
	 * @var Media
	 */
	private $oMedia;
	
	/**
	 * User
	 * @var User
	 */
	private $oUser;
	
	public function __construct($oClassManagement, $sProcessPage, $sTimezone)
	{
		$asClasses = array(
			array('name'=>'feed', 'project'=>true),
			array('name'=>'project', 'project'=>true),
			array('name'=>'media', 'project'=>true),
			array('name'=>'converter', 'project'=>true),
			array('name'=>'user', 'project'=>true),
			array('name'=>'email', 'project'=>true)
		);
		parent::__construct($oClassManagement, $sProcessPage, $asClasses, true, __FILE__, $sTimezone);
		
		$this->oUser = new User($this->oDb);
		
		$this->oClassManagement->incClass('translator');
		$this->oLang = new Translator($this->oUser->getLang(), self::DEFAULT_LANG);
		
		$this->oProject = new Project($this->oDb);
		$this->oMedia = new Media($this->oDb, $this->oProject);
	}
	
	protected function install()
	{
		//Install DB
		$this->oDb->install();
	}
	
	protected function getSqlOptions()
	{
		return array
		(
			'tables' 			=> array
			(
				Feed::MSG_TABLE		=> array('ref_msg_id', Db::getId(Feed::FEED_TABLE), 'type', 'latitude', 'longitude', 'iso_time', 'site_time', 'unix_time', 'content', 'battery_state'),
				Feed::FEED_TABLE	=> array('ref_feed_id', Db::getId(Feed::SPOT_TABLE), Db::getId(Project::PROJ_TABLE), 'name', 'description', 'status', 'last_update'),
				Feed::SPOT_TABLE	=> array('ref_spot_id', 'name', 'model'),
				Project::PROJ_TABLE	=> array('name', 'codename', 'active_from', 'active_to', 'timezone'),
				self::POST_TABLE	=> array(Db::getId(Project::PROJ_TABLE), Db::getId(User::USER_TABLE), 'name', 'content', 'site_time'),
				Media::MEDIA_TABLE	=> array(Db::getId(Project::PROJ_TABLE), 'filename', 'type', 'taken_on', 'posted_on', 'rotate', 'comment'),
				User::USER_TABLE	=> array('name', 'email', 'language', 'active')
			),
			'types' 			=> array
			(
				'ref_msg_id'	=> "INT",
				'type'			=> "VARCHAR(20)",
				'latitude'		=> "DECIMAL(7,5)",
				'longitude'		=> "DECIMAL(8,5)",
				'iso_time'		=> "VARCHAR(24)",
				'site_time'		=> "TIMESTAMP DEFAULT 0",	//DEFAULT 0 removes auto-set to current time
				'unix_time'		=> "INT",
				'content'		=> "LONGTEXT",
				'battery_state'	=> "VARCHAR(10)",
				'ref_spot_id'	=> "VARCHAR(10)",
				'name'			=> "VARCHAR(100)",
				'codename'		=> "VARCHAR(100)",
				'model'			=> "VARCHAR(20)",
				'ref_feed_id'	=> "VARCHAR(40)",
				'description'	=> "VARCHAR(100)",
				'status'		=> "VARCHAR(10)",
				'active_from'	=> "TIMESTAMP DEFAULT 0",
				'active_to'		=> "TIMESTAMP DEFAULT 0",
				'timezone'		=> "VARCHAR(100)",
				'last_update'	=> "TIMESTAMP DEFAULT 0",
				'filename'		=> "VARCHAR(100) NOT NULL",
				'taken_on'		=> "TIMESTAMP DEFAULT 0",
				'posted_on'		=> "TIMESTAMP DEFAULT 0",
				'rotate'		=> "SMALLINT",
				'comment'		=> "LONGTEXT",
				'email'			=> "VARCHAR(320)",
				'language'		=> "VARCHAR(2)",
				'active'		=> "BOOLEAN"
			),
			'constraints'		=> array
			(
				Feed::MSG_TABLE		=> array("UNIQUE KEY `uni_ref_msg_id` (`ref_msg_id`)", "INDEX(`ref_msg_id`)"),
				Feed::FEED_TABLE	=> array("UNIQUE KEY `uni_ref_feed_id` (`ref_feed_id`)", "INDEX(`ref_feed_id`)"),
				Feed::SPOT_TABLE	=> array("UNIQUE KEY `uni_ref_spot_id` (`ref_spot_id`)", "INDEX(`ref_spot_id`)"),
				Project::PROJ_TABLE => "UNIQUE KEY `uni_proj_name` (`codename`)",
				Media::MEDIA_TABLE	=> "UNIQUE KEY `uni_file_name` (`filename`)",
				User::USER_TABLE	=> "UNIQUE KEY `uni_email` (`email`)"
			),
			'cascading_delete'	=> array
			(
				Feed::SPOT_TABLE	=> array(Feed::MSG_TABLE)
			)
		);
	}
	
	public function getMainPage($asGlobalVars = array(), $sMainPage = 'index', $asMainPageTags=array())
	{
		return parent::getMainPage(
			array(
				'vars' => array(
					'chunk_size'				=> self::FEED_CHUNK_SIZE,
					'default_project_codename'	=> $this->oProject->getProjectCodeName(),
					'projects'					=> $this->oProject->getProjects(),
					'user'						=> $this->oUser->getUserInfo()
				),
				'consts' => array(
					'geo_server'				=> Settings::GEO_SERVER,
					'modes'						=> Project::MODES,
					'default_timezone'			=> Settings::TIMEZONE
				)
			),
			'index',
			array(
				'host_url'					=> $this->asContext['serv_name'],
				'filepath_css'				=> self::addTimestampToFilePath('style/spot.css'),
				'filepath_js_d3'			=> self::addTimestampToFilePath('script/d3.min.js'),
				'filepath_js_leaflet'		=> self::addTimestampToFilePath('script/leaflet.min.js'),
				'filepath_js_jquery'		=> self::addTimestampToFilePath('script/jquery.min.js'),
				'filepath_js_jquery_mods'	=> self::addTimestampToFilePath('script/jquery.mods.js'),
				'filepath_js_spot'			=> self::addTimestampToFilePath('script/spot.js'),
				'filepath_js_lightbox'		=> self::addTimestampToFilePath('script/lightbox.js')
			)
		);
	}
	
	/* Managing projects */
	
	public function setProjectId($iProjectId=0) {
		$this->oProject->setProjectId($iProjectId);
	}
	
	public function updateProject() {
		$bNewMsg = false;
		
		//Update all feeds belonging to the project
		$asFeeds = $this->oProject->getFeedIds();
		foreach($asFeeds as $iFeedId) {
			$oFeed = new Feed($this->oDb, $iFeedId);
			$bNewMsg = $bNewMsg || $oFeed->checkUpdateFeed($this->oProject->getMode());
		}
		
		//Send Update Email
		if($bNewMsg) {
			$oEmail = new Email($this->asContext['serv_name'], 'email_update');
			$oEmail->setDestInfo($this->oUser->getActiveUsersInfo());
			
			//Add Position
			$asMessages = $this->getSpotMessages();
			$asLastMessage = end($asMessages);
			$asLastMessage['token'] = Settings::GEO_SERVER_TOKEN;
			$oEmail->oTemplate->setTags($asLastMessage);
			$oEmail->oTemplate->setTag('date_time', 'lang:date_time', array(date('d/m/Y', $asLastMessage['unix_time']), date('H:i', $asLastMessage['unix_time'])));
			
			//Add latest news feed
			$asNews = $this->getNewsFeed(0, true);
			$iPostCount = 0;
			foreach($asNews as $asPost) {
				if($asPost['type'] != 'message') {
					$oEmail->oTemplate->newInstance('news');
					$oEmail->oTemplate->setInstanceTag('news', 'local_server', $this->asContext['serv_name']);
					$oEmail->oTemplate->addInstance($asPost['type'], $asPost);
					$oEmail->oTemplate->setInstanceTag($asPost['type'], 'local_server', $this->asContext['serv_name']);
					$iPostCount++;
				}
				if($iPostCount==5) break;
			}
			
			$oEmail->send();
		}
	}
	
	public function genCronFile() {
		//$bSuccess = (file_put_contents('spot_cron.sh', '#!/bin/bash'."\n".'cd '.dirname($_SERVER['SCRIPT_FILENAME'])."\n".'php -f index.php a=update_feed')!==false);
		$sFileName = 'spot_cron.sh';
		$sContent =
			'#!/bin/bash'."\n".
			'wget -qO- '.$this->asContext['serv_name'].'index.php?a=update_project > /dev/null'."\n".
			'#Crontab job: 0 * * * * . '.dirname($_SERVER['SCRIPT_FILENAME']).'/'.$sFileName.' > /dev/null'."\n";
		$bSuccess = (file_put_contents($sFileName, $sContent)!==false);
		return self::getJsonResult($bSuccess, '');
	}
	
	public function getMarkers()
	{
		$asMessages = $this->getSpotMessages();
		$bSuccess = !empty($this->getMedias('posted_on') + $asMessages + $this->getPosts());
		$sDesc = $bSuccess?'':self::NO_DATA;
		
		//Add medias
		if($bSuccess) {
			$asMedias = $this->getMedias('taken_on');
			
			//Assign medias to closest message
			$iIndex = 0;
			$iMaxIndex = count($asMessages) - 1;
			foreach($asMedias as $asMedia) {
				while($iIndex <= $iMaxIndex && $asMedia['unix_time'] > $asMessages[$iIndex]['unix_time']) {
					$iIndex++;
				}
				if($iIndex == 0) $iMsgIndex = $iIndex;
				elseif($iIndex > $iMaxIndex) $iMsgIndex = $iMaxIndex;
				else {
					$iHalfWayPoint = ($asMessages[$iIndex - 1]['unix_time'] + $asMessages[$iIndex]['unix_time'])/2;
					$iMsgIndex = ($asMedia['unix_time'] >= $iHalfWayPoint)?$iIndex:($iIndex - 1);
				}
				
				$asMessages[$iMsgIndex]['medias'][] = $asMedia;
			}
		}
		
		return self::getJsonResult($bSuccess, $sDesc, $asMessages);
	}
	
	public function subscribe($sEmail) {
		$asResult = $this->oUser->addUser($sEmail, $this->oLang->getLanguage());
		$asUserInfo = $this->oUser->getUserInfo();
		
		//Send Confirmation Email
		if($asResult['result'] && $asResult['desc']=='lang:nl_subscribed') {
			$oConfEmail = new Email($this->asContext['serv_name'], 'email_conf');
			$oConfEmail->setDestInfo($asUserInfo);
			$oConfEmail->send();
		}
		
		return self::getJsonResult($asResult['result'], $asResult['desc'], $asUserInfo);
	}
	
	public function unsubscribe() {
		$asResult = $this->oUser->removeUser();
		return self::getJsonResult($asResult['result'], $asResult['desc'], $asResult['data']);
	}
	
	public function unsubscribeFromEmail($iUserId) {
		$this->oUser->setUserId($iUserId);
		$this->oLang->setLanguage($this->oUser->getLang(), self::DEFAULT_LANG);
		$asResult = $this->oUser->removeUser();
		
		$sDesc = $asResult['desc'];
		if($sDesc=='') $sDesc = $this->oLang->getTranslation('nl_unsubscribed');
		return $sDesc;
	}
	
	private function getSpotMessages()
	{
		$asMessages = array();
		
		//Get messages from all feeds belonging to the project
		$asFeeds = $this->oProject->getFeedIds();
		foreach($asFeeds as $iFeedId) {
			$oFeed = new Feed($this->oDb, $iFeedId);
			$asMessages = $oFeed->getMessages($this->oProject->getActivePeriod());
			foreach($asMessages as &$asMessage)
			{
				$asMessage['latitude'] = floatval($asMessage['latitude']);
				$asMessage['longitude'] = floatval($asMessage['longitude']);
				$asMessage['lat_dms'] = self::decToDms($asMessage['latitude'], 'lat');
				$asMessage['lon_dms'] = self::decToDms($asMessage['longitude'], 'lon');
				
				$this->addTimeStamp($asMessage, $asMessage['unix_time']);
			}
		}
		usort($asMessages, function($a, $b){return $a['unix_time'] > $b['unix_time'];});
		
		$asSortedMessages = array_values($asMessages);
		foreach($asSortedMessages as $iIndex=>&$asSortedMessage) $asSortedMessage['displayed_id'] = $iIndex + 1;
		
		return $asSortedMessages;
	}
	
	/**
	 * Get valid medias based on $sTimeRefField:
	 * - taken_on: Date/time on which the media was taken
	 * - posted_on: Date/time on which the media was uploaded
	 * @param String $sTimeRefField Field to calculate relative times
	 * @return Array Medias info
	 */
	private function getMedias($sTimeRefField)
	{
		$asMedias = $this->oMedia->getMediasInfo();
		$asValidMedias = array();
		foreach($asMedias as $asMedia) {
			$sTimeRef = $asMedia[$sTimeRefField];
			if($sTimeRef >= $this->oProject->getActivePeriod('from') && $sTimeRef <= $this->oProject->getActivePeriod('to')) {
				$asMedia['taken_on_formatted'] = $this->getTimeFormat(strtotime($asMedia['taken_on']));
				$asMedia['posted_on_formatted'] = $this->getTimeFormat(strtotime($asMedia['posted_on']));
				
				$this->addTimeStamp($asMedia, strtotime($sTimeRef));
				$asValidMedias[] = $asMedia;
			}
		}
		
		usort($asValidMedias, function($a, $b){return $a['unix_time'] > $b['unix_time'];});
		
		$asSortedMedias = array_values($asValidMedias);
		foreach($asSortedMedias as $iIndex=>&$asSortedMedia) $asSortedMedia['displayed_id'] = $iIndex + 1;
		
		return $asSortedMedias;
	}
	
	private function getPosts()
	{
		$asInfo = array(
			'from'		=> self::POST_TABLE,
			'constraint'=> array(Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId()),
			'constOpe'	=> array(Db::getId(Project::PROJ_TABLE) => "=")
		);
		if($this->oProject->getMode()==Project::MODE_HISTO) {
			$asInfo['constraint']['site_time'] = $this->oProject->getActivePeriod();
			$asInfo['constOpe']['site_time'] = "BETWEEN";
		}
		$asPosts = $this->oDb->selectRows($asInfo);

		foreach($asPosts as &$asPost) {
			$iUnixTimeStamp = strtotime($asPost['site_time']); //assumes site timezone (Settings::TIMEZONE) 
			$asPost['formatted_name'] = Toolbox::mb_ucwords($asPost['name']);
			unset($asPost[Db::getId(User::USER_TABLE)]);
			
			$this->addTimeStamp($asPost, $iUnixTimeStamp);
		}
		usort($asPosts, function($a, $b){return $a['unix_time'] > $b['unix_time'];});
		
		return $asPosts;
	}
	
	private function addTimeStamp(&$asData, $iTime) {
		$asData['unix_time'] = (int) $iTime;
		$asData['relative_time'] = Toolbox::getDateTimeDesc($iTime, $this->oLang->getLanguage());
		$asData['formatted_time'] = $this->getTimeFormat($iTime);
	}
	
	public function getNewsFeed($iChunk=0, $bInternal=false)
	{
		$bHistoMode = ($this->oProject->getMode() == Project::MODE_HISTO);
		$asFeeds = array();
		$asFeedTypes = array(
			'message'	=> array(
				'table'		=> Feed::MSG_TABLE,
				'feed'		=> $this->getSpotMessages(),
				'priority'	=> 0
			),
			'media'	=> array(
				'table'		=> Media::MEDIA_TABLE,
				'feed'		=> $this->getMedias('posted_on'),
				'priority'	=> 1
			),
			'post'		=> array(
				'table'		=> self::POST_TABLE,
				'feed'		=> $this->getPosts(),
				'priority'	=> 2
			)
		);
		foreach($asFeedTypes as $sFeedType=>$asFeedTypeInfo) {
			foreach($asFeedTypeInfo['feed'] as $asFeed) {
				$sPriority = $asFeedTypeInfo['priority'];
				if($bHistoMode) $sPriority = count($asFeedTypes) - 1 - $asFeedTypeInfo['priority'];
				
				$iId = ($asFeed['unix_time'] * -1).'.'.$sPriority.$asFeed[Db::getId($asFeedTypeInfo['table'])];
				$asFeeds[$iId] = $asFeed;
				$asFeeds[$iId]['type'] = $sFeedType;
			}
		}
		
		//Sort by key
		if($bHistoMode)	krsort($asFeeds);
		else ksort($asFeeds);
		
		//Split chunks
		$asFeeds = array_slice($asFeeds, $iChunk * self::FEED_CHUNK_SIZE, self::FEED_CHUNK_SIZE); 
		
		return $bInternal?$asFeeds:self::getJsonResult(true, '', $asFeeds);
	}
	
	public function syncMedias() {
		return $this->oMedia->syncFileFolder();
	}
	
	public function addPost($sName, $sPost)
	{
		$asData = array(
			Db::getId(Project::PROJ_TABLE)	=> $this->oProject->getProjectId(),
			Db::getId(User::USER_TABLE)		=> $this->oUser->getUserId(),
			'name'							=> mb_strtolower(trim($sName)), 
			'content'						=> trim($sPost),
			'site_time'						=> date(Db::TIMESTAMP_FORMAT) //site time (Settings::TIMEZONE)
		);
		$iPostId = $this->oDb->insertRow(self::POST_TABLE, $asData);
		
		$this->oUser->updateNickname($sName);
		
		return self::getJsonResult(($iPostId > 0), '');
	}
	
	public function upload()
	{
		$this->oClassManagement->incClass('uploader', true);
		$oUploader = new Uploader($this->oMedia, $this->oLang);
		
		return $oUploader->sBody;
	}
	
	public function addComment($iMediaId, $sComment) {
		$oMedia = new Media($this->oDb, $this->oProject, $iMediaId);
		$asResult = $oMedia->setComment($sComment);
		return self::getJsonResult($asResult['result'], $asResult['desc'], $asResult['data']);
	}
	
	public function getAdminSettings() {
		$oFeed = new Feed($this->oDb);
		$asData = array(
			'project'	=> $this->oProject->getProjects(),
			'feed'		=> $oFeed->getFeeds(),
			'spot'		=> $oFeed->getSpots()
		);
		return self::getJsonResult(true, '', $asData);
	}
	
	public function setAdminSettings($sType, $iId, $sField, $sValue) {
		$bSuccess = false;
		$sDesc = '';
		
		switch($sType) {
			case 'project':
				$oProject = new Project($this->oDb, $iId);
				switch($sField) {
					case 'name':
						$bSuccess = $oProject->setProjectName($sValue);
						break;
					case 'codename':
						$bSuccess = $oProject->setProjectCodeName($sValue);
						break;
					case 'active_from':
						$bSuccess = $oProject->setActivePeriod($sValue.' 00:00:00', 'from');
						break;
					case 'active_to':
						$bSuccess = $oProject->setActivePeriod($sValue.' 23:59:59', 'to');
						break;
					case 'timezone':
						$bSuccess = $oProject->setTimeZone($sValue);
						break;
				}
				$asResult = $oProject->getProject();
				break;
			case 'feed':
			case 'spot':
				$oFeed = new Feed($this->oDb, $iId);
				switch($sField) {
					case 'ref_feed_id':
						$bSuccess = $oFeed->setRefFeedId($sValue);
						break;
					case 'spot_id':
						$bSuccess = $oFeed->setSpotId($sValue);
						break;
					case 'project_id':
						$bSuccess = $oFeed->setProjectId($sValue);
						break;
				}
				$asResult = $oFeed->getFeed();
				break;
		}
		if(!$bSuccess) $sDesc = Mask::LANG_PREFIX.'error_commit_db';
		
		return self::getJsonResult($bSuccess, $sDesc, array($sType=>array($asResult)));
	}
	
	public function delAdminSettings($sType, $iId) {
		$bSuccess = false;
		$sDesc = '';
		
		switch($sType) {
			case 'project':
				$oProject = new Project($this->oDb, $iId);
				$sDesc = $oProject->delete();
				break;
			case 'feed':
				$oFeed = new Feed($this->oDb, $iId);
				$sDesc = $oFeed->delete();
				break;
		}
		$bSuccess = ($sDesc=='');
		
		return self::getJsonResult($bSuccess, $sDesc, array($sType=>array(array('id'=>$iId, 'del'=>$bSuccess))));
	}
	
	public function createProject() {
		$oProject = new Project($this->oDb);
		$iNewProjectId = $oProject->createProjectId();
		
		$oFeed = new Feed($this->oDb);
		$oFeed->createFeedId($iNewProjectId);
		
		return self::getJsonResult($iNewProjectId>0, '', array(
			'project'	=> array($oProject->getProject()),
			'feed'		=> array($oFeed->getFeed())
		));
	}

	public function convertGpxToGeojson($sGeoFileName) {
		return Converter::convertToGeoJson($sGeoFileName);
	}
	
	public static function decToDms($dValue, $sType) {
		if($sType=='lat')	$sDirection = ($dValue >= 0)?'N':'S'; //Latitude
		else				$sDirection = ($dValue >= 0)?'E':'W'; //Longitude
		
		$dLeft = abs($dValue);
		
		//Degrees
		$iDegree = floor($dLeft);
		$dLeft -= $iDegree;
		
		//Minutes
		$iMinute = floor($dLeft * 60);
		$dLeft -=  $iMinute / 60;
		
		//Seconds
		$fSecond = round($dLeft * 3600, 1);
		
		return $iDegree.'°'.$iMinute."'".$fSecond.'"'.$sDirection;
	}
	
	public function getTimeFormat($iTime) {
		$sDate = date('d/m/Y', $iTime);
		$sTime = date('H:i', $iTime);
		return $this->oLang->getTranslation('date_time', array($sDate, $sTime));
	}
}