/////////////// CLASSES /////////////// 

function Timezone(countryCode, areas, offset) {
	this.countryCode = countryCode;
	if (!(areas instanceof Array)) areas = [areas];
	this.areas = areas;
	this.offset = offset;
}

function DSTRule(areaExceptions, testFn) {
	if (areaExceptions == '' || areaExceptions == null) areaExceptions = [];
	if (!(areaExceptions instanceof Array)) areaExceptions = [areaExceptions];
	this.areaExceptions = areaExceptions;
	this.testFn = testFn;
	
	this.isDSTUsingAddressDetailWithOffset = function(address, offset) {
		var area = null;
		if (address.Country.AdministrativeArea)
			area = address.Country.AdministrativeArea.AdministrativeAreaName;
		else if (address.Country.Locality) // Jerusalem is a Locality, not an AdministrativeArea
			area = address.Country.LocalityName;
			
		// Unless this address' area is in the exception list...
		if (!this.areaExceptions.include(area)) {
			return this.testFn(new Date(), offset);
		} else return false;
	}
}

/////////////// TIME ZONES /////////////// 

_months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
_weekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];

_timezones = [];

// This data is specific to Google's Geocoder
_timezones.push(new Timezone('CN', '*', 8)); // The whole country is one TZ
_timezones.push(new Timezone('HK', '*', 8)); // Hong Kong has a different country code
_timezones.push(new Timezone('IL', '*', 2));
_timezones.push(new Timezone('US', ['VT','ME','NH','MA','RI','CT','NY','NJ','DE','MD','DC','PA','OH','MI','IN','KY','WV','VA','NC','SC','GA','FL'], -5)); // EST
_timezones.push(new Timezone('US', ['ND','MN','SD','NE','WI','IA','IL','MO','KS','OK','TX','AR','LA','TN','MS','AL'], -6)); // CST
_timezones.push(new Timezone('US', ['MT','ID','WY','CO','UT','NM','AZ'], -7)); // MST
_timezones.push(new Timezone('US', ['WA','OR','NV','CA'], -8)); // PST
_timezones.push(new Timezone('US', 'Hawaii', -10));
_timezones.push(new Timezone('US', 'Alaska', -9));
_timezones.push(new Timezone('CA', 'QC', -5)); // EST


/////////////// DST RULE TEST FUNCTIONS /////////////// 

_US_CA_MX_DSTTestFn = function(today, offset) {
	// Calculates for US, Canada (x/Saskatchewan, parts of Quebec, BC & Ontario), Mexico,
	// Bermuda, St. Johns, Bahamas, Turks & Caicos
	// As of 2008: 2nd Sunday in March to 1st Sunday in November
	var today = new Date();
	var secondSundayInMarch = DSTRule.getNthWeekdayInMonth(2, 0, 2);
	//console.log('%s, %s %d', _weekdays[secondSundayInMarch.getUTCDay()], _months[secondSundayInMarch.getUTCMonth()], secondSundayInMarch.getUTCDate());
	var firstSundayInNovember = DSTRule.getNthWeekdayInMonth(1, 0, 10);
	//console.log('%s, %s %d', _weekdays[firstSundayInNovember.getUTCDay()], _months[firstSundayInNovember.getUTCMonth()], firstSundayInNovember.getUTCDate());
	
	// Compare seconds since epoch -- if today is > A && < B then true, else false
	if (today.getTime() > secondSundayInMarch.getTime() && today.getTime() < firstSundayInNovember.getTime())
		return true;
	else
		return false;
}

_IL_DSTTestFn = function(today, offset) {
	// Calculates DST for Israel
	// As of 2008: Last Friday before April 2 to the Sunday b/w Rosh Hashanah and Yom Kippur
	// Rosh Hashanah is Sept 30 - Oct 1 in 2008, the Sunday after is Oct 5
	lastFridayBeforeApril2 = DSTRule.getNthWeekdayInMonth(-1, 5, 3, 2);
	//console.log('%s, %s %d', _weekdays[lastFridayBeforeApril2.getUTCDay()], _months[lastFridayBeforeApril2.getUTCMonth()], lastFridayBeforeApril2.getUTCDate());
	sundayAfterRoshHashanah = new Date(2008, 9, 5);
	if (today.getTime() > lastFridayBeforeApril2.getTime() && today.getTime() < sundayAfterRoshHashanah.getTime())
		return true;
	else
		return false;
}

/////////////// DST RULES /////////////// 

_dstRules = [];

// DST rules are on a per-country basis, so it's ok to index by country code
_dstRules['US'] = new DSTRule('AZ', _US_CA_MX_DSTTestFn);
_dstRules['MX'] = new DSTRule([], _US_CA_MX_DSTTestFn);
_dstRules['CA'] = new DSTRule(['Saskatchewan'], _US_CA_MX_DSTTestFn);
_dstRules['IL'] = new DSTRule([], _IL_DSTTestFn);
// China doesn't have DST, but we don't want that misleading message
_dstRules['CN'] = new DSTRule([], function() { return false; });
_dstRules['HK'] = new DSTRule([], function() { return false; });

/////////////// CLASS FUNCTIONS /////////////// 

DSTRule.getNthWeekdayInMonth = function(nth, weekday, month, date, hour) {
	var today = new Date();
	var direction = nth < 0 ? -1 : 1;
	
	// First find the first day of month at midnight
	// If nth is negative, it should find the last day of the month
	var month = new Date(new Date().getUTCFullYear(), month, date || 1, hour || 12);
	month.setUTCHours(12);
	
	var i = 0;
	
	// In a loop, increment a counter every time weekday is reached, day by day, until counter == nth
	// If nth is negative, it should count backwards to find the nth-to-last weekday
	while (i < (nth * direction)) {
		//console.log('month=%s', month.toUTCString());
		if (month.getUTCDay() == weekday) i++;
		if (i == nth) break; // End the loop before incrementing the day
		month.setUTCDate(month.getUTCDate() + (direction));
	}
	
	return month;
}

