<?php

/**
 * sfCultureInfo class file.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the BSD License.
 *
 * Copyright(c) 2004 by Qiang Xue. All rights reserved.
 *
 * To contact the author write to {@link mailto:qiang.xue@gmail.com Qiang Xue}
 * The latest version of PRADO can be obtained from:
 * {@link http://prado.sourceforge.net/}
 *
 * @author     Wei Zhuo <weizhuo[at]gmail[dot]com>
 * @version    $Id: sfCultureInfo.class.php 23810 2009-11-12 11:07:44Z Kris.Wallsmith $
 * @package    symfony
 * @subpackage i18n
 */

/**
 * sfCultureInfo class.
 *
 * Represents information about a specific culture including the
 * names of the culture, the calendar used, as well as access to
 * culture-specific objects that provide methods for common operations,
 * such as formatting dates, numbers, and currency.
 *
 * The sfCultureInfo class holds culture-specific information, such as the
 * associated language, sublanguage, country/region, calendar, and cultural
 * conventions. This class also provides access to culture-specific
 * instances of sfDateTimeFormatInfo and sfNumberFormatInfo. These objects
 * contain the information required for culture-specific operations,
 * such as formatting dates, numbers and currency.
 *
 * The culture names follow the format "<languagecode>_<country/regioncode>",
 * where <languagecode> is a lowercase two-letter code derived from ISO 639
 * codes. You can find a full list of the ISO-639 codes at
 * http://www.ics.uci.edu/pub/ietf/http/related/iso639.txt
 *
 * The <country/regioncode2> is an uppercase two-letter code derived from
 * ISO 3166. A copy of ISO-3166 can be found at
 * http://www.chemie.fu-berlin.de/diverse/doc/ISO_3166.html
 *
 * For example, Australian English is "en_AU".
 *
 * @author Xiang Wei Zhuo <weizhuo[at]gmail[dot]com>
 * @version v1.0, last update on Sat Dec 04 13:41:46 EST 2004
 * @package    symfony
 * @subpackage i18n
 */
class sfCultureInfo
{
  /**
   * ICU data filename extension.
   * @var string
   */
  protected $dataFileExt = '.dat';

  /**
   * The ICU data array.
   * @var array
   */
  protected $data = array();

  /**
   * The current culture.
   * @var string
   */
  protected $culture;

  /**
   * Directory where the ICU data is stored.
   * @var string
   */
  protected $dataDir;

  /**
   * A list of ICU date files loaded.
   * @var array
   */
  protected $dataFiles = array();

  /**
   * The current date time format info.
   * @var sfDateTimeFormatInfo
   */
  protected $dateTimeFormat;

  /**
   * The current number format info.
   * @var sfNumberFormatInfo
   */
  protected $numberFormat;
  
  /**
   * A list of properties that are accessable/writable.
   * @var array
   */ 
  protected $properties = array();

  /**
   * Culture type, all.
   * @see getCultures()
   * @var int
   */
  const ALL = 0;

  /**
   * Culture type, neutral.
   * @see getCultures()
   * @var int
   */
  const NEUTRAL = 1;

  /**
   * Culture type, specific.
   *
   * @see getCultures()
   * @var int
   */
  const SPECIFIC = 2;

  /**
   * Gets the sfCultureInfo that for this culture string.
   *
   * @param string  $culture The culture for this instance
   * @return sfCultureInfo Invariant culture info is "en"
   */
  public static function getInstance($culture = 'en')
  {
    static $instances = array();

    if (!isset($instances[$culture]))
    {
      $instances[$culture] = new sfCultureInfo($culture);
    }

    return $instances[$culture];
  }

  /**
   * Displays the culture name.
   *
   * @return string the culture name.
   * @see getName()
   */
  public function __toString()
  {
    return $this->getName();
  }

  /**
   * Allows functions that begins with 'set' to be called directly
   * as an attribute/property to retrieve the value.
   *
   * @param string $name The property to get
   * @return mixed
   */
  public function __get($name)
  {
    $getProperty = 'get'.$name;
    if (in_array($getProperty, $this->properties))
    {
      return $this->$getProperty();
    }
    else
    {
      throw new sfException(sprintf('Property %s does not exists.', $name));
    }
  }

