<?php

/**
 * Vvveb
 *
 * Copyright (C) 2022  Ziadin Givan
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

namespace Vvveb\System\Core;

use function Vvveb\__;
use function Vvveb\addTranslationDomain;
use function Vvveb\config;
use function Vvveb\isEditor;
use Vvveb\System\Component\Component;
use Vvveb\System\Event;
use Vvveb\System\Sites;

include DIR_SYSTEM . 'vtpl' . DS . 'vtpl.php';

#[\AllowDynamicProperties]
class View {
	/** @var self|null */
	static private ?self $instance = null;

	/** @var string|null */
	private ?string $templatePath = null;

	/** @var string|null */
	private ?string $theme = null;

	/** @var string|null */
	private ?string $htmlPath = null;

	/** @var string|false|null */
	private string|false|null $template = null;

	/** @var string|null */
	private ?string $tplFile = null;

	/** @var object|null Template engine instance (Vtpl) */
	private ?object $templateEngine = null;

	/** @var string|null */
	private ?string $compiledTemplate = null;

	/** @var string|null */
	private ?string $serviceTemplate = null;

	/** @var string|null */
	private ?string $component = null;

	/** @var string|null */
	private string|int|null $componentCount = null;

	/** @var string|null */
	private ?string $componentContent = null;

	/** @var string|null */
	private ?string $html = null;

	/** @var bool|null */
	private ?bool $useComponent = null;

	private bool $isEditor = false;

	private string $_type = 'html';

	/**
	 * Get the template engine instance, creating it if needed.
	 *
	 * @return object|null Template engine instance (Vtpl)
	 */
	public function getTemplateEngineInstance(): ?object {
		if (! $this->templateEngine) {
			self :: getInstance();
		}

		return $this->templateEngine;
	}

	/**
	 * @param string|null $app
	 */
	public function __construct(?string $app = null) {
		//$this->theme        = \Vvveb\config($this->app . '.theme', 'default');
		$this->app    = $app ?? APP;
		$relativePath = PUBLIC_PATH . 'themes/';

		if ($this->app === 'app') {
			$this->theme = Sites::getTheme(SITE_ID) ?? 'default';
			$themePath   = DIR_ROOT . join(DS, ['public', 'themes']) . DS . $this->theme . DS;
		} else {
			$this->theme  = config($this->app . '.theme', 'default');
			$relativePath = PUBLIC_PATH . $this->app . '/';
			$themePath    = DIR_ROOT . DS . 'public' . DS . $this->app . DS . $this->theme . DS;
		}

		$this->isEditor = isEditor();

		/*
		$domain = $this->theme . '-domain';
		addTranslationDomain($domain);
		 */

		$this->htmlPath     = $themePath;
		$this->templatePath = $themePath; //\Vvveb\config($this->app . '.theme', 'default') . DS;

		if (isset($_REQUEST['_component_ajax']) && $this->isEditor) {
			//check if user key is correct to allow override for editor
			if (($admin = \Vvveb\System\User\Admin :: current()) && ($userKey = $_POST['user_key'] ?? '') && ($adminId = $_POST['admin_id'] ?? '')) {
				$correctKey   = $adminId . '-' . SITE_ID;
				$key          = \Vvveb\getConfig('app.key');
				$decriptedKey = \Vvveb\decrypt($key, $userKey);

				if (($admin['admin_id'] != $adminId)|| $decriptedKey != $correctKey) {
					die('Invalid user key!');
				}

				$this->component        = \Vvveb\filter('/[a-z\-]+/', $_REQUEST['_component_ajax'], 80);
				$this->componentCount   = \Vvveb\filter('/\d+/', $_REQUEST['_component_id'],  4);

				$this->componentContent = $_POST['_component_content'] ?? '';
				$this->html             = $_POST['html'] ?? '';
			} else {
				die('Invalid request!');
			}
		}

		$selector = $count = null;

		if (isset($this->component)) {
			$selector = '[data-v-component-' . $this->component . ']';
		}

		$templateEngine = 'Vtpl';

		$template = new $templateEngine($selector, $this->componentCount, $this->componentContent);

		$template->addTemplatePath(DIR_ROOT . APP . DS . 'template' . DS);
		$template->setHtmlPath($this->htmlPath);

		if ($this->isEditor) {
			$template->removeVattrs(false);
			$template->setMinifyHtml(false);
		} else {
			$template->removeVattrs(false);
			//$template->setMinifyHtml(true);
			$template->setRelativePath($relativePath . $this->theme . '/');
		}

		$this->templateEngine = $template;
	}

	/**
	 * Get singleton instance of View.
	 */
	static public function getInstance(): self {
		if (self :: $instance === null) {
			return self :: $instance = new self(); //create class instance
		}

		return self :: $instance;
	}

	/**
	 * Remove v-attributes from HTML output.
	 *
	 * @param bool $flag
	 * @return mixed
	 */
	public function removeVattrs(bool $flag = true): mixed {
		return $this->templateEngine->removeVattrs($flag);
	}

	/**
	 * Set the active theme.
	 *
	 * @param string|false $theme Theme name or false to use default
	 * @return void
	 */
	public function setTheme(string|false $theme = false): void {
		$relativePath = PUBLIC_PATH . 'themes/';

		if (! $theme) {
		if ($this->app === 'app') {
				$this->theme = Sites::getTheme(SITE_ID) ?? 'default';
				$themePath   = DIR_ROOT . join(DS, ['public', 'themes']) . DS . $this->theme . DS;
			} else {
				$this->theme  = config($this->app . '.theme', 'default');
				$relativePath = PUBLIC_PATH . $this->app . '/';
				$themePath    = DIR_ROOT . DS . 'public' . DS . $this->app . DS . $this->theme . DS;
			}
		} else {
			$theme       = \Vvveb\filter('/[a-z0-9_-]*/', $theme, 30);
			$this->theme = $theme;
			$themePath   = DIR_ROOT . join(DS, ['public', 'themes']) . DS . $this->theme . DS;
		}

		$this->htmlPath       = $themePath;
		$this->templatePath   = $themePath;
		$this->templateEngine->setHtmlPath($this->htmlPath);

		if (! $this->isEditor) {
			$this->templateEngine->setRelativePath($relativePath . $this->theme . '/');
		}
	}

	/**
	 * Get the current theme name.
	 */
	public function getTheme(): string {
		return $this->theme;
	}

	/**
	 * Get the document type from the template engine.
	 */
	public function getDocumentType(): string {
		return $this->templateEngine->getDocumentType();
	}

	/**
	 * Get the template path.
	 */
	public function getTemplatePath(): string {
		return $this->templatePath;
	}

	/**
	 * Get the compiled template path.
	 */
	public function compiledTemplate(): ?string {
		return $this->compiledTemplate;
	}

	/**
	 * Set dynamic properties from an associative array.
	 *
	 * @param array<string, mixed> $data
	 * @return void
	 */
	public function set(array $data): void {
		if ($data && is_array($data)) {
			foreach ($data as $key => &$value) {
				$this->$key = $value;
			}
		}
	}

	/**
	 * Get the service template path.
	 */
	public function serviceTemplate(): ?string {
		return $this->serviceTemplate;
	}

	/**
	 * Check if the template needs recompilation and recompile if necessary.
	 *
	 * @param bool $service
	 * @return void
	 */
	public function checkNeedRecompile(?object $service = null): void {
		if (! $this->template) {
			return;
		}

		$this->template    = str_replace(['/', '\\'], DS, $this->template);
		$templatePath      = $this->templatePath;
		$template          = $this->template;
		$templateMtime     = null;
		$templateFile      = $templatePath . $template;
		$html              = DIR_ROOT . APP . DS . 'template' . DS . $this->tplFile;

		//absolute path
		if ($this->template[0] === DS) {
			$templatePath = dirname($this->template) . DS;
			$template     = basename($this->template);
			$templateFile = $this->template;
		} else {
			if (str_starts_with($this->template, 'plugins' . DS)) {
				$templatePath = DIR_PUBLIC . 'plugins' . DS;
				//$templatePath = $this->templatePath = DIR_PUBLIC . 'plugins' . DS;

				$template   = str_replace('plugins' . DS, '', $template);
				$p          = strpos($template, DS);
				$pluginName = substr($template, 0, $p);
				$nameSpace  = substr($template, $p + 1);

				if ($this->app === 'admin') {
					$tpl      = $pluginName . DS . join(DS, [$this->app, 'template']) . DS . $nameSpace;
					$template = $pluginName . DS . $this->app . DS . $nameSpace;
				} else {
					$tpl      = $pluginName . DS . join(DS, [$this->app, 'template']) . DS . $nameSpace;
					$template = $pluginName . DS . $nameSpace;
				}

				$this->tplFile      = str_replace(['.html', '.xml', '.json'], '.tpl', $tpl);
				$html               = DIR_PLUGINS . $this->tplFile;
				$templateFile       = $templatePath . $template;
			}
		}

		if (! file_exists($templatePath . $template)) {
			if ($template === 'error404.html' || $template === 'error500.html') {
				//if theme is missing error page then use the default
				$templateFile = DIR_PUBLIC . $template;
			} else {
				FrontController::notFound(true, [
					'message' => __('Html template not found!'),
					'file'    => $templatePath . $template,
				]);
			}
		}

		//$this->tplFile = $templateFile;
		$this->templatePath = $templatePath;

		if (file_exists($templateFile)) {
			$templateMtime = filemtime($templateFile);
		}

		$htmlMtime = 0;

		if (file_exists($html)) {
			$htmlMtime = filemtime($html);
		}

		$compiledMtime = 0;

		if (file_exists($this->compiledTemplate)) {
			$compiledMtime = @filemtime($this->compiledTemplate);
		}

		if ((max($templateMtime, $htmlMtime) > $compiledMtime
			 || ! file_exists($this->compiledTemplate))
			/*|| (defined('DEBUG') && DEBUG) */ || isset($_POST['_component_content'])) {
			$this->compile($templateFile, $this->compiledTemplate, $service);
		}
	}

	/**
	 * Compile the template file.
	 *
	 * @param string $filename
	 * @param string $file
	 * @param bool $service
	 * @return void
	 */
	private function compile(string $filename, string $file, ?object $service = null): void {
		//@touch($file); //if recompiling takes longer avoid avoid other recompile requests
		//regenerate component file
		if ($this->useComponent && ! defined('CLI')) {
			//regenerate components cache
			if (! $service) {
				if ($this->html) {
					$html = $this->html;
				} else {
					$html = $this->componentContent;
				}

				$service = Component::getInstance($this, true, $html);
			}
		}

		if (is_dir($this->htmlPath . 'template')) {
			$this->templateEngine->addTemplatePath($this->htmlPath . 'template' . DS);
		}

		list($this->template, $filename, $this->tplFile) =
		Event :: trigger(__CLASS__,__FUNCTION__, $this->template, $filename, $this->tplFile, $this->templateEngine, $this);

		if ($this->html) {
			$errors = $this->templateEngine->loadHtml($this->html);
		} else {
			$errors = $this->templateEngine->loadHtmlTemplate($filename);
		}

		//if no template defined use the default
		if ($this->tplFile[0] === DS) {
			$this->tplFile = DIR_ROOT . APP . DS . 'template' . DS . 'common.tpl';
			$this->templateEngine->loadTemplateFile($this->tplFile);
		} else {
			if (str_starts_with($this->template, 'plugins' . DS)) {
				//$template   = str_replace('plugins' . DS, '', $this->template);
				$template   = $this->tplFile;
				$p          = strpos($template, DS);
				$pluginName = substr($template, 0, $p);
				$nameSpace  = substr($template, $p + 1);

				if (file_exists(DIR_PLUGINS . $this->tplFile)) {
					$this->tplFile = DIR_PLUGINS . $this->tplFile;
				} else {
					$this->tplFile = DIR_ROOT . APP . DS . 'template' . DS . 'common.tpl';
				}

				//$this->tplFile = $pluginName . DS . $this->app . DS . 'template' . DS . $nameSpace;
				$this->templateEngine->loadTemplateFile($this->tplFile);
				/*
				if ($this->app == 'admin') {
					$this->tplFile = $pluginName . DS . $this->app ."/template/$nameSpace";
				} else {
					$this->tplFile = "$pluginName/admin/template/$nameSpace";
				}*/
			} else {
				if (! file_exists(DIR_ROOT . APP . DS . 'template' . DS . $this->tplFile)) {
					$this->tplFile = 'common.tpl';
				}
				$this->templateEngine->loadTemplateFileFromPath($this->tplFile);
			}
		}

		list($this->template, $filename, $this->tplFile) =
		Event :: trigger(__CLASS__,__FUNCTION__ . ':after', $this->template, $filename, $this->tplFile, $this->templateEngine, $this);
		$this->templateEngine->saveCompiledTemplate($file);
	}

	/**
	 * Set the template file name.
	 *
	 * @param string $filename
	 * @return void
	 */
	public function tplFile(string $filename): void {
		$this->tplFile = $filename;
	}

	/**
	 * Get or set the template filename.
	 *
	 * @param string|false|null $filename Template filename, false to clear, or null to get current
	 * @return string|false|null Current template filename
	 */
	public function template(string|false|null $filename = null): string|false|null {
		if ($filename === false) {
			$this->template = false;

			return null;
		}

		if ($filename) {
			$filename = str_replace('..', '', $filename);

			$compiledFilename = DIR_COMPILED_TEMPLATES
			. $this->app . '_' . (defined('SITE_ID') ? SITE_ID : '-') . '_'
			. ((is_null($this->component)) ? '' : $this->component . $this->componentCount . '_')
			. $this->theme . '_'
			. str_replace([DS, '/', '\\'] , '_', $filename)
			. ($this->isEditor ? '-edit' : '');

			list($filename, $compiledFilename) = Event :: trigger(__CLASS__,__FUNCTION__, $filename, $compiledFilename, $this);

			$this->compiledTemplate        = $compiledFilename;
			$this->serviceTemplate         = $compiledFilename;
			$this->tplFile                 = str_replace(['.html', '.xml', '.json'], '.tpl', $filename);
			$this->template                = $filename;
		}

		return $this->template;
	}

	/**
	 * Set the component selector and index for fragment rendering.
	 *
	 * @param string $selector CSS selector
	 * @param int $index Component index
	 * @return void
	 */
	public function fragment(string $selector, string|int $index = 0): void {
		$this->component      = $selector;
		$this->componentCount = $index;
	}

	/**
	 * Set the output type (html, json, text).
	 *
	 * @param string $type
	 * @return void
	 */
	public function setType(string $type): void {
		$this->_type = $type;
	}

	/**
	 * Get the current output type.
	 */
	public function getType(): string {
		return $this->_type;
	}

	/**
	 * Render the template.
	 *
	 * @param bool $useComponent Whether to use component rendering
	 * @param bool $output Whether to echo output or return it
	 * @param bool $service Whether this is a service template render
	 * @return mixed|string|null Rendered output string or void if outputting directly
	 */
	public function render(bool $useComponent = true, bool $output = true, bool $service = false): mixed {
		Event :: trigger(__CLASS__,__FUNCTION__, $this->template, $this->tplFile, $this->templateEngine, $this);

		$this->useComponent = $useComponent;

		if ($useComponent && ! defined('CLI')) {
			if ($service) {
				$service = new Component($this, $this->componentContent ? true : false, $this->componentContent);
			} else {
				$service = Component::getInstance($this, $this->componentContent ? true : false, $this->componentContent);
			}
		}

		/*
		if (isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest' && ! isset($_REQUEST['_component_ajax'])) {
			$this->_type = 'json';
		}*/

		if ($this->_type === 'text') {
			if (isset($this->text)) {
				if ($output) {
					echo $this->text;
				} else {
					return $this->text;
				}
			}

			return null;
		}

		if ($this->_type === 'json') {
			$jsonFlags = 0;

			//if (defined('CLI')) {
			$jsonFlags = JSON_PRETTY_PRINT;
			//}
			ob_start();

			if (isset($this->json)) {
				$json = json_encode($this->json, $jsonFlags);
			} else {
				$json = json_encode($this, $jsonFlags);
			}

			if ($output) {
				echo $json;
			} else {
				return $json;
			}
		} else { //html
			if (! $this->template) {
				self::template();
			}
			$this->checkNeedRecompile($service);

			if ($this->compiledTemplate) {
				if (! file_exists($this->compiledTemplate)) {
					return FrontController::notFound(true, [
						'message' => __('Compiled template not found!'),
						'file'    => $this->template,
					]);
				}

				try {
					if ($output) {
						include $this->compiledTemplate;
						return '';
					} else {
						ob_start();

						include $this->compiledTemplate;
						$return = ob_get_contents();
						ob_end_clean();

						return $return;
					}
				} catch (\ParseError | \Error $e) {
					static $handled;

					if ($handled) {
						if (DEBUG) {
							echo $e->getMessage() . ' in ' . $e->getFile() . ' on line ' . $e->getLine();
						} else {
							die('Server error, enable debug for more info');
						}
						return null;
					}
					$handled = true;

					$data = \Vvveb\System\Core\exceptionToArray($e, $this->compiledTemplate);
					FrontController :: notFound(false, $data, 500);

					if (DEBUG) {
						echo $e->getMessage() . ' in ' . $e->getFile() . ' on line ' . $e->getLine();
					}
				}
			}
		}
		
		return '';
	}
}
