<?php

namespace XF;

use XF\DesignerOutput\AbstractHandler;
use XF\Entity\Style;
use XF\Mvc\Entity\Entity;
use XF\Util\File;
use XF\Util\Json;

class DesignerOutput
{
	protected $enabled;
	protected $basePath;

	protected $assetsFilename = 'assets.json';

	protected $metadataFilename = '_metadata.json';
	protected $metadataCache = [];

	protected $batchMode = false;
	protected $batchesPending = [];

	protected $handlerCache = [];

	public function __construct($enabled, $basePath)
	{
		$this->enabled = $enabled;
		$this->basePath = File::canonicalizePath($basePath);
	}

	public function hasNameChange(Entity $entity)
	{
		$handler = $this->getHandler($entity->structure()->shortName);
		return $handler->hasNameChange($entity);
	}

	public function export(Entity $entity)
	{
		$handler = $this->getHandler($entity->structure()->shortName);
		return $handler->export($entity);
	}

	public function import($shortName, $name, $styleId, $contents, array $metadata, array $options = [])
	{
		$handler = $this->getHandler($shortName);
		return $handler->import($name, $styleId, $contents, $metadata, $options);
	}

	public function delete(Entity $entity, $new = true)
	{
		$handler = $this->getHandler($entity->structure()->shortName);
		return $handler->delete($entity, $new);
	}

	/**
	 * @template T of \XF\DesignerOutput\AbstractHandler
	 *
	 * @param class-string<T> $shortName
	 *
	 * @return T
	 */
	public function getHandler($shortName)
	{
		$shortName = \XF::classToString($shortName, '%s\DesignerOutput\%s');
		if (isset($this->handlerCache[$shortName]))
		{
			return $this->handlerCache[$shortName];
		}

		$class = \XF::stringToClass($shortName, '%s\DesignerOutput\%s');
		if (!class_exists($class))
		{
			throw new \InvalidArgumentException("No handler for $shortName found ($class expected)");
		}

		$class = \XF::extendClass($class);

		$handler = new $class($this, $shortName);
		if (!($handler instanceof AbstractHandler))
		{
			throw new \InvalidArgumentException("Cannot instantiate development output type handler for class '$class'");
		}
		$this->handlerCache[$shortName] = $handler;

		return $handler;
	}

	public function enableBatchMode()
	{
		$this->batchMode = true;
	}

	public function clearBatchMode()
	{
		$this->flushBatches();
		$this->batchMode = false;
	}

	public function flushBatches()
	{
		foreach ($this->batchesPending AS $typeDir => $designerModeIds)
		{
			foreach ($designerModeIds AS $designerModeId => $null)
			{
				$typeMetadata = $this->metadataCache[$typeDir][$designerModeId];

				$metadataPath = $this->getMetadataFileName($typeDir, $designerModeId);
				file_put_contents($metadataPath, Json::jsonEncodePretty($typeMetadata));
			}
		}

		$this->batchesPending = [];
	}

	protected function isValidDir(\DirectoryIterator $entry)
	{
		/** @var \DirectoryIterator $entry */
		if (!$entry->isDir()
			|| $entry->isDot()
			|| !preg_match('/^[a-z0-9_]+$/i', $entry->getBasename())
		)
		{
			return false;
		}

		return true;
	}

	public function getAvailableDesignerModeIds()
	{
		if (!file_exists($this->basePath))
		{
			return [];
		}

		$designerModeIds = [];
		foreach (new \DirectoryIterator($this->basePath) AS $entry)
		{
			if (!$this->isValidDir($entry))
			{
				continue;
			}

			$designerModeIds[] = $entry->getBasename();
		}

		return $designerModeIds;
	}

	public function getMetadata($typeDir, $designerMode)
	{
		if (isset($this->metadataCache[$typeDir][$designerMode]))
		{
			return $this->metadataCache[$typeDir][$designerMode];
		}

		$metadataPath = $this->getMetadataFileName($typeDir, $designerMode);
		if (file_exists($metadataPath))
		{
			$metadata = json_decode(file_get_contents($metadataPath), true);
		}
		else
		{
			$metadata = [];
		}

		$this->metadataCache[$typeDir][$designerMode] = $metadata;

		return $metadata;
	}