DSTRule.addressDetailIsDST = function(address, timezone) {
	var rule = _dstRules[address.Country.CountryNameCode];
	if (rule)
		return rule.isDSTUsingAddressDetailWithOffset(address, timezone);
	else
		return false;
}

Timezone.forAddressDetails = function(address) {
	//console.log('Looking up timezone for %o', address);
	var offset = 99;
	
	_timezones.each(function(timezone) {
		//console.log('address.countryCode=%o vs timezone.countryCode=%o', address.Country.CountryNameCode, timezone.countryCode);
		if (timezone.countryCode == address.Country.CountryNameCode) {
			
			// A star means 'for all areas', for locations like China
			// where the whole country is one time zone
			if (timezone.areas.include('*')) {
				offset = timezone.offset;
				return false;
			}
			
			var area = null;
			if (address.Country.AdministrativeArea)
				area = address.Country.AdministrativeArea.AdministrativeAreaName;
			else if (address.Country.Locality) // Jerusalem is a Locality, not an AdministrativeArea
				area = address.Country.LocalityName;
			else
				return false; // The necessary information was not found, so it doesn't know what to do
				
			// This needs to check Country.Locality as well because Jerusalem doesn't have an AdministrativeArea!
			if (timezone.areas.include(area)) {
				//console.log('Timezone for %o = %o', address.Country.AdministrativeArea.AdministrativeAreaName, timezone.offset);
				offset = timezone.offset;
				return false;
			}
		}
	});
	
	//console.log('Returning timezone offset of %o', offset);
	// An offset of 99 indicates it couldn't find the timezone,
	// but this is only useful inside this function, so set it to 0 for UTC time
	return offset == 99 ? 0 : offset;
}


//////////////////////////////////////

// Google defines West Longtitudes as negative, whereas NOAA defines them as positive
/* Here, use Google's convention because the coordinates come from Google
timezones[i++] = new Timezone('International Date Line West', 'IDLW', -12, 180);
timezones[i++] = new Timezone('Midway Island Samoa', 'Samoa', -11, 165);
timezones[i++] = new Timezone('Hawaii', 'Hawaii', -10, 150);
timezones[i++] = new Timezone('Alaska', 'Alaska', -9, 0);
timezones[i++] = new Timezone('Pacific Time (US & Canada)', 'PST', -8, 0);
timezones[i++] = new Timezone('Mountain Time (US & Canada), Arizona', 'MST', -7, 0);
timezones[i++] = new Timezone('Central Time (US & Canada), Mexico City, Saskatchewan', 'CST', -6, 0);
timezones[i++] = new Timezone('Eastern Time (US & Canada), Indiana (East), Bogota, Lime', 'EST', -5, 0);
timezones[i++] = new Timezone('Atlantic Time (Canada), Caracas, Santiago, La Paz', 'AST', -4, 0);
timezones[i++] = new Timezone('Newfoundland', 'Newfoundland', -3.5, 0);
timezones[i++] = new Timezone('Greenland, Buenos Aires, Georgetown', 'Greenland', -3, 0);
timezones[i++] = new Timezone('Mid-Atlantic', 'Mid-Atlantic', -2, 0);
timezones[i++] = new Timezone('Azores', 'Azores', -1, 0);
timezones[i++] = new Timezone('Cape Verde Is.', 'Cape Verde', -1, 0);
timezones[i++] = new Timezone('Greenwich Mean Time: Dubling, Edinburgh, Lisbon, London', 'GMT', 0, -7.5);
timezones[i++] = new Timezone('Amersterdam, Berlin, Bern, Rome, Stockholm, Vienna, Prague', 'Amsterdam', 1, 7.5);
timezones[i++] = new Timezone('Jerusalem, Athens, Istanbul', 'Jerusalem', 2, 22.5);
timezones[i++] = new Timezone('Baghdad, Kuwait, Moscow', 'Moscow', 3, 37.5);
timezones[i++] = new Timezone('Abu Dhabi, Muscat, Yerevan', 'Abu Dhabi', 4, 52.5);
timezones[i++] = new Timezone('Ekaterinburg, Islamabad', 'Ekaterinburg', 5, 67.5);
timezones[i++] = new Timezone('Chennai, Mumbai, New Delhi', 'New Delhi', 5.5, 82.5);
timezones[i++] = new Timezone('Katmandu', 'Katmandu', 5.75, 97.5);
timezones[i++] = new Timezone('Almaty, Dhaka, Astana', 'Almaty', 6, 112.5);
timezones[i++] = new Timezone('Rangoon', 'Rangoon', 6.5, 127.5);
timezones[i++] = new Timezone('Bangkok, Hanoi, Jakarta', 'Bangkok', 7, 142.5);
timezones[i++] = new Timezone('Beijing, Hongkong, Singapore, Taipei', 'Beijing', 8, 157.5);
timezones[i++] = new Timezone('Osaka, Sapporo, Tokyo, Seoul', 'Japan Korea', 9, 172.5);
timezones[i++] = new Timezone('Adelaide, Darwin', 'Adelaide', 9.5, 0);
timezones[i++] = new Timezone('Brisbane, Melbourne, Syndney, Guam, Hobart', 'Sydney', 10, 0);
timezones[i++] = new Timezone('Magadan, Solomon Is.', 'Magadan', 11, 0);
timezones[i++] = new Timezone('Auckland, Wellington, Fiji, Marshall Is.', 'Auckland', 12, 0);
timezones[i++] = new Timezone('Nukualofa', 'Nukualofa', 13, 0);
*/