<?php

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

class Media extends PhpObject {

	//DB Tables
	const MEDIA_TABLE = 'medias';

	//Media folders (works because /public/files is a symlink of /files)
	const MEDIA_FOLDER = 'files';
	const THUMB_FOLDER = self::MEDIA_FOLDER.'/thumbs';

	const THUMB_MAX_WIDTH = 400;

	private Db $oDb;
	private Project $oProject;
	private $asMedia;
	private $asMedias;
	//private $sSystemType;

	private $iMediaId;

	public function __construct(Db &$oDb, Project &$oProject, $iMediaId=0) {
		parent::__construct(__CLASS__);
		$this->oDb = &$oDb;
		$this->oProject = &$oProject;
		$this->asMedia = array();
		$this->asMedias = array();
		//$this->sSystemType = (substr(php_uname(), 0, 7) == "Windows")?'win':'unix';
		$this->setMediaId($iMediaId);
	}

	public function setMediaId($iMediaId) {
		$this->iMediaId = $iMediaId;
	}

	public function getMediaId() {
		return $this->iMediaId;
	}

	public function getProjectCodeName() {
		return $this->oProject->getProjectCodeName();
	}

	public function setComment($sComment) {
		$sError = '';
		$asData = array();
		if($this->iMediaId > 0) {
			$bResult = $this->oDb->updateRow(self::MEDIA_TABLE, $this->iMediaId, array('comment'=>$sComment));
			if(!$bResult) $sError = 'error.commit_db';
			else $asData = $this->getInfo();
		}
		else $sError = 'media.no_id';

		return Spot::getResult(($sError==''), $sError, $asData);
	}

	public function getMediasInfo($oMediaIds=null) {
		$bOwnMedia = is_numeric($oMediaIds);		//1 value
		$bConstraintArray = is_array($oMediaIds);	//Custom Constraints

		if($bOwnMedia && empty($this->asMedia) || !$bOwnMedia && empty($this->asMedias) || $bConstraintArray) {
			if($this->oProject->getProjectId()) {
				$asParams = array(
					'select'	=> array(Db::getId(self::MEDIA_TABLE), 'filename', 'taken_on', 'posted_on', 'timezone', 'latitude', 'longitude', 'altitude', 'width', 'height', 'rotate', 'type AS subtype', 'comment'),
					'from'		=> self::MEDIA_TABLE,
					'constraint'=> array(Db::getId(Project::PROJ_TABLE) => $this->oProject->getProjectId())
				);
				if($bOwnMedia) $asParams['constraint'][Db::getId(self::MEDIA_TABLE)] = $oMediaIds;
				if($bConstraintArray) $asParams = array_merge($asParams, $oMediaIds);

				$asMedias = $this->oDb->selectRows($asParams);
				foreach($asMedias as &$asMedia) {
					$asMedia['media_path'] = self::getMediaPath($asMedia['filename']);
					$asMedia['thumb_path'] = $this->getMediaThumbnail($asMedia['filename']);
				}

				if(!empty($asMedias) && !$bConstraintArray) {
					if($bOwnMedia) $this->asMedia = array_shift($asMedias);
					else $this->asMedias = $asMedias;
				}
			}
		}
		return $bOwnMedia?$this->asMedia:$asMedias;
	}

	public function getInfo() {
		return $this->getMediasInfo($this->iMediaId);
	}

	public function isProjectEditable() {
		return $this->oProject->isEditable();
	}

	public function addMedia($sMediaName, $sMethod='upload') {
		$sError = '';
		$asParams = array();
		if(!$this->isProjectEditable() && $sMethod!='sync') {
			$sError = 'upload.mode_archived';
			$asParams[] = $this->oProject->getProjectCodeName();
		}
		elseif($this->oDb->pingValue(self::MEDIA_TABLE, array('filename'=>$sMediaName)) && $sMethod!='sync') {
			$sError = 'upload.media.exists';
			$asParams[] = $sMediaName;
		}
		else {
			$asMediaInfo = $this->getMediaInfoFromFile($sMediaName);

			//Converting times to Site Time Zone, by using date()
			//Media Timezone is kept in a separate field for later conversion to Local Time
			$asDbInfo = array(
				Db::getId(Project::PROJ_TABLE)	=> $this->oProject->getProjectId(),
				'filename'						=> $sMediaName,
				'taken_on'						=> date(Db::TIMESTAMP_FORMAT, ($asMediaInfo['taken_ts'] > 0)?$asMediaInfo['taken_ts']:$asMediaInfo['file_ts']),
				'posted_on'						=> date(Db::TIMESTAMP_FORMAT, $asMediaInfo['file_ts']),
				'timezone'						=> $asMediaInfo['timezone'],
				'latitude'						=> is_null($asMediaInfo['latitude'])?'NULL':$asMediaInfo['latitude'],
				'longitude'						=> is_null($asMediaInfo['longitude'])?'NULL':$asMediaInfo['longitude'],
				'altitude'						=> is_null($asMediaInfo['altitude'])?'NULL':$asMediaInfo['altitude'],
				'width'							=> $asMediaInfo['width'],
				'height'						=> $asMediaInfo['height'],
				'rotate'						=> $asMediaInfo['rotate'],
				'type'							=> $asMediaInfo['type']
			);

			if($sMethod=='sync')	$iMediaId = $this->oDb->insertUpdateRow(self::MEDIA_TABLE, $asDbInfo, array('filename'));
			else					$iMediaId = $this->oDb->insertRow(self::MEDIA_TABLE, $asDbInfo);

			if(!$iMediaId) $sError = 'error.commit_db';
			else {
				$this->setMediaId($iMediaId);
				$asParams = $this->getInfo(); //Creates thumbnail
			}
		}

		return Spot::getResult(($sError==''), $sError, $asParams);
	}

