// Instantiate the object
var I18n = I18n || {};

// Set default locale to english
I18n.defaultLocale = "en";

// Set default separator
I18n.defaultSeparator = ".";

// Set current locale to null
I18n.locale = null;

// Set the placeholder format. Accepts `{{placeholder}}` and `%{placeholder}`.
I18n.PLACEHOLDER = /(?:\{\{|%\{)(.*?)(?:\}\}?)/gm;

I18n.isValidNode = function(obj, node) {
	// local undefined variable in case another library corrupts
	// window.undefined
	var undef;
	return obj[node] !== null && obj[node] !== undef;
}

I18n.lookup = function(scope, options) {
	var translations = this.prepareOptions(I18n.translations);
	var messages = translations[I18n.currentLocale()];
	options = this.prepareOptions(options);

	if (!messages) {
		return;
	}

	if (typeof (scope) == "object") {
		scope = scope.join(this.defaultSeparator);
	}

	if (options.scope) {
		scope = options.scope.toString() + this.defaultSeparator + scope;
	}

	scope = scope.split(this.defaultSeparator);

	while (scope.length > 0) {
		var currentScope = scope.shift();
		messages = messages[currentScope];

		if (!messages) {
			break;
		}
	}

	if (!messages && this.isValidNode(options, "defaultValue")) {
		messages = options.defaultValue;
	}

	return messages;
};

// Merge serveral hash options, checking if value is set before
// overwriting any value. The precedence is from left to right.
//
// I18n.prepareOptions({name: "John Doe"}, {name: "Mary Doe", role: "user"});
// #=> {name: "John Doe", role: "user"}
//
I18n.prepareOptions = function() {
	var options = {};
	var opts;
	var count = arguments.length;

	for ( var i = 0; i < count; i++) {
		opts = arguments[i];

		if (!opts) {
			continue;
		}

		for ( var key in opts) {
			if (!this.isValidNode(options, key)) {
				options[key] = opts[key];
			}
		}
	}

	return options;
};

I18n.interpolate = function(message, options) {
	options = this.prepareOptions(options);
	var matches = message.match(this.PLACEHOLDER);

	if (!matches) {
		return message;
	}

	var placeholder, value, name;

	for ( var i = 0; placeholder = matches[i]; i++) {
		name = placeholder.replace(this.PLACEHOLDER, "$1");

		value = options[name];

		if (!this.isValidNode(options, name)) {
			value = "[missing " + placeholder + " value]";
		}

		regex = new RegExp(placeholder.replace(/\{/gm, "\\{").replace(/\}/gm,
				"\\}"));
		message = message.replace(regex, value);
	}

	return message;
};

I18n.translate = function(scope, options) {
	options = this.prepareOptions(options);
	var translation = this.lookup(scope, options);

	try {
		if (typeof (translation) == "object") {
			if (typeof (options.count) == "number") {
				return this.pluralize(options.count, scope, options);
			} else {
				return translation;
			}
		} else {
			return this.interpolate(translation, options);
		}
	} catch (err) {
		return this.missingTranslation(scope);
	}
};

I18n.localize = function(scope, value) {
	switch (scope) {
	case "currency":
		return this.toCurrency(value);
	case "number":
		scope = this.lookup("number.format");
		return this.toNumber(value, scope);
	case "percentage":
		return this.toPercentage(value);
	default:
		if (scope.match(/^(date|time)/)) {
			return this.toTime(scope, value);
		} else {
			return value.toString();
		}
	}
};

I18n.parseDate = function(date) {
	var matches, convertedDate;

	// we have a date, so just return it.
	if (typeof (date) == "object") {
		return date;
	}
	;

	// it matches the following formats:
	// yyyy-mm-dd
	// yyyy-mm-dd[ T]hh:mm::ss
	// yyyy-mm-dd[ T]hh:mm::ss
	// yyyy-mm-dd[ T]hh:mm::ssZ
	// yyyy-mm-dd[ T]hh:mm::ss+0000
	//
	matches = date
			.toString()
			.match(
					/(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2}):(\d{2}))?(Z|\+0000)?/);

	if (matches) {
		for ( var i = 1; i <= 6; i++) {
			matches[i] = parseInt(matches[i], 10) || 0;
		}

		// month starts on 0
		matches[2] -= 1;

		if (matches[7]) {
			convertedDate = new Date(Date.UTC(matches[1], matches[2],
					matches[3], matches[4], matches[5], matches[6]));
		} else {
			convertedDate = new Date(matches[1], matches[2], matches[3],
					matches[4], matches[5], matches[6]);
		}
	} else if (typeof (date) == "number") {
		// UNIX timestamp
		convertedDate = new Date();
		convertedDate.setTime(date);
	} else if (date.match(/\d+ \d+:\d+:\d+ [+-]\d+ \d+/)) {
		// a valid javascript format with timezone info
		convertedDate = new Date();
		convertedDate.setTime(Date.parse(date))
	} else {
		// an arbitrary javascript string
		convertedDate = new Date();
		convertedDate.setTime(Date.parse(date));
	}

	return convertedDate;
};

