<?php

define('DICTIONARY_GROUP_BACK', 'Backend');
define('DICTIONARY_GROUP_FRONT', 'Frontend');

class Dictionary extends AppModel {

	const POLYGLOT_SKIP_START = 'POLYGLOT_SKIP_START';
	const POLYGLOT_SKIP_END = 'POLYGLOT_SKIP_END';

	var $name = 'Dictionary';
	var $useTable = 'dictionary';

	# ~ Clear cache after each save	 - - - - - - - - - - - - - - - - - - - - - - - #
	public function afterSave($created, $options = []) {
		Cache::delete('dictionary');
	}

	# ~ Extracts all keys for translation and add them into dictionary - - - - - - #
	public function extract() {
		$keys = [ DICTIONARY_GROUP_BACK => [], DICTIONARY_GROUP_FRONT => [] ];
		$database = ConnectionManager::getDataSource('default');

		# Table fields and associated strings
		$tables = $database->listSources();
		foreach ($tables as $table) {

			# Front or back
			$group = substr($table, 0, 4) == 'cms_' ? DICTIONARY_GROUP_FRONT : DICTIONARY_GROUP_BACK;

			# Plural and singular name of the model
			$keys[$group] = array_merge($keys[$group], [
				Inflector::humanize($table),
				Inflector::humanize(Inflector::singularize($table))
			]);

			# Fields
			$fields = $database->describe($table);
			foreach ($fields as $field => $params)
				if (!in_array($field, [ 'id', 'slug', 'parent_id', 'lft', 'rght' ])) {
					$keys[$group][] = Inflector::humanize(preg_replace('~__[a-z]+$~', '', $field));

					# Enum
					if (preg_match('~^enum\(\'(.*)\'\)$~ U', $params['type'], $list) && !in_array($field, [ 'icon' ])) {
						$keys[$group] = array_merge($keys[$group], explode("','", $list[1]));
					}
				}
		}

		# Add field help texts and validation rules
		$keys[$group] = array_merge(
			$keys[$group],
			ClassRegistry::init('ModuleField')->getTranslatableStrings(),
			ClassRegistry::init('ModuleFieldRule')->getTranslatableStrings()
		);

		# Get keys from files
		$files = [

			DICTIONARY_GROUP_FRONT => array_merge(
				rglob('../../app/{Controller,Controller/Component,Model,Model/Behavior,View/Helper,Service}/', '*.php', GLOB_BRACE),
				rglob('../../app/Plugin/Api/{Controller,Model,Validator,View}/', '*.php', GLOB_BRACE),
				rglob('../../app/View/{Elements,Layouts,Templates,Emails/html,Emails/text}/', '*.ctp', GLOB_BRACE)),

			DICTIONARY_GROUP_BACK => array_merge(
				rglob('../{Controller,Model,Service,View/*}/', '*.php', GLOB_BRACE),
				rglob('../{Controller,Model,View/*}/', '*.ctp', GLOB_BRACE))
		];

		foreach ($files as $group => $glob) {
			foreach ($glob as $file) {
				if (strpos(pathinfo($file, PATHINFO_FILENAME), '__') !== false) {
					continue;
				}
				$keys[$group] = $this->match(file_get_contents($file), $keys[$group]);
			}
		}

		# Remove duplicates
		$keys[DICTIONARY_GROUP_BACK] = $this->purge($keys[DICTIONARY_GROUP_BACK]);
		$keys[DICTIONARY_GROUP_FRONT] = $this->purge($keys[DICTIONARY_GROUP_FRONT]);
		foreach (array_intersect($keys[DICTIONARY_GROUP_BACK], $keys[DICTIONARY_GROUP_FRONT]) as $i => $value) {
			unset($keys[DICTIONARY_GROUP_BACK][$i]);
		}

		# Repack the keys
		$found = [];
		$locales = array_keys(Configure::read('Config.Languages'));
		foreach ([ DICTIONARY_GROUP_BACK, DICTIONARY_GROUP_FRONT ] as $group) {
			foreach ($keys[$group] as $key)
				foreach ($locales as $locale) {
					$found[$group][$key][$locale] = '';
				}
		}

		# Retreive current database status
		$archive = [];
		$records = $this->find('all', [ 'fields' => [ 'id', 'key', 'value', 'group', 'locale' ] ]);
		foreach ($records as $record)
			if (!empty($record['Dictionary']['value'])) {
				$archive[$record['Dictionary']['group']][$record['Dictionary']['locale']][$record['Dictionary']['key']] = $record['Dictionary']['value'];
			}

		# Finish the preparation
		$data = [];
		foreach ($found as $group => $keys) {
			foreach ($keys as $key => $values) {
				foreach ($values as $locale => $value) {

					if (!empty($archive[$group][$locale][$key])) {
						$value = $archive[$group][$locale][$key];
					}

					$data[] = compact('key', 'group', 'locale', 'value');
				}
			}
		}

		# Fill the database
		$this->begin();
		$this->truncate();
		if ($this->saveAll($data)) {
			$this->commit();
		} else {
			$this->rollback();
		}
	}

