<?php
/**
 * Nextcloud - Gallery
 *
 * This file is licensed under the Affero General Public License version 3 or
 * later. See the COPYING file.
 *
 * @author Olivier Paroz <galleryapps@oparoz.com>
 *
 * @copyright Olivier Paroz 2017
 */

namespace OCA\Gallery\Service;

use OCP\Files\File;
use OCP\Image;
use OCP\IPreview;
use OCP\ILogger;

use OCA\Gallery\Environment\Environment;

/**
 * Generates previews
 *
 * @package OCA\Gallery\Service
 */
class PreviewService extends Service {

	use Base64Encode;

	/** @var IPreview */
	private $previewManager;

	/**
	 * Constructor
	 *
	 * @param string $appName
	 * @param Environment $environment
	 * @param IPreview $previewManager
	 * @param ILogger $logger
	 */
	public function __construct(
		$appName,
		Environment $environment,
		IPreview $previewManager,
		ILogger $logger
	) {
		parent::__construct($appName, $environment, $logger);

		$this->previewManager = $previewManager;
	}

	/**
	 * Decides if we should download the file instead of generating a preview
	 *
	 * @param File $file
	 * @param bool $animatedPreview
	 *
	 * @return bool
	 */
	public function isPreviewRequired($file, $animatedPreview) {
		$mime = $file->getMimeType();

		if ($mime === 'image/svg+xml') {
			return $this->isSvgPreviewRequired();
		}
		if ($mime === 'image/gif') {
			return $this->isGifPreviewRequired($file, $animatedPreview);
		}

		return true;
	}

	/**
	 * Returns an array containing everything needed by the client to be able to display a preview
	 *
	 *    * fileid:  the file's ID
	 *    * mimetype: the file's media type
	 *    * preview: the preview's content
	 *
	 * Example logger
	 * $this->logger->debug(
	 * "[PreviewService] Path : {path} / mime: {mimetype} / fileid: {fileid}",
	 * [
	 * 'path'     => $preview['data']['path'],
	 * 'mimetype' => $preview['data']['mimetype'],
	 * 'fileid'   => $preview['fileid']
	 * ]
	 * );
	 *
	 * @todo Get the max size from the settings
	 *
	 * @param File $file
	 * @param int $maxX asked width for the preview
	 * @param int $maxY asked height for the preview
	 * @param bool $keepAspect
	 * @param bool $base64Encode
	 *
	 * @return string|\OC_Image|string|false preview data
	 * @throws InternalServerErrorServiceException
	 */
	public function createPreview(
		$file, $maxX = 0, $maxY = 0, $keepAspect = true, $base64Encode = false
	) {
		try {
			$preview = $this->previewManager->getPreview($file, $maxX, $maxY, !$keepAspect);
			$img = new Image($preview->getContent());
			$mimeType = $img->mimeType();
			if ($img && $base64Encode) {
				$img = $this->encode($img);
			}

			return [
				'preview'  => $img,
				'mimetype' => $mimeType
			];
		} catch (\Exception $exception) {
			throw new InternalServerErrorServiceException('Preview generation has failed');
		}
	}

	/**
	 * Returns true if the passed mime type is supported
	 *
	 * In case of a failure, we just return that the media type is not supported
	 *
	 * @param string $mimeType
	 *
	 * @return boolean
	 */
	private function isMimeSupported($mimeType = '*') {
		try {
			return $this->previewManager->isMimeSupported($mimeType);
		} catch (\Exception $exception) {
			unset($exception);

			return false;
		}
	}

	/**
	 * Decides if we should download the SVG or generate a preview
	 *
	 * SVGs are downloaded if the SVG converter is disabled
	 * Files of any media type are downloaded if requested by the client
	 *
	 * @return bool
	 */
	private function isSvgPreviewRequired() {
		return $this->isMimeSupported('image/svg+xml');
	}

	/**
	 * Decides if we should download the GIF or generate a preview
	 *
	 * GIFs are downloaded if they're animated and we want to show
	 * animations
	 *
	 * @param File $file
	 * @param bool $animatedPreview
	 *
	 * @return bool
	 */
	private function isGifPreviewRequired($file, $animatedPreview) {
		$gifSupport = $this->isMimeSupported('image/gif');
		$animatedGif = $this->isGifAnimated($file);

		return $gifSupport && !($animatedGif && $animatedPreview);
	}

	/**
	 * Tests if a GIF is animated
	 *
	 * An animated gif contains multiple "frames", with each frame having a
	 * header made up of:
	 *    * a static 4-byte sequence (\x00\x21\xF9\x04)
	 *    * 4 variable bytes
	 *    * a static 2-byte sequence (\x00\x2C) (Photoshop uses \x00\x21)
	 *
	 * We read through the file until we reach the end of the file, or we've
	 * found at least 2 frame headers
	 *
	 * @link http://php.net/manual/en/function.imagecreatefromgif.php#104473
	 *
	 * @param File $file
	 *
	 * @return bool
	 */
	private function isGifAnimated($file) {
		$count = 0;
		$fileHandle = $this->isFileReadable($file);
		if ($fileHandle) {
			while (!feof($fileHandle) && $count < 2) {
				$chunk = fread($fileHandle, 1024 * 100); //read 100kb at a time
				$count += preg_match_all(
					'#\x00\x21\xF9\x04.{4}\x00(\x2C|\x21)#s', $chunk, $matches
				);
			}
			fclose($fileHandle);
		}

		return $count > 1;
	}

	/**
	 * Determines if we can read the content of the file and returns a file pointer resource
	 *
	 * We can't use something like $node->isReadable() as it's too unreliable
	 * Some storage classes just check for the presence of the file
	 *
	 * @param File $file
	 *
	 * @return resource
	 * @throws InternalServerErrorServiceException
	 */
	private function isFileReadable($file) {
		try {
			$fileHandle = $file->fopen('rb');
			if (!$fileHandle) {
				throw new \Exception();
			}
		} catch (\Exception $exception) {
			throw new InternalServerErrorServiceException(
				'Something went wrong when trying to read' . $file->getPath()
			);
		}

		return $fileHandle;
	}

}
