<?php

namespace Franzz\Spot;
use Franzz\Objects\PhpObject;
use Franzz\Objects\Db;
use Franzz\Objects\Translator;
use \Settings;

/**
 * Feed Class
 * Also manages spots (devices) & messages
 */
class Feed extends PhpObject {

	//Spot feed
	const FEED_HOOK = 'https://api.findmespot.com/spot-main-web/consumer/rest-api/2.0/public/feed/';
	const FEED_TYPE_XML = '/message.xml';
	const FEED_TYPE_JSON = '/message.json';
	const FEED_MAX_REFRESH = 5 * 60; //Seconds

	//Weather
	const WEATHER_HOOK = 'https://weather.visualcrossing.com/VisualCrossingWebServices/rest/services/timeline';
	const WEATHER_PARAM = array(
		'key'		=> Settings::WEATHER_TOKEN,
		'unitGroup'	=> 'metric',
		'lang'		=> 'en',
		'include'	=> 'current',
		'iconSet'	=> 'icons2'
	);

	//Timezone
	const TIMEZONE_HOOK = 'http://api.geonames.org/timezoneJSON';
	const TIMEZONE_PARAM = array(
		'username'	=> Settings::TIMEZONE_USER
	);
	
	//DB Tables
	const SPOT_TABLE = 'spots';
	const FEED_TABLE = 'feeds';
	const MSG_TABLE = 'messages';

	//Hide/Display values
	const MSG_HIDDEN = 0;
	const MSG_DISPLAYED = 1;

	/**
	 * Database Handle
	 * @var Db
	 */
	private $oDb;

	private $iFeedId;
	private $sRefFeedId;
	private $iLastUpdate;

	public function __construct(Db &$oDb, $iFeedId=0) {
		parent::__construct(__CLASS__);
		$this->oDb = &$oDb;
		if($iFeedId > 0) $this->setFeedId($iFeedId);
	}

	public function getFeedId() {
		return $this->iFeedId;
	}

	public function getLastUpdate(): int {
		return $this->iLastUpdate;
	}

	public function setFeedId($iFeedId) {
		$this->iFeedId = $iFeedId;
		$asFeed = $this->getFeed();
		$this->sRefFeedId = $asFeed['ref_feed_id'];
		$this->iLastUpdate = $asFeed['last_update']=='0000-00-00 00:00:00'?0:strtotime($asFeed['last_update']);
	}

	public function createFeedId($oProjectId) {
		$this->setFeedId($this->oDb->insertRow(self::FEED_TABLE, array(
			Db::getId(Project::PROJ_TABLE)	=> $oProjectId,
			'status'						=> 'INACTIVE'
		)));
		return $this->getFeedId();
	}

	public function setRefFeedId($sRefFeedId) {
		return $this->updateField('ref_feed_id', $sRefFeedId);
	}

	public function setSpotId($iSpotId) {
		return $this->updateField(Db::getId(self::SPOT_TABLE), $iSpotId);
	}

	public function setProjectId($iProjectId) {
		return $this->updateField(Db::getId(Project::PROJ_TABLE), $iProjectId);
	}

	public function getSpots() {
		$asSpots = $this->oDb->selectRows(array('from'=>self::SPOT_TABLE));
		foreach($asSpots as &$asSpot) $asSpot['id'] = $asSpot[Db::getId(self::SPOT_TABLE)];
		return $asSpots;
	}

	public function getFeeds($iFeedId=0) {
		$asInfo = array('from'=>self::FEED_TABLE);
		if($iFeedId > 0) $asInfo['constraint'] = array(Db::getId(self::FEED_TABLE)=>$iFeedId);
		$asFeeds = $this->oDb->selectRows($asInfo);

		foreach($asFeeds as &$asFeed) $asFeed['id'] = $asFeed[Db::getId(self::FEED_TABLE)];
		return $asFeeds;
	}

	public function getFeed() {
		$asFeeds = $this->getFeeds($this->getFeedId());
		return array_shift($asFeeds);
	}

