(function () {
	var $D = Date;

	var flattenAndCompact = function (ax) {
		var rx = [];
		for (var i = 0; i < ax.length; i++) {
			if (ax[i] instanceof Array) {
				rx = rx.concat(flattenAndCompact(ax[i]));
			} else {
				if (ax[i]) {
					rx.push(ax[i]);
				}
			}
		}
		return rx;
	};

	var parseMeridian = function () {
		if (this.meridian && (this.hour || this.hour === 0)) {
			if (this.meridian === "a" && this.hour > 11 && Date.Config.strict24hr){
				throw "Invalid hour and meridian combination";
			} else if (this.meridian === "p" && this.hour < 12 && Date.Config.strict24hr){
				throw "Invalid hour and meridian combination";
			} else if (this.meridian === "p" && this.hour < 12) {
				this.hour = this.hour + 12;
			} else if (this.meridian === "a" && this.hour === 12) {
				this.hour = 0;
			}
		}
	};

	var setDefaults = function () {
		var now = new Date();
		if ((this.hour || this.minute) && (!this.month && !this.year && !this.day)) {
			this.day = now.getDate();
		}

		if (!this.year) {
			this.year = now.getFullYear();
		}
		
		if (!this.month && this.month !== 0) {
			this.month = now.getMonth();
		}
		
		if (!this.day) {
			this.day = 1;
		}
		
		if (!this.hour) {
			this.hour = 0;
		}
		
		if (!this.minute) {
			this.minute = 0;
		}

		if (!this.second) {
			this.second = 0;
		}
		if (!this.millisecond) {
			this.millisecond = 0;
		}
	};

	var finishUtils = {
		getToday: function () {
			 if (this.now || "hour minute second".indexOf(this.unit) !== -1) {
				return new Date();
			} else {
				return $D.today();
			}
		},
		setDaysFromWeekday: function (today, orient){
			var gap;
			orient = orient || 1;
			this.unit = "day";
			gap = ($D.getDayNumberFromName(this.weekday) - today.getDay());
			this.days = gap ? ((gap + (orient * 7)) % 7) : (orient * 7);
			return this;
		},
		setMonthsFromMonth: function (today, orient) {
			var gap;
			orient = orient || 1;
			this.unit = "month";
			gap = (this.month - today.getMonth());
			this.months = gap ? ((gap + (orient * 12)) % 12) : (orient * 12);
			this.month = null;
			return this;
		},
		setDMYFromWeekday: function () {
			var d = Date[this.weekday]();
			this.day = d.getDate();
			if (!this.month) {
				this.month = d.getMonth();
			}
			this.year = d.getFullYear();
			return this;
		},
		setUnitValue: function (orient) {
			if (!this.value && this.operator && this.operator !== null && this[this.unit + "s"] && this[this.unit + "s"] !== null) {
				this[this.unit + "s"] = this[this.unit + "s"] + ((this.operator === "add") ? 1 : -1) + (this.value||0) * orient;
			} else if (this[this.unit + "s"] == null || this.operator != null) {
				if (!this.value) {
					this.value = 1;
				}
				this[this.unit + "s"] = this.value * orient;
			}
		},
		generateDateFromWeeks: function () {
			var weekday = (this.weekday !== undefined) ? this.weekday : "today";
			var d = Date[weekday]().addWeeks(this.weeks);
			if (this.now) {
				d.setTimeToNow();
			}
			return d;
		}
	};

	$D.Translator = {
		hour: function (s) {
			return function () {
				this.hour = Number(s);
			};
		},
		minute: function (s) {
			return function () {
				this.minute = Number(s);
			};
		},
		second: function (s) {
			return function () {
				this.second = Number(s);
			};
		},
		/* for ss.s format */
		secondAndMillisecond: function (s) {
			return function () {
				var mx = s.match(/^([0-5][0-9])\.([0-9]{1,3})/);
				this.second = Number(mx[1]);
				this.millisecond = Number(mx[2]);
			};
		},
		meridian: function (s) {
			return function () {
				this.meridian = s.slice(0, 1).toLowerCase();
			};
		},
		timezone: function (s) {
			return function () {
				var n = s.replace(/[^\d\+\-]/g, "");
				if (n.length) {
					this.timezoneOffset = Number(n);
				} else {
					this.timezone = s.toLowerCase();
				}
			};
		},
		day: function (x) {
			var s = x[0];
			return function () {
				this.day = Number(s.match(/\d+/)[0]);
				if (this.day < 1) {
					throw "invalid day";
				}
			};
		},
		month: function (s) {
			return function () {
				this.month = (s.length === 3) ? "jan feb mar apr may jun jul aug sep oct nov dec".indexOf(s)/4 : Number(s) - 1;
				if (this.month < 0) {
					throw "invalid month";
				}
			};
		},
		year: function (s) {
			return function () {
				var n = Number(s);
				this.year = ((s.length > 2) ? n :
					(n + (((n + 2000) < Date.CultureInfo.twoDigitYearMax) ? 2000 : 1900)));
			};
		},
		rday: function (s) {
			return function () {
				switch (s) {
					case "yesterday":
						this.days = -1;
						break;
					case "tomorrow":
						this.days = 1;
						break;
					case "today":
						this.days = 0;
						break;
					case "now":
						this.days = 0;
						this.now = true;
						break;
				}
			};
		},
		finishExact: function (x) {
			var d;
			x = (x instanceof Array) ? x : [x];

			for (var i = 0 ; i < x.length ; i++) {
				if (x[i]) {
					x[i].call(this);
				}
			}
			
			setDefaults.call(this);
			parseMeridian.call(this);

			if (this.day > $D.getDaysInMonth(this.year, this.month)) {
				throw new RangeError(this.day + " is not a valid value for days.");
			}

			d = new Date(this.year, this.month, this.day, this.hour, this.minute, this.second, this.millisecond);
			if (this.year < 100) {
				d.setFullYear(this.year); // means years less that 100 are process correctly. JS will parse it otherwise as 1900-1999.
			}
			if (this.timezone) {
				d.set({ timezone: this.timezone });
			} else if (this.timezoneOffset) {
				d.set({ timezoneOffset: this.timezoneOffset });
			}
			
			return d;
		},
		finish: function (x) {
			var today, expression, orient, temp;

			x = (x instanceof Array) ? flattenAndCompact(x) : [ x ];

			if (x.length === 0) {
				return null;
			}

			for (var i = 0 ; i < x.length ; i++) {
				if (typeof x[i] === "function") {
					x[i].call(this);
				}
			}
			if (this.now && !this.unit && !this.operator) {
				return new Date();
			} else {
				today = finishUtils.getToday.call(this);
			}
			
			expression = !!(this.days && this.days !== null || this.orient || this.operator);
			orient = ((this.orient === "past" || this.operator === "subtract") ? -1 : 1);

			if (this.month && this.unit === "week") {
				this.value = this.month + 1;
				delete this.month;
				delete this.day;
			}

			if ((this.month || this.month === 0) && "year day hour minute second".indexOf(this.unit) !== -1) {
				if (!this.value) {
					this.value = this.month + 1;
				}
				this.month = null;
				expression = true;
			}

			if (!expression && this.weekday && !this.day && !this.days) {
				finishUtils.setDMYFromWeekday.call(this);
			}

			if (expression && this.weekday && this.unit !== "month" && this.unit !== "week") {
				finishUtils.setDaysFromWeekday.call(this, today, orient);
			}

			if (this.weekday && this.unit !== "week" && !this.day && !this.days) {
				temp = Date[this.weekday]();
				this.day = temp.getDate();
				if (temp.getMonth() !== today.getMonth()) {
					this.month = temp.getMonth();
				}
			}

			if (this.month && this.unit === "day" && this.operator) {
				if (!this.value) {
					this.value = (this.month + 1);
				}
				this.month = null;
			}

			if (this.value != null && this.month != null && this.year != null) {
				this.day = this.value * 1;
			}

			if (this.month && !this.day && this.value) {
				today.set({ day: this.value * 1 });
				if (!expression) {
					this.day = this.value * 1;
				}
			}

			if (!this.month && this.value && this.unit === "month" && !this.now) {
				this.month = this.value;
				expression = true;
			}

			if (expression && (this.month || this.month === 0) && this.unit !== "year") {
				finishUtils.setMonthsFromMonth.call(this, today, orient);
			}

			if (!this.unit) {
				this.unit = "day";
			}

			finishUtils.setUnitValue.call(this, orient);
			parseMeridian.call(this);
			
			if ((this.month || this.month === 0) && !this.day) {
				this.day = 1;
			}

			if (!this.orient && !this.operator && this.unit === "week" && this.value && !this.day && !this.month) {
				return Date.today().setWeek(this.value);
			}

			if (this.unit === "week" && this.weeks && !this.day && !this.month) {
				return finishUtils.generateDateFromWeeks.call(this);
			}

			if (expression && this.timezone && this.day && this.days) {
				this.day = this.days;
			}

			if (expression){
				today.add(this);
			} else {
				today.set(this);
			}
			
			if (this.timezone) {
				this.timezone = this.timezone.toUpperCase();
				var offset = $D.getTimezoneOffset(this.timezone);
				var timezone;
				if (today.hasDaylightSavingTime()) {
					// lets check that we're being sane with timezone setting
					timezone = $D.getTimezoneAbbreviation(offset, today.isDaylightSavingTime());
					if (timezone !== this.timezone) {
						// bugger, we're in a place where things like EST vs EDT matters.
						if (today.isDaylightSavingTime()) {
							today.addHours(-1);
						} else {
							today.addHours(1);
						}
					}
				}
				today.setTimezoneOffset(offset);
			}

			return today;
		}
	};
}());
