<?php

namespace SGalinski\Tinymce;

/***************************************************************
 *  Copyright notice
 *
 *  (c) sgalinski Internet Services (https://www.sgalinski.de)
 *
 *  All rights reserved
 *
 *  This script is part of the TYPO3 project. The TYPO3 project is
 *  free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  The GNU General Public License can be found at
 *  http://www.gnu.org/copyleft/gpl.html.
 *
 *  This script 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 General Public License for more details.
 *
 *  This copyright notice MUST APPEAR in all copies of the script!
 ***************************************************************/

use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Core\Page\PageRenderer;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Lang\LanguageService;
use TYPO3\CMS\Core\Localization\Locales;

/**
 * tinyMCE initialisation class
 *
 * Usage: *
 * // @var Loader $tinyMceLoader
 * $tinyMceLoader = GeneralUtility::makeInstance('SGalinski\Tinymce\Loader');
 * $tinyMceLoader->loadConfiguration($configurationFile)
 * $tinyMceLoader->loadJsViaPageRenderer($GLOBALS['TSFE']->getPageRenderer());
 *
 * Basic Configuration: (= content of $configurationFile)
 *
 * tinymce.init({
 *    selector: 'textarea'
 * });
 */
class Loader {
	/**
	 * TinyMCE configuration
	 *
	 * @var array
	 */
	protected $tinymceConfiguration = [];

	/**
	 * Initialization flag
	 *
	 * @var bool
	 */
	static protected $init = FALSE;

	/**
	 * That's a map, which contains the differences in relation to the tinyMce languages.
	 *
	 * @var array
	 */
	protected $typo3LanguagesDifferencesToTinyMceLanguagesMap = [
		'bg' => 'bg_BG',
		'fr' => 'fr_FR',
		'fr_CA' => 'fr_FR',
		'kl' => 'gl',
		'he' => 'he_IL',
		'hi' => 'hi_IN',
		'hu' => 'hu_HU',
		'is' => 'is_IS',
		'km' => 'km_KH',
		'no' => 'nb_NO',
		'pt' => 'pt_PT',
		'sl' => 'sl_SI',
		'sv' => 'sv_SE',
		'th' => 'th_TH',
		'zh' => 'zh_TW',
	];

	/**
	 * @param string $configuration file reference or configuration string (defaults to basic configuration)
	 * @param boolean $forceLanguage set this to true if you want to force your language set by the configuration
	 *
	 * @return void
	 * @throws \InvalidArgumentException
	 */
	public function loadConfiguration($configuration = '', $forceLanguage = FALSE) {
		self::$init = FALSE;
		$this->tinymceConfiguration = $this->prepareTinyMCEConfiguration($configuration);
		if (!$forceLanguage) {
			$this->setLanguage();
		}
	}

	/**
	 * Calculates and sets the current language
	 *
	 * @return void
	 * @throws \InvalidArgumentException
	 */
	protected function setLanguage() {
		/** @var $languageInstance LanguageService */
		$languageInstance = (TYPO3_MODE === 'FE' ? $GLOBALS['TSFE'] : $GLOBALS['LANG']);
		$languageKey = $languageInstance->lang;

		if (TYPO3_MODE === 'BE') {
			$groupOrUserProps = BackendUtility::getModTSconfig('', 'tx_tinyMCE');
			if (trim($groupOrUserProps['properties']['prefLang']) !== '') {
				$languageKey = $groupOrUserProps['properties']['prefLang'];
			}
		}

		// language conversion from TLD to iso631
		$locales = GeneralUtility::makeInstance(Locales::class);
		Locales::initialize();
		$isoArray = (array) $locales->getIsoMapping();

		if (array_key_exists($languageKey, $isoArray)) {
			$languageKey = $isoArray[$languageKey];
		}

		if (array_key_exists($languageKey, $this->typo3LanguagesDifferencesToTinyMceLanguagesMap)) {
			$languageKey = $this->typo3LanguagesDifferencesToTinyMceLanguagesMap[$languageKey];
		}

		$languageFile = PATH_site . ExtensionManagementUtility::siteRelPath('tinymce') .
			'tinymce_node_modules/tinymce/langs/' . $languageKey . '.js';
		if (!is_file($languageFile)) {
			$languageKey = 'en';
		}

		$this->addConfigurationOption('language', $languageKey);
	}

