<?php
/*****************************************************************************
 * Image.php
 * Contains key class Image.
 *
 * Kabuki CMS (C) 2013-2015, Aaron van Geffen
 *****************************************************************************/

class Image extends Asset
{
	const TYPE_PANORAMA = 1;
	const TYPE_LANDSCAPE = 2;
	const TYPE_PORTRAIT = 4;

	protected function __construct(array $data)
	{
		foreach ($data as $attribute => $value)
			$this->$attribute = $value;
	}

	public static function fromId($id_asset, $return_format = 'object')
	{
		$asset = parent::fromId($id_asset, 'array');
		if ($asset)
			return $return_format == 'object' ? new Image($asset) : $asset;
		else
			return false;
	}

	public static function fromIds(array $id_assets, $return_format = 'object')
	{
		if (empty($id_assets))
			return [];

		$assets = parent::fromIds($id_assets, 'array');

		if ($return_format == 'array')
			return $assets;
		else
		{
			$objects = [];
			foreach ($assets as $id => $asset)
				$objects[$id] = new Image($asset);
			return $objects;
		}
	}

	public function save()
	{
		$data = [];
		foreach ($this->meta as $key => $value)
			$data[] = [
				'id_asset' => $this->id_asset,
				'variable' => $key,
				'value' => $value,
			];

		return Registry::get('db')->insert('replace', 'assets_meta', [
			'id_asset' => 'int',
			'variable' => 'string',
			'value' => 'string',
			], $data);
	}

	public function getExif()
	{
		return EXIF::fromFile($this->getPath());
	}

	public function getPath()
	{
		return ASSETSDIR . '/' . $this->subdir . '/' . $this->filename;
	}

	public function getUrl()
	{
		return ASSETSURL . '/' . $this->subdir . '/' . $this->filename;
	}

