<?php

namespace Lychee\Modules;

use ZipArchive;
use Imagick;
use ImagickPixel;

final class Photo {

	private $photoIDs = null;

	public static $validTypes = array(
		IMAGETYPE_JPEG,
		IMAGETYPE_GIF,
		IMAGETYPE_PNG
	);

	public static $validExtensions = array(
		'.jpg',
		'.jpeg',
		'.png',
		'.gif'
	);

	/**
	 * @return boolean Returns true when successful.
	 */
	public function __construct($photoIDs) {

		// Init vars
		$this->photoIDs = $photoIDs;

		return true;

	}

	/**
	 * Creats new photo(s).
	 * Exits on error.
	 * Use $returnOnError if you want to handle errors by your own.
	 * @return string|false ID of the added photo.
	 */
	public function add(array $files, $albumID = 0, $returnOnError = false) {

		// Check permissions
		if (hasPermissions(LYCHEE_UPLOADS)===false||
			hasPermissions(LYCHEE_UPLOADS_BIG)===false||
			hasPermissions(LYCHEE_UPLOADS_THUMB)===false) {
				Log::error(Database::get(), __METHOD__, __LINE__, 'An upload-folder is missing or not readable and writable');
				if ($returnOnError===true) return false;
				Response::error('An upload-folder is missing or not readable and writable!');
		}

		// Call plugins
		Plugins::get()->activate(__METHOD__, 0, func_get_args());

		switch($albumID) {

			case 's':
				// s for public (share)
				$public  = 1;
				$star    = 0;
				$albumID = 0;
				break;

			case 'f':
				// f for starred (fav)
				$star    = 1;
				$public  = 0;
				$albumID = 0;
				break;

			case 'r':
				// r for recent
				$public  = 0;
				$star    = 0;
				$albumID = 0;
				break;

			default:
				$star   = 0;
				$public = 0;
				break;

		}

		// Only process the first photo in the array
		$file = $files[0];

		// Check if file exceeds the upload_max_filesize directive
		if ($file['error']===UPLOAD_ERR_INI_SIZE) {
			Log::error(Database::get(), __METHOD__, __LINE__, 'The uploaded file exceeds the upload_max_filesize directive in php.ini');
			if ($returnOnError===true) return false;
			Response::error('The uploaded file exceeds the upload_max_filesize directive in php.ini!');
		}

		// Check if file was only partially uploaded
		if ($file['error']===UPLOAD_ERR_PARTIAL) {
			Log::error(Database::get(), __METHOD__, __LINE__, 'The uploaded file was only partially uploaded');
			if ($returnOnError===true) return false;
			Response::error('The uploaded file was only partially uploaded!');
		}

		// Check if writing file to disk failed
		if ($file['error']===UPLOAD_ERR_CANT_WRITE) {
			Log::error(Database::get(), __METHOD__, __LINE__, 'Failed to write photo to disk');
			if ($returnOnError===true) return false;
			Response::error('Failed to write photo to disk!');
		}

		// Check if a extension stopped the file upload
		if ($file['error']===UPLOAD_ERR_EXTENSION) {
			Log::error(Database::get(), __METHOD__, __LINE__, 'A PHP extension stopped the file upload');
			if ($returnOnError===true) return false;
			Response::error('A PHP extension stopped the file upload!');
		}

		// Check if the upload was successful
		if ($file['error']!==UPLOAD_ERR_OK) {
			Log::error(Database::get(), __METHOD__, __LINE__, 'Upload contains an error (' . $file['error'] . ')');
			if ($returnOnError===true) return false;
			Response::error('Upload failed!');
		}

		// Verify extension
		$extension = getExtension($file['name'], false);
		if (!in_array(strtolower($extension), self::$validExtensions, true)) {
			Log::error(Database::get(), __METHOD__, __LINE__, 'Photo format not supported');
			if ($returnOnError===true) return false;
			Response::error('Photo format not supported!');
		}

		// Verify image
		$type = @exif_imagetype($file['tmp_name']);
		if (!in_array($type, self::$validTypes, true)) {
			Log::error(Database::get(), __METHOD__, __LINE__, 'Photo type not supported');
			if ($returnOnError===true) return false;
			Response::error('Photo type not supported!');
		}

		// Generate id
		$id = generateID();

		// Set paths
		$tmp_name   = $file['tmp_name'];
		$photo_name = md5($id) . $extension;
		$path       = LYCHEE_UPLOADS_BIG . $photo_name;

		// Calculate checksum
		$checksum = sha1_file($tmp_name);
		if ($checksum===false) {
			Log::error(Database::get(), __METHOD__, __LINE__, 'Could not calculate checksum for photo');
			if ($returnOnError===true) return false;
			Response::error('Could not calculate checksum for photo!');
		}

		// Check if image exists based on checksum
		if ($checksum===false) {

			$checksum = '';
			$exists   = false;

		} else {

			$exists = $this->exists($checksum);

			if ($exists!==false) {
				$photo_name = $exists['photo_name'];
				$path       = $exists['path'];
				$path_thumb = $exists['path_thumb'];
				$medium     = ($exists['medium']==='1' ? 1 : 0);
				$exists     = true;
			}

		}

		if ($exists===false) {

			// Import if not uploaded via web
			if (!is_uploaded_file($tmp_name)) {
				if (!@copy($tmp_name, $path)) {
					Log::error(Database::get(), __METHOD__, __LINE__, 'Could not copy photo to uploads');
					if ($returnOnError===true) return false;
					Response::error('Could not copy photo to uploads!');
				} else @unlink($tmp_name);
			} else {
				if (!@move_uploaded_file($tmp_name, $path)) {
					Log::error(Database::get(), __METHOD__, __LINE__, 'Could not move photo to uploads');
					if ($returnOnError===true) return false;
					Response::error('Could not move photo to uploads!');
				}
			}

		} else {

			// Photo already exists
			// Check if the user wants to skip duplicates
			if (Settings::get()['skipDuplicates']==='1') {
				Log::notice(Database::get(), __METHOD__, __LINE__, 'Skipped upload of existing photo because skipDuplicates is activated');
				if ($returnOnError===true) return false;
				Response::warning('This photo has been skipped because it\'s already in your library.');
			}

		}

		// Read infos
		$info = $this->getInfo($path);

		// Use title of file if IPTC title missing
		if ($info['title']==='') $info['title'] = substr(basename($file['name'], $extension), 0, 30);

		if ($exists===false) {

			// Set orientation based on EXIF data
			if ($file['type']==='image/jpeg'&&isset($info['orientation'])&&$info['orientation']!=='') {
				$adjustFile = $this->adjustFile($path, $info);
				if ($adjustFile!==false) $info = $adjustFile;
				else Log::notice(Database::get(), __METHOD__, __LINE__, 'Skipped adjustment of photo (' . $info['title'] . ')');
			}

			// Set original date
			if ($info['takestamp']!==''&&$info['takestamp']!==0) @touch($path, $info['takestamp']);

			// Create Thumb
			if (!$this->createThumb($path, $photo_name, $info['type'], $info['width'], $info['height'])) {
				Log::error(Database::get(), __METHOD__, __LINE__, 'Could not create thumbnail for photo');
				if ($returnOnError===true) return false;
				Response::error('Could not create thumbnail for photo!');
			}

			// Create Medium
			if ($this->createMedium($path, $photo_name, $info['width'], $info['height'])) $medium = 1;
			else $medium = 0;

			// Set thumb url
			$path_thumb = md5($id) . '.jpeg';

		}

		$values = array(LYCHEE_TABLE_PHOTOS, $id, $info['title'], $photo_name, $info['description'], $info['tags'], $info['type'], $info['width'], $info['height'], $info['size'], $info['iso'], $info['aperture'], $info['make'], $info['model'], $info['shutter'], $info['focal'], $info['takestamp'], $path_thumb, $albumID, $public, $star, $checksum, $medium);
		$query  = Database::prepare(Database::get(), "INSERT INTO ? (id, title, url, description, tags, type, width, height, size, iso, aperture, make, model, shutter, focal, takestamp, thumbUrl, album, public, star, checksum, medium) VALUES ('?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?', '?')", $values);
		$result = Database::execute(Database::get(), $query, __METHOD__, __LINE__);

		if ($result===false) {
			if ($returnOnError===true) return false;
			Response::error('Could not save photo in database!');
		}

		// Call plugins
		Plugins::get()->activate(__METHOD__, 1, func_get_args());

		return $id;

	}

