<?php

/**
 * This file provides some functions to simplify working with time zones.
 *
 * Simple Machines Forum (SMF)
 *
 * @package SMF
 * @author Simple Machines https://www.simplemachines.org
 * @copyright 2022 Simple Machines and individual contributors
 * @license https://www.simplemachines.org/about/smf/license.php BSD
 *
 * @version 2.1.0
 */

if (!defined('SMF'))
	die('No direct access...');

if (!defined('PHP_INT_MIN'))
	define('PHP_INT_MIN', ~PHP_INT_MAX);

/**
 * Returns an array that instructs SMF how to map specific time zones
 * (e.g. "America/Denver") onto the user-friendly "meta-zone" labels that
 * most people think of as time zones (e.g. "Mountain Time").
 *
 * @param string $when The date/time used to determine fallback values.
 *		May be a Unix timestamp or any string that strtotime() can understand.
 *		Defaults to 'now'.
 * @return array An array relating time zones to "meta-zones"
 */
function get_tzid_metazones($when = 'now')
{
	global $txt, $tztxt;

	// This should already have been loaded, but just in case...
	loadLanguage('Timezones');

	/*
		This array lists a series of representative time zones and their
		corresponding "meta-zone" labels.

		The term "representative" here means that a given time zone can
		represent others that use exactly the same rules for DST
		transitions, UTC offsets, and abbreviations. For example,
		Europe/Berlin can be representative for Europe/Rome,
		Europe/Paris, etc., because these cities all use exactly the
		same time zone rules and values.

		Meta-zone labels are the user friendly strings shown to the end
		user, e.g. "Mountain Standard Time". The values of this array
		are keys of strings defined in Timezones.{language}.php, which
		in turn are sprintf format strings used to generate the final
		label text.

		Sometimes several representative time zones will map onto the
		same meta-zone label. This usually happens when there are
		different rules for Daylight Saving time in locations that are
		otherwise the same. For example, both America/Denver and
		America/Phoenix map to North_America_Mountain, but the ultimate
		output will be 'Mountain Time (MST/MDT)' for America/Denver vs.
		'Mountain Standard Time (MST)' for America/Phoenix.

		If you are adding a new meta-zone to this list because the TZDB
		added a new time zone that doesn't fit any existing meta-zone,
		please also add a fallback in the get_tzid_fallbacks() function.
		This helps support SMF installs on servers using outdated
		versions of the TZDB.
	 */
	$tzid_metazones = array(
		// No DST
		'Africa/Abidjan' => 'GMT',

		// No DST
		'Africa/Algiers' => 'Europe_Central',

		// Uses DST
		'Africa/Casablanca' => 'Africa_Morocco',

		// No DST
		'Africa/Johannesburg' => 'Africa_South',

		// No DST
		'Africa/Lagos' => 'Africa_West',

		// No DST
		'Africa/Maputo' => 'Africa_Central',

		// No DST
		'Africa/Nairobi' => 'Africa_East',

		// Uses DST
		'America/Adak' => 'North_America_Hawaii_Aleutian',

		// Uses DST
		'America/Anchorage' => 'North_America_Alaska',

		// No DST
		'America/Argentina/Buenos_Aires' => 'South_America_Argentina',

		// Uses DST
		'America/Asuncion' => 'South_America_Paraguay',

		// No DST
		'America/Belize' => 'North_America_Central',

		// No DST
		'America/Bogota' => 'South_America_Colombia',

		// No DST
		'America/Caracas' => 'South_America_Venezuela',

		// No DST
		'America/Cayenne' => 'South_America_French_Guiana',

		// Uses DST
		'America/Chicago' => 'North_America_Central',

		// Uses DST
		'America/Chihuahua' => 'North_America_Mexico_Pacific',

		// Uses DST
		'America/Denver' => 'North_America_Mountain',

		// Uses DST
		'America/Nuuk' => 'North_America_Greenland_Western',

		// No DST
		'America/Guayaquil' => 'South_America_Ecuador',

		// No DST
		'America/Guyana' => 'South_America_Guyana',

		// Uses DST
		'America/Halifax' => 'North_America_Atlantic',

		// Uses DST
		'America/Havana' => 'North_America_Cuba',

		// No DST
		'America/Jamaica' => 'North_America_Eastern',

		// No DST
		'America/La_Paz' => 'South_America_Bolivia',

		// No DST
		'America/Lima' => 'South_America_Peru',

		// Uses DST
		'America/Los_Angeles' => 'North_America_Pacific',

		// No DST
		'America/Manaus' => 'South_America_Amazon',

		// Uses DST
		'America/Mexico_City' => 'North_America_Mexico_Central',

		// Uses DST
		'America/Miquelon' => 'North_America_St_Pierre_Miquelon',

		// No DST
		'America/Montevideo' => 'South_America_Uruguay',

		// Uses DST
		'America/New_York' => 'North_America_Eastern',

		// No DST
		'America/Noronha' => 'South_America_Noronha',

		// No DST
		'America/Paramaribo' => 'South_America_Suriname',

		// No DST
		'America/Phoenix' => 'North_America_Mountain',

		// No DST
		'America/Port_of_Spain' => 'North_America_Atlantic',

		// No DST
		'America/Punta_Arenas' => 'South_America_Chile_Magallanes',

		// No DST
		'America/Rio_Branco' => 'South_America_Acre',

		// Uses DST
		'America/Santiago' => 'South_America_Chile',

		// No DST
		'America/Sao_Paulo' => 'South_America_Brasilia',

		// Uses DST
		'America/Scoresbysund' => 'North_America_Greenland_Eastern',

		// Uses DST
		'America/St_Johns' => 'North_America_Newfoundland',

		// No DST
		'Antarctica/Casey' => 'Antarctica_Casey',

		// No DST
		'Antarctica/Davis' => 'Antarctica_Davis',

		// No DST
		'Antarctica/DumontDUrville' => 'Antarctica_DumontDUrville',

		// No DST
		'Antarctica/Macquarie' => 'Antarctica_Macquarie',

		// No DST
		'Antarctica/Mawson' => 'Antarctica_Mawson',

		// Uses DST
		'Antarctica/McMurdo' => 'Antarctica_McMurdo',

		// No DST
		'Antarctica/Palmer' => 'Antarctica_Palmer',

		// No DST
		'Antarctica/Rothera' => 'Antarctica_Rothera',

		// No DST
		'Antarctica/Syowa' => 'Antarctica_Syowa',

		// Uses DST
		'Antarctica/Troll' => 'Antarctica_Troll',

		// No DST
		'Antarctica/Vostok' => 'Antarctica_Vostok',

		// No DST
		'Asia/Almaty' => 'Asia_Kazakhstan_Eastern',

		// Uses DST
		'Asia/Amman' => 'Asia_Jordan',

		// No DST
		'Asia/Aqtau' => 'Asia_Kazakhstan_Western',

		// No DST
		'Asia/Ashgabat' => 'Asia_Turkmenistan',

		// No DST
		'Asia/Baku' => 'Asia_Azerbaijan',

		// No DST
		'Asia/Bangkok' => 'Asia_Southeast',

		// Uses DST
		'Asia/Beirut' => 'Asia_Libya',

		// No DST
		'Asia/Bishkek' => 'Asia_Kyrgystan',

		// No DST
		'Asia/Brunei' => 'Asia_Brunei',

		// Uses DST
		'Asia/Damascus' => 'Asia_Damascus',

		// No DST
		'Asia/Dhaka' => 'Asia_Bangladesh',

		// No DST
		'Asia/Dili' => 'Asia_East_Timor',

		// No DST
		'Asia/Dubai' => 'Asia_Gulf',

		// No DST
		'Asia/Dushanbe' => 'Asia_Tajikistan',

		// Uses DST
		'Asia/Gaza' => 'Asia_Palestine',

		// No DST
		'Asia/Hong_Kong' => 'Asia_Hong_Kong',

		// No DST
		'Asia/Hovd' => 'Asia_Mongolia_Western',

		// No DST
		'Asia/Irkutsk' => 'Asia_Irkutsk',

		// No DST
		'Asia/Jakarta' => 'Asia_Indonesia_Western',

		// No DST
		'Asia/Jayapura' => 'Asia_Indonesia_Eastern',

		// Uses DST
		'Asia/Jerusalem' => 'Asia_Israel',

		// No DST
		'Asia/Kabul' => 'Asia_Afghanistan',

		// No DST
		'Asia/Kamchatka' => 'Asia_Kamchatka',

		// No DST
		'Asia/Karachi' => 'Asia_Pakistan',

		// No DST
		'Asia/Kathmandu' => 'Asia_Nepal',

		// No DST
		'Asia/Kolkata' => 'Asia_India',

		// No DST
		'Asia/Krasnoyarsk' => 'Asia_Krasnoyarsk',

		// No DST
		'Asia/Kuala_Lumpur' => 'Asia_Malaysia',

		// No DST
		'Asia/Magadan' => 'Asia_Magadan',

		// No DST
		'Asia/Makassar' => 'Asia_Indonesia_Central',

		// No DST
		'Asia/Manila' => 'Asia_Philippines',

		// No DST
		'Asia/Omsk' => 'Asia_Omsk',

		// No DST
		'Asia/Riyadh' => 'Asia_Arabia',

		// No DST
		'Asia/Seoul' => 'Asia_Korea',

		// No DST
		'Asia/Shanghai' => 'Asia_China',

		// No DST
		'Asia/Singapore' => 'Asia_Singapore',

		// No DST
		'Asia/Taipei' => 'Asia_Taiwan',

		// No DST
		'Asia/Tashkent' => 'Asia_Uzbekistan',

		// No DST
		'Asia/Tbilisi' => 'Asia_Georgia',

		// Uses DST
		'Asia/Tehran' => 'Asia_Iran',

		// No DST
		'Asia/Thimphu' => 'Asia_Bhutan',

		// No DST
		'Asia/Tokyo' => 'Asia_Japan',

		// No DST
		'Asia/Ulaanbaatar' => 'Asia_Mongolia_Eastern',

		// No DST
		'Asia/Vladivostok' => 'Asia_Vladivostok',

		// No DST
		'Asia/Yakutsk' => 'Asia_Yakutsk',

		// No DST
		'Asia/Yangon' => 'Asia_Myanmar',

		// No DST
		'Asia/Yekaterinburg' => 'Asia_Yekaterinburg',

		// No DST
		'Asia/Yerevan' => 'Asia_Armenia',

		// Uses DST
		'Atlantic/Azores' => 'Atlantic_Azores',

		// No DST
		'Atlantic/Cape_Verde' => 'Atlantic_Cape_Verde',

		// No DST
		'Atlantic/South_Georgia' => 'Atlantic_South_Georgia',

		// No DST
		'Atlantic/Stanley' => 'Atlantic_Falkland',

		// Uses DST
		'Australia/Adelaide' => 'Australia_Central',

		// No DST
		'Australia/Brisbane' => 'Australia_Eastern',

		// No DST
		'Australia/Darwin' => 'Australia_Central',

		// No DST
		'Australia/Eucla' => 'Australia_CentralWestern',

		// Uses DST
		'Australia/Lord_Howe' => 'Australia_Lord_Howe',

		// Uses DST
		'Australia/Melbourne' => 'Australia_Eastern',

		// No DST
		'Australia/Perth' => 'Australia_Western',

		// Uses DST
		'Europe/Berlin' => 'Europe_Central',

		// Uses DST
		'Europe/Chisinau' => 'Europe_Moldova',

		// Uses DST
		'Europe/Dublin' => 'Europe_Eire',

		// Uses DST
		'Europe/Helsinki' => 'Europe_Eastern',

		// No DST
		'Europe/Istanbul' => 'Asia_Turkey',

		// No DST
		'Europe/Kaliningrad' => 'Europe_Eastern',

		// Uses DST
		'Europe/Lisbon' => 'Europe_Western',

		// Uses DST
		'Europe/London' => 'Europe_UK',

		// No DST
		'Europe/Minsk' => 'Europe_Minsk',

		// No DST
		'Europe/Moscow' => 'Europe_Moscow',

		// No DST
		'Europe/Samara' => 'Europe_Samara',

		// No DST
		'Europe/Volgograd' => 'Europe_Volgograd',

		// No DST
		'Indian/Chagos' => 'Indian_Chagos',

		// No DST
		'Indian/Christmas' => 'Indian_Christmas',

		// No DST
		'Indian/Cocos' => 'Indian_Cocos',

		// No DST
		'Indian/Kerguelen' => 'Indian_Kerguelen',

		// No DST
		'Indian/Mahe' => 'Indian_Seychelles',

		// No DST
		'Indian/Maldives' => 'Indian_Maldives',

		// No DST
		'Indian/Mauritius' => 'Indian_Mauritius',

		// No DST
		'Indian/Reunion' => 'Indian_Reunion',

		// Uses DST
		'Pacific/Apia' => 'Pacific_Apia',

		// Uses DST
		'Pacific/Auckland' => 'Pacific_New_Zealand',

		// No DST
		'Pacific/Bougainville' => 'Pacific_Bougainville',

		// Uses DST
		'Pacific/Chatham' => 'Pacific_Chatham',

		// No DST
		'Pacific/Chuuk' => 'Pacific_Chuuk',

		// Uses DST
		'Pacific/Easter' => 'Pacific_Easter',

		// No DST
		'Pacific/Efate' => 'Pacific_Vanuatu',

		// No DST
		'Pacific/Enderbury' => 'Pacific_Phoenix_Islands',

		// No DST
		'Pacific/Fakaofo' => 'Pacific_Tokelau',

		// Uses DST
		'Pacific/Fiji' => 'Pacific_Fiji',

		// No DST
		'Pacific/Funafuti' => 'Pacific_Tuvalu',

		// No DST
		'Pacific/Galapagos' => 'Pacific_Galapagos',

		// No DST
		'Pacific/Gambier' => 'Pacific_Gambier',

		// No DST
		'Pacific/Guadalcanal' => 'Pacific_Solomon',

		// No DST
		'Pacific/Guam' => 'Pacific_Chamorro',

		// No DST
		'Pacific/Honolulu' => 'Pacific_Hawaii',

		// No DST
		'Pacific/Kiritimati' => 'Pacific_Line',

		// No DST
		'Pacific/Kwajalein' => 'Pacific_Marshall',

		// No DST
		'Pacific/Marquesas' => 'Pacific_Marquesas',

		// No DST
		'Pacific/Nauru' => 'Pacific_Nauru',

		// No DST
		'Pacific/Niue' => 'Pacific_Niue',

		// No DST
		'Pacific/Norfolk' => 'Pacific_Norfolk',

		// No DST
		'Pacific/Noumea' => 'Pacific_New_Caledonia',

		// No DST
		'Pacific/Pago_Pago' => 'Pacific_Samoa',

		// No DST
		'Pacific/Palau' => 'Pacific_Palau',

		// No DST
		'Pacific/Pitcairn' => 'Pacific_Pitcairn',

		// No DST
		'Pacific/Pohnpei' => 'Pacific_Pohnpei',

		// No DST
		'Pacific/Port_Moresby' => 'Pacific_Papua_New_Guinea',

		// No DST
		'Pacific/Rarotonga' => 'Pacific_Cook',

		// No DST
		'Pacific/Tahiti' => 'Pacific_Tahiti',

		// No DST
		'Pacific/Tarawa' => 'Pacific_Gilbert',

		// No DST
		'Pacific/Tongatapu' => 'Pacific_Tonga',

		// No DST
		'Pacific/Wake' => 'Pacific_Wake',

		// No DST
		'Pacific/Wallis' => 'Pacific_Wallis',
	);

	call_integration_hook('integrate_metazones', array(&$tzid_metazones, $when));

	// Fallbacks in case the server has an old version of the TZDB.
	$tzids = array_keys($tzid_metazones);
	$tzid_fallbacks = get_tzid_fallbacks($tzids, $when);
	foreach ($tzid_fallbacks as $orig_tzid => $alt_tzid)
	{
		// Skip any that are unchanged.
		if ($orig_tzid == $alt_tzid)
			continue;

		// Use fallback where possible.
		if (!empty($alt_tzid))
		{
			$tzid_metazones[$alt_tzid] = $tzid_metazones[$orig_tzid];
			$txt[$alt_tzid] = $txt[$orig_tzid];
		}

		// Either way, get rid of the unknown time zone.
		unset($tzid_metazones[$orig_tzid]);
	}

	return $tzid_metazones;
}