	# ~ Matches the static text inside HTML and PHP data - - - - - - - - - - - - - #
	public function match($data, $matches = [], $type = 'both') {
		$php = '__\(([\'"])(?P<key>.*)\1';
		$html = '(?<=[a-z0-9\s/"\']\>)(?P<key>[^\'\<]*[a-z]+[^\<]*)(?=\</??(?!script|style))';

		# Split data to PHP and HTML
		preg_match_all('~\<\?(?P<php>.*)(\?\>|$)~ Uusi', $data, $match);

		# Match PHP
		if ($type == 'both' || $type = 'php') {
			preg_match_all("~{$php}~ Uusi", join($match['php']), $found);
			$matches = array_merge($matches, $found['key']);
		}

		# Match HTMLs
		if ($type == 'both' || $type = 'html') {
			$data = str_replace($match[0], null, $data);
			preg_match_all("~{$html}~ Uusi", $data, $found);
			$matches = array_merge($matches, $found['key']);
		}

		// Clear the empty values
		foreach ($matches as $i => $key) {
			if (is_string($key)) {
				$key = trim($key);
				if (isEmpty($key)) {
					unset($matches[$i]);
				}
			}
		}

		return array_filter($matches);
	}

	# ~ Returns the list of all translatable texts - - - - - - - - - - - - - - - - #
	public function getList() {
		$list = [];
		$dictionary = $this->find('all');
		foreach ($dictionary as $entry) {
			$list[$entry['Dictionary']['key']]['key'] = $entry['Dictionary']['key'];
			$list[$entry['Dictionary']['key']]['files'] = $entry['Dictionary']['files'];
			$list[$entry['Dictionary']['key']]['domain'] = $entry['Dictionary']['domain'];
			$list[$entry['Dictionary']['key']]['translations'][$entry['Dictionary']['locale']] = [ 'id' => $entry['Dictionary']['id'], 'value' => $entry['Dictionary']['value'] ];
		}
		return $list;
	}

	# ~ Updates the dictionary - - - - - - - - - - - - - - - - - - - - - - - - - - #
	function update($list) {
		$queries = [];
		$dictionary = $this->find('all', [ 'fields' => [ 'Dictionary.key', 'Dictionary.value', 'Dictionary.locale', 'Dictionary.domain', 'Dictionary.modified_by', 'Dictionary.modified', 'Dictionary.created_by', 'Dictionary.created' ] ]);
		foreach ($dictionary as $entry) {
			foreach ($entry['Dictionary'] as $field => $value) {
				$entry['Dictionary'][$field] = mysql_real_escape_string($value);
			}
			extract($entry['Dictionary']);

			$values = "`value` = '{$value}', `modified_by` = '{$modified_by}', `modified` = '{$modified}', `created_by` = '{$created_by}', `created` = '{$created}'";
			$condition = "`key` = '{$key}' AND `locale` = '{$locale}' AND `domain` = '{$domain}'";
			$queries[] = "UPDATE `{$this->useTable}`\nSET {$values}\nWHERE {$condition}";
		}

		$this->truncate();
		$this->saveAll($list);
		foreach ($queries as $query) {
			$this->query($query);
		}
	}