  /**
   * Allows functions that begins with 'set' to be called directly
   * as an attribute/property to set the value.
   *
   * @param string $name  The property to set
   * @param string $value The property value
   */
  public function __set($name, $value)
  {
    $setProperty = 'set'.$name;
    if (in_array($setProperty, $this->properties))
    {
      $this->$setProperty($value);
    }
    else
    {
      throw new sfException(sprintf('Property %s can not be set.', $name));
    }
  }

  /**
   * Initializes a new instance of the sfCultureInfo class based on the 
   * culture specified by name. E.g. <code>new sfCultureInfo('en_AU');</code>
   * The culture indentifier must be of the form 
   * "<language>_(country/region/variant)".
   *
   * @param string $culture a culture name, e.g. "en_AU".
   * @return return new sfCultureInfo.
   */
  public function __construct($culture = 'en')
  {
    $this->properties = get_class_methods($this);

    if (empty($culture))
    {
      $culture = 'en';
    }

    $this->dataDir = self::dataDir();
    $this->dataFileExt = self::fileExt();

    $this->setCulture($culture);

    $this->loadCultureData('root');
    $this->loadCultureData($culture);
  }

  /**
   * Gets the default directory for the ICU data.
   * The default is the "data" directory for this class.
   *
   * @return string directory containing the ICU data.
   */
  protected static function dataDir()
  {
    return dirname(__FILE__).'/data/';
  }

  /**
   * Gets the filename extension for ICU data. Default is ".dat".
   *
   * @return string filename extension for ICU data.
   */
  protected static function fileExt()
  {
    return '.dat';
  }

  /**
   * Determines if a given culture is valid. Simply checks that the
   * culture data exists.
   *
   * @param string $culture a culture
   * @return boolean true if valid, false otherwise.
   */
  static public function validCulture($culture)
  {
    if (preg_match('/^[a-z]{2}(_[A-Z]{2,5}){0,2}$/', $culture))
    {
      return is_file(self::dataDir().$culture.self::fileExt());
    }

    return false;
  }

  /**
   * Sets the culture for the current instance. The culture indentifier
   * must be of the form "<language>_(country/region)".
   *
   * @param string $culture culture identifier, e.g. "fr_FR_EURO".
   */
  protected function setCulture($culture)
  {
    if (!empty($culture))
    {
      if (!preg_match('/^[a-z]{2}(_[A-Z]{2,5}){0,2}$/', $culture))
      {
        throw new sfException(sprintf('Invalid culture supplied: %s', $culture));
      }
    }

    $this->culture = $culture;
  }

  /**
   * Loads the ICU culture data for the specific culture identifier.
   *
   * @param string $culture the culture identifier.
   */
  protected function loadCultureData($culture)
  {
    $file_parts = explode('_', $culture);
    $current_part = $file_parts[0];

    $files = array($current_part);

    for ($i = 1, $max = count($file_parts); $i < $max; $i++)
    {
      $current_part .= '_'.$file_parts[$i];
      $files[] = $current_part;
    }

    foreach ($files as $file)
    {
      $filename = $this->dataDir.$file.$this->dataFileExt;

      if (is_file($filename) == false)
      {
        throw new sfException(sprintf('Data file for "%s" was not found.', $file));
      }

      if (in_array($filename, $this->dataFiles) == false)
      {
        array_unshift($this->dataFiles, $file);

        $data = &$this->getData($filename);
        $this->data[$file] = &$data;

        if (isset($data['__ALIAS']))
        {
          $this->loadCultureData($data['__ALIAS']);
        }
        unset($data);
      }
    }
  }

  /**
   * Gets the data by unserializing the ICU data from disk.
   * The data files are cached in a static variable inside
   * this function.
   *
   * @param string $filename the ICU data filename
   * @return array ICU data 
   */
  protected function &getData($filename)
  {
    static $data  = array();
    static $files = array();

    if (!in_array($filename, $files))
    {
      $data[$filename] = unserialize(file_get_contents($filename));
      $files[] = $filename;
    }

    return $data[$filename];
  }