	/**
	 * @return array|false Returns a subset of a photo when same photo exists or returns false on failure.
	 */
	private function exists($checksum, $photoID = null) {

		// Exclude $photoID from select when $photoID is set
		if (isset($photoID)) $query = Database::prepare(Database::get(), "SELECT id, url, thumbUrl, medium FROM ? WHERE checksum = '?' AND id <> '?' LIMIT 1", array(LYCHEE_TABLE_PHOTOS, $checksum, $photoID));
		else                 $query = Database::prepare(Database::get(), "SELECT id, url, thumbUrl, medium FROM ? WHERE checksum = '?' LIMIT 1", array(LYCHEE_TABLE_PHOTOS, $checksum));

		$result = Database::execute(Database::get(), $query, __METHOD__, __LINE__);

		if ($result===false) return false;

		if ($result->num_rows===1) {

			$result = $result->fetch_object();

			$return = array(
				'photo_name' => $result->url,
				'path'       => LYCHEE_UPLOADS_BIG . $result->url,
				'path_thumb' => $result->thumbUrl,
				'medium'     => $result->medium
			);

			return $return;

		}

		return false;

	}

	/**
	 * @return boolean Returns true when successful.
	 */
	private function createThumb($url, $filename, $type, $width, $height) {

		// Call plugins
		Plugins::get()->activate(__METHOD__, 0, func_get_args());

		// Quality of thumbnails
		$quality = 90;

		// Size of the thumbnail
		$newWidth  = 200;
		$newHeight = 200;

		$photoName = explode('.', $filename);
		$newUrl    = LYCHEE_UPLOADS_THUMB . $photoName[0] . '.jpeg';
		$newUrl2x  = LYCHEE_UPLOADS_THUMB . $photoName[0] . '@2x.jpeg';

		// Create thumbnails with Imagick
		if(Settings::hasImagick()) {

			// Read image
			$thumb = new Imagick();
			$thumb->readImage($url);
			$thumb->setImageCompressionQuality($quality);
			$thumb->setImageFormat('jpeg');

			// Remove metadata to save some bytes
			$thumb->stripImage();

			// Copy image for 2nd thumb version
			$thumb2x = clone $thumb;

			// Create 1st version
			$thumb->cropThumbnailImage($newWidth, $newHeight);
			$thumb->writeImage($newUrl);
			$thumb->clear();
			$thumb->destroy();

			// Create 2nd version
			$thumb2x->cropThumbnailImage($newWidth*2, $newHeight*2);
			$thumb2x->writeImage($newUrl2x);
			$thumb2x->clear();
			$thumb2x->destroy();

		} else {

			// Create image
			$thumb   = imagecreatetruecolor($newWidth, $newHeight);
			$thumb2x = imagecreatetruecolor($newWidth*2, $newHeight*2);

			// Set position
			if ($width<$height) {
				$newSize     = $width;
				$startWidth  = 0;
				$startHeight = $height/2 - $width/2;
			} else {
				$newSize     = $height;
				$startWidth  = $width/2 - $height/2;
				$startHeight = 0;
			}

			// Create new image
			switch($type) {
				case 'image/jpeg': $sourceImg = imagecreatefromjpeg($url); break;
				case 'image/png':  $sourceImg = imagecreatefrompng($url); break;
				case 'image/gif':  $sourceImg = imagecreatefromgif($url); break;
				default:           Log::error(Database::get(), __METHOD__, __LINE__, 'Type of photo is not supported');
				                   return false;
				                   break;
			}

			// Create thumb
			fastImageCopyResampled($thumb, $sourceImg, 0, 0, $startWidth, $startHeight, $newWidth, $newHeight, $newSize, $newSize);
			imagejpeg($thumb, $newUrl, $quality);
			imagedestroy($thumb);

			// Create retina thumb
			fastImageCopyResampled($thumb2x, $sourceImg, 0, 0, $startWidth, $startHeight, $newWidth*2, $newHeight*2, $newSize, $newSize);
			imagejpeg($thumb2x, $newUrl2x, $quality);
			imagedestroy($thumb2x);

			// Free memory
			imagedestroy($sourceImg);

		}

		// Call plugins
		Plugins::get()->activate(__METHOD__, 1, func_get_args());

		return true;

	}

