<?php
/**
 * @obfuscate_disable
 */
namespace Claromentis\Core\Localization;

use Exception;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;

/**
 * Locates, reads, and caches localization files.
 */
class Files implements LoggerAwareInterface
{
	use LoggerAwareTrait;

	/**
	 * @var string
	 */
	public $read_files_dates = '';

	/**
	 * The directory to read localization files from.
	 *
	 * @var string
	 */
	protected $base_dir;

	/**
	 * The directory to store localization cache files.
	 *
	 * @var string
	 */
	protected $cache_dir;

	/**
	 * The directory to store custom localisation in
	 *
	 * @var string
	 */
	protected $custom_dir;

	/**
	 * Create a new localization file reader.
	 *
	 * @param string $base_dir
	 * @param string $cache_dir
	 * @param        $custom_dir
	 */
	public function __construct($base_dir, $cache_dir, $custom_dir)
	{
		$this->base_dir = $base_dir;
		$this->cache_dir = $cache_dir;
		$this->custom_dir = $custom_dir;
		$this->logger = new NullLogger();
	}

	/**
	 * Read localization messages for the specified application and language into given array $lm.
	 *
	 * @param array &$lm
	 * @param string $application
	 * @param string $language
	 */
	public function ReadLM(&$lm, $application, $language)
	{
		$base_dir = $this->base_dir;

		if (!is_dir($base_dir) || !is_file($base_dir . '/en.txt'))
			return;

		$cache_suffix = ($application == 'common' ? '' : '_' . $application);

		$cache_dir = $this->GetCacheDir();
		$date_file = $cache_dir . "lang_date" . $cache_suffix;
		$lang_file_lm = $cache_dir . "messages_lm_${language}${cache_suffix}.php";
		$lang_file_lt = $cache_dir . "messages_lt_${language}${cache_suffix}.php";

		[$need_rebuild, $files_dates] = $this->GetCacheStatus($language, $lang_file_lm, $lang_file_lt, $date_file, $cache_suffix);

		// rebuilding localization file
		if ($need_rebuild && !$this->RebuildCache($application, $language, $lang_file_lm, $lang_file_lt))
		{
			[$lm_str] = $this->BuildCacheContent($application, $language);

			eval($lm_str);

			if (!defined('INSTALL_PROGRESS'))
				$this->logger->error("Localization cache '$application $language' was not created because of error or permissions");

			return;
		}

		if ($need_rebuild)
			$this->WriteDatesFile($date_file, $files_dates);

		add_time_point();
		/** @noinspection PhpExpressionResultUnusedInspection */
		$lm;
		require($lang_file_lm);
		print_time_point('Read/store phrases');
	}

	/**
	 * Returns a value that changes whenever messages cache is rebuilt (value derived from modification date).
	 *
	 * Used by templater to detect whether precompiled cache is valid.
	 *
	 * Returns null if both the Localization Message and Localization Templater files are missing.
	 *
	 * @param string $application
	 * @param string $language
	 * @return string|null
	 */
	public function GetDataHash($application, $language)
	{
		$cache_suffix = ($application == 'common' ? '' : '_' . $application);

		$cache_dir = $this->GetCacheDir();
		$lang_file_lm = $cache_dir . "messages_lm_${language}${cache_suffix}.php";
		$lang_file_lt = $cache_dir . "messages_lt_${language}${cache_suffix}.php";

		$lm_date = is_file($lang_file_lm) ? filemtime($lang_file_lm) : 0;
		$lt_date = is_file($lang_file_lt) ? filemtime($lang_file_lt) : 0;

		if ($lm_date === 0 && $lt_date === 0)
			return null;

		return $lm_date . '_' . $lt_date;
	}

	/**
	 * Read templater localization data into given array $lt_parsed
	 *
	 * @param array &$lm
	 * @param array &$lt_parsed
	 * @param string $application
	 * @param string $language
	 */
	public function ReadLT(&$lm, &$lt_parsed, $application, $language)
	{
		$base_dir = $this->base_dir;

		if (!is_dir($base_dir) || !is_file($base_dir . '/en.txt'))
			return;

		$cache_suffix = ($application == 'common' ? '' : '_' . $application);

		$cache_dir = $this->GetCacheDir();
		$lang_file_lt = $cache_dir . "messages_lt_${language}${cache_suffix}.php";

		if (file_exists($lang_file_lt))
		{
			require($lang_file_lt);
		} else
		{
			[, $lt_str] = $this->BuildCacheContent($application, $language);
			eval($lt_str);
		}
	}

	/**
	 * Get the cache directory.
	 *
	 * @return string
	 */
	protected function GetCacheDir()
	{
		return $this->cache_dir;
	}

