<?php
/**
 * Zend Framework (http://framework.zend.com/)
 *
 * @link      http://github.com/zendframework/zf2 for the canonical source repository
 * @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
 * @license   http://framework.zend.com/license/new-bsd New BSD License
 */

namespace Zend\I18n\Translator;

use 
Locale;
use 
Traversable;
use 
Zend\Cache;
use 
Zend\Cache\Storage\StorageInterface as CacheStorage;
use 
Zend\I18n\Exception;
use 
Zend\I18n\Translator\Loader\FileLoaderInterface;
use 
Zend\I18n\Translator\Loader\RemoteLoaderInterface;
use 
Zend\Stdlib\ArrayUtils;

/**
 * Translator.
 */
class Translator
{
    
/**
     * Messages loaded by the translator.
     *
     * @var array
     */
    
protected $messages = array();

    
/**
     * Files used for loading messages.
     *
     * @var array
     */
    
protected $files = array();

    
/**
     * Patterns used for loading messages.
     *
     * @var array
     */
    
protected $patterns = array();

    
/**
     * Remote locations for loading messages.
     *
     * @var array
     */
    
protected $remote = array();

    
/**
     * Default locale.
     *
     * @var string
     */
    
protected $locale;

    
/**
     * Locale to use as fallback if there is no translation.
     *
     * @var string
     */
    
protected $fallbackLocale;

    
/**
     * Translation cache.
     *
     * @var CacheStorage
     */
    
protected $cache;

    
/**
     * Plugin manager for translation loaders.
     *
     * @var LoaderPluginManager
     */
    
protected $pluginManager;

    
/**
     * Instantiate a translator
     *
     * @param  array|Traversable                  $options
     * @return Translator
     * @throws Exception\InvalidArgumentException
     */
    
public static function factory($options)
    {
        if (
$options instanceof Traversable) {
            
$options ArrayUtils::iteratorToArray($options);
        } elseif (!
is_array($options)) {
            throw new 
Exception\InvalidArgumentException(sprintf(
                
'%s expects an array or Traversable object; received "%s"',
                
__METHOD__,
                (
is_object($options) ? get_class($options) : gettype($options))
            ));
        }

        
$translator = new static();

        
// locales
        
if (isset($options['locale'])) {
            
$locales = (array) $options['locale'];
            
$translator->setLocale(array_shift($locales));
            if (
count($locales) > 0) {
                
$translator->setFallbackLocale(array_shift($locales));
            }
        }

        
// file patterns
        
if (isset($options['translation_file_patterns'])) {
            if (!
is_array($options['translation_file_patterns'])) {
                throw new 
Exception\InvalidArgumentException(
                    
'"translation_file_patterns" should be an array'
                
);
            }

            
$requiredKeys = array('type''base_dir''pattern');
            foreach (
$options['translation_file_patterns'] as $pattern) {
                foreach (
$requiredKeys as $key) {
                    if (!isset(
$pattern[$key])) {
                        throw new 
Exception\InvalidArgumentException(
                            
"'{$key}' is missing for translation pattern options"
                        
);
                    }
                }

                
$translator->addTranslationFilePattern(
                    
$pattern['type'],
                    
$pattern['base_dir'],
                    
$pattern['pattern'],
                    isset(
$pattern['text_domain']) ? $pattern['text_domain'] : 'default'
                
);
            }
        }

        
// files
        
if (isset($options['translation_files'])) {
            if (!
is_array($options['translation_files'])) {
                throw new 
Exception\InvalidArgumentException(
                    
'"translation_files" should be an array'
                
);
            }

            
$requiredKeys = array('type''filename');
            foreach (
$options['translation_files'] as $file) {
                foreach (
$requiredKeys as $key) {
                    if (!isset(
$file[$key])) {
                        throw new 
Exception\InvalidArgumentException(
                            
"'{$key}' is missing for translation file options"
                        
);
                    }
                }

                
$translator->addTranslationFile(
                    
$file['type'],
                    
$file['filename'],
                    isset(
$file['text_domain']) ? $file['text_domain'] : 'default',
                    isset(
$file['locale']) ? $file['locale'] : null
                
);
            }
        }

        
// remote
        
if (isset($options['remote_translation'])) {
            if (!
is_array($options['remote_translation'])) {
                throw new 
Exception\InvalidArgumentException(
                    
'"remote_translation" should be an array'
                
);
            }

            
$requiredKeys = array('type');
            foreach (
$options['remote_translation'] as $remote) {
                foreach (
$requiredKeys as $key) {
                    if (!isset(
$remote[$key])) {
                        throw new 
Exception\InvalidArgumentException(
                            
"'{$key}' is missing for remote translation options"
                        
);
                    }
                }

                
$translator->addRemoteTranslations(
                    
$remote['type'],
                    isset(
$remote['text_domain']) ? $remote['text_domain'] : 'default'
                
);
            }
        }

        
// cache
        
if (isset($options['cache'])) {
            if (
$options['cache'] instanceof CacheStorage) {
                
$translator->setCache($options['cache']);
            } else {
                
$translator->setCache(Cache\StorageFactory::factory($options['cache']));
            }
        }

        return 
$translator;
    }

    
/**
     * Set the default locale.
     *
     * @param  string     $locale
     * @return Translator
     */
    
public function setLocale($locale)
    {
        
$this->locale $locale;

        return 
$this;
    }

    
/**
     * Get the default locale.
     *
     * @return string
     */
    
public function getLocale()
    {
        if (
$this->locale === null) {
            
$this->locale Locale::getDefault();
        }

        return 
$this->locale;
    }

    
/**
     * Set the fallback locale.
     *
     * @param  string     $locale
     * @return Translator
     */
    
public function setFallbackLocale($locale)
    {
        
$this->fallbackLocale $locale;

        return 
$this;
    }

    
/**
     * Get the fallback locale.
     *
     * @return string
     */
    
public function getFallbackLocale()
    {
        return 
$this->fallbackLocale;
    }

    
/**
     * Sets a cache
     *
     * @param  CacheStorage $cache
     * @return Translator
     */
    
public function setCache(CacheStorage $cache null)
    {
        
$this->cache $cache;

        return 
$this;
    }

    
/**
     * Returns the set cache
     *
     * @return CacheStorage The set cache
     */
    
public function getCache()
    {
        return 
$this->cache;
    }

    
/**
     * Set the plugin manager for translation loaders
     *
     * @param  LoaderPluginManager $pluginManager
     * @return Translator
     */
    
public function setPluginManager(LoaderPluginManager $pluginManager)
    {
        
$this->pluginManager $pluginManager;

        return 
$this;
    }

    
/**
     * Retrieve the plugin manager for translation loaders.
     *
     * Lazy loads an instance if none currently set.
     *
     * @return LoaderPluginManager
     */
    
public function getPluginManager()
    {
        if (!
$this->pluginManager instanceof LoaderPluginManager) {
            
$this->setPluginManager(new LoaderPluginManager());
        }

        return 
$this->pluginManager;
    }

    
/**
     * Translate a message.
     *
     * @param  string $message
     * @param  string $textDomain
     * @param  string $locale
     * @return string
     */
    
public function translate($message$textDomain 'default'$locale null)
    {
        
$locale      = ($locale ?: $this->getLocale());
        
$translation $this->getTranslatedMessage($message$locale$textDomain);

        if (
$translation !== null && $translation !== '') {
            return 
$translation;
        }

        if (
null !== ($fallbackLocale $this->getFallbackLocale())
            && 
$locale !== $fallbackLocale
        
) {
            return 
$this->translate($message$textDomain$fallbackLocale);
        }

        return 
$message;
    }

    
/**
     * Translate a plural message.
     *
     * @param  string                         $singular
     * @param  string                         $plural
     * @param  int                            $number
     * @param  string                         $textDomain
     * @param  string|null                    $locale
     * @return string
     * @throws Exception\OutOfBoundsException
     */
    
public function translatePlural(
        
$singular,
        
$plural,
        
$number,
        
$textDomain 'default',
        
$locale null
    
) {
        
$locale      $locale ?: $this->getLocale();
        
$translation $this->getTranslatedMessage($singular$locale$textDomain);

        if (
$translation === null || $translation === '') {
            if (
null !== ($fallbackLocale $this->getFallbackLocale())
                && 
$locale !== $fallbackLocale
            
) {
                return 
$this->translatePlural(
                    
$singular,
                    
$plural,
                    
$number,
                    
$textDomain,
                    
$fallbackLocale
                
);
            }

            return (
$number == $singular $plural);
        }

        
$index $this->messages[$textDomain][$locale]
                      ->
getPluralRule()
                      ->
evaluate($number);

        if (!isset(
$translation[$index])) {
            throw new 
Exception\OutOfBoundsException(sprintf(
                
'Provided index %d does not exist in plural array'$index
            
));
        }

        return 
$translation[$index];
    }

    
/**
     * Get a translated message.
     *
     * @param  string $message
     * @param  string $locale
     * @param  string $textDomain
     * @return string|null
     */
    
protected function getTranslatedMessage(
        
$message,
        
$locale null,
        
$textDomain 'default'
    
) {
        if (
$message === '') {
            return 
'';
        }

        if (!isset(
$this->messages[$textDomain][$locale])) {
            
$this->loadMessages($textDomain$locale);
        }

        if (isset(
$this->messages[$textDomain][$locale][$message])) {
            return 
$this->messages[$textDomain][$locale][$message];
        }

        return 
null;
    }

    
/**
     * Add a translation file.
     *
     * @param  string     $type
     * @param  string     $filename
     * @param  string     $textDomain
     * @param  string     $locale
     * @return Translator
     */
    
public function addTranslationFile(
        
$type,
        
$filename,
        
$textDomain 'default',
        
$locale null
    
) {
        
$locale $locale ?: '*';

        if (!isset(
$this->files[$textDomain])) {
            
$this->files[$textDomain] = array();
        }

        
$this->files[$textDomain][$locale][] = array(
            
'type' => $type,
            
'filename' => $filename,
        );

        return 
$this;
    }

    
/**
     * Add multiple translations with a file pattern.
     *
     * @param  string     $type
     * @param  string     $baseDir
     * @param  string     $pattern
     * @param  string     $textDomain
     * @return Translator
     */
    
public function addTranslationFilePattern(
        
$type,
        
$baseDir,
        
$pattern,
        
$textDomain 'default'
    
) {
        if (!isset(
$this->patterns[$textDomain])) {
            
$this->patterns[$textDomain] = array();
        }

        
$this->patterns[$textDomain][] = array(
            
'type'    => $type,
            
'baseDir' => rtrim($baseDir'/'),
            
'pattern' => $pattern,
        );

        return 
$this;
    }

    
/**
     * Add remote translations.
     *
     * @param  string     $type
     * @param  string     $textDomain
     * @return Translator
     */
    
public function addRemoteTranslations($type$textDomain 'default')
    {
        if (!isset(
$this->remote[$textDomain])) {
            
$this->remote[$textDomain] = array();
        }

        
$this->remote[$textDomain][] = $type;

        return 
$this;
    }

    
/**
     * Load messages for a given language and domain.
     *
     * @param  string                     $textDomain
     * @param  string                     $locale
     * @throws Exception\RuntimeException
     * @return void
     */
    
protected function loadMessages($textDomain$locale)
    {
        if (!isset(
$this->messages[$textDomain])) {
            
$this->messages[$textDomain] = array();
        }

        if (
null !== ($cache $this->getCache())) {
            
$cacheId 'Zend_I18n_Translator_Messages_' md5($textDomain $locale);

            if (
null !== ($result $cache->getItem($cacheId))) {
                
$this->messages[$textDomain][$locale] = $result;

                return;
            }
        }

        
$messagesLoaded  false;
        
$messagesLoaded |= $this->loadMessagesFromRemote($textDomain$locale);
        
$messagesLoaded |= $this->loadMessagesFromPatterns($textDomain$locale);
        
$messagesLoaded |= $this->loadMessagesFromFiles($textDomain$locale);

        if (!
$messagesLoaded) {
            
$this->messages[$textDomain][$locale] = null;
        } elseif (
$cache !== null) {
            
$cache->setItem($cacheId$this->messages[$textDomain][$locale]);
        }
    }

    
/**
     * Load messages from remote sources.
     *
     * @param  string $textDomain
     * @param  string $locale
     * @return boolean
     * @throws Exception\RuntimeException When specified loader is not a remote loader
     */
    
protected function loadMessagesFromRemote($textDomain$locale)
    {
        
$messagesLoaded false;

        if (isset(
$this->remote[$textDomain])) {
            foreach (
$this->remote[$textDomain] as $loaderType) {
                
$loader $this->getPluginManager()->get($loaderType);

                if (!
$loader instanceof RemoteLoaderInterface) {
                    throw new 
Exception\RuntimeException('Specified loader is not a remote loader');
                }

                if (isset(
$this->messages[$textDomain][$locale])) {
                    
$this->messages[$textDomain][$locale]->merge($loader->load($locale$textDomain));
                } else {
                    
$this->messages[$textDomain][$locale] = $loader->load($locale$textDomain);
                }

                
$messagesLoaded true;
            }
        }

        return 
$messagesLoaded;
    }

    
/**
     * Load messages from patterns.
     *
     * @param  string $textDomain
     * @param  string $locale
     * @return boolean
     * @throws Exception\RuntimeException When specified loader is not a file loader
     */
    
protected function loadMessagesFromPatterns($textDomain$locale)
    {
        
$messagesLoaded false;

        if (isset(
$this->patterns[$textDomain])) {
            foreach (
$this->patterns[$textDomain] as $pattern) {
                
$filename $pattern['baseDir'] . '/' sprintf($pattern['pattern'], $locale);

                if (
is_file($filename)) {
                    
$loader $this->getPluginManager()->get($pattern['type']);

                    if (!
$loader instanceof FileLoaderInterface) {
                        throw new 
Exception\RuntimeException('Specified loader is not a file loader');
                    }

                    if (isset(
$this->messages[$textDomain][$locale])) {
                        
$this->messages[$textDomain][$locale]->merge($loader->load($locale$filename));
                    } else {
                        
$this->messages[$textDomain][$locale] = $loader->load($locale$filename);
                    }

                    
$messagesLoaded true;
                }
            }
        }

        return 
$messagesLoaded;
    }

    
/**
     * Load messages from files.
     *
     * @param  string $textDomain
     * @param  string $locale
     * @return boolean
     * @throws Exception\RuntimeException When specified loader is not a file loader
     */
    
protected function loadMessagesFromFiles($textDomain$locale)
    {
        
$messagesLoaded false;

        foreach (array(
$locale'*') as $currentLocale) {
            if (!isset(
$this->files[$textDomain][$currentLocale])) {
                continue;
            }

            foreach (
$this->files[$textDomain][$currentLocale] as $file) {
                
$loader $this->getPluginManager()->get($file['type']);

                if (!
$loader instanceof FileLoaderInterface) {
                    throw new 
Exception\RuntimeException('Specified loader is not a file loader');
                }

                if (isset(
$this->messages[$textDomain][$locale])) {
                    
$this->messages[$textDomain][$locale]->merge($loader->load($locale$file['filename']));
                } else {
                    
$this->messages[$textDomain][$locale] = $loader->load($locale$file['filename']);
                }

                
$messagesLoaded true;
            }

            unset(
$this->files[$textDomain][$currentLocale]);
        }

        return 
$messagesLoaded;
    }
}