	private function getMediaInfoFromFile($sMediaName)
	{
		$sMediaPath = self::getMediaPath($sMediaName);
		$sType = self::getMediaType($sMediaName);
		$iPostedOn = filemtime($sMediaPath);
		$sTimeZone = date_default_timezone_get();
		$iWidth = 0;
		$iHeight = 0;
		$sRotate = '0';
		$sTakenOn = '';
		$fLat = null;
		$fLng = null;
		$iAlt = null;
		switch($sType) {
			case 'video':
				$asResult = array();
				$sParams = implode(' ', array(
					'-loglevel error', //Remove comments
					'-select_streams v:0', //First video channel
					'-show_entries '. //filter tags : Width, Height, Creation Time, Location & Rotation
						'format_tags=creation_time,com.apple.quicktime.creationdate,com.apple.quicktime.location.ISO6709:'.
						'stream_tags=rotate,creation_time:'.
						'stream=width,height',
					'-print_format json', //output format: json
					'-i' //input file
				));
				exec('ffprobe '.$sParams.' '.escapeshellarg($sMediaPath), $asResult);
				$asExif = json_decode(implode('', $asResult), true);

				//Taken On
				if(isset($asExif['format']['tags']['com.apple.quicktime.creationdate'])) {
					$sTakenOn = $asExif['format']['tags']['com.apple.quicktime.creationdate']; //contains Time Zone
					$sTimeZone = Spot::getTimeZoneFromDate($sTakenOn) ?? $sTimeZone;
				}
				else $sTakenOn = $asExif['format']['tags']['creation_time'] ?? $asExif['streams'][0]['tags']['creation_time'];

				//Location
				if(isset($asExif['format']['tags']['com.apple.quicktime.location.ISO6709'])) {
					list($fLat, $fLng, $iAlt) = self::getLatLngAltFromISO6709($asExif['format']['tags']['com.apple.quicktime.location.ISO6709']);
				}

				//Width & Height
				$iWidth = $asExif['streams'][0]['width'];
				$iHeight = $asExif['streams'][0]['height'];

				//Orientation
				if(isset($asExif['streams'][0]['tags']['rotate'])) $sRotate = $asExif['streams'][0]['tags']['rotate'];
				break;
			case 'image':
				$asExif = @exif_read_data($sMediaPath, 0, true);
				if($asExif === false) $asExif = array();
				list($iWidth, $iHeight) = getimagesize($sMediaPath);

				//Posted On
				if(array_key_exists('FILE', $asExif) && array_key_exists('FileDateTime', $asExif['FILE'])) $iPostedOn = $asExif['FILE']['FileDateTime'];

				//Taken On & Timezone
				if(array_key_exists('EXIF', $asExif)) {
					if(array_key_exists('DateTimeOriginal', $asExif['EXIF'])) $sTakenOn = $asExif['EXIF']['DateTimeOriginal'];

					/* Priorities:
					 * 1. OffsetTimeOriginal: timezone for DateTimeOriginal (exif version >= 2.31)
					 * 2. 0x9011: same as above, but unidentified
					 * 3. Timezone extracted from DateTimeOriginal
					 * 4. Uploader Browser Time Zone (PHP Default Time Zone)
					*/
					$sTimeZone = $asExif['EXIF']['OffsetTimeOriginal'] ?? $asExif['EXIF']['UndefinedTag:0x9011'] ?? Spot::getTimeZoneFromDate($sTakenOn) ?? $sTimeZone;
				}

				//Location
				if(array_key_exists('GPS', $asExif)) {
					$asGps = $asExif['GPS'];
					$fLat = self::getLatLngFromExif($asGps['GPSLatitudeRef'] ?? $asGps['UndefinedTag:0x0001'], $asGps['GPSLatitude'] ?? $asGps['UndefinedTag:0x0002']);
					$fLng = self::getLatLngFromExif($asGps['GPSLongitudeRef'] ?? $asGps['UndefinedTag:0x0003'], $asGps['GPSLongitude'] ?? $$asGps['UndefinedTag:0x0004']);
					$iAlt = (($asGps['GPSAltitudeRef'] ?? $asGps['UndefinedTag:0x0005'] ?? '0') == '1'?-1:1) * ($asGps['GPSAltitude'] ?? $asGps['UndefinedTag:0x0006'] ?? 0);
				}

				//Orientation
				if(array_key_exists('IFD0', $asExif) && array_key_exists('Orientation', $asExif['IFD0'])) {
					switch($asExif['IFD0']['Orientation'])
					{
						case 1:	$sRotate = '0';		break; //None
						case 3:	$sRotate = '180';	break; //Flip over
						case 6:	$sRotate = '90';	break; //Clockwise
						case 8:	$sRotate = '-90';	break; //Trigo
					}
				}
				break;
		}

		//Assign the correct Time Zone to $sTakenOn if it is not already contained in it. Then get Unix Timestamp
		//Time Zone (2nd parameter) will be ignored if already contained in $sTakenOn
		$iTakenOn = 0;
		if($sTakenOn != '') {
			$oTakenOn = new \DateTime($sTakenOn, new \DateTimeZone($sTimeZone));
			$iTakenOn = $oTakenOn->format('U');
		}

		return array(
			'timezone'	=> $sTimeZone,
			'latitude'	=> $fLat,
			'longitude'	=> $fLng,
			'altitude'	=> $iAlt,
			'taken_ts'	=> $iTakenOn,
			'file_ts'	=> $iPostedOn,
			'width'		=> $iWidth,
			'height'	=> $iHeight,
			'rotate'	=> $sRotate,
			'type'		=> $sType
		);
	}

