<?php

/**
 * Timezones manipulation object.
 *
 * @author     Time.ly Network, Inc.
 * @since      2.0
 * @package    Ai1EC
 * @subpackage Ai1EC.Date
 */
class Ai1ec_Date_Timezone extends Ai1ec_Base {

    /**
     * @var Ai1ec_Cache_Interface In-memory storage for timezone objects.
     */
    protected $_cache           = null;

    /**
     * @var array Map of timezone names and their Olson TZ counterparts.
     */
    protected $_zones           = array(
        '+00:00'                           => 'UTC',
        'Z'                                => 'UTC',
        'AUS Central Standard Time'        => 'Australia/Darwin',
        'AUS Eastern Standard Time'        => 'Australia/Sydney',
        'Acre'                             => 'America/Rio_Branco',
        'Afghanistan'                      => 'Asia/Kabul',
        'Afghanistan Standard Time'        => 'Asia/Kabul',
        'Africa_Central'                   => 'Africa/Maputo',
        'Africa_Eastern'                   => 'Africa/Nairobi',
        'Africa_FarWestern'                => 'Africa/El_Aaiun',
        'Africa_Southern'                  => 'Africa/Johannesburg',
        'Africa_Western'                   => 'Africa/Lagos',
        'Aktyubinsk'                       => 'Asia/Aqtobe',
        'Alaska'                           => 'America/Juneau',
        'Alaska_Hawaii'                    => 'America/Anchorage',
        'Alaskan Standard Time'            => 'America/Anchorage',
        'Almaty'                           => 'Asia/Almaty',
        'Amazon'                           => 'America/Manaus',
        'America_Central'                  => 'America/Chicago',
        'America_Eastern'                  => 'America/New_York',
        'America_Mountain'                 => 'America/Denver',
        'America_Pacific'                  => 'America/Los_Angeles',
        'Anadyr'                           => 'Asia/Anadyr',
        'Aqtau'                            => 'Asia/Aqtau',
        'Aqtobe'                           => 'Asia/Aqtobe',
        'Arab Standard Time'               => 'Asia/Riyadh',
        'Arabian'                          => 'Asia/Riyadh',
        'Arabian Standard Time'            => 'Asia/Dubai',
        'Arabic Standard Time'             => 'Asia/Baghdad',
        'Argentina'                        => 'America/Buenos_Aires',
        'Argentina Standard Time'          => 'America/Buenos_Aires',
        'Argentina_Western'                => 'America/Mendoza',
        'Armenia'                          => 'Asia/Yerevan',
        'Armenian Standard Time'           => 'Asia/Yerevan',
        'Ashkhabad'                        => 'Asia/Ashgabat',
        'Atlantic'                         => 'America/Halifax',
        'Atlantic Standard Time'           => 'America/Halifax',
        'Australia_Central'                => 'Australia/Adelaide',
        'Australia_CentralWestern'         => 'Australia/Eucla',
        'Australia_Eastern'                => 'Australia/Sydney',
        'Australia_Western'                => 'Australia/Perth',
        'Azerbaijan'                       => 'Asia/Baku',
        'Azerbaijan Standard Time'         => 'Asia/Baku',
        'Azores'                           => 'Atlantic/Azores',
        'Azores Standard Time'             => 'Atlantic/Azores',
        'Baku'                             => 'Asia/Baku',
        'Bangladesh'                       => 'Asia/Dhaka',
        'Bering'                           => 'America/Adak',
        'Bhutan'                           => 'Asia/Thimphu',
        'Bolivia'                          => 'America/La_Paz',
        'Borneo'                           => 'Asia/Kuching',
        'Brasilia'                         => 'America/Sao_Paulo',
        'British'                          => 'Europe/London',
        'Brunei'                           => 'Asia/Brunei',
        'Canada Central Standard Time'     => 'America/Regina',
        'Cape Verde Standard Time'         => 'Atlantic/Cape_Verde',
        'Cape_Verde'                       => 'Atlantic/Cape_Verde',
        'Caucasus Standard Time'           => 'Asia/Yerevan',
        'Cen. Australia Standard Time'     => 'Australia/Adelaide',
        'Central America Standard Time'    => 'America/Guatemala',
        'Central Asia Standard Time'       => 'Asia/Dhaka',
        'Central Brazilian Standard Time'  => 'America/Manaus',
        'Central Europe Standard Time'     => 'Europe/Budapest',
        'Central European Standard Time'   => 'Europe/Warsaw',
        'Central Pacific Standard Time'    => 'Pacific/Guadalcanal',
        'Central Standard Time'            => 'America/Chicago',
        'Central Standard Time (Mexico)'   => 'America/Mexico_City',
        'Chamorro'                         => 'Pacific/Saipan',
        'Changbai'                         => 'Asia/Harbin',
        'Chatham'                          => 'Pacific/Chatham',
        'Chile'                            => 'America/Santiago',
        'China'                            => 'Asia/Shanghai',
        'China Standard Time'              => 'Asia/Shanghai',
        'Choibalsan'                       => 'Asia/Choibalsan',
        'Christmas'                        => 'Indian/Christmas',
        'Cocos'                            => 'Indian/Cocos',
        'Colombia'                         => 'America/Bogota',
        'Cook'                             => 'Pacific/Rarotonga',
        'Cuba'                             => 'America/Havana',
        'Dacca'                            => 'Asia/Dhaka',
        'Dateline Standard Time'           => 'Etc/GMT+12',
        'Davis'                            => 'Antarctica/Davis',
        'Dominican'                        => 'America/Santo_Domingo',
        'DumontDUrville'                   => 'Antarctica/DumontDUrville',
        'Dushanbe'                         => 'Asia/Dushanbe',
        'Dutch_Guiana'                     => 'America/Paramaribo',
        'E. Africa Standard Time'          => 'Africa/Nairobi',
        'E. Australia Standard Time'       => 'Australia/Brisbane',
        'E. Europe Standard Time'          => 'Europe/Minsk',
        'E. South America Standard Time'   => 'America/Sao_Paulo',
        'East_Timor'                       => 'Asia/Dili',
        'Easter'                           => 'Pacific/Easter',
        'Eastern Standard Time'            => 'America/New_York',
        'Ecuador'                          => 'America/Guayaquil',
        'Egypt Standard Time'              => 'Africa/Cairo',
        'Ekaterinburg Standard Time'       => 'Asia/Yekaterinburg',
        'Europe_Central'                   => 'Europe/Paris',
        'Europe_Eastern'                   => 'Europe/Bucharest',
        'Europe_Western'                   => 'Atlantic/Canary',
        'FLE Standard Time'                => 'Europe/Kiev',
        'Falkland'                         => 'Atlantic/Stanley',
        'Fiji'                             => 'Pacific/Fiji',
        'Fiji Standard Time'               => 'Pacific/Fiji',
        'French_Guiana'                    => 'America/Cayenne',
        'French_Southern'                  => 'Indian/Kerguelen',
        'Frunze'                           => 'Asia/Bishkek',
        'GMT'                              => 'UTC', // seems better than 'Atlantic/Reykjavik'
        'GMT Standard Time'                => 'Europe/London',
        'GTB Standard Time'                => 'Europe/Istanbul',
        'Galapagos'                        => 'Pacific/Galapagos',
        'Gambier'                          => 'Pacific/Gambier',
        'Georgia'                          => 'Asia/Tbilisi',
        'Georgian Standard Time'           => 'Etc/GMT-3',
        'Gilbert_Islands'                  => 'Pacific/Tarawa',
        'Goose_Bay'                        => 'America/Goose_Bay',
        'Greenland Standard Time'          => 'America/Godthab',
        'Greenland_Central'                => 'America/Scoresbysund',
        'Greenland_Eastern'                => 'America/Scoresbysund',
        'Greenland_Western'                => 'America/Godthab',
        'Greenwich Standard Time'          => 'Atlantic/Reykjavik',
        'Guam'                             => 'Pacific/Guam',
        'Gulf'                             => 'Asia/Dubai',
        'Guyana'                           => 'America/Guyana',
        'Hawaii_Aleutian'                  => 'Pacific/Honolulu',
        'Hawaiian Standard Time'           => 'Pacific/Honolulu',
        'Hong_Kong'                        => 'Asia/Hong_Kong',
        'Hovd'                             => 'Asia/Hovd',
        'India'                            => 'Asia/Calcutta',
        'India Standard Time'              => 'Asia/Calcutta',
        'Indian_Ocean'                     => 'Indian/Chagos',
        'Indochina'                        => 'Asia/Saigon',
        'Indonesia_Central'                => 'Asia/Makassar',
        'Indonesia_Eastern'                => 'Asia/Jayapura',
        'Indonesia_Western'                => 'Asia/Jakarta',
        'Iran'                             => 'Asia/Tehran',
        'Iran Standard Time'               => 'Asia/Tehran',
        'Irish'                            => 'Europe/Dublin',
        'Irkutsk'                          => 'Asia/Irkutsk',
        'Israel'                           => 'Asia/Jerusalem',
        'Israel Standard Time'             => 'Asia/Jerusalem',
        'Japan'                            => 'Asia/Tokyo',
        'Jordan Standard Time'             => 'Asia/Amman',
        'Kamchatka'                        => 'Asia/Kamchatka',
        'Karachi'                          => 'Asia/Karachi',
        'Kashgar'                          => 'Asia/Kashgar',
        'Kazakhstan_Eastern'               => 'Asia/Almaty',
        'Kazakhstan_Western'               => 'Asia/Aqtobe',
        'Kizilorda'                        => 'Asia/Qyzylorda',
        'Korea'                            => 'Asia/Seoul',
        'Korea Standard Time'              => 'Asia/Seoul',
        'Kosrae'                           => 'Pacific/Kosrae',
        'Krasnoyarsk'                      => 'Asia/Krasnoyarsk',
        'Kuybyshev'                        => 'Europe/Samara',
        'Kwajalein'                        => 'Pacific/Kwajalein',
        'Kyrgystan'                        => 'Asia/Bishkek',
        'Lanka'                            => 'Asia/Colombo',
        'Liberia'                          => 'Africa/Monrovia',
        'Line_Islands'                     => 'Pacific/Kiritimati',
        'Long_Shu'                         => 'Asia/Chongqing',
        'Lord_Howe'                        => 'Australia/Lord_Howe',
        'Macau'                            => 'Asia/Macau',
        'Magadan'                          => 'Asia/Magadan',
        'Malaya'                           => 'Asia/Kuala_Lumpur',
        'Malaysia'                         => 'Asia/Kuching',
        'Maldives'                         => 'Indian/Maldives',
        'Marquesas'                        => 'Pacific/Marquesas',
        'Marshall_Islands'                 => 'Pacific/Majuro',
        'Mauritius'                        => 'Indian/Mauritius',
        'Mauritius Standard Time'          => 'Indian/Mauritius',
        'Mawson'                           => 'Antarctica/Mawson',
        'Mexico Standard Time'             => 'America/Mexico_City',
        'Mexico Standard Time 2'           => 'America/Chihuahua',
        'Mid-Atlantic Standard Time'       => 'Atlantic/South_Georgia',
        'Middle East Standard Time'        => 'Asia/Beirut',
        'Mongolia'                         => 'Asia/Ulaanbaatar',
        'Montevideo Standard Time'         => 'America/Montevideo',
        'Morocco Standard Time'            => 'Africa/Casablanca',
        'Moscow'                           => 'Europe/Moscow',
        'Mountain Standard Time'           => 'America/Denver',
        'Mountain Standard Time (Mexico)'  => 'America/Chihuahua',
        'Myanmar'                          => 'Asia/Rangoon',
        'Myanmar Standard Time'            => 'Asia/Rangoon',
        'N. Central Asia Standard Time'    => 'Asia/Novosibirsk',
        'Namibia Standard Time'            => 'Africa/Windhoek',
        'Nauru'                            => 'Pacific/Nauru',
        'Nepal'                            => 'Asia/Katmandu',
        'Nepal Standard Time'              => 'Asia/Katmandu',
        'New Zealand Standard Time'        => 'Pacific/Auckland',
        'New_Caledonia'                    => 'Pacific/Noumea',
        'New_Zealand'                      => 'Pacific/Auckland',
        'Newfoundland'                     => 'America/St_Johns',
        'Newfoundland Standard Time'       => 'America/St_Johns',
        'Niue'                             => 'Pacific/Niue',
        'Norfolk'                          => 'Pacific/Norfolk',
        'Noronha'                          => 'America/Noronha',
        'North Asia East Standard Time'    => 'Asia/Irkutsk',
        'North Asia Standard Time'         => 'Asia/Krasnoyarsk',
        'North_Mariana'                    => 'Pacific/Saipan',
        'Novosibirsk'                      => 'Asia/Novosibirsk',
        'Omsk'                             => 'Asia/Omsk',
        'Oral'                             => 'Asia/Oral',
        'Pacific SA Standard Time'         => 'America/Santiago',
        'Pacific Standard Time'            => 'America/Los_Angeles',
        'Pacific Standard Time (Mexico)'   => 'America/Tijuana',
        'Pakistan'                         => 'Asia/Karachi',
        'Pakistan Standard Time'           => 'Asia/Karachi',
        'Palau'                            => 'Pacific/Palau',
        'Papua_New_Guinea'                 => 'Pacific/Port_Moresby',
        'Paraguay'                         => 'America/Asuncion',
        'Peru'                             => 'America/Lima',
        'Philippines'                      => 'Asia/Manila',
        'Phoenix_Islands'                  => 'Pacific/Enderbury',
        'Pierre_Miquelon'                  => 'America/Miquelon',
        'Pitcairn'                         => 'Pacific/Pitcairn',
        'Ponape'                           => 'Pacific/Ponape',
        'Qyzylorda'                        => 'Asia/Qyzylorda',
        'Reunion'                          => 'Indian/Reunion',
        'Romance Standard Time'            => 'Europe/Paris',
        'Rothera'                          => 'Antarctica/Rothera',
        'Russian Standard Time'            => 'Europe/Moscow',
        'SA Eastern Standard Time'         => 'Etc/GMT+3',
        'SA Pacific Standard Time'         => 'America/Bogota',
        'SA Western Standard Time'         => 'America/La_Paz',
        'SE Asia Standard Time'            => 'Asia/Bangkok',
        'Sakhalin'                         => 'Asia/Sakhalin',
        'Samara'                           => 'Europe/Samara',
        'Samarkand'                        => 'Asia/Samarkand',
        'Samoa'                            => 'Pacific/Apia',
        'Samoa Standard Time'              => 'Pacific/Apia',
        'Seychelles'                       => 'Indian/Mahe',
        'Shevchenko'                       => 'Asia/Aqtau',
        'Singapore'                        => 'Asia/Singapore',
        'Singapore Standard Time'          => 'Asia/Singapore',
        'Solomon'                          => 'Pacific/Guadalcanal',
        'South Africa Standard Time'       => 'Africa/Johannesburg',
        'South_Georgia'                    => 'Atlantic/South_Georgia',
        'Sri Lanka Standard Time'          => 'Asia/Colombo',
        'Suriname'                         => 'America/Paramaribo',
        'Sverdlovsk'                       => 'Asia/Yekaterinburg',
        'Syowa'                            => 'Antarctica/Syowa',
        'Tahiti'                           => 'Pacific/Tahiti',
        'Taipei'                           => 'Asia/Taipei',
        'Taipei Standard Time'             => 'Asia/Taipei',
        'Tajikistan'                       => 'Asia/Dushanbe',
        'Tashkent'                         => 'Asia/Tashkent',
        'Tasmania Standard Time'           => 'Australia/Hobart',
        'Tbilisi'                          => 'Asia/Tbilisi',
        'Tokelau'                          => 'Pacific/Fakaofo',
        'Tokyo Standard Time'              => 'Asia/Tokyo',
        'Tonga'                            => 'Pacific/Tongatapu',
        'Tonga Standard Time'              => 'Pacific/Tongatapu',
        'Truk'                             => 'Pacific/Truk',
        'Turkey'                           => 'Europe/Istanbul',
        'Turkmenistan'                     => 'Asia/Ashgabat',
        'Tuvalu'                           => 'Pacific/Funafuti',
        'US/Eastern'                       => 'America/New_York',
        'US Eastern Standard Time'         => 'Etc/GMT+5',
        'US Mountain Standard Time'        => 'America/Phoenix',
        'Uralsk'                           => 'Asia/Oral',
        'Uruguay'                          => 'America/Montevideo',
        'Urumqi'                           => 'Asia/Urumqi',
        'Uzbekistan'                       => 'Asia/Tashkent',
        'Vanuatu'                          => 'Pacific/Efate',
        'Venezuela'                        => 'America/Caracas',
        'Venezuela Standard Time'          => 'America/Caracas',
        'Vladivostok'                      => 'Asia/Vladivostok',
        'Vladivostok Standard Time'        => 'Asia/Vladivostok',
        'Volgograd'                        => 'Europe/Volgograd',
        'Vostok'                           => 'Antarctica/Vostok',
        'W. Australia Standard Time'       => 'Australia/Perth',
        'W. Central Africa Standard Time'  => 'Africa/Lagos',
        'W. Europe Standard Time'          => 'Europe/Berlin',
        'Wake'                             => 'Pacific/Wake',
        'Wallis'                           => 'Pacific/Wallis',
        'West Asia Standard Time'          => 'Asia/Tashkent',
        'West Pacific Standard Time'       => 'Pacific/Port_Moresby',
        'Yakutsk'                          => 'Asia/Yakutsk',
        'Yakutsk Standard Time'            => 'Asia/Yakutsk',
        'Yekaterinburg'                    => 'Asia/Yekaterinburg',
        'Yerevan'                          => 'Asia/Yerevan',
        'Yukon'                            => 'America/Yakutat',
    );