	/**
	 * @param width: width of the thumbnail.
	 * @param height: height of the thumbnail.
	 * @param crop: whether and how to crop original image to fit. [false|true|'top'|'center'|'bottom']
	 * @param fit: whether to fit the image to given boundaries [true], or use them merely as an estimation [false].
	 */
	public function getThumbnailUrl($width, $height, $crop = true, $fit = true)
	{
		// First, assert the image's dimensions are properly known in the database.
		if (!isset($this->image_height, $this->image_width))
			throw new UnexpectedValueException('Image width or height is undefined -- inconsistent database?');

		// Inferring width or height?
		if (!$height)
			$height = ceil($width / $this->image_width * $this->image_height);
		elseif (!$width)
			$width = ceil($height / $this->image_height * $this->image_width);

		// Inferring the height from the original image's ratio?
		if (!$fit)
			$height = floor($width / ($this->image_width / $this->image_height));

		// Assert we have both, now...
		if (empty($width) || empty($height))
			throw new InvalidArgumentException('Expecting at least either width or height as argument.');

		// If we're cropping, verify we're in the right mode.
		if ($crop)
		{
			// If the original image's aspect ratio is much wider, take a slice instead.
			if ($this->image_width / $this->image_height > $width / $height)
				$crop = 'slice';

			// We won't be cropping if the thumbnail is proportional to its original.
			if (abs($width / $height - $this->image_width / $this->image_height) <= 0.05)
				$crop = false;
		}

		// Do we have an exact crop boundary for these dimensions?
		$crop_selector = "crop_{$width}x{$height}";
		if (isset($this->meta[$crop_selector]))
			$crop = 'exact';

		// Now, do we need to suffix the filename?
		if ($crop)
			$suffix = '_c' . (is_string($crop) && $crop !== 'center' ? substr($crop, 0, 1) : '');
		else
			$suffix = '';

		// Check whether we already resized this earlier.
		$thumb_selector = "thumb_{$width}x{$height}{$suffix}";
		if (isset($this->meta[$thumb_selector]) && file_exists(THUMBSDIR . '/' . $this->subdir . '/' . $this->meta[$thumb_selector]))
			return THUMBSURL . '/' . $this->subdir . '/' . $this->meta[$thumb_selector];

		// Do we have a custom thumbnail on file?
		$custom_selector = "custom_{$width}x{$height}";
		if (isset($this->meta[$custom_selector]))
		{
			if (file_exists(ASSETSDIR . '/' . $this->subdir . '/' . $this->meta[$custom_selector]))
			{
				// Copy the custom thumbail to the general thumbnail directory.
				copy(ASSETSDIR . '/' . $this->subdir . '/' . $this->meta[$custom_selector],
					THUMBSDIR . '/' . $this->subdir . '/' . $this->meta[$custom_selector]);

				// Let's remember this for future reference.
				$this->meta[$thumb_selector] = $this->meta[$custom_selector];
				$this->save();

				return THUMBSURL . '/' . $this->subdir . '/' . $this->meta[$custom_selector];
			}
			else
				throw new UnexpectedValueException('Custom thumbnail expected, but missing in file system!');
		}

		// Let's try some arcane stuff...
		try
		{
			if (!class_exists('Imagick'))
				throw new Exception("The PHP module 'imagick' appears to be disabled. Please enable it to use image resampling functions.");

			$thumb = new Imagick(ASSETSDIR . '/' . $this->subdir . '/' . $this->filename);

			// The image might have some orientation set through EXIF. Let's apply this first.
			self::applyRotation($thumb);

			// Just resizing? Easy peasy.
			if (!$crop)
				$thumb->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
			// Cropping in the center?
			elseif ($crop === true || $crop === 'center')
				$thumb->cropThumbnailImage($width, $height);
			// Exact cropping? We can do that.
			elseif ($crop === 'exact')
			{
				list($crop_width, $crop_height, $crop_x_pos, $crop_y_pos) = explode(',', $this->meta[$crop_selector]);
				$thumb->cropImage($crop_width, $crop_height, $crop_x_pos, $crop_y_pos);
				$thumb->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
			}
			// Advanced cropping? Fun!
			else
			{
				$size = $thumb->getImageGeometry();

				// Taking a horizontal slice from the top or bottom of the original image?
				if ($crop === 'top' || $crop === 'bottom')
				{
					$crop_width = $size['width'];
					$crop_height = floor($size['width'] / $width * $height);
					$target_x = 0;
					$target_y = $crop === 'top' ? 0 : $size['height'] - $crop_height;
				}
				// Otherwise, we're taking a vertical slice from the centre.
				else
				{
					$crop_width = floor($size['height'] / $height * $width);
					$crop_height = $size['height'];
					$target_x = floor(($size['width'] - $crop_width) / 2);
					$target_y = 0;
				}

				$thumb->cropImage($crop_width, $crop_height, $target_x, $target_y);
				$thumb->resizeImage($width, $height, Imagick::FILTER_LANCZOS, 1);
			}

			// What sort of image is this? Fall back to PNG if we must.
			switch ($thumb->getImageFormat())
			{
				case 'JPEG':
					$ext = 'jpg';
					break;

				case 'GIF':
					$ext = 'gif';
					break;

				case 'PNG':
				default:
					$thumb->setFormat('PNG');
					$ext = 'png';
					break;
			}

			// So, how do we name this?
			$thumbfilename = substr($this->filename, 0, strrpos($this->filename, '.')) . "_{$width}x{$height}{$suffix}.$ext";

			// Ensure the thumbnail subdirectory exists.
			if (!is_dir(THUMBSDIR . '/' . $this->subdir))
				mkdir(THUMBSDIR . '/' . $this->subdir, 0755, true);

			// Save it in a public spot.
			$thumb->writeImage(THUMBSDIR . '/' . $this->subdir . '/' . $thumbfilename);
			$thumb->clear();
			$thumb->destroy();
		}
		// Blast! Curse your sudden but inevitable betrayal!
		catch (ImagickException $e)
		{
			throw new Exception('ImageMagick error occurred while generating thumbnail. Output: ' . $e->getMessage());
		}

		// Let's remember this for future reference.
		$this->meta[$thumb_selector] = $thumbfilename;
		$this->save();

		// Ah yes, you wanted a URL, didn't you...
		return THUMBSURL . '/' . $this->subdir . '/' . $this->meta[$thumb_selector];
	}

