/**
 * ActiveSupport for JavaScript library, version '0.1'
 * (c) 2007 Nicolas Sanguinetti
 *
 * ActiveSupport for JavaScript is freely distributable under the terms of an
 * MIT-style license. For details, see our web site:
 *   http://code.google.com/p/active-support-for-javascript/
 *
 */

var ActiveSupport = {
	Version: '0.1',

	pluralizeMethods: function(module) {
		$H(module).each(function(pair) {
			module[pair.first().pluralize()] = module[pair.first()];
		});
	},
	pluralize: function(count, singular) {
		return count.abs() == 1 ? count + " " + singular : count + " " + singular.pluralize();
	}
}

Object.alias = function(obj, mappings) {
	for (var m in mappings) {
		obj[m] = obj[mappings[m]];
	}
	return obj;
};

window.pluralize = ActiveSupport.pluralize;


// String Interpolation

var InterpolatableString = Class.create();
InterpolatableString.prototype = {
	initialize: function(string, binding) {
		this.string = string;
		this.tokens = (string.match(/#\{([^\}]+)\}/ig) || []).map(function(token) {
			return new InterpolatableString.Token(token, binding);
		});
	},
	toString: function() {
		return this.tokens.inject(this.string.toString(), function(result, token) {
			return result.gsub(token.toRegExp(), token.evaluate()).toString();
		}.bind(this));
	}
};

InterpolatableString.Token = Class.create();
InterpolatableString.Token.prototype = {
	initialize: function(token, binding) {
		this.token = token.replace(/^#\{(.*)\}$/, "$1");
		this.binding = binding;
	},
	toRegExp: function() {
		var token = ("#{" + this.token + "}").replace(/\(/, "\\(").replace(/\)/, "\\)").replace(/\[/, "\\[").replace(/\]/, "\\]").replace(/\./, "\\.").replace(/\-/, "\\-");
		return new RegExp(token, "i");
	},
	evaluate: function() {
		var token = this.token;
		return (function() { return eval(token); }.bind(this.binding))();
	}
};

var $Q = function(string, binding) {
	return string.interpolate(binding === 0 ? binding : (binding || window));
};

// Inflector

var Inflector = {
	pluralize: function(word) {
		if (Inflections.uncountables.include(word.toLowerCase()))
			return word;
		return Inflections.plurals.map(function(pair) {
			return word.replace(pair.first(), pair.last());
		}).detect(function(plural) { return word != plural; }) || word;
	},
	singularize: function(word) {
		if (Inflections.uncountables.include(word.toLowerCase()))
			return word;
		return Inflections.singulars.map(function(pair) {
			return word.replace(pair.first(), pair.last());
		}).detect(function(singular) { return word != singular; }) || word;
	},
	ordinalize: function(number) {
		if ($R(11, 13).include(number % 100)) {
			return number + "th";
		}
		switch (number % 10) {
			case 1: return number + "st";
			case 2: return number + "nd";
			case 3: return number + "rd";
			default: return number + "th";
		}
	}
};

var Inflections = {
	plurals: [],
	singulars: [],
	uncountables: [],

	plural: function(rule, replacement) {
		this.plurals.unshift([rule, replacement]);
	},
	singular: function(rule, replacement) {
		this.singulars.unshift([rule, replacement]);
	},
	irregular: function(singular, plural) {
		this.plural(new RegExp("(" + singular.charAt(0) + ")" + singular.substring(1) + "$", "i"), "$1" + plural.substring(1));
		this.singular(new RegExp("(" + plural.charAt(0) + ")" + plural.substring(1) + "$", "i"), "$1" + singular.substring(1));
	},
	uncountable: function(uncountable) {
		this.uncountables = this.uncountables.concat($A(arguments));
	}
};

with (Inflections) {
	plural(/$/, "s");
	plural(/s$/i, "s");
	plural(/(ax|test)is$/i, "$1es");
	plural(/(octop|vir)us$/i, "$1i");
	plural(/(alias|status)$/i, "$1es");
	plural(/(bu)s$/i, "$1ses");
	plural(/(buffal|tomat)o$/i, "$1oes");
	plural(/([ti])um$/i, "$1a");
	plural(/sis$/i, "ses");
	plural(/(?:([^f])fe|([lr])f)$/i, "$1$2ves");
	plural(/(hive)$/i, "$1s");
	plural(/([^aeiouy]|qu)y$/i, "$1ies");
	plural(/([^aeiouy]|qu)ies$/i, "$1y");
	plural(/(x|ch|ss|sh)$/i, "$1es");
	plural(/(matr|vert|ind)ix|ex$/i, "$1ices");
	plural(/([m|l])ouse$/i, "$1ice");
	plural(/^(ox)$/i, "$1en");
	plural(/(quiz)$/i, "$1zes");

	singular(/s$/i, '');
	singular(/(n)ews$/i, '$1ews');
	singular(/([ti])a$/i, '$1um');
	singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i, '$1$2sis');
	singular(/(^analy)ses$/i, '$1sis');
	singular(/([^f])ves$/i, '$1fe');
	singular(/(hive)s$/i, '$1');
	singular(/(tive)s$/i, '$1');
	singular(/([lr])ves$/i, '$1f');
	singular(/([^aeiouy]|qu)ies$/i, '$1y');
	singular(/(s)eries$/i, '$1eries');
	singular(/(m)ovies$/i, '$1ovie');
	singular(/(x|ch|ss|sh)es$/i, '$1');
	singular(/([m|l])ice$/i, '$1ouse');
	singular(/(bus)es$/i, '$1');
	singular(/(o)es$/i, '$1');
	singular(/(shoe)s$/i, '$1');
	singular(/(cris|ax|test)es$/i, '$1is');
	singular(/([octop|vir])i$/i, '$1us');
	singular(/(alias|status)es$/i, '$1');
	singular(/^(ox)en/i, '$1');
	singular(/(vert|ind)ices$/i, '$1ex');
	singular(/(matr)ices$/i, '$1ix');
	singular(/(quiz)zes$/i, '$1');

	irregular("person", "people");
	irregular("man", "men");
	irregular("child", "children");
	irregular("sex", "sexes");
	irregular("move", "moves");

	uncountable("equipment", "information", "rice", "money", "species", "series", "fish", "sheep");
};