  /**
   * Finds the specific ICU data information from the data.
   * The path to the specific ICU data is separated with a slash "/".
   * E.g. To find the default calendar used by the culture, the path
   * "calendar/default" will return the corresponding default calendar.
   * Use merge=true to return the ICU including the parent culture.
   * E.g. The currency data for a variant, say "en_AU" contains one
   * entry, the currency for AUD, the other currency data are stored
   * in the "en" data file. Thus to retrieve all the data regarding 
   * currency for "en_AU", you need to use findInfo("Currencies,true);.
   *
   * @param string  $path   the data you want to find.
   * @param boolean $merge  merge the data from its parents.
   * @return mixed the specific ICU data.
   */
  protected function findInfo($path = '/', $merge = false)
  {
    $result = array();
    foreach ($this->dataFiles as $section)
    {
      $info = $this->searchArray($this->data[$section], $path);

      if ($info)
      {
        if ($merge)
        {
          $result = $this->array_add($result, $info);
        }
        else
        {
          return $info;
        }
      }
    }

    return $result;
  }

  /**
   * Adds an array to an already existing array.
   * If an element is already existing in array1 it is not overwritten.
   * If this element is an array this logic will be applied recursively.
   */
  private function array_add($array1, $array2)
  {
    foreach ($array2 as $key => $value)
    {
      if (isset($array1[$key]))
      {
        if(is_array($array1[$key]) && is_array($value))
        {
          $array1[$key] = $this->array_add($array1[$key], $value);
        }
      }
      else
      {
        $array1[$key] = $value;
      }
    }
    return $array1;
  }

  /**
   * Searches the array for a specific value using a path separated using
   * slash "/" separated path. e.g to find $info['hello']['world'],
   * the path "hello/world" will return the corresponding value.
   *
   * @param array   $info  the array for search
   * @param string  $path  slash "/" separated array path.
   * @return mixed the value array using the path
   */
  protected function searchArray($info, $path = '/')
  {
    $index = explode('/', $path);

    $array = $info;

    for ($i = 0, $max = count($index); $i < $max; $i++)
    {
      $k = $index[$i];
      if ($i < $max - 1 && isset($array[$k]))
      {
        $array = $array[$k];
      }
      else if ($i == $max - 1 && isset($array[$k]))
      {
        return $array[$k];
      }
    }
  }
  
  /**
   * Gets the culture name in the format 
   * "<languagecode2>_(country/regioncode2)".
   *
   * @return string culture name.
   */
  public function getName()
  {
    return $this->culture;
  }

  /**
   * Gets the sfDateTimeFormatInfo that defines the culturally appropriate
   * format of displaying dates and times.
   *
   * @return sfDateTimeFormatInfo date time format information for the culture.
   */
  public function getDateTimeFormat()
  {
    if (null === $this->dateTimeFormat)
    {
      $calendar = $this->getCalendar();
      $info = $this->findInfo("calendar/{$calendar}", true);
      $this->setDateTimeFormat(new sfDateTimeFormatInfo($info));
    }

    return $this->dateTimeFormat;
  }

  /**
   * Sets the date time format information.
   *
   * @param sfDateTimeFormatInfo $dateTimeFormat the new date time format info.
   */
  public function setDateTimeFormat($dateTimeFormat)
  {
    $this->dateTimeFormat = $dateTimeFormat;
  }

  /**
   * Gets the default calendar used by the culture, e.g. "gregorian".
   *
   * @return string the default calendar.
   */
  public function getCalendar()
  {
    return $this->findInfo('calendar/default');
  }

  /**
   * Gets the culture name in the language that the culture is set
   * to display. Returns <code>array('Language','Country');</code>
   * 'Country' is omitted if the culture is neutral.
   *
   * @return array array with language and country as elements, localized.
   */
  public function getNativeName()
  {
    $lang = substr($this->culture, 0, 2);
    $reg = substr($this->culture, 3, 2);
    $language = $this->findInfo("Languages/{$lang}");
    $region = $this->findInfo("Countries/{$reg}");
    if ($region)
    {
      return $language.' ('.$region.')';
    }
    else
    {
      return $language;
    }
  }

  /**
   * Gets the culture name in English.
   * Returns <code>array('Language','Country');</code>
   * 'Country' is omitted if the culture is neutral.
   *
   * @return array array with language and country as elements.
   */
  public function getEnglishName()
  {
    $lang = substr($this->culture, 0, 2);
    $reg = substr($this->culture, 3, 2);
    $culture = $this->getInvariantCulture();

    $language = $culture->findInfo("Languages/{$lang}");
    if (count($language) == 0)
    {
      return $this->culture;
    }

    $region = $culture->findInfo("Countries/{$reg}");

    return $region ? $language.' ('.$region.')' : $language;
  }