	/**
	 * Creates a smaller version of a photo when its size is bigger than a preset size.
	 * Photo must be big enough and Imagick must be installed and activated.
	 * @return boolean Returns true when successful.
	 */
	private function createMedium($url, $filename, $width, $height) {

		// Excepts the following:
		// (string) $url = Path to the photo-file
		// (string) $filename = Name of the photo-file
		// (int) $width = Width of the photo
		// (int) $height = Height of the photo

		// Call plugins
		Plugins::get()->activate(__METHOD__, 0, func_get_args());

		// Quality of medium-photo
		$quality = 90;

		// Set to true when creation of medium-photo failed
		$error = false;

		// Size of the medium-photo
		// When changing these values,
		// also change the size detection in the front-end
		$newWidth  = 1920;
		$newHeight = 1080;

		// Check permissions
		if (hasPermissions(LYCHEE_UPLOADS_MEDIUM)===false) {

			// Permissions are missing
			Log::notice(Database::get(), __METHOD__, __LINE__, 'Skipped creation of medium-photo, because uploads/medium/ is missing or not readable and writable.');
			$error = true;

		}

		// Is photo big enough?
		// Is Imagick installed and activated?
		if (($error===false)&&
			($width>$newWidth||$height>$newHeight)&&
			(extension_loaded('imagick')&&Settings::get()['imagick']==='1')) {

			$newUrl = LYCHEE_UPLOADS_MEDIUM . $filename;

			// Read image
			$medium = new Imagick();
			$medium->readImage($url);

			// Adjust image
			$medium->scaleImage($newWidth, $newHeight, true);
			$medium->stripImage();
			$medium->setImageCompressionQuality($quality);

			// Save image
			try { $medium->writeImage($newUrl); }
			catch (ImagickException $err) {
				Log::notice(Database::get(), __METHOD__, __LINE__, 'Could not save medium-photo (' . $err->getMessage() . ')');
				$error = true;
			}

			$medium->clear();
			$medium->destroy();

		} else {

			// Photo too small or
			// Medium is deactivated or
			// Imagick not installed
			$error = true;

		}

		// Call plugins
		Plugins::get()->activate(__METHOD__, 1, func_get_args());

		if ($error===true) return false;
		return true;

	}