/**
 * Returns an array of all the time zones in a country, ranked according
 * to population and/or political significance.
 *
 * @param string $country_code The two-character ISO-3166 code for a country.
 * @param string $when The date/time used to determine fallback values.
 *		May be a Unix timestamp or any string that strtotime() can understand.
 *		Defaults to 'now'.
 * @return array An array relating time zones to "meta-zones"
 */
function get_sorted_tzids_for_country($country_code, $when = 'now')
{
	static $country_tzids = array();

	/*
		This array lists all the individual time zones in each country,
		sorted by population (as reported in statistics available on
		Wikipedia in November 2020). Sorting this way enables us to
		consistently select the most appropriate individual time zone to
		represent all others that share its DST transition rules and values.
		For example, this ensures that New York will be preferred over
		random small towns in Indiana.

		If future versions of the time zone database add new time zone
		identifiers beyond those included here, they should be added to this
		list as appropriate. However, SMF will gracefully handle unexpected
		new time zones, so nothing will break in the meantime.
	 */
	$sorted_tzids = array(
		// '??' means international.
		'??' => array(
			'UTC',
		),
		'AD' => array(
			'Europe/Andorra',
		),
		'AE' => array(
			'Asia/Dubai',
		),
		'AF' => array(
			'Asia/Kabul',
		),
		'AG' => array(
			'America/Antigua',
		),
		'AI' => array(
			'America/Anguilla',
		),
		'AL' => array(
			'Europe/Tirane',
		),
		'AM' => array(
			'Asia/Yerevan',
		),
		'AO' => array(
			'Africa/Luanda',
		),
		'AQ' => array(
			// Sorted based on summer population.
			'Antarctica/McMurdo',
			'Antarctica/Casey',
			'Antarctica/Davis',
			'Antarctica/Mawson',
			'Antarctica/Rothera',
			'Antarctica/Syowa',
			'Antarctica/Palmer',
			'Antarctica/Troll',
			'Antarctica/DumontDUrville',
			'Antarctica/Vostok',
		),
		'AR' => array(
			'America/Argentina/Buenos_Aires',
			'America/Argentina/Cordoba',
			'America/Argentina/Tucuman',
			'America/Argentina/Salta',
			'America/Argentina/Jujuy',
			'America/Argentina/La_Rioja',
			'America/Argentina/San_Luis',
			'America/Argentina/Catamarca',
			'America/Argentina/Mendoza',
			'America/Argentina/San_Juan',
			'America/Argentina/Rio_Gallegos',
			'America/Argentina/Ushuaia',
		),
		'AS' => array(
			'Pacific/Pago_Pago',
		),
		'AT' => array(
			'Europe/Vienna',
		),
		'AU' => array(
			'Australia/Sydney',
			'Australia/Melbourne',
			'Australia/Brisbane',
			'Australia/Perth',
			'Australia/Adelaide',
			'Australia/Hobart',
			'Australia/Darwin',
			'Australia/Broken_Hill',
			'Australia/Currie',
			'Australia/Lord_Howe',
			'Australia/Eucla',
			'Australia/Lindeman',
			'Antarctica/Macquarie',
		),
		'AW' => array(
			'America/Aruba',
		),
		'AX' => array(
			'Europe/Mariehamn',
		),
		'AZ' => array(
			'Asia/Baku',
		),
		'BA' => array(
			'Europe/Sarajevo',
		),
		'BB' => array(
			'America/Barbados',
		),
		'BD' => array(
			'Asia/Dhaka',
		),
		'BE' => array(
			'Europe/Brussels',
		),
		'BF' => array(
			'Africa/Ouagadougou',
		),
		'BG' => array(
			'Europe/Sofia',
		),
		'BH' => array(
			'Asia/Bahrain',
		),
		'BI' => array(
			'Africa/Bujumbura',
		),
		'BJ' => array(
			'Africa/Porto-Novo',
		),
		'BL' => array(
			'America/St_Barthelemy',
		),
		'BM' => array(
			'Atlantic/Bermuda',
		),
		'BN' => array(
			'Asia/Brunei',
		),
		'BO' => array(
			'America/La_Paz',
		),
		'BQ' => array(
			'America/Kralendijk',
		),
		'BR' => array(
			'America/Sao_Paulo',
			'America/Bahia',
			'America/Fortaleza',
			'America/Manaus',
			'America/Recife',
			'America/Belem',
			'America/Maceio',
			'America/Campo_Grande',
			'America/Cuiaba',
			'America/Porto_Velho',
			'America/Rio_Branco',
			'America/Boa_Vista',
			'America/Santarem',
			'America/Araguaina',
			'America/Eirunepe',
			'America/Noronha',
		),
		'BS' => array(
			'America/Nassau',
		),
		'BT' => array(
			'Asia/Thimphu',
		),
		'BW' => array(
			'Africa/Gaborone',
		),
		'BY' => array(
			'Europe/Minsk',
		),
		'BZ' => array(
			'America/Belize',
		),
		'CA' => array(
			'America/Toronto',
			'America/Vancouver',
			'America/Edmonton',
			'America/Winnipeg',
			'America/Halifax',
			'America/Regina',
			'America/St_Johns',
			'America/Moncton',
			'America/Thunder_Bay',
			'America/Whitehorse',
			'America/Glace_Bay',
			'America/Yellowknife',
			'America/Swift_Current',
			'America/Dawson_Creek',
			'America/Goose_Bay',
			'America/Iqaluit',
			'America/Creston',
			'America/Fort_Nelson',
			'America/Inuvik',
			'America/Atikokan',
			'America/Rankin_Inlet',
			'America/Nipigon',
			'America/Cambridge_Bay',
			'America/Pangnirtung',
			'America/Dawson',
			'America/Blanc-Sablon',
			'America/Rainy_River',
			'America/Resolute',
		),
		'CC' => array(
			'Indian/Cocos',
		),
		'CD' => array(
			'Africa/Kinshasa',
			'Africa/Lubumbashi',
		),
		'CF' => array(
			'Africa/Bangui',
		),
		'CG' => array(
			'Africa/Brazzaville',
		),
		'CH' => array(
			'Europe/Zurich',
		),
		'CI' => array(
			'Africa/Abidjan',
		),
		'CK' => array(
			'Pacific/Rarotonga',
		),
		'CL' => array(
			'America/Santiago',
			'America/Punta_Arenas',
			'Pacific/Easter',
		),
		'CM' => array(
			'Africa/Douala',
		),
		'CN' => array(
			'Asia/Shanghai',
			'Asia/Urumqi',
		),
		'CO' => array(
			'America/Bogota',
		),
		'CR' => array(
			'America/Costa_Rica',
		),
		'CU' => array(
			'America/Havana',
		),
		'CV' => array(
			'Atlantic/Cape_Verde',
		),
		'CW' => array(
			'America/Curacao',
		),
		'CX' => array(
			'Indian/Christmas',
		),
		'CY' => array(
			'Asia/Nicosia',
			'Asia/Famagusta',
		),
		'CZ' => array(
			'Europe/Prague',
		),
		'DE' => array(
			'Europe/Berlin',
			'Europe/Busingen',
		),
		'DJ' => array(
			'Africa/Djibouti',
		),
		'DK' => array(
			'Europe/Copenhagen',
		),
		'DM' => array(
			'America/Dominica',
		),
		'DO' => array(
			'America/Santo_Domingo',
		),
		'DZ' => array(
			'Africa/Algiers',
		),
		'EC' => array(
			'America/Guayaquil',
			'Pacific/Galapagos',
		),
		'EE' => array(
			'Europe/Tallinn',
		),
		'EG' => array(
			'Africa/Cairo',
		),
		'EH' => array(
			'Africa/El_Aaiun',
		),
		'ER' => array(
			'Africa/Asmara',
		),
		'ES' => array(
			'Europe/Madrid',
			'Atlantic/Canary',
			'Africa/Ceuta',
		),
		'ET' => array(
			'Africa/Addis_Ababa',
		),
		'FI' => array(
			'Europe/Helsinki',
		),
		'FJ' => array(
			'Pacific/Fiji',
		),
		'FK' => array(
			'Atlantic/Stanley',
		),
		'FM' => array(
			'Pacific/Chuuk',
			'Pacific/Kosrae',
			'Pacific/Pohnpei',
		),
		'FO' => array(
			'Atlantic/Faroe',
		),
		'FR' => array(
			'Europe/Paris',
		),
		'GA' => array(
			'Africa/Libreville',
		),
		'GB' => array(
			'Europe/London',
		),
		'GD' => array(
			'America/Grenada',
		),
		'GE' => array(
			'Asia/Tbilisi',
		),
		'GF' => array(
			'America/Cayenne',
		),
		'GG' => array(
			'Europe/Guernsey',
		),
		'GH' => array(
			'Africa/Accra',
		),
		'GI' => array(
			'Europe/Gibraltar',
		),
		'GL' => array(
			'America/Nuuk',
			'America/Thule',
			'America/Scoresbysund',
			'America/Danmarkshavn',
		),
		'GM' => array(
			'Africa/Banjul',
		),
		'GN' => array(
			'Africa/Conakry',
		),
		'GP' => array(
			'America/Guadeloupe',
		),
		'GQ' => array(
			'Africa/Malabo',
		),
		'GR' => array(
			'Europe/Athens',
		),
		'GS' => array(
			'Atlantic/South_Georgia',
		),
		'GT' => array(
			'America/Guatemala',
		),
		'GU' => array(
			'Pacific/Guam',
		),
		'GW' => array(
			'Africa/Bissau',
		),
		'GY' => array(
			'America/Guyana',
		),
		'HK' => array(
			'Asia/Hong_Kong',
		),
		'HN' => array(
			'America/Tegucigalpa',
		),
		'HR' => array(
			'Europe/Zagreb',
		),
		'HT' => array(
			'America/Port-au-Prince',
		),
		'HU' => array(
			'Europe/Budapest',
		),
		'ID' => array(
			'Asia/Jakarta',
			'Asia/Makassar',
			'Asia/Pontianak',
			'Asia/Jayapura',
		),
		'IE' => array(
			'Europe/Dublin',
		),
		'IL' => array(
			'Asia/Jerusalem',
		),
		'IM' => array(
			'Europe/Isle_of_Man',
		),
		'IN' => array(
			'Asia/Kolkata',
		),
		'IO' => array(
			'Indian/Chagos',
		),
		'IQ' => array(
			'Asia/Baghdad',
		),
		'IR' => array(
			'Asia/Tehran',
		),
		'IS' => array(
			'Atlantic/Reykjavik',
		),
		'IT' => array(
			'Europe/Rome',
		),
		'JE' => array(
			'Europe/Jersey',
		),
		'JM' => array(
			'America/Jamaica',
		),
		'JO' => array(
			'Asia/Amman',
		),
		'JP' => array(
			'Asia/Tokyo',
		),
		'KE' => array(
			'Africa/Nairobi',
		),
		'KG' => array(
			'Asia/Bishkek',
		),
		'KH' => array(
			'Asia/Phnom_Penh',
		),
		'KI' => array(
			'Pacific/Tarawa',
			'Pacific/Kiritimati',
			'Pacific/Enderbury',
		),
		'KM' => array(
			'Indian/Comoro',
		),
		'KN' => array(
			'America/St_Kitts',
		),
		'KP' => array(
			'Asia/Pyongyang',
		),
		'KR' => array(
			'Asia/Seoul',
		),
		'KW' => array(
			'Asia/Kuwait',
		),
		'KY' => array(
			'America/Cayman',
		),
		'KZ' => array(
			'Asia/Almaty',
			'Asia/Aqtobe',
			'Asia/Atyrau',
			'Asia/Qostanay',
			'Asia/Qyzylorda',
			'Asia/Aqtau',
			'Asia/Oral',
		),
		'LA' => array(
			'Asia/Vientiane',
		),
		'LB' => array(
			'Asia/Beirut',
		),
		'LC' => array(
			'America/St_Lucia',
		),
		'LI' => array(
			'Europe/Vaduz',
		),
		'LK' => array(
			'Asia/Colombo',
		),
		'LR' => array(
			'Africa/Monrovia',
		),
		'LS' => array(
			'Africa/Maseru',
		),
		'LT' => array(
			'Europe/Vilnius',
		),
		'LU' => array(
			'Europe/Luxembourg',
		),
		'LV' => array(
			'Europe/Riga',
		),
		'LY' => array(
			'Africa/Tripoli',
		),
		'MA' => array(
			'Africa/Casablanca',
		),
		'MC' => array(
			'Europe/Monaco',
		),
		'MD' => array(
			'Europe/Chisinau',
		),
		'ME' => array(
			'Europe/Podgorica',
		),
		'MF' => array(
			'America/Marigot',
		),
		'MG' => array(
			'Indian/Antananarivo',
		),
		'MH' => array(
			'Pacific/Majuro',
			'Pacific/Kwajalein',
		),
		'MK' => array(
			'Europe/Skopje',
		),
		'ML' => array(
			'Africa/Bamako',
		),
		'MM' => array(
			'Asia/Yangon',
		),
		'MN' => array(
			'Asia/Ulaanbaatar',
			'Asia/Choibalsan',
			'Asia/Hovd',
		),
		'MO' => array(
			'Asia/Macau',
		),
		'MP' => array(
			'Pacific/Saipan',
		),
		'MQ' => array(
			'America/Martinique',
		),
		'MR' => array(
			'Africa/Nouakchott',
		),
		'MS' => array(
			'America/Montserrat',
		),
		'MT' => array(
			'Europe/Malta',
		),
		'MU' => array(
			'Indian/Mauritius',
		),
		'MV' => array(
			'Indian/Maldives',
		),
		'MW' => array(
			'Africa/Blantyre',
		),
		'MX' => array(
			'America/Mexico_City',
			'America/Tijuana',
			'America/Monterrey',
			'America/Chihuahua',
			'America/Merida',
			'America/Hermosillo',
			'America/Cancun',
			'America/Matamoros',
			'America/Mazatlan',
			'America/Bahia_Banderas',
			'America/Ojinaga',
		),
		'MY' => array(
			'Asia/Kuala_Lumpur',
			'Asia/Kuching',
		),
		'MZ' => array(
			'Africa/Maputo',
		),
		'NA' => array(
			'Africa/Windhoek',
		),
		'NC' => array(
			'Pacific/Noumea',
		),
		'NE' => array(
			'Africa/Niamey',
		),
		'NF' => array(
			'Pacific/Norfolk',
		),
		'NG' => array(
			'Africa/Lagos',
		),
		'NI' => array(
			'America/Managua',
		),
		'NL' => array(
			'Europe/Amsterdam',
		),
		'NO' => array(
			'Europe/Oslo',
		),
		'NP' => array(
			'Asia/Kathmandu',
		),
		'NR' => array(
			'Pacific/Nauru',
		),
		'NU' => array(
			'Pacific/Niue',
		),
		'NZ' => array(
			'Pacific/Auckland',
			'Pacific/Chatham',
		),
		'OM' => array(
			'Asia/Muscat',
		),
		'PA' => array(
			'America/Panama',
		),
		'PE' => array(
			'America/Lima',
		),
		'PF' => array(
			'Pacific/Tahiti',
			'Pacific/Marquesas',
			'Pacific/Gambier',
		),
		'PG' => array(
			'Pacific/Port_Moresby',
			'Pacific/Bougainville',
		),
		'PH' => array(
			'Asia/Manila',
		),
		'PK' => array(
			'Asia/Karachi',
		),
		'PL' => array(
			'Europe/Warsaw',
		),
		'PM' => array(
			'America/Miquelon',
		),
		'PN' => array(
			'Pacific/Pitcairn',
		),
		'PR' => array(
			'America/Puerto_Rico',
		),
		'PS' => array(
			'Asia/Gaza',
			'Asia/Hebron',
		),
		'PT' => array(
			'Europe/Lisbon',
			'Atlantic/Madeira',
			'Atlantic/Azores',
		),
		'PW' => array(
			'Pacific/Palau',
		),
		'PY' => array(
			'America/Asuncion',
		),
		'QA' => array(
			'Asia/Qatar',
		),
		'RE' => array(
			'Indian/Reunion',
		),
		'RO' => array(
			'Europe/Bucharest',
		),
		'RS' => array(
			'Europe/Belgrade',
		),
		'RU' => array(
			'Europe/Moscow',
			'Asia/Novosibirsk',
			'Asia/Yekaterinburg',
			'Europe/Samara',
			'Asia/Omsk',
			'Asia/Krasnoyarsk',
			'Europe/Volgograd',
			'Europe/Saratov',
			'Asia/Barnaul',
			'Europe/Ulyanovsk',
			'Asia/Irkutsk',
			'Asia/Vladivostok',
			'Asia/Tomsk',
			'Asia/Novokuznetsk',
			'Europe/Astrakhan',
			'Europe/Kirov',
			'Europe/Kaliningrad',
			'Asia/Chita',
			'Asia/Yakutsk',
			'Asia/Sakhalin',
			'Asia/Kamchatka',
			'Asia/Magadan',
			'Asia/Anadyr',
			'Asia/Khandyga',
			'Asia/Ust-Nera',
			'Asia/Srednekolymsk',
		),
		'RW' => array(
			'Africa/Kigali',
		),
		'SA' => array(
			'Asia/Riyadh',
		),
		'SB' => array(
			'Pacific/Guadalcanal',
		),
		'SC' => array(
			'Indian/Mahe',
		),
		'SD' => array(
			'Africa/Khartoum',
		),
		'SE' => array(
			'Europe/Stockholm',
		),
		'SG' => array(
			'Asia/Singapore',
		),
		'SH' => array(
			'Atlantic/St_Helena',
		),
		'SI' => array(
			'Europe/Ljubljana',
		),
		'SJ' => array(
			'Arctic/Longyearbyen',
		),
		'SK' => array(
			'Europe/Bratislava',
		),
		'SL' => array(
			'Africa/Freetown',
		),
		'SM' => array(
			'Europe/San_Marino',
		),
		'SN' => array(
			'Africa/Dakar',
		),
		'SO' => array(
			'Africa/Mogadishu',
		),
		'SR' => array(
			'America/Paramaribo',
		),
		'SS' => array(
			'Africa/Juba',
		),
		'ST' => array(
			'Africa/Sao_Tome',
		),
		'SV' => array(
			'America/El_Salvador',
		),
		'SX' => array(
			'America/Lower_Princes',
		),
		'SY' => array(
			'Asia/Damascus',
		),
		'SZ' => array(
			'Africa/Mbabane',
		),
		'TC' => array(
			'America/Grand_Turk',
		),
		'TD' => array(
			'Africa/Ndjamena',
		),
		'TF' => array(
			'Indian/Kerguelen',
		),
		'TG' => array(
			'Africa/Lome',
		),
		'TH' => array(
			'Asia/Bangkok',
		),
		'TJ' => array(
			'Asia/Dushanbe',
		),
		'TK' => array(
			'Pacific/Fakaofo',
		),
		'TL' => array(
			'Asia/Dili',
		),
		'TM' => array(
			'Asia/Ashgabat',
		),
		'TN' => array(
			'Africa/Tunis',
		),
		'TO' => array(
			'Pacific/Tongatapu',
		),
		'TR' => array(
			'Europe/Istanbul',
		),
		'TT' => array(
			'America/Port_of_Spain',
		),
		'TV' => array(
			'Pacific/Funafuti',
		),
		'TW' => array(
			'Asia/Taipei',
		),
		'TZ' => array(
			'Africa/Dar_es_Salaam',
		),
		'UA' => array(
			'Europe/Kiev',
			'Europe/Zaporozhye',
			'Europe/Simferopol',
			'Europe/Uzhgorod',
		),
		'UG' => array(
			'Africa/Kampala',
		),
		'UM' => array(
			'Pacific/Midway',
			'Pacific/Wake',
		),
		'US' => array(
			'America/New_York',
			'America/Los_Angeles',
			'America/Chicago',
			'America/Denver',
			'America/Phoenix',
			'America/Indiana/Indianapolis',
			'America/Detroit',
			'America/Kentucky/Louisville',
			'Pacific/Honolulu',
			'America/Anchorage',
			'America/Boise',
			'America/Juneau',
			'America/Indiana/Vincennes',
			'America/Sitka',
			'America/Menominee',
			'America/Indiana/Tell_City',
			'America/Kentucky/Monticello',
			'America/Nome',
			'America/Indiana/Knox',
			'America/North_Dakota/Beulah',
			'America/Indiana/Winamac',
			'America/Indiana/Petersburg',
			'America/Indiana/Vevay',
			'America/Metlakatla',
			'America/North_Dakota/New_Salem',
			'America/Indiana/Marengo',
			'America/Yakutat',
			'America/North_Dakota/Center',
			'America/Adak',
		),
		'UY' => array(
			'America/Montevideo',
		),
		'UZ' => array(
			'Asia/Tashkent',
			'Asia/Samarkand',
		),
		'VA' => array(
			'Europe/Vatican',
		),
		'VC' => array(
			'America/St_Vincent',
		),
		'VE' => array(
			'America/Caracas',
		),
		'VG' => array(
			'America/Tortola',
		),
		'VI' => array(
			'America/St_Thomas',
		),
		'VN' => array(
			'Asia/Ho_Chi_Minh',
		),
		'VU' => array(
			'Pacific/Efate',
		),
		'WF' => array(
			'Pacific/Wallis',
		),
		'WS' => array(
			'Pacific/Apia',
		),
		'YE' => array(
			'Asia/Aden',
		),
		'YT' => array(
			'Indian/Mayotte',
		),
		'ZA' => array(
			'Africa/Johannesburg',
		),
		'ZM' => array(
			'Africa/Lusaka',
		),
		'ZW' => array(
			'Africa/Harare',
		),
	);

	// Just in case...
	$country_code = strtoupper(trim($country_code));

	// Avoid unnecessary repetition.
	if (!isset($country_tzids[$country_code]))
	{
		call_integration_hook('integrate_country_timezones', array(&$sorted_tzids, $country_code, $when));

		$country_tzids[$country_code] = isset($sorted_tzids[$country_code]) ? $sorted_tzids[$country_code] : array();

		// If something goes wrong, we want an empty array, not false.
		$recognized_country_tzids = array_filter((array) @timezone_identifiers_list(DateTimeZone::PER_COUNTRY, $country_code));

		// Make sure that no time zones are missing.
		$country_tzids[$country_code] = array_unique(array_merge($country_tzids[$country_code], array_intersect($recognized_country_tzids, timezone_identifiers_list())));

		// Get fallbacks where necessary.
		$country_tzids[$country_code] = array_values(get_tzid_fallbacks($country_tzids[$country_code], $when));

		// Filter out any time zones that are still undefined.
		$country_tzids[$country_code] = array_intersect(array_filter($country_tzids[$country_code]), timezone_identifiers_list(DateTimeZone::ALL_WITH_BC));
	}

	return $country_tzids[$country_code];
}