  /**
   * Gets the sfCultureInfo that is culture-independent (invariant).
   * Any changes to the invariant culture affects all other
   * instances of the invariant culture.
   * The invariant culture is assumed to be "en";
   *
   * @return sfCultureInfo invariant culture info is "en".
   */
  static function getInvariantCulture()
  {
    static $invariant;

    if (null === $invariant)
    {
      $invariant = new sfCultureInfo();
    }

    return $invariant;
  }

  /**
   * Gets a value indicating whether the current sfCultureInfo 
   * represents a neutral culture. Returns true if the culture
   * only contains two characters.
   *
   * @return boolean true if culture is neutral, false otherwise.
   */
  public function getIsNeutralCulture()
  {
    return strlen($this->culture) == 2;
  }

  /**
   * Gets the sfNumberFormatInfo that defines the culturally appropriate
   * format of displaying numbers, currency, and percentage.
   *
   * @return sfNumberFormatInfo the number format info for current culture.
   */
  public function getNumberFormat()
  {
    if (null === $this->numberFormat)
    {
      $elements = $this->findInfo('NumberElements');
      $patterns = $this->findInfo('NumberPatterns');
      $currencies = $this->getCurrencies(null, true);
      $data = array('NumberElements' => $elements, 'NumberPatterns' => $patterns, 'Currencies' => $currencies);

      $this->setNumberFormat(new sfNumberFormatInfo($data));
    }

    return $this->numberFormat;
  }

  /**
   * Sets the number format information.
   *
   * @param sfNumberFormatInfo $numberFormat the new number format info.
   */
  public function setNumberFormat($numberFormat)
  {
    $this->numberFormat = $numberFormat;
  }

  /**
   * Gets the sfCultureInfo that represents the parent culture of the 
   * current sfCultureInfo
   *
   * @return sfCultureInfo parent culture information.
   */
  public function getParent()
  {
    if (strlen($this->culture) == 2)
    {
      return $this->getInvariantCulture();
    }

    return new sfCultureInfo(substr($this->culture, 0, 2));
  }

  /**
   * Gets the list of supported cultures filtered by the specified 
   * culture type. This is an EXPENSIVE function, it needs to traverse
   * a list of ICU files in the data directory.
   * This function can be called statically.
   *
   * @param int $type culture type, sfCultureInfo::ALL, sfCultureInfo::NEUTRAL
   * or sfCultureInfo::SPECIFIC.
   * @return array list of culture information available. 
   */
  static function getCultures($type = sfCultureInfo::ALL)
  {
    $dataDir = sfCultureInfo::dataDir();
    $dataExt = sfCultureInfo::fileExt();
    $dir = dir($dataDir);

    $neutral = array();
    $specific = array();

    while (false !== ($entry = $dir->read()))
    {
      if (is_file($dataDir.$entry) && substr($entry, -4) == $dataExt && $entry != 'root'.$dataExt)
      {
        $culture = substr($entry, 0, -4);
        if (strlen($culture) == 2)
        {
          $neutral[] = $culture;
        }
        else
        {
          $specific[] = $culture;
        }
      }
    }
    $dir->close();

    switch ($type)
    {
      case sfCultureInfo::ALL:
        $all =  array_merge($neutral, $specific);
        sort($all);
        return $all;
        break;
      case sfCultureInfo::NEUTRAL:
        return $neutral;
        break;
      case sfCultureInfo::SPECIFIC:
        return $specific;
        break;
    }
  }

  /**
   * Get the country name in the current culture for the given code.
   *
   * @param  string $code A valid country code
   *
   * @return string The country name in the current culture
   */
  public function getCountry($code)
  {
    $countries = $this->findInfo('Countries', true);

    if (!isset($countries[$code]))
    {
      throw new InvalidArgumentException(sprintf('The country %s does not exist.', $code));
    }

    return $countries[$code];
  }

  /**
   * Get the currency name in the current culture for the given code.
   *
   * @param  string $code A valid currency code
   *
   * @return string The currency name in the current culture
   */
  public function getCurrency($code)
  {
    $currencies = $this->findInfo('Currencies', true);

    if (!isset($currencies[$code]))
    {
      throw new InvalidArgumentException(sprintf('The currency %s does not exist.', $code));
    }

    return $currencies[$code][1];
  }