	/**
	 * Returns a file that contains the tinyMCE configuration
	 *
	 * Note: The load dom event cannot be used, because e.g. IRRE adds the javascript
	 * later on. This leads to code that is never executed. The interval timer hack fixes this
	 * issue.
	 *
	 * @return string
	 * @throws \UnexpectedValueException
	 */
	protected function getConfiguration(): string {
		$configuration = $this->tinymceConfiguration['preJS'];
		$configuration .= '
			var SG = SG || {};
			SG.domIsReady = (function(domIsReady) {
			   var isBrowserIeOrNot = function() {
				  return (!document.attachEvent || typeof document.attachEvent === "undefined" ? "not-ie" : "ie");
			   }

			   domIsReady = function(callback) {
				  if(callback && typeof callback === "function"){
					 if(isBrowserIeOrNot() !== "ie") {
						document.addEventListener("DOMContentLoaded", function() {
						   return callback();
						});
					 } else {
						document.attachEvent("onreadystatechange", function() {
						   if(document.readyState === "complete") {
							  return callback();
						   }
						});
					 }
				  } else {
					 console.error("The callback is not a function!");
				  }
			   }

			   return domIsReady;
			})(SG.domIsReady || {});

			SG.initTinyMceLoadFunction = function() {
				if (SG.initializedTinyMceLoaderInstance) {
					if (SG.initTinyMceLoadInterval) {
						clearInterval(SG.initTinyMceLoadInterval);
					}

					return;
				}

				if (SG.TinyMceLoader && window.tinymce && window.tinymce.init) {
					SG.initializedTinyMceLoaderInstance = new SG.TinyMceLoader(window.tinymce, {
						' . $this->replaceTypo3Paths($this->tinymceConfiguration['configurationData']) . '
					});

					if (SG.initTinyMceLoadInterval) {
						clearInterval(SG.initTinyMceLoadInterval);
					}
				}
			};

			SG.domIsReady(function() {
				SG.initTinyMceLoadFunction();
			});

			// the content ready event is not thrown if RTE fields are loaded via IRRE
			// so we need to check at least after some time if the function was really called
			SG.initTinyMceLoadInterval = window.setInterval(SG.initTinyMceLoadFunction, 1500);
		';
		$configuration .= $this->tinymceConfiguration['postJS'];

		$filename = 'tinymceConfiguration' . sha1($configuration) . '.js';
		$file = PATH_site . 'typo3temp/' . $filename;
		if (!is_file($file)) {
			file_put_contents($file, $configuration);
			GeneralUtility::fixPermissions($file);
		}

		return $this->getPath($file, TRUE);
	}

	/**
	 * Returns the needed javascript inclusion code
	 *
	 * Note: This function can only be called once.
	 *
	 * @return string
	 */
	public function getJS(): string {
		$output = '';
		if (!self::$init) {
			self::$init = TRUE;
			$pathToTinyMceExtension = ExtensionManagementUtility::extRelPath('tinymce');

			$script = $GLOBALS['BACK_PATH'] . $pathToTinyMceExtension . 'tinymce_node_modules/tinymce/tinymce.min.js';
			$output = '<script type="text/javascript" src="' . $script . '"></script>';

			$script = $GLOBALS['BACK_PATH'] . $pathToTinyMceExtension . 'Contrib/WeakMap/WeakMap.js';
			$output .= '<script type="text/javascript" src="' . $script . '"></script>';

			$script = $GLOBALS['BACK_PATH'] . $pathToTinyMceExtension . 'Contrib/MutationObserver/MutationObserver.js';
			$output .= '<script type="text/javascript" src="' . $script . '"></script>';

			$script = $GLOBALS['BACK_PATH'] . $pathToTinyMceExtension . 'Resources/Public/JavaScript/Loader.js';
			$output .= '<script type="text/javascript" src="' . $script . '"></script>';

			$script = $this->getConfiguration();
			$output .= '<script type="text/javascript" src="' . $script . '"></script>';
		}

		return $output;
	}

