<?php
/**
 * Matomo - free/libre analytics platform
 *
 * @link https://matomo.org
 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 */

namespace Piwik\Translation;

use Piwik\Config;
use Piwik\Piwik;
use Piwik\Translation\Loader\LoaderInterface;

/**
 * Translates messages.
 *
 * @api
 */
class Translator
{
    /**
     * Contains the translated messages, indexed by the language name.
     *
     * @var array
     */
    private $translations = array();

    /**
     * @var string
     */
    private $currentLanguage;

    /**
     * @var string
     */
    private $fallback = 'en';

    /**
     * Directories containing the translations to load.
     *
     * @var string[]
     */
    private $directories = array();

    /**
     * @var LoaderInterface
     */
    private $loader;

    public function __construct(LoaderInterface $loader, array $directories = null)
    {
        $this->loader = $loader;
        $this->currentLanguage = $this->getDefaultLanguage();

        if ($directories === null) {
            // TODO should be moved out of this class
            $directories = array(PIWIK_INCLUDE_PATH . '/lang');
        }
        $this->directories = $directories;
    }

    /**
     * Clean a string that may contain HTML special chars, single/double quotes, HTML entities, leading/trailing whitespace
     *
     * @param string $s
     * @return string
     */
    public static function clean($s)
    {
        return html_entity_decode(trim($s), ENT_QUOTES, 'UTF-8');
    }

    /**
     * Returns an internationalized string using a translation ID. If a translation
     * cannot be found for the ID, the ID is returned.
     *
     * @param string $translationId Translation ID, eg, `General_Date`.
     * @param array|string|int $args `sprintf` arguments to be applied to the internationalized
     *                               string.
     * @param string|null $language Optionally force the language.
     * @return string The translated string or `$translationId`.
     * @api
     */
    public function translate($translationId, $args = array(), $language = null)
    {
        $args = is_array($args) ? $args : array($args);
        $translationId = $translationId ?? '';

        if (strpos($translationId, "_") !== false) {
            list($plugin, $key) = explode("_", $translationId, 2);
            $language = is_string($language) ? $language : $this->currentLanguage;

            $translationId = $this->getTranslation($translationId, $language, $plugin, $key);
        }

        if (count($args) == 0) {
            return str_replace('%%', '%', $translationId);
        }
        return vsprintf($translationId, $args);
    }

    /**
     * @return string
     */
    public function getCurrentLanguage()
    {
        return $this->currentLanguage;
    }

    /**
     * @param string $language
     */
    public function setCurrentLanguage($language)
    {
        if (!$language) {
            $language = $this->getDefaultLanguage();
        }

        $this->currentLanguage = $language;
    }

    /**
     * @return string The default configured language.
     */
    public function getDefaultLanguage()
    {
        $generalSection = Config::getInstance()->General;

        // the config may not be available (for example, during environment setup), so we default to 'en'
        // if the config cannot be found.
        return @$generalSection['default_language'] ?: 'en';
    }

    /**
     * Generate javascript translations array
     */
    public function getJavascriptTranslations()
    {
        $clientSideTranslations = array();
        foreach ($this->getClientSideTranslationKeys() as $id) {
            list($plugin, $key) = explode('_', $id, 2);
            $clientSideTranslations[$id] = $this->getTranslation($id, $this->currentLanguage, $plugin, $key);
        }

        $js = 'var translations = ' . json_encode($clientSideTranslations) . ';';
        $js .= "\n" . 'if (typeof(piwik_translations) == \'undefined\') { var piwik_translations = new Object; }' .
            'for(var i in translations) { piwik_translations[i] = translations[i];} ';
        return $js;
    }

    /**
     * Returns the list of client side translations by key. These translations will be outputted
     * to the translation JavaScript.
     */
    private function getClientSideTranslationKeys()
    {
        $result = array();

        /**
         * Triggered before generating the JavaScript code that allows i18n strings to be used
         * in the browser.
         *
         * Plugins should subscribe to this event to specify which translations
         * should be available to JavaScript.
         *
         * Event handlers should add whole translation keys, ie, keys that include the plugin name.
         *
         * **Example**
         *
         *     public function getClientSideTranslationKeys(&$result)
         *     {
         *         $result[] = "MyPlugin_MyTranslation";
         *     }
         *
         * @param array &$result The whole list of client side translation keys.
         */
        Piwik::postEvent('Translate.getClientSideTranslationKeys', array(&$result));

        $result = array_unique($result);

        return $result;
    }

    /**
     * Add a directory containing translations.
     *
     * @param string $directory
     */
    public function addDirectory($directory)
    {
        if (isset($this->directories[$directory])) {
            return;
        }
        // index by name to avoid duplicates
        $this->directories[$directory] = $directory;

        // clear currently loaded translations to force reloading them
        $this->translations = array();
    }

    /**
     * Should be used by tests only, and this method should eventually be removed.
     */
    public function reset()
    {
        $this->currentLanguage = $this->getDefaultLanguage();
        $this->directories = array(PIWIK_INCLUDE_PATH . '/lang');
        $this->translations = array();
    }

    /**
     * @param string $translation
     * @return null|string
     */
    public function findTranslationKeyForTranslation($translation)
    {
        foreach ($this->getAllTranslations() as $key => $translations) {
            $possibleKey = array_search($translation, $translations);
            if (!empty($possibleKey)) {
                return $key . '_' . $possibleKey;
            }
        }

        return null;
    }

    /**
     * Returns all the translation messages loaded.
     *
     * @return array
     */
    public function getAllTranslations()
    {
        $this->loadTranslations($this->currentLanguage);

        if (!isset($this->translations[$this->currentLanguage])) {
            return array();
        }

        return $this->translations[$this->currentLanguage];
    }

    private function getTranslation($id, $lang, $plugin, $key)
    {
        $this->loadTranslations($lang);

        if (isset($this->translations[$lang][$plugin])
            && isset($this->translations[$lang][$plugin][$key])
        ) {
            return $this->translations[$lang][$plugin][$key];
        }

        /**
         * Fallback for keys moved to new Intl plugin to avoid untranslated string in non core plugins
         * @todo remove this in Piwik 3.0
         */
        if ($plugin != 'Intl') {
            if (isset($this->translations[$lang]['Intl'])
                && isset($this->translations[$lang]['Intl'][$key])
            ) {
                return $this->translations[$lang]['Intl'][$key];
            }
        }

        // fallback
        if ($lang !== $this->fallback) {
            return $this->getTranslation($id, $this->fallback, $plugin, $key);
        }

        return $id;
    }

    private function loadTranslations($language)
    {
        if (empty($language) || isset($this->translations[$language])) {
            return;
        }

        $this->translations[$language] = $this->loader->load($language, $this->directories);
    }
}