// String Extensions

Object.extend(String.prototype, {
	interpolate: function(binding) {
		return new InterpolatableString(this, binding === 0 ? binding : (binding || window)).toString();
	},
	pluralize: function() {
		return Inflector.pluralize(this.toString());
	},
	singularize: function() {
		return Inflector.singularize(this.toString());
	},
	toInt: function() {
		return parseInt(this);
	},
	toFloat: function() {
		return parseFloat(this);
	},
	firstToUpper: function() {
		// doesn't force the rest of the string to lowercase, as capitalize does
		return this.charAt(0).toUpperCase() + this.substring(1);
	}
});

// Array Extensions

Object.extend(Array.prototype, {
	sum: function() {
		return this.inject(0, function(sum, value) { return sum += value; });
	},
	product: function() {
		return this.inject(1, function(product, value) { return product *= value; });
	},
	toSentence: function() {
		var options = Object.extend({
			"connector": "and",
			"skip_last_comma": false
		}, arguments[0] || {});

		switch (this.size()) {
			case 0: return "";
			case 1: return this.reduce();
			case 2: return $Q("#{this.first()} #{options['connector']} #{this.last()}", this);
			default: return $Q("#{this.slice(0, -1).join(', ')}#{options['skip_last_comma'] ? '' : ','} #{options['connector']} #{this.last()}", this);
		}
	}
});

// Number Extensions

$w("acos asin atan cos exp log pow sin sqrt tan").each(function(method) {
	Number.prototype[method] = Math[method].methodize();
});

Object.extend(Number.prototype, {
	ordinalize: function() {
		return Inflector.ordinalize(this);
	},
	sign: function(zero_is_positive) {
		if (this < 0)
			return "-";
		if (this > 0)
			return "+";
		return zero_is_positive ? "+" : "";
	}
});