	/**
	 * Get the cache status of a localization file.
	 *
	 * @param string $cur_language
	 * @param string $lang_file_lm
	 * @param string $lang_file_lt
	 * @param string $date_file
	 * @param string $cache_suffix
	 * @return array($need_rebuild, $files_dates)
	 */
	protected function GetCacheStatus($cur_language, $lang_file_lm, $lang_file_lt, $date_file, $cache_suffix)
	{
		$base_dir = $this->base_dir;
		$need_rebuild = !file_exists($lang_file_lm) || !file_exists($lang_file_lt);

		$languages = array_unique(array('en', $cur_language));
		if (file_exists($date_file))
			$dates = @file($date_file);
		else
			$dates = array();

		// read dates of last cache updates
		$files_dates = array();
		foreach ($dates as $file_date)
		{
			[$file, $f_date] = explode(':', $file_date);
			$files_dates[$file] = $f_date;
		}
		unset($dates);

		// compare last generation time and modification dates of localization files
		foreach ($languages as $lang)
		{
			$date1 = isset($files_dates[$lang]) ? (int)$files_dates[$lang] : 0;
			$date1_c = isset($files_dates[$lang . '_custom']) ? (int)$files_dates[$lang . '_custom'] : 0;

			$f = $base_dir . '/' . ($lang == 'zz' ? 'en' : $lang);
			$date2 = (file_exists($f . '.txt') ? filemtime($f . '.txt') : 0);
			$date2_c = (file_exists($f . '_custom.txt') ? filemtime($f . '_custom.txt') : 0);

			if ($date1 != $date2 || $date1_c != $date2_c)
			{
				// if dates differ - need to rebuild localization file
				$need_rebuild = true;
				$files_dates[$lang] = $date2;
				$files_dates[$lang . '_custom'] = $date2_c;
				$cache_dir = $this->GetCacheDir();
				@unlink($cache_dir . "messages_lm_${lang}${cache_suffix}_utf8.php");
				@unlink($cache_dir . "messages_lt_${lang}${cache_suffix}_utf8.php");
			}
		}

		return array($need_rebuild, $files_dates);
	}

	/**
	 * @param string $application
	 * @param string $cur_language
	 * @param string $lang_file_lm Location of "lm" cache file
	 * @param string $lang_file_lt Location of "lt" cache file
	 *
	 * @return bool
	 */
	protected function RebuildCache($application, $cur_language, $lang_file_lm, $lang_file_lt)
	{
		@unlink($lang_file_lm);
		@unlink($lang_file_lt);

		[$lm_str, $lt_str, $was_duplicate] = $this->BuildCacheContent($application, $cur_language);

		// Create the cache directory if it doesn't exist
		if (!is_dir($this->GetCacheDir())) {
			mkdir($this->GetCacheDir(), 0775, true);
		}

		try {
			if ($was_duplicate)
				throw new Exception();

			$written = @file_put_contents($lang_file_lm, "<?" . "php\n" . $lm_str . "\n?" . ">");
			if (!$written)
				throw new Exception("Write failed");

			$written = @file_put_contents($lang_file_lt, "<?" . "php\n" . $lt_str . "\n?" . ">");
			if (!$written)
				throw new Exception("Write failed");

		} catch (Exception $e)
		{
			//eval($lm_str);
			//eval($lt_str);
			return false;
		}
		return true;
	}

	/**
	 * @param $date_file
	 * @param $files_dates
	 */
	protected function WriteDatesFile($date_file, $files_dates)
	{
		$fp = fopen($date_file, 'w');
		foreach ($files_dates as $fname => $fdate)
		{
			$fname = trim($fname);
			$fdate = intval($fdate);
			if ($fname == '' || $fdate == 0)
				continue;
			fputs($fp, "$fname:$fdate\n");
		}
		fclose($fp);
	}