    /**
     * @var array Map of timezones acceptable by DateTimeZone but not strtotime.
     */
    protected $_invalid_legacy  = array(
        'US/Eastern' => true,
    );

    /**
     * @var array|bool List of system identifiers or false if none available.
     */
    protected $_identifiers     = false;

    /**
     * Initialize local cache and identifiers.
     *
     * @param Ai1ec_Registry_Object $registry Registry to use.
     *
     * @return void
     */
    public function __construct( Ai1ec_Registry_Object $registry ) {
        parent::__construct( $registry );
        $this->_cache = $this->_registry->get( 'cache.memory' );
        $this->_init_identifiers();
    }

    /**
     * Get default timezone to use in input/output.
     *
     * Approach is as follows:
     * - check user profile for timezone preference;
     * - if user has no preference - check site for timezone selection;
     * - if site has no selection - raise notice and use 'UTC'.
     *
     * @return string Olson timezone string identifier.
     */
    public function get_default_timezone() {
        static $default_timezone = null;
        if ( null === $default_timezone ) {
            $candidates = array();
            $candidates[] = (string)$this->_registry->get( 'model.meta-user' )
                ->get_current( 'ai1ec_timezone' );
            $candidates[] = (string)$this->_registry->get( 'model.option' )
                ->get( 'timezone_string' );
            $candidates[] = (string)$this->_registry->get( 'model.option' )
                ->get( 'gmt_offset' );
            $candidates   = array_filter( $candidates, 'strlen' );
            foreach ( $candidates as $timezone ) {
                $timezone = $this->get_name( $timezone );
                if ( false !== $timezone ) {
                    $default_timezone = $timezone;
                    break;
                }
            }
            if ( null === $default_timezone ) {
                $default_timezone = 'UTC';
                $this->_registry->get( 'notification.admin' )->store(
                    sprintf(
                        Ai1ec_I18n::__(
                            'Please select site timezone in %s <em>Timezone</em> dropdown menu.'
                        ),
                        '<a href="' . ai1ec_admin_url( 'options-general.php' ) .
                        '">' . Ai1ec_I18n::__( 'Settings' ) . '</a>'
                    ),
                    'error'
                );
            }
        }
        return $default_timezone;
    }