/**
 * Checks a list of time zone identifiers to make sure they are all defined in
 * the installed version of the time zone database, and returns an array of
 * key-value substitution pairs.
 *
 * For defined time zone identifiers, the substitution value will be identical
 * to the original value. For undefined ones, the substitute will be a time zone
 * identifier that was equivalent to the missing one at the specified time, or
 * an empty string if there was no equivalent at that time.
 *
 * Note: These fallbacks do not need to include every new time zone ever. They
 * only need to cover any that are used in $tzid_metazones.
 *
 * To find the date & time when a new time zone comes into effect, check
 * the TZDB changelog at https://data.iana.org/time-zones/tzdb/NEWS
 *
 * @param array $tzids The time zone identifiers to check.
 * @param string $when The date/time used to determine substitute values.
 *		May be a Unix timestamp or any string that strtotime() can understand.
 *		Defaults to 'now'.
 * @return array Substitute values for any missing time zone identifiers.
 */
function get_tzid_fallbacks($tzids, $when = 'now')
{
	$tzids = (array) $tzids;

	$when = is_numeric($when) ? intval($when) : (is_int(@strtotime($when)) ? strtotime($when) : time());

	// 'ts' is the timestamp when the substitution first becomes valid.
	// 'tzid' is the alternative time zone identifier to use.
	$fallbacks = array(
		// 1. Simple renames. PHP_INT_MIN because these are valid for all dates.
		'Asia/Kolkata' => array(
			array(
				'ts' => PHP_INT_MIN,
				'tzid' => 'Asia/Calcutta',
			),
		),
		'Pacific/Chuuk' => array(
			array(
				'ts' => PHP_INT_MIN,
				'tzid' => 'Pacific/Truk',
			),
		),
		'Pacific/Pohnpei' => array(
			array(
				'ts' => PHP_INT_MIN,
				'tzid' => 'Pacific/Ponape',
			),
		),
		'Asia/Yangon' => array(
			array(
				'ts' => PHP_INT_MIN,
				'tzid' => 'Asia/Rangoon',
			),
		),
		'America/Nuuk' => array(
			array(
				'ts' => PHP_INT_MIN,
				'tzid' => 'America/Godthab',
			),
		),

		// 2. Newly created time zones.

		// The same as Tasmania, except it stayed on DST all year in 2010.
		// Australia/Tasmania is an otherwise unused backwards compatibility
		// link to Australia/Hobart, so we can borrow it here without conflict.
		'Antarctica/Macquarie' => array(
			array(
				'ts' => PHP_INT_MIN,
				'tzid' => 'Australia/Tasmania',
			),
			array(
				'ts' => strtotime('2010-04-03T16:00:00+0000'),
				'tzid' => 'Etc/GMT-11',
			),
			array(
				'ts' => strtotime('2011-04-07T17:00:00+0000'),
				'tzid' => 'Australia/Tasmania',
			),
		),

		// This place uses two hours for DST. No substitutes are possible.
		'Antarctica/Troll' => array(
			array(
				'ts' => PHP_INT_MIN,
				'tzid' => '',
			),
		),

		// Diverged from Pacific/Port_Moresby in version 2014i.
		'Pacific/Bougainville' => array(
			// Before the divergence, we don't actually need this one at all.
			array(
				'ts' => PHP_INT_MIN,
				'tzid' => '',
			),
			// For dates after divergence, it is the same as Pacific/Kosrae.
			// If this ever ceases to be true, add another entry.
			array(
				'ts' => strtotime('2014-12-27T16:00:00+0000'),
				'tzid' => 'Pacific/Kosrae',
			),
		),

		// Diverged from America/Santiago in version 2017a.
		'America/Punta_Arenas' => array(
			// Before the divergence, we don't actually need this one at all.
			array(
				'ts' => PHP_INT_MIN,
				'tzid' => '',
			),
			// For dates after divergence, it is the same as Antarctica/Palmer.
			// If this ever ceases to be true, add another entry.
			array(
				'ts' => strtotime('2017-05-14T03:00:00+0000'),
				'tzid' => 'Antarctica/Palmer',
			),
		),
	);

	$missing = array_diff($tzids, timezone_identifiers_list(DateTimeZone::ALL_WITH_BC));

	call_integration_hook('integrate_timezone_fallbacks', array(&$fallbacks, &$missing, $tzids, $when));

	$replacements = array();

	foreach ($tzids as $tzid)
	{
		// Not missing.
		if (!in_array($tzid, $missing))
			$replacements[$tzid] = $tzid;

		// Missing and we have no fallback.
		elseif (empty($fallbacks[$tzid]))
			$replacements[$tzid] = '';

		// Missing, but we have a fallback.
		else
		{
			usort(
				$fallbacks[$tzid],
				function ($a, $b)
				{
					return $a['ts'] > $b['ts'];
				}
			);

			foreach ($fallbacks[$tzid] as $alt)
			{
				if ($when < $alt['ts'])
					break;

				$replacements[$tzid] = $alt['tzid'];
			}

			if (empty($replacements[$tzid]))
				$replacements[$tzid] = '';
		}
	}

	return $replacements;
}

/**
 * Validates a set of two-character ISO 3166-1 country codes.
 *
 * @param array|string $country_codes Array or CSV string of country codes.
 * @param bool $as_csv If true, return CSV string instead of array.
 * @return array|string Array or CSV string of valid country codes.
 */
function validate_iso_country_codes($country_codes, $as_csv = false)
{
	if (is_string($country_codes))
		$country_codes = explode(',', $country_codes);
	else
		$country_codes = array_map('strval', (array) $country_codes);

	foreach ($country_codes as $key => $country_code)
	{
		$country_code = strtoupper(trim($country_code));
		$country_tzids = strlen($country_code) !== 2 ? null : @timezone_identifiers_list(DateTimeZone::PER_COUNTRY, $country_code);
		$country_codes[$key] = empty($country_tzids) ? null : $country_code;
	}

	$country_codes = array_filter($country_codes);

	if (!empty($as_csv))
		$country_codes = implode(',', $country_codes);

	return $country_codes;
}

?>