(function() {
	var ByteExtensions = {
		byte:     function() { return this; },
		kilobyte: function() { return this * 1024; },
		megabyte: function() { return this * (1024).kilobytes(); },
		gigabyte: function() { return this * (1024).megabytes(); },
		terabyte: function() { return this * (1024).gigabytes(); },
		petabyte: function() { return this * (1024).terabytes(); },
		exabyte:  function() { return this * (1024).petabytes(); }
	};
	ActiveSupport.pluralizeMethods(ByteExtensions);
	Object.extend(Number.prototype, ByteExtensions);

	var IntervalExtensions = {
	  second:    function() { return this; },
	  minute:    function() { return this.seconds() * 60; },
	  hour:      function() { return this.minutes() * 60; },
	  day:       function() { return this.hours() * 24; },
	  week:      function() { return this.days() * 7; },
		fortnight: function() { return this.weeks() * 2; },
		month:     function() { return this.days() * 30; },
		year:      function() { return this.months() * 12 }
	};
	ActiveSupport.pluralizeMethods(IntervalExtensions);
	Object.extend(Number.prototype, IntervalExtensions);

	var TimeExtensions = {
	  since: function(reference) { return new Date((reference || new Date()).getTime() + this.seconds()); },
	  until: function(reference) { return new Date((reference || new Date()).getTime() - this.seconds()); }
	};
	TimeExtensions.fromNow = TimeExtensions.since.curry(null);
	TimeExtensions.ago = TimeExtensions.until.curry(null);
	Object.extend(Number.prototype, TimeExtensions);
})();

// Date Extensions