    /**
     * Attempt to decode GMT offset to some Olson timezone.
     *
     * @param float $zone GMT offset.
     *
     * @return string Valid Olson timezone name (UTC is last resort).
     */
    public function decode_gmt_timezone( $zone ) {
        $auto_zone = timezone_name_from_abbr( null, $zone * 3600, true );
        if ( false !== $auto_zone ) {
            return $auto_zone;
        }
        $auto_zone = timezone_name_from_abbr(
            null,
            ( (int) $zone ) * 3600,
            true
        );
        if ( false !== $auto_zone ) {
            return $auto_zone;
        }
        $this->_registry->get( 'notification.admin' )->store(
            sprintf(
                Ai1ec_I18n::__(
                    'Timezone "UTC%+d" is not recognized. Please %suse valid%s timezone name, until then events will be created in UTC timezone.'
                ),
                $zone,
                '<a href="' . ai1ec_admin_url( 'options-general.php' ) . '">',
                '</a>'
            ),
            'error'
        );
        return 'UTC';
    }

    /**
     * Get valid timezone name from input.
     *
     * @param string $zone Name to check/parse.
     *
     * @return string Timezone name to use
     */
    public function get_name( $zone ) {
        if ( is_numeric( $zone ) ) {
            $decoded_zone = $this->decode_gmt_timezone( $zone );
            if ( 'UTC' !== $decoded_zone ) {
                $message = sprintf(
                    Ai1ec_I18n::__(
                        'Selected timezone "UTC%+d" will be treated as %s.'
                    ),
                    $zone,
                    $decoded_zone
                );
                $this->_registry->get( 'notification.admin' )
                    ->store( $message );
            }
            $zone = $decoded_zone;
        }
        if ( false === $this->_identifiers ) {
            return $zone; // anything should do, as zones are not supported
        }
        if ( ! isset( $this->_identifiers[$zone] ) ) {
            $zone         = $this->_olson_lookup( $zone );
            $valid_legacy = false;
            try {
                new DateTimeZone( $zone ); // throw away instantly
                $valid_legacy = true;
            } catch ( Exception $excpt ) {
                $valid_legacy = false;
            }
            if ( ! $valid_legacy || isset( $this->_invalid_legacy[$zone] ) ) {
                return $this->guess_zone( $zone );
            }
            $this->_identifiers[$zone] = $zone;
            unset( $valid_legacy );
        }
        return $zone;
    }

