<?php
/**
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link          https://cakephp.org CakePHP(tm) Project
 * @since         3.0.0
 * @license       https://opensource.org/licenses/mit-license.php MIT License
 */
namespace Cake\I18n;

use Aura\Intl\Exception;
use Aura\Intl\FormatterLocator;
use Aura\Intl\PackageLocator;
use Aura\Intl\TranslatorLocator;
use Cake\Cache\CacheEngine;

/**
 * Constructs and stores instances of translators that can be
 * retrieved by name and locale.
 */
class TranslatorRegistry extends TranslatorLocator
{
    /**
     * A list of loader functions indexed by domain name. Loaders are
     * callables that are invoked as a default for building translation
     * packages where none can be found for the combination of translator
     * name and locale.
     *
     * @var callable[]
     */
    protected $_loaders = [];

    /**
     * Fallback loader name
     *
     * @var string
     */
    protected $_fallbackLoader = '_fallback';

    /**
     * The name of the default formatter to use for newly created
     * translators from the fallback loader
     *
     * @var string
     */
    protected $_defaultFormatter = 'default';

    /**
     * Use fallback-domain for translation loaders.
     *
     * @var bool
     */
    protected $_useFallback = true;

    /**
     * A CacheEngine object that is used to remember translator across
     * requests.
     *
     * @var \Cake\Cache\CacheEngine
     */
    protected $_cacher;

    /**
     * Constructor.
     *
     * @param \Aura\Intl\PackageLocator $packages The package locator.
     * @param \Aura\Intl\FormatterLocator $formatters The formatter locator.
     * @param \Cake\I18n\TranslatorFactory $factory A translator factory to
     *   create translator objects for the locale and package.
     * @param string $locale The default locale code to use.
     */
    public function __construct(
        PackageLocator $packages,
        FormatterLocator $formatters,
        TranslatorFactory $factory,
        $locale
    ) {
        parent::__construct($packages, $formatters, $factory, $locale);

        $this->registerLoader($this->_fallbackLoader, function ($name, $locale) {
            $chain = new ChainMessagesLoader([
                new MessagesFileLoader($name, $locale, 'mo'),
                new MessagesFileLoader($name, $locale, 'po'),
            ]);

            // \Aura\Intl\Package by default uses formatter configured with key "basic".
            // and we want to make sure the cake domain always uses the default formatter
            $formatter = $name === 'cake' ? 'default' : $this->_defaultFormatter;
            $chain = function () use ($formatter, $chain) {
                $package = $chain();
                $package->setFormatter($formatter);

                return $package;
            };

            return $chain;
        });
    }

    /**
     * Sets the CacheEngine instance used to remember translators across
     * requests.
     *
     * @param \Cake\Cache\CacheEngine $cacher The cacher instance.
     * @return void
     */
    public function setCacher(CacheEngine $cacher)
    {
        $this->_cacher = $cacher;
    }

    /**
     * Gets a translator from the registry by package for a locale.
     *
     * @param string|null $name The translator package to retrieve.
     * @param string|null $locale The locale to use; if empty, uses the default
     * locale.
     * @return \Aura\Intl\TranslatorInterface|null A translator object.
     * @throws \Aura\Intl\Exception If no translator with that name could be found
     * for the given locale.
     */
    public function get($name, $locale = null)
    {
        if (!$name) {
            return null;
        }

        if ($locale === null) {
            $locale = $this->getLocale();
        }

        if (isset($this->registry[$name][$locale])) {
            return $this->registry[$name][$locale];
        }

        if (!$this->_cacher) {
            return $this->registry[$name][$locale] = $this->_getTranslator($name, $locale);
        }

        $key = "translations.$name.$locale";
        $translator = $this->_cacher->read($key);
        gc_collect_cycles();
        if (!$translator || !$translator->getPackage()) {
            $translator = $this->_getTranslator($name, $locale);
            $this->_cacher->write($key, $translator);
        }

        return $this->registry[$name][$locale] = $translator;
    }

    /**
     * Gets a translator from the registry by package for a locale.
     *
     * @param string $name The translator package to retrieve.
     * @param string|null $locale The locale to use; if empty, uses the default
     * locale.
     * @return \Aura\Intl\TranslatorInterface A translator object.
     */
    protected function _getTranslator($name, $locale)
    {
        try {
            return parent::get($name, $locale);
        } catch (Exception $e) {
        }

        if (!isset($this->_loaders[$name])) {
            $this->registerLoader($name, $this->_partialLoader());
        }

        return $this->_getFromLoader($name, $locale);
    }

    /**
     * Registers a loader function for a package name that will be used as a fallback
     * in case no package with that name can be found.
     *
     * Loader callbacks will get as first argument the package name and the locale as
     * the second argument.
     *
     * @param string $name The name of the translator package to register a loader for
     * @param callable $loader A callable object that should return a Package
     * @return void
     */
    public function registerLoader($name, callable $loader)
    {
        $this->_loaders[$name] = $loader;
    }

    /**
     * Sets the name of the default messages formatter to use for future
     * translator instances.
     *
     * If called with no arguments, it will return the currently configured value.
     *
     * @param string|null $name The name of the formatter to use.
     * @return string The name of the formatter.
     */
    public function defaultFormatter($name = null)
    {
        if ($name === null) {
            return $this->_defaultFormatter;
        }

        return $this->_defaultFormatter = $name;
    }

    /**
     * Set if the default domain fallback is used.
     *
     * @param bool $enable flag to enable or disable fallback
     * @return void
     */
    public function useFallback($enable = true)
    {
        $this->_useFallback = $enable;
    }

    /**
     * Returns a new translator instance for the given name and locale
     * based of conventions.
     *
     * @param string $name The translation package name.
     * @param string $locale The locale to create the translator for.
     * @return \Aura\Intl\Translator
     */
    protected function _fallbackLoader($name, $locale)
    {
        return $this->_loaders[$this->_fallbackLoader]($name, $locale);
    }

    /**
     * Returns a function that can be used as a loader for the registerLoaderMethod
     *
     * @return callable
     */
    protected function _partialLoader()
    {
        return function ($name, $locale) {
            return $this->_fallbackLoader($name, $locale);
        };
    }

    /**
     * Registers a new package by passing the register loaded function for the
     * package name.
     *
     * @param string $name The name of the translator package
     * @param string $locale The locale that should be built the package for
     * @return \Aura\Intl\TranslatorInterface A translator object.
     */
    protected function _getFromLoader($name, $locale)
    {
        $loader = $this->_loaders[$name]($name, $locale);
        $package = $loader;

        if (!is_callable($loader)) {
            $loader = function () use ($package) {
                return $package;
            };
        }

        $loader = $this->setLoaderFallback($name, $loader);

        $this->packages->set($name, $locale, $loader);

        return parent::get($name, $locale);
    }

    /**
     * Set domain fallback for loader.
     *
     * @param string $name The name of the loader domain
     * @param callable $loader invokable loader
     * @return callable loader
     */
    public function setLoaderFallback($name, callable $loader)
    {
        $fallbackDomain = 'default';
        if (!$this->_useFallback || $name === $fallbackDomain) {
            return $loader;
        }
        $loader = function () use ($loader, $fallbackDomain) {
            /** @var \Aura\Intl\Package $package */
            $package = $loader();
            if (!$package->getFallback()) {
                $package->setFallback($fallbackDomain);
            }

            return $package;
        };

        return $loader;
    }
}
