<?php

namespace XF\Service;

use XF\Entity\ImageProxy;
use XF\Repository\ImageProxyRepository;
use XF\Util\File;

use function defined, in_array;

class ImageProxyService extends AbstractService
{
	protected $forceRefresh = false;
	protected $maxConcurrent = 10;

	/**
	 * @var ImageProxyRepository
	 */
	protected $proxyRepo;

	protected function setup()
	{
		$this->proxyRepo = $this->repository(ImageProxyRepository::class);
	}

	public function forceRefresh($value = true)
	{
		$this->forceRefresh = (bool) $value;
	}

	public function isRefreshForced()
	{
		return $this->forceRefresh;
	}

	public function getImage($url)
	{
		$image = $this->proxyRepo->getImageByUrl($url);
		if ($image)
		{
			if ($this->isRefreshRequired($image))
			{
				$this->refetchImage($image);
			}
		}
		else
		{
			// no image, if we can fetch an image, create it
			if ($this->canFetchImage())
			{
				$image = $this->fetchNewImage($url);
			}
		}

		return $image;
	}

	protected function isRefreshRequired(ImageProxy $image)
	{
		if ($this->forceRefresh)
		{
			return true;
		}

		return $image->isRefreshRequired() && $this->canFetchImage();
	}

	public function canFetchImage()
	{
		if ($this->forceRefresh)
		{
			return true;
		}

		$active = $this->proxyRepo->getTotalActiveFetches();
		return ($active < $this->maxConcurrent);
	}

	public function fetchNewImage($url)
	{
		/** @var ImageProxy $image */
		$image = $this->em()->create(ImageProxy::class);
		$image->url = $url;
		$image->pruned = true;
		$image->is_processing = time(); // may have slept, need to set to now

		try
		{
			$image->save();
		}
		catch (\XF\Db\Exception $e)
		{
			// this is mostly a duplicate key issue
			return null;
		}

		$fetchResults = $this->fetchImageDataFromUrl($image->url);
		$this->finalizeFromFetchResults($image, $fetchResults);

		return $image;
	}

	public function refetchImage(ImageProxy $image)
	{
		$image->is_processing = time();
		$image->save();

		$fetchResults = $this->fetchImageDataFromUrl($image->url);
		$this->finalizeFromFetchResults($image, $fetchResults);

		return $image;
	}

	public function testImageFetch($url)
	{
		$results = $this->fetchImageDataFromUrl($url);
		if ($results['dataFile'])
		{
			@unlink($results['dataFile']);
			$results['dataFile'] = null;
		}

		return $results;
	}

	protected function fetchImageDataFromUrl($url)
	{
		$url = $this->proxyRepo->cleanUrlForFetch($url);
		if (!preg_match('#^https?://#i', $url))
		{
			throw new \InvalidArgumentException("URL must be http or https");
		}

		$urlParts = @parse_url($url);

		$validImage = false;
		$fileName = !empty($urlParts['path']) ? basename($urlParts['path']) : null;
		$mimeType = null;
		$error = null;
		$streamFile = File::getTempDir() . '/' . strtr(md5($url) . '-' . uniqid(), '/\\.', '---') . '.temp';
		$imageProxyMaxSize = $this->app->options()->imageProxyMaxSize * 1024;

		try
		{
			$options = [
				'headers' => [
					'Accept' => 'image/*,*/*;q=0.8',
				],
			];
			$limits = [
				'time' => 8,
				'bytes' => $imageProxyMaxSize ?: -1,
			];
			$response = $this->app->http()->reader()->getUntrusted($url, $limits, $streamFile, $options, $error);
		}
		catch (\Exception $e)
		{
			$response = null;
			$error = $e->getMessage();
		}

		if ($response)
		{
			$response->getBody()->close();

			if ($response->getStatusCode() == 200)
			{
				$disposition = $response->getHeader('Content-Disposition');
				if (!empty($disposition) && preg_match('/filename=(\'|"|)(.+)\\1/siU', $disposition[0], $match))
				{
					$fileName = $match[2];
				}
				if (!$fileName)
				{
					$fileName = 'image';
				}

				$imageInfo = filesize($streamFile) ? @getimagesize($streamFile) : false;
				if ($imageInfo)
				{
					$imageType = $imageInfo[2];

					$extension = File::getFileExtension($fileName);
					$extensionMap = [
						IMAGETYPE_GIF => ['gif'],
						IMAGETYPE_JPEG => ['jpg', 'jpeg', 'jpe'],
						IMAGETYPE_PNG => ['png'],
						IMAGETYPE_ICO => ['ico'],
						IMAGETYPE_WEBP => ['webp'],
					];

					if (defined('IMAGETYPE_WEBP'))
					{
						$extensionMap[IMAGETYPE_WEBP] = ['webp'];
					}

					if (isset($extensionMap[$imageType]))
					{
						$mimeType = $imageInfo['mime'];

						$validExtensions = $extensionMap[$imageType];
						if (!in_array($extension, $validExtensions))
						{
							$extensionStart = strrpos($fileName, '.');
							$fileName = ($extensionStart ? substr($fileName, 0, $extensionStart) : $fileName)
								. '.'
								. $validExtensions[0];
						}

						$validImage = true;
					}
					else
					{
						$error = \XF::phraseDeferred('image_is_invalid_type');
					}
				}
				else
				{
					$error = \XF::phraseDeferred('file_not_an_image');
				}
			}
			else
			{
				$error = \XF::phraseDeferred('received_unexpected_response_code_x_message_y', [
					'code' => $response->getStatusCode(),
					'message' => $response->getReasonPhrase(),
				]);
			}
		}

		if (!$validImage)
		{
			@unlink($streamFile);
		}

		return [
			'valid' => $validImage,
			'error' => $error,
			'dataFile' => $validImage ? $streamFile : null,
			'fileName' => $fileName,
			'mimeType' => $mimeType,
		];
	}

	protected function finalizeFromFetchResults(ImageProxy $image, array $fetchResults)
	{
		$image->is_processing = 0;

		if ($fetchResults['valid'])
		{
			$fileHash = md5_file($fetchResults['dataFile']);
			if (!$image->pruned && $image->file_hash === $fileHash)
			{
				$saved = true;
			}
			else
			{
				$newImagePath = $image->getAbstractedImagePath();
				$saved = File::copyFileToAbstractedPath(
					$fetchResults['dataFile'],
					$newImagePath
				);
			}

			$imageManager = $this->app->imageManager();
			$tempImage = $imageManager->imageFromFile($fetchResults['dataFile']);

			if ($saved)
			{
				$image->fetch_date = time();
				$image->file_name = $fetchResults['fileName'];
				$image->file_size = filesize($fetchResults['dataFile']);
				$image->file_hash = $fileHash;
				$image->mime_type = $fetchResults['mimeType'];
				$image->pruned = false;
				$image->failed_date = 0;
				$image->fail_count = 0;
				$image->file_metadata = [
					'width' => $tempImage ? $tempImage->getWidth() : 0,
					'height' => $tempImage ? $tempImage->getHeight() : 0,
				];
			}
			else
			{
				$image->pruned = true;
				$image->file_hash = '';
			}

			@unlink($fetchResults['dataFile']);
		}
		else
		{
			$image->failed_date = time();
			$image->fail_count++;
		}

		$image->save();
	}
}