Object.extend(Date, {
	MONTHS: $w("January February March April May June July August September October November December"),
	ABBR_MONTHS: $w("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec"),
	WEEKDAYS: $w("Sunday Monday Tuesday Wednesday Thursday Friday Saturday"),
	ABBR_WEEKDAYS: $w("Sun Mon Tue Wed Thu Fri Sat"),
	TIMEZONES: {
		"ACDT": 630,  "ACIT": 480,  "ACST": 570,  "ACT": -300,  "ADT": 240,   "ADT": -180,  "AEDT": 660,  "AEST": 600,  "AFT": 270,
		"AKDT": -480, "AKST": -540, "AMDT": 300,  "AMST": -180, "AMST": 240,  "AMT": -240,  "ANAST": 780, "ANAT": 720,  "APO": 495,
		"ART": -180,  "AST": 180,   "AST": 180,   "AST": 180,   "AST": -240,  "ACWDT": 585, "ACWST": 525, "AZODT": 0,   "AZOST": -60,
		"AZST": 300,  "AZT": 240,   "BIT": -720,  "BDT": 360,   "BDT": 480,   "BIOT": 360,  "BOT": -240,  "BRST": -120, "BRT": -180,
		"BST": 60,    "BTT": 360,   "CAST": 300,  "CAT": 120,   "CCT": 390,   "CDT": -300,  "CEDT": 120,  "CET": 60,    "CGST": -120,
		"CGT": -180,  "CHADT": 825, "CHAST": 765, "CIST": -480, "CKT": -600,  "CLDT": -180, "CLST": -240, "COT": -300,  "CST": -360,
		"CST": 480,   "CVT": -60,   "CXT": 420,   "DAVT": 420,  "DTAT": 600,  "EADT": -300, "EAST": -360, "EAT": 180,   "FJT": 720,
		"FKDT": -180, "FKST": -240, "FNT": -120,  "GALT": -360, "GIT": -540,  "GEDT": 240,  "GEST": 180,  "GFT": -180,  "GMT": 0,
		"GILT": 720,  "GST": 240,   "GST": -120,  "GYT": -240,  "HADT": -540, "HAST": -600, "HKST": 480,  "HMT": 300,   "ICT": 240,
		"ICT": 420,   "IDT": 60,    "IDT": 180,   "IRKST": 540, "IRKT": 480,  "IRDT": 270,  "IRST": 210,  "IST": 330,   "IST": 0,
		"IST": 120,   "JFDT": -180, "JFST": -240, "JST": 540,   "KGST": 360,  "KGT": 300,   "KRAST": 480, "KRAT": 420,  "KOST": 660,
		"KOVT": 420,  "KOVST": 480, "KST": 540,   "LHDT": 660,  "LHST": 630,  "LINT": 840,  "LKT": 360,   "MAGST": 720, "MAGT": 660,
		"MIT": -570,  "MHT": 720,   "MAWT": 360,  "MMT": 390,   "MNT": 480,   "MNST": 540,  "MSDT": 240,  "MSST": 180,  "MDT": -360,
		"MST": -420,  "MUT": 240,   "MVT": 300,   "MYT": 480,   "NCT": 660,   "NDT": -150,  "NFT": 690,   "NMIT": 600,  "NPT": 345,
		"NRT": 720,   "NOVST": 420, "NOVT": 360,  "NST": -210,  "NUT": -660,  "NZDT": 780,  "NZST": 720,  "OMSST": 420, "OMST": 360,
		"PDT": -420,  "PETST": 780, "PET": -300,  "PETT": 720,  "PGT": 600,   "PHOT": 780,  "PHT": 480,   "PIT": 480,   "PIT": -360,
		"PKT": 300,   "PMDT": -120, "PMST": -180, "PONT": 660,  "PST": -480,  "PST": -480,  "PWT": 540,   "PYST": -180, "PYT": -240,
		"RET": 240,   "ROTT": -180, "SAMST": 300, "SAMT": 240,  "SAST": 120,  "SBT": 660,   "SCDT": 780,  "SCST": 720,  "SCT": 240,
		"SEST": 60,   "SGT": 480,   "SIT": 480,   "SLT": 0,     "SRT": -180,  "SST": -660,  "SYST": 180,  "SYT": 120,   "TAHT": -600,
		"TFT": 300,   "TJT": 300,   "TKT": -600,  "TMT": 300,   "TOT": 780,   "TPT": 540,   "TRUT": 600,  "TVT": 720,   "TWT": 480,
		"UTC": 0,     "UYT": -180,  "UYST": -120, "UZT": 300,   "VET": -240,  "VLAST": 660, "VLAT": 600,  "VOST": 360,  "VUT": 660,
		"WAST": 120,  "WAT": 60,    "WEST": 60,   "WET": 0,     "WFT": 720,   "WKST": 300,  "WDT": 540,   "WST": 480,   "WIB": 420,
		"WITA": 480,  "WIT": 540,   "YAKST": 600, "YAKT": 540,  "YAPT": 600,  "YEKST": 360, "YEKT": 300
	},
	RELATIVE_DATE_OUTPUT: {
		today: "today",
		yesterday: "yesterday",
		tomorrow: "tomorrow",
		hour_format: "%I:%M%p %Z",
		date_format: "%b %o",
		year_format: ", %Y"
	},
	RELATIVE_TIME_RANGES: {
		0:  "less than a minute",
		15: "#{pluralize(this, 'minute')}",
		25: "less than half an hour",
		35: "about half an hour",
		55: "less than an hour",
		65: "about an hour",
		85: "less than an hour and a half",
		95: "about an hour and a half",
		115: "less than 2 hours",
		125: "about 2 hours",
		145: "less than 2 hours and a half",
		155: "about 2 hours and a half",
		175: "less than 3 hours",
		185: "around 3 hours"
	},
	STRING_FORMATS: {
		"%a": function() { return Date.ABBR_WEEKDAYS[this.getDay()]; },
		"%A": function() { return Date.WEEKDAYS[this.getDay()]; },
		"%b": function() { return Date.ABBR_MONTHS[this.getMonth()]; },
		"%B": function() { return Date.MONTHS[this.getMonth()]; },
		"%c": function() { return this.toLocaleString(); },
		"%d": function() { return this.getDate().toPaddedString(2); },
        "%D": function() { return this.getDate(); },
		"%H": function() { return this.getHours().toPaddedString(2); },
        "%h": function() { return this.getHours(); },
		"%I": function() { return (this.getHours() % 12 || 12).toPaddedString(2); },
		"%i": function() { return this.getHours() % 12 || 12; },
		"%j": function() { return this.dayOfTheYear(); },
		"%m": function() { return (this.getMonth() + 1).toPaddedString(2); },
		"%M": function() { return this.getMinutes().toPaddedString(2); },
		"%n": function() { return this.getMonth() + 1; },
		"%o": function() { return this.getDate().ordinalize(); },
		"%p": function() { return (this.getHours() / 12).floor() == 0 ? "AM" : "PM"; },
		"%S": function() { return this.getSeconds().toPaddedString(2); },
		"%U": function() { throw Error("not implemented"); },
		"%W": function() { throw Error("not implemented"); },
		"%w": function() { return this.getDay(); },
		"%x": function() { throw Error("not implemented"); },
		"%X": function() { throw Error("not implemented"); },
		"%y": function() { return this.getYear().toPaddedString(2); },
		"%Y": function() { return this.getFullYear().toPaddedString(4); },
		"%Z": function() { return this.getTimezone(); }
	},
	now: function() {
		return new Date();
	},
	today: function() {
		return Date.now().atBeginningOfDay();
	},
	yesterday: function() {
		return Date.today().yesterday();
	},
	tomorrow: function() {
		return Date.today().tomorrow();
	}
});