	/**
	 * Rotates and flips a photo based on its EXIF orientation.
	 * @return array|false Returns an array with the new orientation, width, height or false on failure.
	 */
	public function adjustFile($path, array $info) {

		// Excepts the following:
		// (string) $path = Path to the photo-file
		// (array) $info = ['orientation', 'width', 'height']

		// Call plugins
		Plugins::get()->activate(__METHOD__, 0, func_get_args());

		$swapSize = false;

		if (extension_loaded('imagick')&&Settings::get()['imagick']==='1') {

			$image = new Imagick();
			$image->readImage($path);

			$orientation = $image->getImageOrientation();

			switch ($orientation) {

				case Imagick::ORIENTATION_TOPLEFT:
					return false;
					break;

				case Imagick::ORIENTATION_TOPRIGHT:
					$image->flopImage();
					break;

				case Imagick::ORIENTATION_BOTTOMRIGHT:
					$image->rotateImage(new ImagickPixel(), 180);
					break;

				case Imagick::ORIENTATION_BOTTOMLEFT:
					$image->flopImage();
					$image->rotateImage(new ImagickPixel(), 180);
					break;

				case Imagick::ORIENTATION_LEFTTOP:
					$image->flopImage();
					$image->rotateImage(new ImagickPixel(), -90);
					$swapSize = true;
					break;

				case Imagick::ORIENTATION_RIGHTTOP:
					$image->rotateImage(new ImagickPixel(), 90);
					$swapSize = true;
					break;

				case Imagick::ORIENTATION_RIGHTBOTTOM:
					$image->flopImage();
					$image->rotateImage(new ImagickPixel(), 90);
					$swapSize = true;
					break;

				case Imagick::ORIENTATION_LEFTBOTTOM:
					$image->rotateImage(new ImagickPixel(), -90);
					$swapSize = true;
					break;

				default:
					return false;
					break;

			}

			// Adjust photo
			$image->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);
			$image->writeImage($path);

			// Free memory
			$image->clear();
			$image->destroy();

		} else {

			$newWidth  = $info['width'];
			$newHeight = $info['height'];
			$sourceImg = imagecreatefromjpeg($path);

			switch ($info['orientation']) {

				case 1:
					// do nothing
					return false;
					break;

				case 2:
					// mirror
					imageflip($sourceImg, IMG_FLIP_HORIZONTAL);
					break;

				case 3:
					$sourceImg = imagerotate($sourceImg, -180, 0);
					break;

				case 4:
					// rotate 180 and mirror
					imageflip($sourceImg, IMG_FLIP_VERTICAL);
					break;

				case 5:
					// rotate 90 and mirror
					$sourceImg = imagerotate($sourceImg, -90, 0);
					$newWidth  = $info['height'];
					$newHeight = $info['width'];
					$swapSize  = true;
					imageflip($sourceImg, IMG_FLIP_HORIZONTAL);
					break;

				case 6:
					$sourceImg = imagerotate($sourceImg, -90, 0);
					$newWidth  = $info['height'];
					$newHeight = $info['width'];
					$swapSize  = true;
					break;

				case 7:
					// rotate -90 and mirror
					$sourceImg = imagerotate($sourceImg, 90, 0);
					$newWidth  = $info['height'];
					$newHeight = $info['width'];
					$swapSize  = true;
					imageflip($sourceImg, IMG_FLIP_HORIZONTAL);
					break;

				case 8:
					$sourceImg = imagerotate($sourceImg, 90, 0);
					$newWidth  = $info['height'];
					$newHeight = $info['width'];
					$swapSize  = true;
					break;

				default:
					return false;
					break;

			}

			// Recreate photo
			// In this step the photos also loses its metadata :(
			$newSourceImg = imagecreatetruecolor($newWidth, $newHeight);
			imagecopyresampled($newSourceImg, $sourceImg, 0, 0, 0, 0, $newWidth, $newHeight, $newWidth, $newHeight);
			imagejpeg($newSourceImg, $path, 100);

			// Free memory
			imagedestroy($sourceImg);
			imagedestroy($newSourceImg);

		}

		// Call plugins
		Plugins::get()->activate(__METHOD__, 1, func_get_args());

		// SwapSize should be true when the image has been rotated
		// Return new dimensions in this case
		if ($swapSize===true) {
			$swapSize       = $info['width'];
			$info['width']  = $info['height'];
			$info['height'] = $swapSize;
		}

