<?php

namespace Gettext;

use Gettext\Generators\PhpArray;

class Translator extends BaseTranslator implements TranslatorInterface
{
    private $domain;
    private $dictionary = array();
    private $context_glue = "\004";
    private $plurals = array();

    /**
     * Loads translation from a Translations instance, a file on an array.
     *
     * @param Translations|string|array $translations
     *
     * @return self
     */
    public function loadTranslations($translations)
    {
        if (is_object($translations) && $translations instanceof Translations) {
            $translations = PhpArray::toArray($translations);
        } elseif (is_string($translations) && is_file($translations)) {
            $translations = include $translations;
        } elseif (!is_array($translations)) {
            throw new \InvalidArgumentException('Invalid Translator: only arrays, files or instance of Translations are allowed');
        }

        foreach ($translations as $translation) {
            $this->addTranslations($translation);
        }

        return $this;
    }

    /**
     * @see TranslatorInterface
     *
     * {@inheritdoc}
     */
    public function gettext($original)
    {
        return $this->dpgettext($this->domain, null, $original);
    }

    /**
     * @see TranslatorInterface
     *
     * {@inheritdoc}
     */
    public function ngettext($original, $plural, $value)
    {
        return $this->dnpgettext($this->domain, null, $original, $plural, $value);
    }

    /**
     * @see TranslatorInterface
     *
     * {@inheritdoc}
     */
    public function dngettext($domain, $original, $plural, $value)
    {
        return $this->dnpgettext($domain, null, $original, $plural, $value);
    }

    /**
     * @see TranslatorInterface
     *
     * {@inheritdoc}
     */
    public function npgettext($context, $original, $plural, $value)
    {
        return $this->dnpgettext($this->domain, $context, $original, $plural, $value);
    }

    /**
     * @see TranslatorInterface
     *
     * {@inheritdoc}
     */
    public function pgettext($context, $original)
    {
        return $this->dpgettext($this->domain, $context, $original);
    }

    /**
     * @see TranslatorInterface
     *
     * {@inheritdoc}
     */
    public function dgettext($domain, $original)
    {
        return $this->dpgettext($domain, null, $original);
    }

    /**
     * @see TranslatorInterface
     *
     * {@inheritdoc}
     */
    public function dpgettext($domain, $context, $original)
    {
        $translation = $this->getTranslation($domain, $context, $original);

        if (isset($translation[1]) && $translation[1] !== '') {
            return $translation[1];
        }

        return $original;
    }

    /**
     * @see TranslatorInterface
     *
     * {@inheritdoc}
     */
    public function dnpgettext($domain, $context, $original, $plural, $value)
    {
        $key = $this->isPlural($domain, $value);
        $translation = $this->getTranslation($domain, $context, $original);

        if (isset($translation[$key]) && $translation[$key] !== '') {
            return $translation[$key];
        }

        return ($key === 1) ? $original : $plural;
    }

    /**
     * Set new translations to the dictionary.
     *
     * @param array $translations
     */
    protected function addTranslations(array $translations)
    {
        $info = isset($translations['']) ? $translations[''] : null;
        unset($translations['']);

        $domain = isset($info['domain']) ? $info['domain'] : 'messages';

        //Set the first domain loaded as default domain
        if (!$this->domain) {
            $this->domain = $domain;
        }

        if (!isset($this->dictionary[$domain])) {
            // If a plural form is set we extract those values
            $pluralForms = empty($info['plural-forms']) ? 'nplurals=2; plural=(n != 1)' : $info['plural-forms'];

            list($count, $code) = explode(';', $pluralForms, 2);

            // extract just the expression turn 'n' into a php variable '$n'.
            // Slap on a return keyword and semicolon at the end.
            $this->plurals[$domain] = array(
                'count' => (int) str_replace('nplurals=', '', $count),
                'code' => str_replace('plural=', 'return ', str_replace('n', '$n', $code)).';',
            );

            $this->dictionary[$domain] = $translations;
        } else {
            $this->dictionary[$domain] = array_replace_recursive($this->dictionary[$domain], $translations);
        }
    }

    /**
     * Search and returns a translation.
     *
     * @param string $domain
     * @param string $context
     * @param string $original
     *
     * @return array
     */
    protected function getTranslation($domain, $context, $original)
    {
        $key = isset($context) ? $context.$this->context_glue.$original : $original;

        return isset($this->dictionary[$domain][$key]) ? $this->dictionary[$domain][$key] : false;
    }

    /**
     * Executes the plural decision code given the number to decide which
     * plural version to take.
     *
     * @param string $domain
     * @param string $n
     *
     * @return int
     */
    protected function isPlural($domain, $n)
    {
        //Not loaded domain, use a fallback
        if (!isset($this->plurals[$domain])) {
            return $n == 1 ? 1 : 2;
        }

        if (!isset($this->plurals[$domain]['function'])) {
            $this->plurals[$domain]['function'] = create_function('$n', self::fixTerseIfs($this->plurals[$domain]['code']));
        }

        if ($this->plurals[$domain]['count'] <= 2) {
            return (call_user_func($this->plurals[$domain]['function'], $n)) ? 2 : 1;
        }

        // We need to +1 because while (GNU) gettext codes assume 0 based,
        // this gettext actually stores 1 based.
        return (call_user_func($this->plurals[$domain]['function'], $n)) + 1;
    }

    /**
     * This function will recursively wrap failure states in brackets if they contain a nested terse if.
     *
     * This because PHP can not handle nested terse if's unless they are wrapped in brackets.
     *
     * This code probably only works for the gettext plural decision codes.
     *
     * return ($n==1 ? 0 : $n%10>=2 && $n%10<=4 && ($n%100<10 || $n%100>=20) ? 1 : 2);
     * becomes
     * return ($n==1 ? 0 : ($n%10>=2 && $n%10<=4 && ($n%100<10 || $n%100>=20) ? 1 : 2));
     *
     * @param string $code  the terse if string
     * @param bool   $inner If inner is true we wrap it in brackets
     *
     * @return string A formatted terse If that PHP can work with.
     */
    private static function fixTerseIfs($code, $inner = false)
    {
        /*
         * (?P<expression>[^?]+)   Capture everything up to ? as 'expression'
         * \?                      ?
         * (?P<success>[^:]+)      Capture everything up to : as 'success'
         * :                       :
         * (?P<failure>[^;]+)      Capture everything up to ; as 'failure'
         */
        preg_match('/(?P<expression>[^?]+)\?(?P<success>[^:]+):(?P<failure>[^;]+)/', $code, $matches);

        // If no match was found then no terse if was present
        if (!isset($matches[0])) {
            return $code;
        }

        $expression = $matches['expression'];
        $success = $matches['success'];
        $failure = $matches['failure'];

        // Go look for another terse if in the failure state.
        $failure = self::fixTerseIfs($failure, true);
        $code = $expression.' ? '.$success.' : '.$failure;

        if ($inner) {
            return "($code)";
        }

        // note the semicolon. We need that for executing the code.
        return "$code;";
    }
}