I18n.toTime = function(scope, d) {
	var date = this.parseDate(d);
	var format = this.lookup(scope);

	if (date.toString().match(/invalid/i)) {
		return date.toString();
	}

	if (!format) {
		return date.toString();
	}

	return this.strftime(date, format);
};

I18n.strftime = function(date, format) {
	var options = this.lookup("date");

	if (!options) {
		return date.toString();
	}
	options.meridian = options.meridian || [ "AM", "PM" ];

	var weekDay = date.getDay();
	var day = date.getDate();
	var year = date.getFullYear();
	var month = date.getMonth() + 1;
	var hour = date.getHours();
	var hour12 = hour;
	var meridian = hour > 11 ? 1 : 0;
	var secs = date.getSeconds();
	var mins = date.getMinutes();
	var offset = date.getTimezoneOffset();
	var absOffsetHours = Math.floor(Math.abs(offset / 60));
	var absOffsetMinutes = Math.abs(offset) - (absOffsetHours * 60);
	var timezoneoffset = (offset > 0 ? "-" : "+")
			+ (absOffsetHours.toString().length < 2 ? "0" + absOffsetHours
					: absOffsetHours)
			+ (absOffsetMinutes.toString().length < 2 ? "0" + absOffsetMinutes
					: absOffsetMinutes);

	if (hour12 > 12) {
		hour12 = hour12 - 12;
	} else if (hour12 === 0) {
		hour12 = 12;
	}

	var padding = function(n) {
		var s = "0" + n.toString();
		return s.substr(s.length - 2);
	};

	var f = format;
	f = f.replace("%a", options.abbr_day_names[weekDay]);
	f = f.replace("%A", options.day_names[weekDay]);
	f = f.replace("%b", options.abbr_month_names[month]);
	f = f.replace("%B", options.month_names[month]);
	f = f.replace("%d", padding(day));
	f = f.replace("%e", day);
	f = f.replace("%-d", day);
	f = f.replace("%H", padding(hour));
	f = f.replace("%-H", hour);
	f = f.replace("%I", padding(hour12));
	f = f.replace("%-I", hour12);
	f = f.replace("%m", padding(month));
	f = f.replace("%-m", month);
	f = f.replace("%M", padding(mins));
	f = f.replace("%-M", mins);
	f = f.replace("%p", options.meridian[meridian]);
	f = f.replace("%S", padding(secs));
	f = f.replace("%-S", secs);
	f = f.replace("%w", weekDay);
	f = f.replace("%y", padding(year));
	f = f.replace("%-y", padding(year).replace(/^0+/, ""));
	f = f.replace("%Y", year);
	f = f.replace("%z", timezoneoffset);

	return f;
};

