<?php
namespace Melbahja\Seo\Sitemap;

use XMLWriter;
use Melbahja\Seo\{
	Utils\Utils,
	Exceptions\SitemapException,
	Interfaces\SitemapBuilderInterface
};

/**
 * @package Melbahja\Seo
 * @see https://git.io/phpseo
 * @license MIT
 * @copyright Mohamed Elbahja
 */
class IndexBuilder implements SitemapBuilderInterface
{
	/**
	 * Current sitemap being built
	 */
	protected array $sitemap = [];

	/**
	 * XMLWriter instance
	 */
	protected readonly XMLWriter $writer;

	/**
	 * The tmp file path in case of OutputMode is TEMP.
	 */
	protected readonly string $tempPath;

	/**
	 * Sitemap options
	 */
	protected array $defaultOptions = [
		'indent' => null,
	];

	/**
	 * Initialize sitemap index builder
	 */
	public function __construct(

		/**
		 * @param string $baseUrl The base URL for sitemap URLs.
		 */
		public readonly string $baseUrl,

		/**
		 * @param string|null $filePath The sitemap index file name.
		 */
		public readonly ?string $filePath = null,

		/**
		 * @param int Maximum sitemaps in index
		 */
		public int $maxUrls = 25000,

		/**
		 * @param array Sitemap config options
		 */
		protected array $options = [],

		/**
		 * The output mode of generated sitemaps.
		 *
		 * @param OutputMode
		 */
		protected OutputMode $mode = OutputMode::TEMP,

		/**
		 * The resource in case of STREAM mode, defaults to stdout.
		 *
		 * @param resource
		 */
		private $stream = STDOUT,
	) {

		switch ($mode)
		{
			case OutputMode::TEMP:

				if ($filePath === null) {
					throw new SitemapException("Output \$filePath can not be empty!");
				}

				$this->tempPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . md5(uniqid()) . '.xml';
				$this->writer   = new XMLWriter();
				$this->writer->openUri($this->tempPath);
				break;

			case OutputMode::FILE:

				if ($filePath === null) {
					throw new SitemapException("Output \$filePath can not be empty!");
				}

				$this->writer = new XMLWriter();
				$this->writer->openUri($filePath);
				break;

			case OutputMode::STREAM:

				if (method_exists(XMLWriter::class, 'toStream')) {

					$this->writer = XMLWriter::toStream($stream);

				} else { // php < 8.4 workaround

					$this->tempPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . md5(uniqid()) . '.xml';
					$this->writer   = new XMLWriter();
					$this->writer->openUri($this->tempPath);
				}

				break;

			default:
				$this->writer = new XMLWriter();
				$this->writer->openMemory();
				break;
		}

		$this->options = array_merge($this->defaultOptions, $options);
		if ($this->options['indent'] !== null) {
			$this->writer->setIndent(true);
			$this->writer->setIndentString($this->options['indent']);
		}

		// Start xml doc
		$this->writer->startDocument(version: '1.0', encoding: 'UTF-8');
		$this->writer->writeComment(' Generated by https://git.io/phpseo ');
		$this->writer->startElement('sitemapindex');
		$this->writer->writeAttribute('xmlns', 'http://www.sitemaps.org/schemas/sitemap/0.9');
	}

	/**
	 * addSitemap will commit last sitemap and start new one.
	 */
	public function addSitemap(string $url): self
	{
		return $this->commit()->url($url);
	}

	/**
	 * Register new sitemap URL
	 */
	public function url(string $url): self
	{
		if ($this->maxUrls <= 0) {
			throw new SitemapException("The maximum sitemaps has been exhausted");
		}

		$this->sitemap['loc'] = Utils::encodeSitemapUrl(Utils::resolveRelativeUrl($this->baseUrl, $url));
		return $this;
	}

	/**
	 * Set last modification date
	 */
	public function lastMod(string|int $date): self
	{
		$this->sitemap['lastmod'] = Utils::formatDate($date);
		return $this;
	}

	/**
	 * Commit the pending sitemap to index
	 */
	public function commit(): self
	{
		if (empty($this->sitemap)) {
			return $this;
		}

		$this->writer->startElement('sitemap');
		foreach ($this->sitemap as $name => $value)
		{
			$this->writer->writeElement($name, (string) $value);
		}
		$this->writer->endElement();

		$this->maxUrls--;
		$this->sitemap = [];

		return $this;
	}

	/**
	 * Save/Render generated sitemap xml
	 *
	 * @param  string|null $uriPath can only be passed on OutputMode::MEMORY
	 * @return bool|string bool in case of mode is not memory, and string if writing to memory.
	 */
	public function render(?string $uriPath = null): bool|string
	{
		$this->commit();
		$this->writer->endElement();
		$this->writer->endDocument();

		if ($this->mode === OutputMode::MEMORY) {

			if ($uriPath !== null) {
				return file_put_contents($uriPath, $this->writer->outputMemory()) !== false;
			}

			return $this->writer->outputMemory();
		}

		$this->writer->flush();
		if ($this->mode === OutputMode::TEMP) {
			return rename($this->tempPath, $this->filePath);
		}

		// php < 8.4 workaround
		if ($this->mode === OutputMode::STREAM && method_exists(XMLWriter::class, 'toStream') === false) {

			$tempFd = fopen($this->tempPath, 'r');
			$stcopy = stream_copy_to_stream($tempFd, $this->stream);
			fclose($tempFd);

			return (bool) $stcopy;
		}

		return true;
	}

	/**
	 * Get XML as string in case of memory mode, other modes will write to target.
	 */
	public function __toString(): string
	{
		return $this->render();
	}

	/**
	 * Cleanup
	 */
	public function __destruct()
	{
		if (isset($this->tempPath) && file_exists($this->tempPath)) {
			@unlink($this->tempPath);
		}
	}
}