	protected function getMetadataFileName($typeDir, $designerMode)
	{
		$ds = \XF::$DS;
		return "{$this->basePath}{$ds}{$designerMode}{$ds}{$typeDir}{$ds}{$this->metadataFilename}";
	}

	protected function writeTypeMetadata($typeDir, $designerMode, array $typeMetadata)
	{
		ksort($typeMetadata);

		$this->metadataCache[$typeDir][$designerMode] = $typeMetadata;

		$metadataPath = $this->getMetadataFileName($typeDir, $designerMode);
		file_put_contents($metadataPath, Json::jsonEncodePretty($typeMetadata));

		if ($this->batchMode)
		{
			$this->batchesPending[$typeDir][$designerMode] = true;
		}
		else
		{
			$metadataPath = $this->getMetadataFileName($typeDir, $designerMode);
			file_put_contents($metadataPath, Json::jsonEncodePretty($typeMetadata));
		}
	}

	protected function updateMetadata($typeDir, $designerMode, $fileName, array $metadata)
	{
		$typeMetadata = $this->getMetadata($typeDir, $designerMode);

		$existing = $typeMetadata[$fileName] ?? null;
		if ($existing == $metadata)
		{
			return;
		}

		$typeMetadata[$fileName] = $metadata;
		$this->writeTypeMetadata($typeDir, $designerMode, $typeMetadata);
	}

	public function getTypes()
	{
		$types = [];
		$ds = \XF::$DS;
		$dir = $this->basePath;

		$designerModeIds = $this->getAvailableDesignerModeIds();
		foreach ($designerModeIds AS $designerModeId)
		{
			$designerModeDir = "{$dir}{$ds}{$designerModeId}";
			if (!file_exists($designerModeDir))
			{
				continue;
			}
			foreach (new \DirectoryIterator($designerModeDir) AS $designerModeEntry)
			{
				if (!$this->isValidDir($designerModeEntry))
				{
					continue;
				}

				$types[] = $designerModeEntry->getBasename();
			}
		}

		$types = array_unique($types);
		sort($types);
		return $types;
	}

	public function getAvailableTypeFiles($typeDir, $designerMode)
	{
		$files = [];

		$ds = \XF::$DS;
		$dir = $this->basePath;

		$designerModeTypeDir = "{$dir}{$ds}{$designerMode}{$ds}{$typeDir}";
		if (file_exists($designerModeTypeDir))
		{
			foreach (new \DirectoryIterator($designerModeTypeDir) AS $file)
			{
				/** @var \DirectoryIterator $file */
				$fileName = $file->getBasename();
				if (!$file->isDot() && $file->isFile() && $fileName != $this->metadataFilename && substr($fileName, 0, 1) != '.')
				{
					$files[$fileName] = $file->getPathname();
				}
				else if (!$file->isDot() && $file->isDir() && substr($fileName, 0, 1) != '.')
				{
					foreach (new \DirectoryIterator($file->getPathname()) AS $childFile)
					{
						if (!$childFile->isDot() && $childFile->isFile())
						{
							$files[$fileName . '/' . $childFile->getBasename()] = $childFile->getPathname();
						}
					}
				}
			}
		}

		return $files;
	}

	public function rebuildTypeMetadata($type, $designerMode)
	{
		$changes = [];

		$typeFiles = $this->getAvailableTypeFiles($type, $designerMode);

		$metadata = $this->getMetadata($type, $designerMode);
		$changed = false;

		foreach ($typeFiles AS $fileName => $path)
		{
			if (isset($metadata[$fileName]))
			{
				$data = $metadata[$fileName];
				$hash = $this->hashContents(file_get_contents($path));
				if (!isset($data['hash']) || $hash != $data['hash'])
				{
					$metadata[$fileName]['hash'] = $hash;
					$changes[$designerMode][] = $fileName;
					$changed = true;
				}
			}
		}

		if ($changed)
		{
			$this->writeTypeMetadata($type, $designerMode, $metadata);
		}

		return $changes;
	}