		return $info;

	}

	/**
	 * Rurns photo-attributes into a front-end friendly format. Note that some attributes remain unchanged.
	 * @return array Returns photo-attributes in a normalized structure.
	 */
	public static function prepareData(array $data) {

		// Excepts the following:
		// (array) $data = ['id', 'title', 'tags', 'public', 'star', 'album', 'thumbUrl', 'takestamp', 'url', 'medium']

		// Init
		$photo = null;

		// Set unchanged attributes
		$photo['id']     = $data['id'];
		$photo['title']  = $data['title'];
		$photo['tags']   = $data['tags'];
		$photo['public'] = $data['public'];
		$photo['star']   = $data['star'];
		$photo['album']  = $data['album'];

		// Parse medium
		if ($data['medium']==='1') $photo['medium'] = LYCHEE_URL_UPLOADS_MEDIUM . $data['url'];
		else                       $photo['medium'] = '';

		// Parse paths
		$photo['thumbUrl'] = LYCHEE_URL_UPLOADS_THUMB . $data['thumbUrl'];
		$photo['url']      = LYCHEE_URL_UPLOADS_BIG . $data['url'];

		// Use takestamp as sysdate when possible
		if (isset($data['takestamp'])&&$data['takestamp']!=='0') {

			// Use takestamp
			$photo['cameraDate'] = '1';
			$photo['sysdate']    = strftime('%d %B %Y', $data['takestamp']);

		} else {

			// Use sysstamp from the id
			$photo['cameraDate'] = '0';
			$photo['sysdate']    = strftime('%d %B %Y', substr($data['id'], 0, -4));

		}

		return $photo;

	}

	/**
	 * @return array|false Returns an array with information about the photo or false on failure.
	 */
	public function get($albumID) {

		// Excepts the following:
		// (string) $albumID = Album which is currently visible to the user

		// Check dependencies
		Validator::required(isset($this->photoIDs), __METHOD__);

		// Call plugins
		Plugins::get()->activate(__METHOD__, 0, func_get_args());

		// Get photo
		$query  = Database::prepare(Database::get(), "SELECT * FROM ? WHERE id = '?' LIMIT 1", array(LYCHEE_TABLE_PHOTOS, $this->photoIDs));
		$photos = Database::execute(Database::get(), $query, __METHOD__, __LINE__);

		if ($photos===false) return false;

		// Get photo object
		$photo = $photos->fetch_assoc();

		// Photo not found?
		if ($photo===null) {
			Log::error(Database::get(), __METHOD__, __LINE__, 'Could not find specified photo');
			return false;
		}

		// Parse photo
		$photo['sysdate'] = strftime('%d %b. %Y', substr($photo['id'], 0, -4));
		if (strlen($photo['takestamp'])>1) $photo['takedate'] = strftime('%d %b. %Y', $photo['takestamp']);

		// Parse medium
		if ($photo['medium']==='1') $photo['medium'] = LYCHEE_URL_UPLOADS_MEDIUM . $photo['url'];
		else                        $photo['medium'] = '';

		// Parse paths
		$photo['url']      = LYCHEE_URL_UPLOADS_BIG . $photo['url'];
		$photo['thumbUrl'] = LYCHEE_URL_UPLOADS_THUMB . $photo['thumbUrl'];

		if ($albumID!='false') {

			// Only show photo as public when parent album is public
			// Check if parent album is not 'Unsorted'
			if ($photo['album']!=='0') {

				// Get album
				$query  = Database::prepare(Database::get(), "SELECT public FROM ? WHERE id = '?' LIMIT 1", array(LYCHEE_TABLE_ALBUMS, $photo['album']));
				$albums = Database::execute(Database::get(), $query, __METHOD__, __LINE__);

				if ($albums===false) return false;

				// Get album object
				$album = $albums->fetch_assoc();

				// Photo not found?
				if ($photo===null) {
					Log::error(Database::get(), __METHOD__, __LINE__, 'Could not find specified album');
					return false;
				}

				// Parse album
				$photo['public'] = ($album['public']==='1' ? '2' : $photo['public']);

			}

			$photo['original_album'] = $photo['album'];
			$photo['album']          = $albumID;

		}

		// Call plugins
		Plugins::get()->activate(__METHOD__, 1, func_get_args());

		return $photo;

	}

	/**
	 * Reads and parses information and metadata out of a photo.
	 * @return array Returns an array of photo information and metadata.
	 */
	public function getInfo($url) {

		// Functions returns information and metadata of a photo
		// Excepts the following:
		// (string) $url = Path to photo-file
		// Returns the following:
		// (array) $return

		// Call plugins
		Plugins::get()->activate(__METHOD__, 0, func_get_args());

		$iptcArray = array();
		$info      = getimagesize($url, $iptcArray);

		// General information
		$return['type']        = $info['mime'];
		$return['width']       = $info[0];
		$return['height']      = $info[1];
		$return['title']       = '';
		$return['description'] = '';
		$return['orientation'] = '';
		$return['iso']         = '';
		$return['aperture']    = '';
		$return['make']        = '';
		$return['model']       = '';
		$return['shutter']     = '';
		$return['focal']       = '';
		$return['takestamp']   = 0;
		$return['lens']        = '';
		$return['tags']        = '';
		$return['position']    = '';
		$return['latitude']    = '';
		$return['longitude']   = '';
		$return['altitude']    = '';

		// Size
		$size = filesize($url)/1024;
		if ($size>=1024) $return['size'] = round($size/1024, 1) . ' MB';
		else $return['size'] = round($size, 1) . ' KB';

		// IPTC Metadata
		// See https://www.iptc.org/std/IIM/4.2/specification/IIMV4.2.pdf for mapping
		if(isset($iptcArray['APP13'])) {

			$iptcInfo = iptcparse($iptcArray['APP13']);
			if (is_array($iptcInfo)) {

				// Title
				if (!empty($iptcInfo['2#105'][0])) $return['title'] = $iptcInfo['2#105'][0];
				else if (!empty($iptcInfo['2#005'][0])) $return['title'] = $iptcInfo['2#005'][0];

				// Description
				if (!empty($iptcInfo['2#120'][0])) $return['description'] = $iptcInfo['2#120'][0];

				// Tags
				if (!empty($iptcInfo['2#025'])) $return['tags'] = implode(',', $iptcInfo['2#025']);

				// Position
				$fields = array();
				if (!empty($iptcInfo['2#090'])) $fields[] = trim($iptcInfo['2#090'][0]);
				if (!empty($iptcInfo['2#092'])) $fields[] = trim($iptcInfo['2#092'][0]);
				if (!empty($iptcInfo['2#095'])) $fields[] = trim($iptcInfo['2#095'][0]);
				if (!empty($iptcInfo['2#101'])) $fields[] = trim($iptcInfo['2#101'][0]);

				if (!empty($fields)) $return['position'] = implode(', ', $fields);

			}

		}

		// Read EXIF
		if ($info['mime']=='image/jpeg') $exif = @exif_read_data($url, 'EXIF', false, false);
		else $exif = false;

		// EXIF Metadata
		if ($exif!==false) {

			// Orientation
			if (isset($exif['Orientation'])) $return['orientation'] = $exif['Orientation'];
			else if (isset($exif['IFD0']['Orientation'])) $return['orientation'] = $exif['IFD0']['Orientation'];

			// ISO
			if (!empty($exif['ISOSpeedRatings'])) $return['iso'] = $exif['ISOSpeedRatings'];

			// Aperture
			if (!empty($exif['COMPUTED']['ApertureFNumber'])) $return['aperture'] = $exif['COMPUTED']['ApertureFNumber'];

			// Make
			if (!empty($exif['Make'])) $return['make'] = trim($exif['Make']);

			// Model
			if (!empty($exif['Model'])) $return['model'] = trim($exif['Model']);

			// Exposure
			if (!empty($exif['ExposureTime'])) $return['shutter'] = $exif['ExposureTime'] . ' s';

			// Focal Length
			if (!empty($exif['FocalLength'])) {
				if (strpos($exif['FocalLength'], '/')!==false) {
					$temp = explode('/', $exif['FocalLength'], 2);
					$temp = $temp[0] / $temp[1];
					$temp = round($temp, 1);
					$return['focal'] = $temp . ' mm';
				} else {
					$return['focal'] = $exif['FocalLength'] . ' mm';
				}
			}

			// Takestamp
			if (!empty($exif['DateTimeOriginal'])) $return['takestamp'] = strtotime($exif['DateTimeOriginal']);

			// Lens field from Lightroom
			if (!empty($exif['UndefinedTag:0xA434'])) $return['lens'] = trim($exif['UndefinedTag:0xA434']);

			// Deal with GPS coordinates
			if (!empty($exif['GPSLatitude']) && !empty($exif['GPSLatitudeRef'])) $return['latitude'] = getGPSCoordinate($exif['GPSLatitude'], $exif['GPSLatitudeRef']);
			if (!empty($exif['GPSLongitude']) && !empty($exif['GPSLongitudeRef'])) $return['longitude'] = getGPSCoordinate($exif['GPSLongitude'], $exif['GPSLongitudeRef']);

		}

		// Call plugins
		Plugins::get()->activate(__METHOD__, 1, func_get_args());

		return $return;

	}

	/**
	 * Starts a download of a photo.
	 * @return resource|boolean Sends a ZIP-file or returns false on failure.
	 */
	public function getArchive() {

		// Check dependencies
		Validator::required(isset($this->photoIDs), __METHOD__);

		// Call plugins
		Plugins::get()->activate(__METHOD__, 0, func_get_args());

		// Get photo
		$query  = Database::prepare(Database::get(), "SELECT title, url FROM ? WHERE id = '?' LIMIT 1", array(LYCHEE_TABLE_PHOTOS, $this->photoIDs));
		$photos = Database::execute(Database::get(), $query, __METHOD__, __LINE__);

		if ($photos===false) return false;

		// Get photo object
		$photo = $photos->fetch_object();

		// Photo not found?
		if ($photo===null) {
			Log::error(Database::get(), __METHOD__, __LINE__, 'Could not find specified photo');
			return false;
		}

		// Get extension
		$extension = getExtension($photo->url, false);
		if (empty($extension)===true) {
			Log::error(Database::get(), __METHOD__, __LINE__, 'Invalid photo extension');
			return false;
		}

		// Illicit chars
		$badChars = array_merge(
			array_map('chr', range(0,31)),
			array("<", ">", ":", '"', "/", "\\", "|", "?", "*")
		);

		// Parse title
		if ($photo->title=='') $photo->title = 'Untitled';

		// Escape title
		$photo->title = str_replace($badChars, '', $photo->title);

		// Set headers
		header("Content-Type: application/octet-stream");
		header("Content-Disposition: attachment; filename=\"" . $photo->title . $extension . "\"");
		header("Content-Length: " . filesize(LYCHEE_UPLOADS_BIG . $photo->url));

		// Send file
		readfile(LYCHEE_UPLOADS_BIG . $photo->url);

		// Call plugins
		Plugins::get()->activate(__METHOD__, 1, func_get_args());

		return true;

	}

	/**
	 * Sets the title of a photo.
	 * @return boolean Returns true when successful.
	 */
	public function setTitle($title = 'Untitled') {

		// Check dependencies
		Validator::required(isset($this->photoIDs), __METHOD__);

		// Call plugins
		Plugins::get()->activate(__METHOD__, 0, func_get_args());

		// Set title
		$query  = Database::prepare(Database::get(), "UPDATE ? SET title = '?' WHERE id IN (?)", array(LYCHEE_TABLE_PHOTOS, $title, $this->photoIDs));
		$result = Database::execute(Database::get(), $query, __METHOD__, __LINE__);

		// Call plugins
		Plugins::get()->activate(__METHOD__, 1, func_get_args());

		if ($result===false) return false;
		return true;

	}

	/**
	 * Sets the description of a photo.
	 * @return boolean Returns true when successful.
	 */
	public function setDescription($description) {

		// Check dependencies
		Validator::required(isset($this->photoIDs), __METHOD__);

		// Call plugins
		Plugins::get()->activate(__METHOD__, 0, func_get_args());

		// Set description
		$query  = Database::prepare(Database::get(), "UPDATE ? SET description = '?' WHERE id IN ('?')", array(LYCHEE_TABLE_PHOTOS, $description, $this->photoIDs));
		$result = Database::execute(Database::get(), $query, __METHOD__, __LINE__);

		// Call plugins
		Plugins::get()->activate(__METHOD__, 1, func_get_args());

		if ($result===false) return false;
		return true;

	}

	/**
	 * Toggles the star property of a photo.
	 * @return boolean Returns true when successful.
	 */
	public function setStar() {

		// Check dependencies
		Validator::required(isset($this->photoIDs), __METHOD__);

		// Call plugins
		Plugins::get()->activate(__METHOD__, 0, func_get_args());

		// Init vars
		$error = false;

		// Get photos
		$query  = Database::prepare(Database::get(), "SELECT id, star FROM ? WHERE id IN (?)", array(LYCHEE_TABLE_PHOTOS, $this->photoIDs));
		$photos = Database::execute(Database::get(), $query, __METHOD__, __LINE__);

		if ($photos===false) return false;

		// For each photo
		while ($photo = $photos->fetch_object()) {

			// Invert star
			$star = ($photo->star==0 ? 1 : 0);

			// Set star
			$query  = Database::prepare(Database::get(), "UPDATE ? SET star = '?' WHERE id = '?'", array(LYCHEE_TABLE_PHOTOS, $star, $photo->id));
			$result = Database::execute(Database::get(), $query, __METHOD__, __LINE__);

			if ($result===false) $error = true;

		}

		// Call plugins
		Plugins::get()->activate(__METHOD__, 1, func_get_args());

		if ($error===true) return false;
		return true;

	}

	/**
	 * Checks if photo or parent album is public.
	 * @return integer 0 = Photo private and parent album private
	 *                 1 = Album public, but password incorrect
	 *                 2 = Photo public or album public and password correct
	 */
	public function getPublic($password) {

		// Check dependencies
		Validator::required(isset($this->photoIDs), __METHOD__);

		// Call plugins
		Plugins::get()->activate(__METHOD__, 0, func_get_args());

		// Get photo
		$query  = Database::prepare(Database::get(), "SELECT public, album FROM ? WHERE id = '?' LIMIT 1", array(LYCHEE_TABLE_PHOTOS, $this->photoIDs));
		$photos = Database::execute(Database::get(), $query, __METHOD__, __LINE__);

		if ($photos===false) return 0;

		// Get photo object
		$photo = $photos->fetch_object();

		// Photo not found?
		if ($photo===null) {
			Log::error(Database::get(), __METHOD__, __LINE__, 'Could not find specified photo');
			return false;
		}

		// Check if public
		if ($photo->public==='1') {

			// Photo public
			return 2;

		} else {

			// Check if album public
			$album = new Album($photo->album);
			$agP   = $album->getPublic();
			$acP   = $album->checkPassword($password);

			// Album public and password correct
			if ($agP===true&&$acP===true) return 2;

			// Album public, but password incorrect
			if ($agP===true&&$acP===false) return 1;

		}

		// Call plugins
		Plugins::get()->activate(__METHOD__, 1, func_get_args());

		// Photo private
		return 0;

	}

	/**
	 * Toggles the public property of a photo.
	 * @return boolean Returns true when successful.
	 */
	public function setPublic() {

		// Check dependencies
		Validator::required(isset($this->photoIDs), __METHOD__);

		// Call plugins
		Plugins::get()->activate(__METHOD__, 0, func_get_args());

		// Get public
		$query  = Database::prepare(Database::get(), "SELECT public FROM ? WHERE id = '?' LIMIT 1", array(LYCHEE_TABLE_PHOTOS, $this->photoIDs));
		$photos = Database::execute(Database::get(), $query, __METHOD__, __LINE__);

		if ($photos===false) return false;

		// Get photo object
		$photo = $photos->fetch_object();

		// Photo not found?
		if ($photo===null) {
			Log::error(Database::get(), __METHOD__, __LINE__, 'Could not find specified photo');
			return false;
		}

		// Invert public
		$public = ($photo->public==0 ? 1 : 0);

		// Set public
		$query  = Database::prepare(Database::get(), "UPDATE ? SET public = '?' WHERE id = '?'", array(LYCHEE_TABLE_PHOTOS, $public, $this->photoIDs));
		$result = Database::execute(Database::get(), $query, __METHOD__, __LINE__);

		// Call plugins
		Plugins::get()->activate(__METHOD__, 1, func_get_args());

		if ($result===false) return false;
		return true;

	}

	/**
	 * Sets the parent album of a photo.
	 * @return boolean Returns true when successful.
	 */
	function setAlbum($albumID) {

		// Check dependencies
		Validator::required(isset($this->photoIDs), __METHOD__);

		// Call plugins
		Plugins::get()->activate(__METHOD__, 0, func_get_args());

		// Set album
		$query  = Database::prepare(Database::get(), "UPDATE ? SET album = '?' WHERE id IN (?)", array(LYCHEE_TABLE_PHOTOS, $albumID, $this->photoIDs));
		$result = Database::execute(Database::get(), $query, __METHOD__, __LINE__);

		// Call plugins
		Plugins::get()->activate(__METHOD__, 1, func_get_args());

		if ($result===false) return false;
		return true;

	}

	/**
	 * Sets the tags of a photo.
	 * @return boolean Returns true when successful.
	 */
	public function setTags($tags) {

		// Excepts the following:
		// (string) $tags = Comma separated list of tags with a maximum length of 1000 chars

		// Check dependencies
		Validator::required(isset($this->photoIDs), __METHOD__);

		// Call plugins
		Plugins::get()->activate(__METHOD__, 0, func_get_args());

		// Parse tags
		$tags = preg_replace('/(\ ,\ )|(\ ,)|(,\ )|(,{1,}\ {0,})|(,$|^,)/', ',', $tags);
		$tags = preg_replace('/,$|^,|(\ ){0,}$/', '', $tags);

		// Set tags
		$query  = Database::prepare(Database::get(), "UPDATE ? SET tags = '?' WHERE id IN (?)", array(LYCHEE_TABLE_PHOTOS, $tags, $this->photoIDs));
		$result = Database::execute(Database::get(), $query, __METHOD__, __LINE__);

		// Call plugins
		Plugins::get()->activate(__METHOD__, 1, func_get_args());

		if ($result===false) return false;
		return true;

	}

	/**
	 * Duplicates a photo.
	 * @return boolean Returns true when successful.
	 */
	public function duplicate() {

		// Check dependencies
		Validator::required(isset($this->photoIDs), __METHOD__);

		// Call plugins
		Plugins::get()->activate(__METHOD__, 0, func_get_args());

		// Init vars
		$error = false;

		// Get photos
		$query  = Database::prepare(Database::get(), "SELECT id, checksum FROM ? WHERE id IN (?)", array(LYCHEE_TABLE_PHOTOS, $this->photoIDs));
		$photos = Database::execute(Database::get(), $query, __METHOD__, __LINE__);

		if ($photos===false) return false;

		// For each photo
		while ($photo = $photos->fetch_object()) {

			// Generate id
			$id = generateID();

			// Duplicate entry
			$values = array(LYCHEE_TABLE_PHOTOS, $id, LYCHEE_TABLE_PHOTOS, $photo->id);
			$query  = Database::prepare(Database::get(), "INSERT INTO ? (id, title, url, description, tags, type, width, height, size, iso, aperture, make, model, shutter, focal, takestamp, thumbUrl, album, public, star, checksum) SELECT '?' AS id, title, url, description, tags, type, width, height, size, iso, aperture, make, model, shutter, focal, takestamp, thumbUrl, album, public, star, checksum FROM ? WHERE id = '?'", $values);
			$result = Database::execute(Database::get(), $query, __METHOD__, __LINE__);

			if ($result===false) $error = true;

		}

		if ($error===true) return false;
		return true;

	}

	/**
	 * Deletes a photo with all its data and files.
	 * @return boolean Returns true when successful.
	 */
	public function delete() {

		// Check dependencies
		Validator::required(isset($this->photoIDs), __METHOD__);

		// Call plugins
		Plugins::get()->activate(__METHOD__, 0, func_get_args());

		// Init vars
		$error = false;

		// Get photos
		$query  = Database::prepare(Database::get(), "SELECT id, url, thumbUrl, checksum FROM ? WHERE id IN (?)", array(LYCHEE_TABLE_PHOTOS, $this->photoIDs));
		$photos = Database::execute(Database::get(), $query, __METHOD__, __LINE__);

		if ($photos===false) return false;

		// For each photo
		while ($photo = $photos->fetch_object()) {

			// Check if other photos are referring to this images
			// If so, only delete the db entry
			if ($this->exists($photo->checksum, $photo->id)===false) {

				// Get retina thumb url
				$thumbUrl2x = explode(".", $photo->thumbUrl);
				$thumbUrl2x = $thumbUrl2x[0] . '@2x.' . $thumbUrl2x[1];

				// Delete big
				if (file_exists(LYCHEE_UPLOADS_BIG . $photo->url)&&!unlink(LYCHEE_UPLOADS_BIG . $photo->url)) {
					Log::error(Database::get(), __METHOD__, __LINE__, 'Could not delete photo in uploads/big/');
					$error = true;
				}

				// Delete medium
				if (file_exists(LYCHEE_UPLOADS_MEDIUM . $photo->url)&&!unlink(LYCHEE_UPLOADS_MEDIUM . $photo->url)) {
					Log::error(Database::get(), __METHOD__, __LINE__, 'Could not delete photo in uploads/medium/');
					$error = true;
				}

				// Delete thumb
				if (file_exists(LYCHEE_UPLOADS_THUMB . $photo->thumbUrl)&&!unlink(LYCHEE_UPLOADS_THUMB . $photo->thumbUrl)) {
					Log::error(Database::get(), __METHOD__, __LINE__, 'Could not delete photo in uploads/thumb/');
					$error = true;
				}

				// Delete thumb@2x
				if (file_exists(LYCHEE_UPLOADS_THUMB . $thumbUrl2x)&&!unlink(LYCHEE_UPLOADS_THUMB . $thumbUrl2x)) {
					Log::error(Database::get(), __METHOD__, __LINE__, 'Could not delete high-res photo in uploads/thumb/');
					$error = true;
				}

			}

			// Delete db entry
			$query  = Database::prepare(Database::get(), "DELETE FROM ? WHERE id = '?'", array(LYCHEE_TABLE_PHOTOS, $photo->id));
			$result = Database::execute(Database::get(), $query, __METHOD__, __LINE__);

			if ($result===false) $error = true;

		}

		// Call plugins
		Plugins::get()->activate(__METHOD__, 1, func_get_args());

		if ($error===true) return false;
		return true;

	}

}

?>