I18n.toNumber = function(number, options) {
	options = this.prepareOptions(options, this.lookup("number.format"), {
		precision : 3,
		separator : ".",
		delimiter : ",",
		strip_insignificant_zeros : false
	});

	var negative = number < 0;
	var string = Math.abs(number).toFixed(options.precision).toString();
	var parts = string.split(".");

	number = parts[0];
	var precision = parts[1];

	var n = [];

	while (number.length > 0) {
		n.unshift(number.substr(Math.max(0, number.length - 3), 3));
		number = number.substr(0, number.length - 3);
	}

	var formattedNumber = n.join(options.delimiter);

	if (options.precision > 0) {
		formattedNumber += options.separator + parts[1];
	}

	if (negative) {
		formattedNumber = "-" + formattedNumber;
	}

	if (options.strip_insignificant_zeros) {
		var regex = {
			separator : new RegExp(options.separator.replace(/\./, "\\.") + "$"),
			zeros : /0+$/
		};

		formattedNumber = formattedNumber.replace(regex.zeros, "").replace(
				regex.separator, "");
	}

	return formattedNumber;
};

I18n.toCurrency = function(number, options) {
	options = this.prepareOptions(options, this
			.lookup("number.currency.format"), this.lookup("number.format"), {
		unit : "$",
		precision : 2,
		format : "%u%n",
		delimiter : ",",
		separator : "."
	});

	number = this.toNumber(number, options);
	number = options.format.replace("%u", options.unit).replace("%n", number);

	return number;
};

I18n.toHumanSize = function(number, options) {
	var kb = 1024, size = number, iterations = 0, unit, precision;

	while (size >= kb && iterations < 4) {
		size = size / kb;
		iterations += 1;
	}

	if (iterations === 0) {
		unit = this.t("number.human.storage_units.units.byte", {
			count : size
		});
		precision = 0;
	} else {
		unit = this.t("number.human.storage_units.units."
				+ [ null, "kb", "mb", "gb", "tb" ][iterations]);
		precision = (size - Math.floor(size) === 0) ? 0 : 1;
	}

	options = this.prepareOptions(options, {
		precision : precision,
		format : "%n%u",
		delimiter : ""
	});

	number = this.toNumber(size, options);
	number = options.format.replace("%u", unit).replace("%n", number);

	return number;
};

I18n.toPercentage = function(number, options) {
	options = this.prepareOptions(options, this
			.lookup("number.percentage.format"), this.lookup("number.format"),
			{
				precision : 3,
				separator : ".",
				delimiter : ""
			});

	number = this.toNumber(number, options);
	return number + "%";
};

I18n.pluralize = function(count, scope, options) {
	var translation;

	try {
		translation = this.lookup(scope, options);
	} catch (error) {
	}

	if (!translation) {
		return this.missingTranslation(scope);
	}

	var message;
	options = this.prepareOptions(options);
	options.count = count.toString();

	switch (Math.abs(count)) {
	case 0:
		message = this.isValidNode(translation, "zero") ? translation.zero
				: this.isValidNode(translation, "none") ? translation.none
						: this.isValidNode(translation, "other") ? translation.other
								: this.missingTranslation(scope, "zero");
		break;
	case 1:
		message = this.isValidNode(translation, "one") ? translation.one : this
				.missingTranslation(scope, "one");
		break;
	default:
		message = this.isValidNode(translation, "other") ? translation.other
				: this.missingTranslation(scope, "other");
	}

	return this.interpolate(message, options);
};

I18n.missingTranslation = function() {
	var message = '[missing "' + this.currentLocale();
	var count = arguments.length;

	for ( var i = 0; i < count; i++) {
		message += "." + arguments[i];
	}

	message += '" translation]';

	return message;
};

I18n.currentLocale = function() {
	return (I18n.locale || I18n.defaultLocale);
};

// shortcuts
I18n.t = I18n.translate;
I18n.l = I18n.localize;
I18n.p = I18n.pluralize;