	private static function applyRotation(Imagick $image)
	{
		switch ($image->getImageOrientation())
		{
			// Clockwise rotation
			case Imagick::ORIENTATION_RIGHTTOP:
				$image->rotateImage("#000", 90);
				break;

			// Counter-clockwise rotation
			case Imagick::ORIENTATION_LEFTBOTTOM:
				$image->rotateImage("#000", 270);
				break;

			// Upside down?
			case Imagick::ORIENTATION_BOTTOMRIGHT:
				$image->rotateImage("#000", 180);
		}

		// Having rotated the image, make sure the EXIF data is set properly.
		$image->setImageOrientation(Imagick::ORIENTATION_TOPLEFT);
	}

	public function bestColor()
	{
		// Save some computations if we can.
		if (isset($this->meta['best_color']))
			return $this->meta['best_color'];

		// Find out what colour is most prominent.
		$color = new BestColor($this);
		$this->meta['best_color'] = $color->hex();
		$this->save();

		// There's your colour.
		return $this->meta['best_color'];
	}

	public function bestLabelColor()
	{
		// Save some computations if we can.
		if (isset($this->meta['best_color_label']))
			return $this->meta['best_color_label'];

		// Find out what colour is most prominent.
		$color = new BestColor($this);
		$this->meta['best_color_label'] = $color->rgba();
		$this->save();

		// There's your colour.
		return $this->meta['best_color_label'];
	}

	public function width()
	{
		return $this->image_width;
	}

	public function height()
	{
		return $this->image_height;
	}

	public function isPanorama()
	{
		return $this->image_width / $this->image_height > 2;
	}

	public function isPortrait()
	{
		return $this->image_width / $this->image_height < 1;
	}

	public function isLandscape()
	{
		$ratio = $this->image_width / $this->image_height;
		return $ratio >= 1 && $ratio <= 2;
	}

	public function removeAllThumbnails()
	{
		foreach ($this->meta as $key => $value)
		{
			if (substr($key, 0, 6) !== 'thumb_')
				continue;

			$thumb_path = THUMBSDIR . '/' . $this->subdir . '/' . $value;
			if (is_file($thumb_path))
				unlink($thumb_path);

			unset($this->meta[$key]);
		}

		$this->saveMetaData();
	}

	public function replaceThumbnail($descriptor, $tmp_file)
	{
		if (!is_file($tmp_file))
			return -1;

		if (!isset($this->meta[$descriptor]))
			return -2;

		$image = new Imagick($tmp_file);
		$d = $image->getImageGeometry();
		unset($image);

		// Check whether dimensions match.
		$test_descriptor = 'thumb_' . $d['width'] . 'x' . $d['height'];
		if ($descriptor !== $test_descriptor && strpos($descriptor, $test_descriptor . '_') === false)
			return -3;

		// Save the custom thumbnail in the assets directory.
		$destination = ASSETSDIR . '/' . $this->subdir . '/' . $this->meta[$descriptor];
		if (file_exists($destination) && !is_writable($destination))
			return -4;

		if (!copy($tmp_file, $destination))
			return -5;

		// Copy it to the thumbnail directory, overwriting the automatically generated one, too.
		$destination = THUMBSDIR . '/' . $this->subdir . '/' . $this->meta[$descriptor];
		if (file_exists($destination) && !is_writable($destination))
			return -6;

		if (!copy($tmp_file, $destination))
			return -7;

		// A little bookkeeping
		$this->meta['custom_' . $d['width'] . 'x' . $d['height']] = $this->meta[$descriptor];
		$this->saveMetaData();
		return 0;
	}
}