	# ~ Purge invalied keys	 - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	public function purge($keys) {
		sort($keys);
		$keys = array_unique($keys);

		foreach ($keys as $i => $key) {
			$key = trim(str_replace('&nbsp;', ' ', $key));
			if (
				empty($key) ||
				!preg_match('~[a-z]+~ i', $key) ||
				preg_match('~^{\$.*}$~ i', $key) ||
				preg_match('~[a-z]+(\.[A-Z][a-z]+)+~ U', $key)
			) {
				unset($keys[$i]);
			} else {

				# Remove non-human languages
				$symbols = strlen(preg_replace('~[\w\d ]~ u', '', $key));
				if ($symbols > 3 && $symbols * 7 > strlen($key)) {
					unset($keys[$i]);
				} else {
					$keys[$i] = $key;
				}
			}
		}

		return $keys;
	}

	# ~ Get cached elements	 - - - - - - - - - - - - - - - - - - - - - - - - - - - #
	public function cached($activeLocale = true) {
		$domain = 'default';

		# Get locale from Config
		if ($activeLocale === true) {
			$activeLocale = Configure::read('Config.language');
		}

		# Try from cache
		if (!$cache = Cache::read('dictionary')) {
			$dictionary = $this->find('all');
			foreach ($dictionary as $entry) {
				extract($entry['Dictionary']);
				$cache[$domain][$locale]['LC_MESSAGES'][$key] = $value;
			}
			Cache::write('dictionary', $this->_domains);
		}

		return $activeLocale && isset($cache[$domain][$activeLocale]['LC_MESSAGES']) ? $cache[$domain][$activeLocale]['LC_MESSAGES'] : $cache;
	}

	# ~ Replaces all static texts in HTML part of the document - - - - - - - - - - #
	public function replace($document) {

		$polyglotSkips = [];
		$polyglotSkipRegex = '~<!-- ' . self::POLYGLOT_SKIP_START . '_(\d+) -->' . '(.*)<!-- ' . self::POLYGLOT_SKIP_END . '_(\d+) -->~ Uusi';

		if (preg_match_all($polyglotSkipRegex, $document, $polyglotMatches, PREG_OFFSET_CAPTURE)) {
			foreach ($polyglotMatches[2] as $index => $polyglotMatch) {

				// Extract replace id
				$replaceId = $polyglotMatches[1][$index][0];
				$replace = '$$$$$$' . $polyglotMatches[1][$index][0] . '$$$$$$';

				// Replace template with placeholder
				$replaceRegex =
					'<!-- ' . self::POLYGLOT_SKIP_START . '_' . $replaceId . ' -->' .
					$polyglotMatch[0] .
					'<!-- ' . self::POLYGLOT_SKIP_END . '_' . $replaceId . ' -->';

				// Replace content with placeholder
				$document = str_replace($replaceRegex, $replace, $document);
				$polyglotSkips[$replace] = $polyglotMatch[0];

			}
		}

		$cache = $this->cached();
		$html = '(?<=[a-z0-9\s/"\']\>)(?P<key>[^\'\<]*[a-z]+[^\<]*)(?=\</??(?!script|style))';
		preg_match_all("~{$html}~ Uusi", $document, $matches, PREG_OFFSET_CAPTURE);
		# Replace with translation
		for ($i = sizeof($matches[0]) - 1; $i + 1; $i--) {
			# White space fix
			$key = ltrim($matches[0][$i][0]);
			$pos = $matches[0][$i][1] + (strlen($matches[0][$i][0]) - strlen($key));
			$key = trim($key);

			# Replace if translation found
			if (!empty($cache[$key])) {
				$pre = substr($document, 0, $pos);
				$post = substr($document, $pos + strlen($key));
				$document = $pre . str_replace($key, $cache[$key], $matches[0][$i][0]) . $post;
			}
		}

		// Replace polyglot skip placeholders
		foreach ($polyglotSkips as $placeholder => $replace) {
			$document = str_replace($placeholder, $replace, $document);
		}

		return $document;
	}

}