	public function getMessages($asConstraints=array()) {
		$sFeedIdCol = Db::getId(self::FEED_TABLE, true);
		$asInfo = array(
			'select'	=> array(
				Db::getId(self::MSG_TABLE), 'ref_msg_id', 'type',						//ID
				'latitude', 'longitude', 												//Position
				'site_time', 'timezone', 'unix_time',									//Time
				'weather_icon', 'weather_cond', 'weather_temp'							//Weather
			),
			'from'		=> self::MSG_TABLE,
			'join'		=> array(self::FEED_TABLE => Db::getId(self::FEED_TABLE)),
			'constraint'=> array($sFeedIdCol => $this->getFeedId(),	'display' => self::MSG_DISPLAYED),
			'constOpe'	=> array($sFeedIdCol => "=",				'display' => "="),
			'orderBy'	=> array('site_time'=>'ASC')
		);
		if(!empty($asConstraints)) $asInfo = array_merge($asInfo, $asConstraints);
		
		$asResult = $this->oDb->selectRows($asInfo);

		/* Temporary lookup - Start */
		$iCount = 0;
		foreach($asResult as &$asMsg) {
			if($asMsg['weather_icon'] == '' && $iCount < 3) {
				$asWeather = $this->getWeather(array($asMsg['latitude'], $asMsg['longitude']), $asMsg['unix_time']);
				$asMsg = array_merge($asMsg, $asWeather);
				$this->oDb->updateRow(self::MSG_TABLE, $asMsg[Db::getId(self::MSG_TABLE)], $asWeather, false);
				$iCount++;
			}
		}
		/* Temporary lookup - End */

		return $asResult;
	}

	public function getLastMessageId($asConstraints=array()) {
		$asMessages = $this->getMessages($asConstraints);
		return end($asMessages)[Db::getId(self::MSG_TABLE)] ?? 0;
	}

	public function checkUpdateFeed($sProjectMode) {
		$bNewMsg = false;

		//Spam Check: no more than 1 API request per 5 minutes
		if($sProjectMode == Project::MODE_BLOG) {
			$oLastUpdate = new \DateTime('@'.$this->iLastUpdate);
			$oNow = new \DateTime('now');
			$iSecDiff = $oNow->getTimestamp() - $oLastUpdate->getTimestamp();
			
			if($iSecDiff > self::FEED_MAX_REFRESH) $bNewMsg = $this->updateFeed();
		}

		return $bNewMsg;
	}

	public function addManualPosition($sLat, $sLng, $iTimestamp) {
		$sTimeZone = date_default_timezone_get();
		$oDateTime = new \DateTime('@'.$iTimestamp);
		$oDateTime->setTimezone(new \DateTimeZone($sTimeZone));
		$asWeather = $this->getWeather(array($sLat, $sLng), $iTimestamp);

		$asMsg = [
			'ref_msg_id'	=> $iTimestamp.'/man',
			'id_feed'		=> $this->getFeedId(),
			'type'			=> 'OK',
			'latitude'		=> $sLat,
			'longitude'		=> $sLng,
			'iso_time' 		=> $oDateTime->format("Y-m-d\TH:i:sO"), //Incorrect ISO 8601 format, but compliant with Spot data
			'site_time'		=> $oDateTime->format(Db::TIMESTAMP_FORMAT),
			'timezone'		=> $sTimeZone,
			'unix_time'		=> $iTimestamp,
			'content'		=> '',
			'battery_state'	=> '',
			'posted_on'		=> date(Db::TIMESTAMP_FORMAT),
		];

		$iMessageId = $this->oDb->insertRow(self::MSG_TABLE, array_merge($asMsg, $asWeather));
		return $iMessageId;
	}

