<?php

namespace XF\Http;

use Symfony\Component\DomCrawler\Crawler;
use XF\App;
use XF\Util\File;
use XF\Util\Str;
use XF\Validator\Url;

use function count, is_array, strlen;

class Metadata
{
	/**
	 * @var App
	 */
	protected $app;

	/**
	 * @var string
	 */
	protected $body;

	/**
	 * @var string[][]
	 */
	protected $headers;

	/**
	 * @var string|null
	 */
	protected $requestedFrom;

	public function __construct(App $app, $body, array $headers, $requestedFrom = null)
	{
		$this->app = $app;
		$this->headers = $headers;
		$this->body = $body;
		$this->body = $this->convertBodyCharset();
		$this->requestedFrom = $requestedFrom;
	}

	public function getBody()
	{
		return $this->body;
	}

	public function getHeaders()
	{
		return $this->headers;
	}

	public function getContentType()
	{
		return $this->headers['content-type'] ?? [];
	}

	public function getCharset()
	{
		$contentType = $this->getContentType();
		$charset = $this->app->http()->reader()->getCharset($contentType);

		if (function_exists('mb_check_encoding'))
		{
			try
			{
				$isValid = @mb_check_encoding('', $charset);
			}
			catch (\Error $e)
			{
				$isValid = false;
			}
		}
		else
		{
			$isValid = true;
		}

		if (!$charset || !$isValid)
		{
			$charset = $this->getXPathMetaByAttr('@charset', 'charset');

			if (!$charset)
			{
				$contentTypeTag = $this->getXPathMetaByAttr('@http-equiv="Content-Type"');

				if (!$contentTypeTag)
				{
					$contentTypeTag = $this->getXPathMetaByAttr('@http-equiv="content-type"');
				}

				if ($contentTypeTag)
				{
					preg_match('/charset=([^;"\\s]+|"[^;"]+")/i', $contentTypeTag, $contentTypeMatch);

					if (isset($contentTypeMatch[1]))
					{
						$charset = trim($contentTypeMatch[1], " \t\n\r\0\x0B\"");
					}
				}
			}

			if (!$charset)
			{
				if (substr($this->body, 0, 3) == "\xEF\xBB\xBF")
				{
					$charset = 'utf-8';
				}
				else
				{
					$charset = 'windows-1252';
				}
			}
		}

		return $charset;
	}

	public function getTitle()
	{
		$title = $this->getXPathMetaByAttr(['@property="og:title"', '@property="twitter:title"']);

		if (!$title)
		{
			$title = $this->getXPathMetaByAttr(['@name="title"']);
		}

		if (!$title && preg_match('#<title[^>]*>(.*)</title>#siU', $this->body, $match))
		{
			$title = $match[1];
		}

		return $this->cleanMetadataString($title);
	}

	public function getDescription()
	{
		$description = $this->getXPathMetaByAttr(['@property="og:description"', '@property="twitter:description"']);

		if (!$description)
		{
			$description = $this->getXPathMetaByAttr(['@name="description"']);
		}

		return $this->cleanMetadataString($description);
	}

	public function getImage()
	{
		$image = $this->getXPathMetaByAttr(['@property="og:image"', '@property="twitter:image"']);
		return $this->cleanMetadataString($image, true);
	}

	public function getFavicon()
	{
		$crawler = new Crawler($this->body);

		$xPath = '//*/link[@rel=\'icon\']';

		$content = $crawler
			->filterXPath($xPath)
			->extract(['href']);

		if (!count($content))
		{
			$xPath = '//*/link[contains(@rel, \'icon\') or contains(@rel, \'shortcut\')]';

			$content = $crawler
				->filterXPath($xPath)
				->extract(['href']);

			if (!count($content))
			{
				return '';
			}
		}

		$favicons = [];

		/** @var Url $urlValidator */
		$urlValidator = $this->app->validator(Url::class);

		foreach ($content AS $value)
		{
			// if we don't have something which looks like an absolute URL attempt to convert it to one
			if (parse_url($value, PHP_URL_SCHEME) == '')
			{
				$value = \XF::convertToAbsoluteUrl($value, $this->requestedFrom);
			}

			$value = $urlValidator->coerceValue($value);

			if (!$urlValidator->isValid($value))
			{
				continue;
			}

			$favicons[] = $value;
		}

		$proxyableFavicons = array_filter($favicons, function ($favicon)
		{
			$path = parse_url($favicon, PHP_URL_PATH);
			$extension = File::getFileExtension($path);
			return File::isImageInlineDisplaySafe($extension);
		});

		$favicon = count($proxyableFavicons) ? reset($proxyableFavicons) : reset($favicons);
		return $this->cleanMetadataString($favicon, true);
	}

	public function convertBodyCharset()
	{
		if (!$this->body)
		{
			return '';
		}

		$charset = $this->getCharset();

		$body = $this->body;
		$newBody = false;
		if (function_exists('iconv'))
		{
			$newBody = @iconv($charset, 'utf-8//IGNORE', $body);
		}
		if (!$newBody && function_exists('mb_convert_encoding'))
		{
			try
			{
				$newBody = @mb_convert_encoding($body, 'utf-8', $charset);
			}
			catch (\Error $e)
			{
			}
		}
		$body = ($newBody ?: preg_replace('/[\x80-\xff]/', '', $body));

		$body = preg_replace_callback(
			'#<meta[^>]+>#siU',
			function ($match)
			{
				return preg_replace_callback(
					'/charset=([^;"\\s]+|"[^;"]+")/i',
					function ($charsetMatch)
					{
						if (strpos($charsetMatch[0], '"') !== false)
						{
							return 'charset="utf-8"';
						}
						else
						{
							return 'charset=utf-8';
						}
					},
					$match[0]
				);
			},
			$body
		);

		return $body;
	}

	public function cleanMetadataString($string, $isUrl = false)
	{
		if (!$string)
		{
			return '';
		}

		$string = \XF::cleanString($string);
		$string = Str::from_html($string, true);
		$string = html_entity_decode($string, ENT_QUOTES | ENT_HTML5, 'UTF-8');
		$string = Str::from_html($string);
		$string = str_replace("\n", ' ', trim($string));
		$string = \XF::cleanString($string);

		if ($isUrl)
		{
			/** @var Url $validator */
			$validator = $this->app->validator(Url::class);

			$string = $validator->coerceValue($string);

			if (!$validator->isValid($string))
			{
				$string = '';
			}
		}

		return $string;
	}

	protected function getXPathMetaByAttr($properties, $returnAttr = 'content')
	{
		$body = $this->body;

		if (!is_array($properties))
		{
			$properties = [$properties];
		}

		$xPath = '//*/meta[' . implode(' or ', $properties) . ']';

		$crawler = new Crawler($body);

		$content = $crawler
			->filterXPath($xPath)
			->extract([$returnAttr]);

		if (!count($content))
		{
			return '';
		}

		$return = '';

		foreach ($content AS $value)
		{
			// "one page apps" may just contain placeholders
			if (preg_match('/^{{.*}}$/', $value))
			{
				continue;
			}

			if (strlen($value))
			{
				$return = $value;
				break;
			}
		}

		return $return;
	}
}