  /**
   * Get the language name in the current culture for the given code.
   *
   * @param  string $code A valid language code
   *
   * @return string The language name in the current culture
   */
  public function getLanguage($code)
  {
    $languages = $this->findInfo('Languages', true);

    if (!isset($languages[$code]))
    {
      throw new InvalidArgumentException(sprintf('The language %s does not exist.', $code));
    }

    return $languages[$code];
  }

  /**
   * Gets a list of countries in the language of the localized version.
   *
   * @param  array $countries An array of countries used to restrict the returned array (null by default, which means all countries)
   *
   * @return array a list of localized country names. 
   */
  public function getCountries($countries = null)
  {
    $allCountries = $this->findInfo('Countries', true);

    // restrict countries to a sub-set
    if (null !== $countries)
    {
      if ($problems = array_diff($countries, array_keys($allCountries)))
      {
        throw new InvalidArgumentException(sprintf('The following countries do not exist: %s.', implode(', ', $problems)));
      }

      $allCountries = array_intersect_key($allCountries, array_flip($countries));
    }

    $this->sortArray($allCountries);

    return $allCountries;
  }

  /**
   * Gets a list of currencies in the language of the localized version.
   *
   * @param  array   $currencies An array of currencies used to restrict the returned array (null by default, which means all currencies)
   * @param  Boolean $full       Whether to return the symbol and the name or not (false by default)
   *
   * @return array a list of localized currencies.
   */
  public function getCurrencies($currencies = null, $full = false)
  {
    $allCurrencies = $this->findInfo('Currencies', true);

    // restrict countries to a sub-set
    if (null !== $currencies)
    {
      if ($problems = array_diff($currencies, array_keys($allCurrencies)))
      {
        throw new InvalidArgumentException(sprintf('The following currencies do not exist: %s.', implode(', ', $problems)));
      }

      $allCurrencies = array_intersect_key($allCurrencies, array_flip($currencies));
    }

    if (!$full)
    {
      foreach ($allCurrencies as $key => $value)
      {
        $allCurrencies[$key] = $value[1];
      }
    }

    $this->sortArray($allCurrencies);

    return $allCurrencies;
  }

  /**
   * Gets a list of languages in the language of the localized version.
   *
   * @param  array $languages An array of languages used to restrict the returned array (null by default, which means all languages)
   *
   * @return array list of localized language names.
   */
  public function getLanguages($languages = null)
  {
    $allLanguages = $this->findInfo('Languages', true);

    // restrict languages to a sub-set
    if (null !== $languages)
    {
      if ($problems = array_diff($languages, array_keys($allLanguages)))
      {
        throw new InvalidArgumentException(sprintf('The following languages do not exist: %s.', implode(', ', $problems)));
      }

      $allLanguages = array_intersect_key($allLanguages, array_flip($languages));
    }

    $this->sortArray($allLanguages);

    return $allLanguages;
  }

  /**
   * Gets a list of scripts in the language of the localized version.
   *
   * @return array list of localized script names.
   */
  public function getScripts()
  {
    return $this->findInfo('Scripts', true);
  }

  /**
   * Gets a list of timezones in the language of the localized version.
   *
   * @return array list of localized timezones.
   */
  public function getTimeZones()
  {
    //new format since ICU 3.8
    //zoneStrings contains metaTimezones
    $metadata = $this->findInfo('zoneStrings', true);
    //TimeZones contains the Timezone name => metaTimezone identifier
    $timeZones = $this->findInfo('TimeZones', true);
    foreach ($timeZones as $key => $value)
    {
      $timeZones[$key] = $metadata['meta:'.$value];
      $timeZones[$key]['identifier'] = $key;
      $timeZones[$key]['city'] = str_replace('_', ' ', substr($key, strpos($key, '/') + 1));
    }
    return $timeZones;
  }

  /**
   * sorts the passed array according to the locale of this sfCultureInfo class
   *
   * @param  array the array to pe sorted wiht "asort" and this locale
   */
  public function sortArray(&$array)
  {
    $oldLocale = setlocale(LC_COLLATE, 0);
    setlocale(LC_COLLATE, $this->getName());
    asort($array, SORT_LOCALE_STRING);
    setlocale(LC_COLLATE, $oldLocale);
  }
}