	private function updateFeed() {
		$bNewMsg = false;
		$asData = $this->retrieveFeed();
		$sNow = date(Db::TIMESTAMP_FORMAT);
		if(!isset($asData['response']['errors']) || empty($asData['response']['errors'])) {
			$asMsgs = $asData['response']['feedMessageResponse']['messages'];
			$asFeed = $asData['response']['feedMessageResponse']['feed'];

			//Fix unstable Spot API Structure
			if(array_key_exists('message', $asMsgs)) $asMsgs = $asMsgs['message'];	//Sometimes adds an extra "message" level
			if(!array_key_exists(0, $asMsgs)) $asMsgs = array($asMsgs);				//Jumps a level when there is only 1 message

			//Update Spot, Feed & Messages
			if(!empty($asMsgs) && array_key_exists('messengerId', $asMsgs[0])) {

				//Update Spot Info from the first message
				$asSpotInfo = array(
					'ref_spot_id'	=> $asMsgs[0]['messengerId'],
					'name'			=> $asMsgs[0]['messengerName'],
					'model'			=> $asMsgs[0]['modelId']
				);
				$iSpotId = $this->oDb->insertUpdateRow(self::SPOT_TABLE, $asSpotInfo, array('ref_spot_id'));

				//Update Feed Info and last update date
				$asFeedInfo = array(
					'ref_feed_id'					=> $asFeed['id'],
					Db::getId(self::SPOT_TABLE)		=> $iSpotId,
					'name'							=> $asFeed['name'],
					'description'					=> $asFeed['description'],
					'status'						=> $asFeed['status'],
					'last_update'					=> $sNow
				);
				$iFeedId = $this->oDb->insertUpdateRow(self::FEED_TABLE, $asFeedInfo, array('ref_feed_id'));

				//Update Messages
				foreach($asMsgs as $asMsg) {
					$asMsg = array(
						'ref_msg_id'				=> $asMsg['id'],
						Db::getId(self::FEED_TABLE)	=> $iFeedId,
						'type'						=> $asMsg['messageType'],
						'latitude'					=> $asMsg['latitude'],
						'longitude'					=> $asMsg['longitude'],
						'iso_time'					=> $asMsg['dateTime'],								//ISO 8601 time (backup)
						'site_time'					=> date(Db::TIMESTAMP_FORMAT, $asMsg['unixTime']),	//Conversion to Site Time
						'timezone'					=> $this->getTimeZone(array($asMsg['latitude'], $asMsg['longitude']), $asMsg['unixTime']),
						'unix_time'					=> $asMsg['unixTime'], 								//UNIX Time (backup)
						'content'					=> $asMsg['messageContent'],
						'battery_state'				=> $asMsg['batteryState']
					);

					$iMsgId = $this->oDb->selectId(self::MSG_TABLE, array('ref_msg_id'=>$asMsg['ref_msg_id']));
					if(!$iMsgId) {
						//First Catch
						$asMsg['posted_on'] = $sNow;

						//Weather Data
						$asMsg = array_merge($asMsg, $this->getWeather(array($asMsg['latitude'], $asMsg['longitude']), $asMsg['unix_time']));

						$this->oDb->insertRow(self::MSG_TABLE, $asMsg);
						$bNewMsg = true;
					}
					else $this->oDb->updateRow(self::MSG_TABLE, $iMsgId, $asMsg);
				}
			}
		}
		else $this->oDb->updateRow(self::FEED_TABLE, $this->getFeedId(), array('last_update'=>$sNow));

		return $bNewMsg;
	}

	private function getWeather($asLatLng, $iTimeStamp) {
		$sApiUrl = self::WEATHER_HOOK.'/'.$asLatLng[0].','.$asLatLng[1].'/'.$iTimeStamp.'?'.http_build_query(self::WEATHER_PARAM);
		$asWeather = json_decode(file_get_contents($sApiUrl), true);

		if(array_key_exists('currentConditions', $asWeather)) {	//Current conditions
			$sWeatherIcon = $asWeather['currentConditions']['icon'];
			$sWeatherCond = $asWeather['currentConditions']['conditions'];
			$sWeatherTemp = $asWeather['currentConditions']['temp'];
		}
		elseif($asWeather['days'][0]['icon'] != '') {			//Daily Conditions
			$sWeatherIcon = $asWeather['days'][0]['icon'];
			$sWeatherCond = $asWeather['days'][0]['conditions'];
			$sWeatherTemp = $asWeather['days'][0]['temp'];
		}
		else {
			$sWeatherIcon = 'unknown';
		}

		//Get Condition ID
		$sCondKey = (new Translator(self::WEATHER_PARAM['lang']))->getTranslationKey($sWeatherCond);

		return array(
			'weather_icon'	=> $sWeatherIcon,
			'weather_cond'	=> $sCondKey,
			'weather_temp'	=> floatval($sWeatherTemp)
		);
	}

	private function getTimeZone($asLatLng, $iTimeStamp) {
		$asParams = self::TIMEZONE_PARAM;
		$asParams['lat'] = $asLatLng[0];
		$asParams['lng'] = $asLatLng[1];
		$sApiUrl = self::TIMEZONE_HOOK.'?'.http_build_query($asParams);

		$asTimeZone = json_decode(file_get_contents($sApiUrl), true);

		return $asTimeZone['timezoneId'] ?? Settings::TIMEZONE;
	}

	private function retrieveFeed() {
		$sContent = '[]';
		if($this->sRefFeedId !='') {
			$sUrl = self::FEED_HOOK.$this->sRefFeedId.self::FEED_TYPE_JSON;
			$sContent = file_get_contents($sUrl);
		}
		return json_decode($sContent, true);
	}

	private function updateField($sField, $oValue) {
		$bResult = ($this->oDb->updateRow(self::FEED_TABLE, $this->getFeedId(), array($sField=>$oValue)) > 0);
		$this->setFeedId($this->getFeedId());

		return $bResult;
	}

	public function delete() {
		$asResult = array();
		if($this->getFeedId() > 0) {
			$asResult = array(
				'id'	=> $this->getFeedId(),
				'del'	=> $this->oDb->deleteRow(self::FEED_TABLE, $this->getFeedId()),
				'desc'	=> $this->oDb->getLastError()
			);
		}
		else $asResult = array('del'=>false, 'desc'=>'Error while setting project: no Feed ID');

		return $asResult;
	}
}