    /**
     * Quick map look-up to discard zones that have limited recognition.
     *
     * @param string $zone Name of timezone to lookup.
     *
     * @return string Timezone name to use. Might be the same as $zone.
     */
    protected function _olson_lookup( $zone ) {
        if ( isset( $this->_zones[$zone] ) ) {
            return $this->_zones[$zone];
        }
        return $zone;
    }

    /**
     * Check if timezone is set in wp_option
     *
     */
    public function is_timezone_not_set() {
        $timezone = $this->_registry->get( 'model.option' )
            ->get( 'timezone_string' );
        return empty( $timezone );
    }

    /**
     * Render options for select in settings
     *
     * @return array
     */
    public function get_timezones( $only_zones = false ) {
        $zones = DateTimeZone::listIdentifiers();
        if (
            empty( $zones )
        ) {
            return array();
        }
        if ( ! $only_zones ) {
            $manual =  __( 'Manual Offset', AI1EC_PLUGIN_NAME );
            $options = array();
            $options[$manual][] = array(
                'text'  => __( 'Choose your timezone', AI1EC_PLUGIN_NAME ),
                'value' => '',
                'args'  => array(
                    'selected' => 'selected'
                )
            );
        }
        foreach ( $zones as $zone ) {
            $exploded_zone = explode( '/', $zone );
            if ( ! isset( $exploded_zone[1] ) && ! $only_zones ) {
                $exploded_zone[1] = $exploded_zone[0];
                $exploded_zone[0] = $manual;
            }
            $optgroup = $exploded_zone[0];
            unset( $exploded_zone[0] );
            $options[$optgroup][] = array(
                'text'  => implode( '/', $exploded_zone ),
                'value' => $zone,
            );
        }

        return $options;
    }