	/**
	 *
	 * Note: Order of precedence for localisations for a given key are as follows.
	 *		If a custom value for the key for cur_language exists, use it
	 * 		else fall back to core cur_language value if exists
	 * 		else fall back to custom 'en' value if exists
	 * 		else fall back to core 'en' value, which should always exist
	 *
	 * @param string $application
	 * @param string $cur_language
	 * @return array
	 */
	protected function BuildCacheContent($application, $cur_language)
	{
		$base_dir = $this->base_dir;
		$was_duplicate = 0;

		$english_loc = array();
		if ($cur_language != 'en')
		{
			// read English localization to use as default if national doesn't exist
			$english_loc_raw = @file($base_dir . '/en.txt');
			if (!is_array($english_loc_raw))
			{
				$this->logger->critical("Unable to read English localization file for '$application'");
			} else
			{
				$cnt = count($english_loc_raw);
				for ($i = 1; $i < $cnt; $i++)
				{
					$columns = read_tsv($english_loc_raw[$i]);
					$key = $columns[0] . ' ' . $columns[1];
					$val = array($columns[2]);
					if (isset($columns[3]))
						$val[] = $columns[3];
					if (isset($english_loc[$key]))
					{
						$was_duplicate = 1;
						$this->logger->error("Error in en.txt! Duplicate key '$key' in '$application'");
					}
					$english_loc[$key] = $val;
				}
			}
			$english_loc_raw = @file($this->custom_dir . '/en.txt');
			if (is_array($english_loc_raw))
			{
				$cnt = count($english_loc_raw);
				for ($i = 1; $i < $cnt; $i++)
				{
					$columns = read_tsv($english_loc_raw[$i]);
					$key = $columns[0] . ' ' . $columns[1];
					// don't overwrite default en value with a custom one that exists but is empty
					if ($columns[2] !== '' || $columns[3] !== '')
						$english_loc[$key] = [$columns[2], $columns[3]];
				}
			}
			unset($english_loc_raw);
		}


		$lm_str = '';
		$lt_str = '';

		// Now read national language file
		if ($cur_language === 'zz')
			$files = array('en.txt', 'en.txt');
		else
			$files = array($cur_language . '.txt', $cur_language . '.txt');
		$files = [$base_dir . '/' . $files[0], $this->custom_dir . '/' . $files[1]];
		$native_loc = array();
		for ($fi = 0; $fi < sizeof($files); $fi++)
		{
			$s = @file($files[$fi]);
			if (!is_array($s))
				continue;

			//$headers = read_tsv($s[0]);
			array_shift($s);

			$dupes = array();

			// walk through all lines
			foreach ($s as $line)
			{
				$columns = read_tsv($line);

				$columns[0] = $columns[0] ?? '';   // Type
				$columns[1] = $columns[1] ?? '';   // Phrase key
				$columns[2] = $columns[2] ?? null; // Aux
				$columns[3] = $columns[3] ?? null; // Phrase string

				$key = $columns[0] . ' ' . $columns[1];
				if ($columns[2] != '' || $columns[3] != '')
				{
					if (!isset($columns[3]))
						$columns[3] = null;
					$value = array($columns[2], $columns[3]);
				} else
				{
					$value = $english_loc[$key] ?? [$columns[2] ?? '', $columns[3] ?? ''];
				}
				unset($english_loc[$key]);
				if ($cur_language == 'zz' && !empty($value[1]) && strncmp($columns[1], '_system.', 8) !== 0)
				{
					$value[1] = '◄' . $value[1] . '►';
				}

				// don't overwrite a fallback value with an empty value
				if ($value[0] !== '' || $value[1] !== '') {
					$native_loc[$key] = $value;
				}

				// check for dupes
				if (isset($dupes[$key]))
				{
					$was_duplicate = 1;
					$this->logger->error("Error in " . $files[$fi] . "! Duplicate string '$key' in '$application'");
				} else
				{
					$dupes[$key] = 1;
				}
			}

		} //for each files
		unset($dupes);
		unset($s);

		if (count($english_loc) > 0)
		{
			//echo "Warning: language '$cur_language' doesn't have some strings:<br>";
			//echo join("<br>\n", array_keys($english_loc))."<br>\n";
			$native_loc = $native_loc + $english_loc;
		}

		// now go create actual localization file
		foreach ($native_loc as $key => $data)
		{
			[$array_name, $key] = explode(' ', $key);
			$add_info = $data[0];
			$value = $data[1];

			if ($array_name == 'lm')
			{
				$lm_str .= '$lm["' . $key . '"] = ';
			} else // for lt array - preparse data for templater
			{
				@list($name, $attr_name, $sub_attr) = explode('.', $key);
				$attr_name = strtoupper($attr_name);

				if ($attr_name === 'VISIBLE')
				{
					if ($value == '1' || !strcasecmp($value, 'ON'))
						$value = 1;
					else
						$value = 0;
				}
				if (isset($sub_attr))
					$lt_str .= '$' . $array_name . "_parsed[\"" . strtoupper($name) . "\"][\"sub_attr\"][\"${attr_name}\"][\"" . strtoupper($sub_attr) . "\"] = ";
				elseif ($attr_name != '')
					$lt_str .= '$' . $array_name . "_parsed[\"" . strtoupper($name) . "\"][\"attr\"][\"${attr_name}\"] = ";
				else
					$lt_str .= '$' . $array_name . "_parsed[\"" . strtoupper($name) . "\"] = ";
			}

			if ($add_info != "")
			{
				$matches = null;
				if (substr($add_info, 0, 3) == '$lm')
				{
					if ($array_name == 'lm')
						$str_val = "isset($add_info) ? $add_info : ['".addcslashes(substr($add_info, 5, -2), "'\\")."']";
					else
						$str_val = "isset($add_info) && !is_array($add_info) ? $add_info : lmsg('".addcslashes(substr($add_info, 5, -2), "'\\")."')";
				} else
				{
					$str_val = "'" . addcslashes($add_info, "'\\") . "'";
				}
			} else
			{
				$str_val = "'" . addcslashes($value, "'\\") . "'";
			}

			if ($array_name == 'lm')
				$lm_str .= $str_val . ";\n";
			else
				$lt_str .= $str_val . ";\n";
		}
		return array($lm_str, $lt_str, $was_duplicate); // end generating $lm_str and $lt_str
	}
}