	public function writeFile($typeDir, Style $style, $fileName, $fileContents, array $metadata = [], $verifyChange = true)
	{
		if (!$this->isDesignerModeEnabled($style))
		{
			return false;
		}

		$fullPath = $this->getFilePath($typeDir, $style->designer_mode, $fileName);

		if ($verifyChange)
		{
			if (!file_exists($fullPath))
			{
				$write = true;
			}
			else
			{
				$write = file_get_contents($fullPath) != $fileContents;
			}
		}
		else
		{
			$write = true;
		}

		if ($write)
		{
			File::writeFile($fullPath, $fileContents, false);
		}

		$metadata['hash'] = $this->hashContents($fileContents);
		$this->updateMetadata($typeDir, $style->designer_mode, $fileName, $metadata);

		return true;
	}

	public function removeMetadata($typeDir, $designerMode, $fileName)
	{
		$typeMetadata = $this->getMetadata($typeDir, $designerMode);
		if (isset($typeMetadata[$fileName]))
		{
			unset($typeMetadata[$fileName]);

			$this->writeTypeMetadata($typeDir, $designerMode, $typeMetadata);
		}
	}

	public function getFilePath($typeDir, $designerMode, $fileName)
	{
		$ds = \XF::$DS;
		return "{$this->basePath}{$ds}{$designerMode}{$ds}{$typeDir}{$ds}{$fileName}";
	}

	public function getDesignerModePath($id)
	{
		$ds = \XF::$DS;
		return "{$this->basePath}{$ds}{$id}";
	}

	public function deleteFile($typeDir, Style $style, $fileName)
	{
		if (!$this->enabled)
		{
			return false;
		}

		$fullPath = $this->getFilePath($typeDir, $style->designer_mode, $fileName);
		if (file_exists($fullPath))
		{
			unlink($fullPath);
		}

		$this->removeMetadata($typeDir, $style->designer_mode, $fileName);

		return true;
	}

	protected function getAssetsFileName($designerMode)
	{
		$ds = \XF::$DS;
		return "{$this->basePath}{$ds}{$designerMode}{$ds}{$this->assetsFilename}";
	}

	public function getAssets($designerMode)
	{
		$assetsPath = $this->getAssetsFileName($designerMode);
		if (file_exists($assetsPath))
		{
			$assets = json_decode(file_get_contents($assetsPath), true);
		}
		else
		{
			$assets = [];
		}

		return $assets;
	}

	public function rebuildAssetsFile(Style $style)
	{
		if (!$this->isDesignerModeEnabled($style))
		{
			return false;
		}

		return $this->writeSpecialFile(
			$style->designer_mode,
			$this->assetsFilename,
			Json::jsonEncodePretty($style->assets)
		);
	}

	public function writeSpecialFile($designerMode, $fileName, $fileContents)
	{
		if (!$this->enabled)
		{
			return false;
		}

		$fullPath = $this->getSpecialFilePath($designerMode, $fileName);
		File::writeFile($fullPath, $fileContents, false);

		return true;
	}

	public function deleteSpecialFile($designerMode, $fileName)
	{
		if (!$this->enabled)
		{
			return false;
		}

		$fullPath = $this->getSpecialFilePath($designerMode, $fileName);
		if (file_exists($fullPath))
		{
			unlink($fullPath);
		}

		return true;
	}

	public function getSpecialFilePath($designerMode, $fileName)
	{
		$ds = \XF::$DS;
		return "{$this->basePath}{$ds}{$designerMode}{$ds}{$fileName}";
	}

	public function hashContents($contents)
	{
		$contents = str_replace("\r", '', $contents);
		return md5($contents);
	}

	public function isEnabled()
	{
		return $this->enabled;
	}

	protected function isDesignerModeEnabled(Style $style)
	{
		return ($style->designer_mode && $this->enabled);
	}
}