    /**
     * Guess valid timezone identifier from arbitrary input.
     *
     * @param string $meta_name Arbitrary input.
     *
     * @return string|bool Parsed timezone name or false if none found.
     */
    public function guess_zone( $meta_name ) {
        if ( isset( $this->_zones[$meta_name] ) ) {
            return $this->_zones[$meta_name];
        }
        $name_variants = array(
            strtr( $meta_name, ' ', '_' ),
            strtr( $meta_name, '_', ' ' ),
        );
        if ( false !== ( $parenthesis_pos = strpos( $meta_name, '(' ) ) ) {
            foreach ( $name_variants as $name ) {
                $name_variants[] = substr( $name, 0, $parenthesis_pos - 1 );
            }
        }
        foreach ( $name_variants as $name ) {
            if ( isset( $this->_zones[$name] ) ) {
                // cache to avoid future lookups and return
                $this->_zones[$meta_name] = $this->_zones[$name];
                return $this->_zones[$name];
            }
        }
        if (
            isset( $meta_name[0] ) &&
            '(' === $meta_name[0] &&
            $closing_pos = strpos( $meta_name, ')' )
        ) {
            $meta_name = trim( substr( $meta_name, $closing_pos + 1 ) );
            return $this->guess_zone( $meta_name );
        }
        if (
            false === strpos( $meta_name, ' Standard ' ) &&
            false !== ( $time_pos = strpos( $meta_name, ' Time' ) )
        ) {
            $meta_name = substr( $meta_name, 0, $time_pos ) .
                ' Standard' . substr( $meta_name, $time_pos );
            return $this->guess_zone( $meta_name );
        }
        return false;
    }