	private function getMediaThumbnail($sMediaName)
	{
		$sMediaPath = self::getMediaPath($sMediaName);
		$sThumbPath = self::getMediaPath($sMediaName, 'thumbnail');

		if(!file_exists($sThumbPath)) {
			$sType = self::getMediaType($sMediaName);
			switch($sType) {
				case 'image':
					$asThumbInfo = ToolBox::createThumbnail($sMediaPath, self::THUMB_MAX_WIDTH, 0, $sThumbPath);
					break;
				case 'video':
					//Get a screenshot of the video 1 second in
					$sTempPath = self::getMediaPath(uniqid('temp_').'.png');
					$asResult = array();
					$sParams = implode(' ', array(
						'-i '.escapeshellarg($sMediaPath),	//input file
						'-ss 00:00:01.000',					//Image taken after x seconds
						'-vframes 1',						//number of video frames to output
						escapeshellarg($sTempPath),			//output file
					));
					exec('ffmpeg '.$sParams, $asResult);

					//Resize
					$asThumbInfo = ToolBox::createThumbnail($sTempPath, self::THUMB_MAX_WIDTH, 0, $sThumbPath, true);
					break;
			}

		}
		else $asThumbInfo = array('error'=>'', 'out'=>$sThumbPath);

		return ($asThumbInfo['error']=='')?$asThumbInfo['out']:$sMediaPath;
	}

	private static function getMediaPath($sMediaName, $sFileType='media') {
		if($sFileType=='thumbnail') return self::THUMB_FOLDER.'/'.$sMediaName.(strtolower(substr($sMediaName, -3))=='mov'?'.png':'');
		else return self::MEDIA_FOLDER.'/'.$sMediaName;
	}

	private static function getMediaType($sMediaName) {
		$sMediaPath = self::getMediaPath($sMediaName);
		$sMediaMime = mime_content_type($sMediaPath);
		switch($sMediaMime) {
			case 'video/quicktime':
			case 'video/mp4':		$sType = 'video';	break;
			default:				$sType = 'image';	break;
		}

		return $sType;
	}

	private static function getLatLngFromExif($sDirection, $asCoords) {
		$fCoord = 0;
		foreach($asCoords as $iIndex=>$sCoord) {
			$fValue = 0;
			$asCoordParts = explode('/', $sCoord);
			switch(count($asCoordParts)) {
				case 1:
					$fValue = $asCoordParts[0];
					break;
				case 2:
					$fValue = floatval($asCoordParts[0]) / floatval($asCoordParts[1]);
					break;
			}

			$fCoord += $fValue / pow(60, $iIndex);
		}

		return $fCoord * (($sDirection == 'W' || $sDirection == 'S')?-1:1);
	}

	private static function getLatLngAltFromISO6709($sIso6709) {
		preg_match('/^(?P<lat>[\+\-][0,1]?\d{2}\.\d+)(?P<lng>[\+\-][0,1]?\d{2}\.\d+)(?P<alt>[\+\-]\d+)?/', $sIso6709, $asMatches);
		return array(floatval($asMatches['lat']), floatval($asMatches['lng']), floatval($asMatches['alt'] ?? 0));
	}
}