	/**
	 * Loads the required javascript via the given page renderer instance
	 *
	 * Note: This function can only be called once.
	 *
	 * @param PageRenderer $pageRenderer
	 * @return void
	 */
	public function loadJsViaPageRenderer(PageRenderer $pageRenderer) {
		if (self::$init) {
			return;
		}
		self::$init = TRUE;

		$pathToTinyMceExtension = ExtensionManagementUtility::extRelPath('tinymce');
		$script = $GLOBALS['BACK_PATH'] . $pathToTinyMceExtension . 'tinymce_node_modules/tinymce/tinymce.min.js';
		$pageRenderer->addJsLibrary('tinymce', $script, 'text/javascript', FALSE, TRUE, '', TRUE);

		$script = $GLOBALS['BACK_PATH'] . $pathToTinyMceExtension . 'Contrib/MutationObserver/MutationObserver.js';
		$pageRenderer->addJsLibrary('MutationObserver', $script, 'text/javascript', FALSE, TRUE, '', TRUE);

		$script = $GLOBALS['BACK_PATH'] . $pathToTinyMceExtension . 'Contrib/WeakMap/WeakMap.js';
		$pageRenderer->addJsLibrary('WeakMap', $script, 'text/javascript', FALSE, TRUE, '', TRUE);

		$script = $GLOBALS['BACK_PATH'] . $pathToTinyMceExtension . 'Resources/Public/JavaScript/Loader.js';
		$pageRenderer->addJsFile($script, 'text/javascript', FALSE, TRUE, '', TRUE);

		$script = $this->getConfiguration();
		$pageRenderer->addJsFile($script, 'text/javascript', FALSE, TRUE, '', TRUE);
	}

	/**
	 * Loads the required javascript via the require.js
	 *
	 * @return array
	 * @throws \BadFunctionCallException
	 * @throws \UnexpectedValueException
	 * @see \SGalinski\Tinymce4Rte\Form\Element\RichTextElement->loadRequireModulesForRTE
	 */
	public function loadJsViaRequireJS(): array {
		if (self::$init) {
			return [];
		}
		self::$init = TRUE;

		$pathToTinyMceExtension = ExtensionManagementUtility::extPath('tinymce');
		$tinymceSource = $pathToTinyMceExtension . 'tinymce_node_modules/tinymce/tinymce.min.js';

		$configuration = $this->tinymceConfiguration['preJS'];
		$configuration .= '
			var $ = jQuery = window.TYPO3.jQuery;
			var RTEarea = RTEarea || window.RTEarea;

			define([\'TYPO3/CMS/Tinymce/../../../../typo3conf/ext/tinymce/tinymce_node_modules/tinymce/jquery.tinymce.min.js\'], function () {
				$(\'.tinymce4_rte#RTEarea' . strtr($this->tinymceConfiguration['configurationDataArray']['editornumber'], array('.' => '\\\\.', '\'' => '')) . '\').tinymce({
					script_url : \'' . $this->getPath($tinymceSource, TRUE) . '\',
					' . $this->replaceTypo3Paths($this->tinymceConfiguration['configurationData']) . ',
					selector: \'.tinymce4_rte#RTEarea' . strtr($this->tinymceConfiguration['configurationDataArray']['editornumber'], array('.' => '\\\\.', '\'' => '')) . '\'
				});
				$(\'#t3js-ui-block\').remove();
			});
		';
		$configuration .= $this->tinymceConfiguration['postJS'];

		$filename = 'tinymceConfiguration' . sha1($configuration) . '.js';
		$file = PATH_site . 'typo3temp/' . $filename;
		if (!is_file($file)) {
			file_put_contents($file, $configuration);
			GeneralUtility::fixPermissions($file);
		}

		return [$this->getPath($file, TRUE)];
	}