    /**
     * Get timezone object instance.
     *
     * @param string $timezone Name of timezone to get instance for.
     *
     * @return DateTimeZone Instance of timezone object.
     *
     * @throws Ai1ec_Date_Timezone_Exception If an error occurs.
     */
    public function get( $timezone ) {
        if ( 'sys.default' === $timezone ) {
            $timezone = $this->get_default_timezone();
        }
        $name = $this->get_name( $timezone );
        if ( ! $name ) {
            $name = $this->get_name( $this->get_default_timezone() );
        }
        $zone = $this->_cache->get( $name, null );
        if ( null === $zone ) {
            $exception = null;
            try {
                $zone = new DateTimeZone( $name );
            } catch ( Exception $invalid_tz ) {
                $exception = $invalid_tz;
            }
            if ( null !== $exception ) {
                throw new Ai1ec_Date_Timezone_Exception( $exception->getMessage() );
            }
            $this->_cache->set( $name, $zone );
        }
        return $zone;
    }

    /**
     * Add system identifiers to object registry.
     *
     * @return bool Success
     */
    protected function _init_identifiers() {
        $identifiers  = DateTimeZone::listIdentifiers();
        if ( ! $identifiers ) {
            return false;
        }
        $mapped = array();
        foreach ( $identifiers as $zone ) {
            $zone                = (string)$zone;
            $mapped[$zone]       = true;
            $this->_zones[$zone] = $zone;
        }
        unset( $identifiers, $zone );
        $this->_identifiers = $mapped;
        return true;
    }

}