Date.DEFAULT_TIMEZONE = (function() {
	// is the string representation the only place you can get the code?
	// this sucks, is too error-prone, and has to be triple-checked in all browsers...
	// (for example, firefox win/mac differ in behaviour)
	var date = new Date().toString();
	if (date.match(/.*\(([A-Z]+)\)$/))
		return date.replace(/.*\(([A-Z]+)\)$/, "$1");
	else
		return date.gsub(/^.*(?:GMT|UTC)([+-])([0-9:]{4,5}).*$/, function(matches) {
			var sign = matches[1], time = matches[2].gsub(":", "");
			var minutes = (time.substring(0, 2).toInt() * 60 + time.substring(2).toInt()) * (sign == "-" ? -1 : 1);
			return minutes == 0 ? "UTC" : $H(Date.TIMEZONES).find(function(pair) {
				return pair.last() == minutes;
			}).first();
		});
})();

Object.extend(Date.prototype, {
	equals: function(otherDate) {
		var checkTime = checkTime || false;
		return this.getFullYear() == otherDate.getFullYear() && this.getMonth() == otherDate.getMonth() && this.getDate() == otherDate.getDate() &&
			(!checkTime || (this.getHours() == otherDate.getHours() && this.getMinutes() == otherDate.getMinutes() && this.getSeconds() == otherDate.getSeconds()));
	},
	isLeapYear: function() {
		var year = this.getFullYear();
		return (year % 4 == 0 && year % 100 != 0) || year % 400;
	},
	getMonthName: function() {
		return Date.MONTHS[this.getMonth()];
	},
	getDaysInMonth: function() {
		switch (this.getMonth() + 1) {
			case 2:
				return this.isLeapYear() ? 29 : 28;
			case 4:
			case 6:
			case 9:
			case 11:
				return 30;
			default:
				return 31;
		}
	},
	dayOfTheYear: function() {
		return $R(0, this.getMonth(), true).map(function(month) {
			return new Date(this).setMonth(month).getDaysInMonth();
		}.bind(this)).sum() + this.getDate();
	},
	isToday: function() {
		return this.midnight().equals(new Date().midnight());
	},
	inThePast: function() {
		return this.getTime() < Date.now().getTime();
	},
	inTheFuture: function() {
		return this.getTime() > Date.now().getTime();
	},
	since: function(seconds) {
	 	return seconds.since(this);
	},
	ago: function(seconds) {
		return this.since(-seconds);
	},
	yesterday: function() {
		return this.setDate(this.getDate() - 1);
	},
	tomorrow: function() {
		return this.setDate(this.getDate() + 1);
	}
});

Date.prototype.getTimezoneOffset = Date.prototype.getTimezoneOffset.wrap(function(localizedDefault) {
	try {
		return Date.TIMEZONES[this.getTimezone()];
	} catch(e) {
		return localizedDefault();
	}
});

