<?php

class XenForo_Model_Language extends XenForo_Model
{
	public function getLanguageById($id, $fetchMaster = false)
	{
		if (strval($id) === '0')
		{
			if ($fetchMaster)
			{
				return array(
					'language_id' => 0,
					'title' => new XenForo_Phrase('master_language'),
					'parent_id' => 0
				);
			}
			else
			{
				return false;
			}
		}

		$localCacheKey = 'language_' . $id;
		if (($data = $this->_getLocalCacheData($localCacheKey)) === false)
		{
			$data = $this->_getDb()->fetchRow('
				SELECT *
				FROM xf_language
				WHERE language_id = ?
			', $id);

			$this->setLocalCacheData($localCacheKey, $data);
		}

		return $data;
	}

	public function getAllLanguages()
	{
		if (($languages = $this->_getLocalCacheData('allLanguages')) === false)
		{
			$languages = $this->fetchAllKeyed('
				SELECT *
				FROM xf_language
				ORDER BY title
			', 'language_id');

			$this->setLocalCacheData('allLanguages', $languages);
		}

		return $languages;
	}

	/**
	 * Return results for admin quick search
	 *
	 * @param string Keywords for which to search
	 *
	 * @return array
	 */
	public function getLanguagesForAdminQuickSearch($searchText)
	{
		return $this->fetchAllKeyed('
			SELECT *
			FROM xf_language
			WHERE title LIKE ' . XenForo_Db::quoteLike($searchText, 'lr', $this->_getDb()) . '
			ORDER BY title
		', 'language_id');
	}

	/**
	 * Gets the parent => language associations from a flat list of language info.
	 *
	 * @param array $languageList
	 *
	 * @return array [parent id][] => language id
	 */
	public function getLanguageTreeAssociations(array $languageList)
	{
		$parents = array();
		foreach ($languageList AS $language)
		{
			$parents[$language['parent_id']][] = $language['language_id'];
		}

		return $parents;
	}

	/**
	 * Gets a list of child language IDs that are direct children of the specified language.
	 *
	 * @param integer $languageId
	 *
	 * @return array Array of language IDs
	 */
	public function getDirectChildLanguageIds($languageId)
	{
		$languages = $this->getAllLanguages();
		$languageTree = $this->getLanguageTreeAssociations($languages);

		if (isset($languageTree[$languageId]))
		{
			return $languageTree[$languageId];
		}
		else
		{
			return array();
		}
	}

	/**
	 * Gets all children of a language ID, no matter how many levels below.
	 *
	 * @param integer $languageId
	 *
	 * @return array Array of language IDs
	 */
	public function getAllChildLanguageIds($languageId)
	{
		$languages = $this->getAllLanguages();
		$languageTree = $this->getLanguageTreeAssociations($languages);

		if (isset($languageTree[$languageId]))
		{
			return $this->_getAllChildLanguageIds($languageId, $languageTree);
		}
		else
		{
			return array();
		}
	}

	/**
	 * Internal handler to get call child language IDs.
	 *
	 * @param integer $parentId Parent language ID
	 * @param array $languageTree Tree of languages ([parent id][] => language id)
	 *
	 * @return array
	 */
	protected function _getAllChildLanguageIds($parentId, array $languageTree)
	{
		if (!isset($languageTree[$parentId]))
		{
			return array();
		}

		$children = array();
		foreach ($languageTree[$parentId] AS $childId)
		{
			$children[] = $childId;
			$children = array_merge($children, $this->_getAllChildLanguageIds($childId, $languageTree));
		}

		return $children;
	}

	/**
	 * Gets a list of languages in the form of a flattened tree. The return
	 * is an array containing all languages and their related info. Each language
	 * additionally includes a "depth" element that repesents the depth from
	 * the (implicit) master. Children of the master have a depth 0, unless
	 * $baseDepth is overridden.
	 *
	 * @param integer $baseDepth Starting depth value.
	 *
	 * @return array Format: [language id] => (array) language info, including depth
	 */
	public function getAllLanguagesAsFlattenedTree($baseDepth = 0)
	{
		$languages = $this->getAllLanguages();
		$tree = $this->getLanguageTreeAssociations($languages);

		return $this->_buildFlattenedLanguageTree($languages, $tree, 0, $baseDepth);
	}

	/**
	 * Returns an array of all languages, suitable for use in ACP template syntax as options source.
	 *
	 * @param array $languageTree
	 *
	 * @return array
	 */
	public function getLanguagesForOptionsTag($selectedId = null, $languageTree = null)
	{
		if ($languageTree === null)
		{
			$languageTree = $this->getAllLanguagesAsFlattenedTree();
		}

		$languages = array();
		foreach ($languageTree AS $id => $language)
		{
			$languages[$id] = array(
				'value' => $id,
				'label' => $language['title'],
				'selected' => ($selectedId == $id),
				'depth' => $language['depth']
			);
		}

		return $languages;
	}

	/**
	 * Builds the flattened tree recursively, incrementing the depth each time.
	 *
	 * @param array $languageList List of languages and their information
	 * @param array $tree Tree structure of languages ([parent id][] => language id)
	 * @param integer $root Where to start in the tree
	 * @param integer $depth Current/starting depth
	 *
	 * @return array List of languages with additional depth key
	 */
	protected function _buildFlattenedLanguageTree(array $languageList, array $tree, $root = 0, $depth = 0)
	{
		if (!isset($tree[$root]) || !is_array($tree[$root]))
		{
			return array();
		}

		$output = array();

		foreach ($tree[$root] AS $languageId)
		{
			$output[$languageId] = $languageList[$languageId];
			$output[$languageId]['depth'] = $depth;

			$output += $this->_buildFlattenedLanguageTree($languageList, $tree, $languageId, $depth + 1);
		}

		return $output;
	}

	/**
	 * Gets the base parent list for a language. This list starts with the *parent* of the given language ID, then
	 * works up the tree, eventually ending with 0.
	 *
	 * @param integer $languageId
	 *
	 * @return array List of parent language IDs (including 0)
	 */
	public function getLanguageBaseParentList($languageId)
	{
		$languages = $this->getAllLanguages();

		$parents = array();
		while (isset($languages[$languageId]) && $language = $languages[$languageId])
		{
			$parents[] = $language['parent_id'];
			$languageId = $language['parent_id'];
		}

		return $parents;
	}

	/**
	 * Recursively rebuilds the parent list in part of the language tree.
	 *
	 * @param integer $languageId First language to start with. All child will be rebuild.
	 */
	public function rebuildLanguageParentListRecursive($languageId)
	{
		$languages = $this->getAllLanguages();

		if (isset($languages[$languageId]))
		{
			$languageTree = $this->getLanguageTreeAssociations($languages);

			$baseParentList = $this->getLanguageBaseParentList($languageId);
			$this->_rebuildLanguageParentListRecursive($languageId, $baseParentList, $languages, $languageTree);
		}
	}

	/**
	 * Internal function to rebuild the language parent list recursively.
	 *
	 * @param integer $languageId Base lanaguage Id
	 * @param array $baseParentList Base parent list for the language. Should not include this language ID in it.
	 * @param array $languages List of languages
	 * @param array $languageTree Language tree
	 */
	protected function _rebuildLanguageParentListRecursive($languageId, array $baseParentList, array $languages, array $languageTree)
	{
		if (isset($languages[$languageId]))
		{
			$parentList = $baseParentList;
			array_unshift($parentList, $languageId);

			$db = $this->_getDb();
			$db->update(
				'xf_language',
				array('parent_list' => implode(',', $parentList)),
				'language_id = ' . $db->quote($languageId)
			);

			if (isset($languageTree[$languageId]))
			{
				foreach ($languageTree[$languageId] AS $childLanguageId)
				{
					$this->_rebuildLanguageParentListRecursive($childLanguageId, $parentList, $languages, $languageTree);
				}
			}
		}
	}

	/**
	 * Rebuilds the global phrase cache for all languages.
	 *
	 * @param array|null $globalPhraseTitles List of phrases that should be included. If null, uses flag from phrase table.
	 */
	public function rebuildLanguageGlobalPhraseCache(array $globalPhraseTitles = null)
	{
		$phraseModel = $this->_getPhraseModel();

		if ($globalPhraseTitles === null)
		{
			$globalPhraseTitles = $phraseModel->getGlobalPhraseCacheTitles();
		}

		$db = $this->_getDb();

		$languages = $this->getAllLanguages();
		$globalPhrases = $phraseModel->getEffectivePhraseValuesInAllLanguages($globalPhraseTitles);

		foreach ($languages AS $languageId => $language)
		{
			if (isset($globalPhrases[$languageId]))
			{
				$phrases = $globalPhrases[$languageId];
			}
			else
			{
				$phrases = array();
			}

			$db->update('xf_language',
				array('phrase_cache' => serialize($phrases)),
				'language_id = ' . $db->quote($languageId)
			);
		}
	}

	/**
	* Helper to determine whether the master language should be shown in lists.
	*
	* @return boolean
	*/
	public function showMasterLanguage()
	{
		return XenForo_Application::debugMode();
	}

	/**
	* Helper to get the default language
	*
	* @return array
	*/
	public function getDefaultLanguage()
	{
		return array(
			'language_id' => 0,
			'parent_id' => 0,
			'parent_list' => '',
			'title' => '',
			'date_format' => 'M j, Y',
			'time_format' => 'g:i A',
			'decimal_point' => '.',
			'thousands_separator' => ',',
			'text_direction' => 'LTR',
			'language_code' => 'en-US'
		);
	}

	/**
	 * Gets all languages in the format expected by the language cache.
	 *
	 * @return array Format: [language id] => info, with phrase cache as array
	 */
	public function getAllLanguagesForCache()
	{
		$this->resetLocalCacheData('allLanguages');

		$languages = $this->getAllLanguages();
		foreach ($languages AS &$language)
		{
			$language['phrase_cache'] = unserialize($language['phrase_cache']);
		}

		return $languages;
	}

	/**
	 * Rebuilds the full language cache.
	 *
	 * @return array Format: [language id] => info, with phrase cache as array
	 */
	public function rebuildLanguageCache()
	{
		$this->resetLocalCacheData('allLanguages');

		$languages = $this->getAllLanguagesForCache();

		$this->_getDataRegistryModel()->set('languages', $languages);

		return $languages;
	}

	/**
	 * Rebuilds all language caches (including global phrase cache).
	 */
	public function rebuildLanguageCaches()
	{
		$this->rebuildLanguageGlobalPhraseCache();
		$this->rebuildLanguageCache();
	}

	/**
	 * Generates the date and time format examples based on the
	 * current time.
	 *
	 * @return array [0] => date formats, [1] => time formats; keyed by format string
	 */
	public function getLanguageFormatExamples()
	{
		$dateFormatsRaw = array(
			'M j, Y',
			'F j, Y',
			'j M Y',
			'j F Y',
			'j/n/y',
			'n/j/y'
		);

		$time = gmmktime(18, 30, 48, 8, 29, date('Y'));

		$dateFormats = array();
		foreach ($dateFormatsRaw AS $dateFormat)
		{
			$dateFormats[$dateFormat] = XenForo_Locale::getFormattedDate($time, $dateFormat);
		}

		$timeFormatsRaw = array(
			'g:i A',
			'H:i'
		);

		$timeFormats = array();
		foreach ($timeFormatsRaw AS $timeFormat)
		{
			$timeFormats[$timeFormat] = XenForo_Locale::getFormattedDate($time, $timeFormat);
		}

		return array($dateFormats, $timeFormats);
	}

	/**
	 * Returns the total number of phrases in the master language
	 *
	 * @return integer
	 */
	public function countMasterPhrases()
	{
		return $this->_getDb()->fetchOne('SELECT COUNT(*) FROM xf_phrase WHERE language_id = 0');
	}

	/**
	 * Counts the number of translated phrases in each non-master language
	 *
	 * @param array $languages Array of languages
	 *
	 * @return array The $languages array including a phraseCount key
	 */
	public function countTranslatedPhrasesPerLanguage(array $languages = array())
	{
		$totals = $this->_getDb()->fetchPairs('
			SELECT language_id, COUNT(*) AS phraseCount
			FROM xf_phrase
			WHERE language_id <> 0
			GROUP BY language_id
		');

		foreach ($totals AS $languageId => $phraseCount)
		{
			if (isset($languages[$languageId]))
			{
				$languages[$languageId]['phraseCount'] = $phraseCount;
			}
		}

		return $languages;
	}

	/**
	 * Gets the DOM document that represents a language file.
	 * This must be turned into XML (or HTML) by the caller.
	 *
	 * @param array $language
	 * @param string|null $limitAddOnId If specified, only exports phrases from the specified add-on
	 * @param boolean $getUntranslated If true, gets untranslated phrases in this language
	 *
	 * @return DOMDocument
	 */
	public function getLanguageXml(array $language, $limitAddOnId = null, $getUntranslated = false)
	{
		$document = new DOMDocument('1.0', 'utf-8');
		$document->formatOutput = true;

		$rootNode = $document->createElement('language');
		$rootNode->setAttribute('title', $language['title']);
		$rootNode->setAttribute('date_format', $language['date_format']);
		$rootNode->setAttribute('time_format', $language['time_format']);
		$rootNode->setAttribute('decimal_point', $language['decimal_point']);
		$rootNode->setAttribute('thousands_separator', $language['thousands_separator']);
		$rootNode->setAttribute('language_code', $language['language_code']);
		$rootNode->setAttribute('text_direction', $language['text_direction']);
		if ($limitAddOnId !== null)
		{
			$rootNode->setAttribute('addon_id', $limitAddOnId);
		}
		$document->appendChild($rootNode);

		$db = $this->_getDb();

		if ($getUntranslated)
		{
			$phrases = $db->fetchAll('
				SELECT phrase.*,
					IF(master.phrase_id, master.addon_id, phrase.addon_id) AS addon_id
				FROM xf_phrase_map AS map
				INNER JOIN xf_phrase AS phrase ON (map.phrase_id = phrase.phrase_id)
				LEFT JOIN xf_phrase AS master ON (master.title = phrase.title AND master.language_id = 0)
				WHERE map.language_id = ?
					AND ' . ($limitAddOnId === null ? '1=1' : 'master.addon_id = ' . $db->quote($limitAddOnId)) . '
				ORDER BY map.title
			', $language['language_id']);
		}
		else
		{
			$phrases = $db->fetchAll('
				SELECT phrase.*,
					IF(master.phrase_id, master.addon_id, phrase.addon_id) AS addon_id
				FROM xf_phrase AS phrase
				LEFT JOIN xf_phrase AS master ON (master.title = phrase.title AND master.language_id = 0)
				WHERE phrase.language_id = ?
					AND ' . ($limitAddOnId === null ? '1=1' : 'master.addon_id = ' . $db->quote($limitAddOnId)) . '
				ORDER BY phrase.title
			', $language['language_id']);
		}

		foreach ($phrases AS $phrase)
		{
			$phraseNode = $document->createElement('phrase');
			$phraseNode->setAttribute('title', $phrase['title']);
			$phraseNode->setAttribute('addon_id', $phrase['addon_id']);
			if ($phrase['global_cache'])
			{
				$phraseNode->setAttribute('global_cache', $phrase['global_cache']);
			}
			$phraseNode->setAttribute('version_id', $phrase['version_id']);
			$phraseNode->setAttribute('version_string', $phrase['version_string']);
			$phraseNode->appendChild($document->createCDATASection($phrase['phrase_text']));

			$rootNode->appendChild($phraseNode);
		}

		return $document;
	}

	/**
	 * Imports a language XML file.
	 *
	 * @param SimpleXMLElement $document
	 * @param integer $parentLanguageId If not overwriting, the ID of the parent lang
	 * @param integer $overwriteLanguageId If non-0, parent lang is ignored
	 */
	public function importLanguageXml(SimpleXMLElement $document, $parentLanguageId = 0, $overwriteLanguageId = 0)
	{
		if ($document->getName() != 'language')
		{
			throw new XenForo_Exception(new XenForo_Phrase('provided_file_is_not_valid_language_xml'), true);
		}

		$title = (string)$document['title'];
		if ($title === '')
		{
			throw new XenForo_Exception(new XenForo_Phrase('provided_file_is_not_valid_language_xml'), true);
		}

		$db = $this->_getDb();
		XenForo_Db::beginTransaction($db);

		$limitAddOnId = (string)$document['addon_id'];

		if ($overwriteLanguageId)
		{
			$db->delete('xf_phrase',
				'language_id = ' . $db->quote($overwriteLanguageId)
					. ($limitAddOnId != '' ? ' AND addon_id = ' . $db->quote($limitAddOnId) : '')
			);

			$existingPhrases = $this->_getPhraseModel()->getAllPhrasesInLanguage($overwriteLanguageId);
		}
		else
		{
			$existingPhrases = array();
		}

		$writer = XenForo_DataWriter::create('XenForo_DataWriter_Language');
		if ($overwriteLanguageId)
		{
			$writer->setExistingData($overwriteLanguageId);
		}
		else
		{
			$writer->set('title', (string)$document['title']);
			$writer->set('parent_id', $parentLanguageId);

			$textDirection = (string)$document['text_direction'];
			$writer->bulkSet(array(
				'date_format' => (string)$document['date_format'],
				'time_format' => (string)$document['time_format'],
				'decimal_point' => (string)$document['decimal_point'],
				'thousands_separator' => (string)$document['thousands_separator'],
				'language_code' => (string)$document['language_code'],
				'text_direction' => ($textDirection ? $textDirection : 'LTR')
			));
		}

		$writer->save();

		$languageId = $writer->get('language_id');

		$this->_getPhraseModel()->importPhrasesXml($document, $languageId, null, $existingPhrases);

		$this->triggerLanguageRebuildDeferred();

		XenForo_Db::commit($db);
	}

	public function triggerLanguageRebuildDeferred()
	{
		XenForo_Application::defer('Atomic',
			array('simple' => array('Phrase', 'Template', 'AdminTemplate', 'EmailTemplate')),
			'languageRebuild', true
		);
	}

	/**
	 * Fetches $languageId from cookie if it's available, or returns the default language ID.
	 *
	 * @return integer
	 */
	public function getLanguageIdFromCookie()
	{
		$languageId = XenForo_Helper_Cookie::getCookie('edit_language_id');
		if ($languageId === false)
		{
			$languageId = (XenForo_Application::debugMode()
				? 0
				: XenForo_Application::get('options')->defaultLanguageId
			);
		}

		if (!XenForo_Application::debugMode() && !$languageId)
		{
			$languageId = XenForo_Application::get('options')->defaultLanguageId;
		}

		return $languageId;
	}

	/**
	 * @return XenForo_Model_Phrase
	 */
	protected function _getPhraseModel()
	{
		return $this->getModelFromCache('XenForo_Model_Phrase');
	}
}