	/**
	 * Parses and processes the tinyMCE configuration
	 *
	 * @param string $configuration file reference or configuration string
	 * @return array
	 */
	protected function prepareTinyMCEConfiguration($configuration): array {
		$configurationArray = [];

		// try to resolve a potential TYPO3 file path
		$configurationFile = GeneralUtility::getFileAbsFileName($configuration);
		if (is_file($configurationFile)) {
			$configuration = file_get_contents($configurationFile);
		}

		// first try to find the configuration via the "subpart" ###TINYMCE_INIT###
		$pattern = '/(.*)?tinymce\.init\s*\(\s*\{\s*\/\*\s?###TINYMCE_INIT###.*?\*\/(.*)\/\*\s*###TINYMCE_INIT###.*?\*\/\s*\}\s*\);*(.*)?/is';
		if (@preg_match($pattern, $configuration, $matches)) {
			// fine :)
		} else {
			// if nothing is found, try it the legacy way (note: this may cause problems with a complex setups, since parenthesis-matching is not perfect here)
			$pattern = '/(.*)tinymce\.init\s*\(\s*\{(.*?)\}\s*\)\s*;?(.*)/is';
			preg_match($pattern, $configuration, $matches);
		}

		// add preJS and postJS
		$configurationArray['preJS'] = trim($matches[1]);
		$configurationArray['configurationData'] = trim($matches[2]);
		$configurationArray['postJS'] = trim($matches[3]);

		return $configurationArray;
	}

	/**
	 * Adds a basic configuration value to the parsed configuration
	 *
	 * @param string $key
	 * @param mixed $value
	 */
	public function addConfigurationOption($key, $value) {
		if (is_numeric($value)) {
			if (strpos($value, '.')) {
				$value = (float) $value;
			} else {
				$value = (int) $value;
			}
		} elseif (strpos(trim($value), '[') === FALSE && strpos(trim($value), '{') === FALSE &&
			strpos(trim($value), 'function') === FALSE
		) {
			$value = '\'' . $value . '\'';
		}

		if ($this->tinymceConfiguration['configurationData'] !== '') {
			$this->tinymceConfiguration['configurationData'] .= "\n,";
		}

		$this->tinymceConfiguration['configurationData'] .= $key . ': ' . $value;
		$this->tinymceConfiguration['configurationDataArray'][$key] = $value;
	}

	/**
	 * Replaces any TYPO3 extension path with the domain prefixed one.
	 *
	 * @param string $configuration
	 * @return string
	 */
	protected function replaceTypo3Paths($configuration): string {
		$replacementFunction = function ($value) {
			// getPath should be used, but this causes a php exception with PHP 5.3 as $this isn't set there
			return '\'' . GeneralUtility::getIndpEnv('TYPO3_SITE_URL') .
			str_replace(PATH_site, '', GeneralUtility::getFileAbsFileName($value[1])) . '\'';
		};

		return preg_replace_callback('/["\'](EXT:[^"\']*)["\']/is', $replacementFunction, $configuration);
	}

	/**
	 * Resolves a relative path like EXT:tinymce/... into an absolute one that contains either the
	 * current host or the path to the file in the file system.
	 *
	 * @param string $relativePath
	 * @param bool $returnWithDomain
	 *
	 * @return string
	 * @throws \UnexpectedValueException
	 */
	protected function getPath($relativePath, $returnWithDomain = FALSE): string {
		$finalPath = $absolutePath = GeneralUtility::getFileAbsFileName($relativePath);
		if ($returnWithDomain) {
			$finalPath = GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . str_replace(PATH_site, '', $absolutePath);
		}
		return $finalPath;
	}
}