(function() {
	var TimezoneMethods = {
		setTimezone: function(tz) {
			if (Date.TIMEZONES[tz] === undefined)
				throw Error("Unknown Timezone '" + tz + "'");
			this.setTime(this.getTime() + (Date.TIMEZONES[tz] - Date.TIMEZONES[this.getTimezone()]).minutes());
			this._tz = tz;
			return this;
		},
		getTimezone: function() {
			return this._tz || Date.DEFAULT_TIMEZONE;
		},
		getTimezoneOffsetInHours: function() {
			var offset = this.getTimezoneOffset().minutes();
			return offset.sign(true) + offset.abs().since(new Date().midnight()).strftime("%H:%M");
		}
	};
	Object.extend(Date.prototype, TimezoneMethods);

	if (!Prototype.Browser.IE) {
		// need to find a workaround for explorer, right now it throws a stack overflow
		Date.prototype.toString = Date.prototype.toString.wrap(function(withoutTimezones) {
			return withoutTimezones().
				gsub(/\([A-Z]+\)$/, "(" + this.getTimezone() + ")").
				gsub(/(?:GMT|UTC)[+-][0-9:]{4,5}/, "UTC" + this.getTimezoneOffsetInHours());
		});
	}

	var ImplicitSetters = {
		beginningOfDay: function() {
			return new Date(this).setHours(0).setMinutes(0).setSeconds(0);
		},
		beginningOfWeek: function() {
			var daysToSunday = this.getDay() == 0 ? 6 : this.getDay() - 1;
			return daysToSunday.days().until(this.beginningOfDay());
		},
		beginningOfMonth: function() {
			return this.beginningOfDay().setDate(1);
		},
		beginningOfQuarter: function() {
			return this.beginningOfMonth().setMonth([9, 6, 3, 0].detect(function(m) { return m <= this.getMonth(); }.bind(this)));
		},
		beginningOfYear: function() {
			return this.beginningOfMonth().setMonth(0);
		},
		endOfDay: function() {
			return new Date(this).setHours(23).setMinutes(59).setSeconds(59);
		},
		endOfMonth: function() {
			return this.beginningOfDay().setDate(this.getDaysInMonth());
		},
		endOfQuarter: function() {
			return this.setMonth([2, 5, 8, 11].detect(function(m) { return m >= this.getMonth(); }.bind(this))).endOfMonth();
		}
	};
	Object.extend(Date.prototype, ImplicitSetters);
	$H(ImplicitSetters).keys().each(function(method) {
		Date.prototype[method] = ImplicitSetters[method];
		Date.prototype["at" + method.firstToUpper()] = Date.prototype[method];
	});

	var ReadableDates = {
		toFormattedString: function(format) {
			return format.gsub(/%[a-zA-Z]/, function(pattern) {
				return Date.STRING_FORMATS[pattern].bind(this)().toString();
			}.bind(this)).replace(/%%/, "%");
		},
		relativeDate: function() {
			var targetTime = this.atBeginningOfDay();
			var today = Date.today();

			if (targetTime.equals(today)) {
				return Date.RELATIVE_DATE_OUTPUT["today"];
			} else if (targetTime.equals(Date.yesterday())) {
				return Date.RELATIVE_DATE_OUTPUT["yesterday"];
			} else if (targetTime.equals(Date.tomorrow())) {
				return Date.RELATIVE_DATE_OUTPUT["tomorrow"];
			} else {
				var format = Date.RELATIVE_DATE_OUTPUT["date_format"];
				format += targetTime.getFullYear() == today.getFullYear() ? "" : Date.RELATIVE_DATE_OUTPUT["year_format"];
				return this.strftime(format);
			}
		},
		relativeTime: function() {
			var options = Object.extend({ prefix: this.inTheFuture() ? "in " : "", suffix: this.inThePast() ? " ago" : "" }, arguments[0] || {});
			var distanceInMinutes = ((Date.now().getTime() - this.getTime()).abs() / 60000).floor();
			return $H(Date.RELATIVE_TIME_RANGES).map(function(pair) {
				return (distanceInMinutes <= pair.first()) ?
					(options["prefix"] + " " + $Q(pair.last(), distanceInMinutes) + " " + options["suffix"]).strip() : false;
			}).find(Prototype.K) || (this.relativeDate() + " at " + this.strftime(Date.RELATIVE_DATE_OUTPUT.hour_format));
		}
	}
	Object.extend(Date.prototype, ReadableDates);

	Object.alias(Date.prototype, {
		strftime: "toFormattedString",
		midnight: "beginningOfDay",
		monday:   "beginningOfWeek"
	});
})();

$w("setDate setMonth setFullYear setYear setHours setMinutes setSeconds setMilliseconds setTime").each(function(method) {
	Date.prototype[method + "WithoutChaining"] = Date.prototype[method];
	Date.prototype[method] = function() {
		this[method + "WithoutChaining"].apply(this, $A(arguments));
		return this;
	}
});

Date.WEEKDAYS.each(function(dayName, dayIndex) {
	Date.prototype["is" + dayName] = function() {
		return this.getDay() % 7 == dayIndex;
	}
});
