/*
	This has original and modified versions of Date and Range extensions.
	It includes:
	- A modified version of Date.js by Jason S. Kerchner 
		(see http://livingmachines.net/)
	- Modified Date.succ extension to support Range object by Matthew FOster.
		(see http://positionabsolute.net/blog/2007/09/prototype-object-range-calendar-extension.php)
	- Some original extensions (MonthRange class, toObject Date extension, etc.) to support YaCal
*/

/**
 * Extensions to the native JavaScript Date object.
 * 
 * "Static" members:
 * daysInMonth, dayNames, shortDayNames, dayChars
 * 
 * Instance members:
 * copy, getDayName, getShortDayName, getDayChar, getMonthName, getShortMonthName, 
 * getMonthChar, getMonthNumber, daysInMonth, addDays, addMonths, addYears, addHours,
 * addMinutes, addSeconds, clearTime, compareTo, isBefore, isAfter, equals, toFormat, 
 * toObject, succ
 * 
 * Extension to String object:
 * toDate
 */
 
/**
 * Full, short and single character names for the days of the week.
 */
Date.dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
Date.shortDayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
Date.dayChars = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];

/**
 * Full, short and single character names for the months.  Override these to provide 
 * multi-language support.
 */
Date.monthNames = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
Date.shortMonthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
Date.monthChars = ['J', 'F', 'M', 'A', 'M', 'J', 'J', 'A', 'S', 'O', 'N', 'D'];

/**
 * Objects that give access to the month index from the full and short month
 * names.  This is faster that searching the monthNames or shortMonthNames arrays. 
 */
Date.monthNumbers = { };
Date.shortMonthNumbers = { };
for (var i = 0; i < 12; i++) {
	Date.monthNumbers[Date.monthNames[i]] = i;
	Date.shortMonthNumbers[Date.shortMonthNames[i]] = i;
}

/**
 * Returns the number of days in the current month and year.  Note that the 
 * month is zero-based, so January = 0, February = 1, etc.
 * @param {Number} month The month who's days are to be counted.
 * @param {Number} year The year of the to check, to allow for leap years.
 * @return {Number} The number of days in the given month for the given year.
 */
Date.daysInMonth = function(month, year){
	// If February, check for leap year
	if ((month == 1) && (((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0))) {
		return 29;
	}
	else { //		J	F	M	A	M	J	J	A	S	O	N	D
		var days = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
		return days[month];
	}
};

/**
 * Creates a copy of the current date object.  Assigning one date variable
 * to another simply points both variables to the same date object.  This
 * function is useful when you need a unique copy of the date object. 
 * @return {Date} A copy of this date object.
 */
Date.prototype.copy = function(){
	return new Date(this);
};

/**
 * @return {String} The full name of the day of the week.
 */
Date.prototype.getDayName = function(){
	return Date.dayNames[this.getDay()];
};

/**
 * @return {String} The short name of the day of the week.
 */
Date.prototype.getShortDayName = function(){
	return Date.shortDayNames[this.getDay()];
};

/**
 * @return {String} The single character representation of the day of the week.
 */
Date.prototype.getDayChar = function(){
	return Date.dayChars[this.getDay()];
};

/**
 * @return {String} The full name of the month.
 */
Date.prototype.getMonthName = function(){
	return Date.monthNames[this.getMonth()];
};

/**
 * @return {String} The short name of the month.
 */
Date.prototype.getShortMonthName = function(){
	return Date.shortMonthNames[this.getMonth()];
};

/**
 * @return {String} The single character representation of the month.
 */
Date.prototype.getMonthChar = function(){
	return Date.monthChars[this.getMonth()];
};

/**
 * Returns the normalized numeric representation of the month.
 * (i.e. January = 1, February = 2, ..., December = 12)
 * @return {Number} The normalized month number (January = 1)
 */
Date.prototype.getMonthNumber = function(){
	return this.getMonth() + 1;
};

/**
 * Returns the number of days in the current month and year, adjusting February
 * for leap years.
  * @return {Number} The number of days in the month. 
 */
Date.prototype.daysInMonth = function(){
	return Date.daysInMonth(this.getMonth(), this.getFullYear());
}

/**
 * Adds the given number of days to the date. To subtract days, pass in a negative value for offset.
 * @param {Number} offset The number of days (positive or negative) to adjust the date.
 * @return {Date} Returns the date object itself, with the additional days added.
 */
Date.prototype.addDays = function(offset){
	this.setDate(this.getDate() + offset);
	return this;
};

/**
 * Adds the given number of months to the date.  To subtract
 * months, pass in a negative value for offset.
 * @param {Number} offset The number of months (positive or negative) to adjust the date.
 * @return {Date} Returns the date object itself, with the additional months added.
 */
Date.prototype.addMonths = function(offset){
	this.setMonth(this.getMonth() + offset);
	return this;
};

/**
 * Adds the given number of years to the date.  To subtract 
 * years, pass in a negative value for offset.
 * @param {Number} offset The number of years (positive or negative) to adjust the date.
 * @return {Date} Returns the date object itself, with the additional years added.
 */
Date.prototype.addYears = function(offset){
	this.setFullYear(this.getFullYear() + offset);
	return this;
};

/**
 * Adds the given number of hours to the time portion of a date.  
 * To subtract time, pass in a negative value for offset.
 * @param {Number} offset The number of hours (positive or negative) to adjust the time.
 * @return {Date} Returns the date object itself, with the additional hours added.
 */
Date.prototype.addHours = function(offset){
	this.setHours(this.getHours() + offset);
	return this;
};

/**
 * Adds the given number of minutes to the time portion of a date.  
 * To subtract time, pass in a negative value for offset.
 * @param {Number} offset The number of minutes (positive or negative) to adjust the time.
 * @return {Date} Returns the date object itself, with the additional minutes added.
 */
Date.prototype.addMinutes = function(offset){
	this.setMinutes(this.getMinutes() + offset);
	return this;
};

/**
 * Adds the given number of seconds to the time portion of a date.  
 * To subtract time, pass in a negative value for offset.
 * @param {Number} offset The number of seconds (positive or negative) to adjust the time.
 * @return {Date} Returns the date object itself, with the additional seconds added.
 */
Date.prototype.addSeconds = function(offset){
	this.setSeconds(this.getSeconds() + offset);
	return this;
};

/**
 * @return {Date} Returns the date object itself, with the time cleared.
 */
Date.prototype.clearTime = function(){
  this.setHours(0); 
  this.setMinutes(0);
  this.setSeconds(0); 
  this.setMilliseconds(0);
  return this;
};

/** 
 * Date comparison functions.  If ignoreTime is true, then the time portion will
 * be ignored during the comparison.
 *
 * For a comparison such as myDate.compareTo(otherDate), read it
 * as "How does myDate compare to otherDate?"
 *
 *   if myDate is greater than otherDate, return > 0
 *   if myDate is less than otherDate, return < 0
 *   if myDate is equal to otherDate, return 0
 *   
 * @param {Date} date The date to compare to.
 * @param {Boolean} ignoreTime If true, ignores the time component of the date.
 * @return {Number} Returns > 0 if greater than date, < 0 if less than date, or 0 if equal to date. 
 */
Date.prototype.compareTo = function(date, ignoreTime){
	if (date == null) {
		return false;
	}
	if (ignoreTime) {
		var d1 = this.copy().clearTime();
		var d2 = date.copy().clearTime();
		return (d1.getTime() - d2.getTime());
	}
	return (this.getTime() - date.getTime());
}

/**
 * Returns true if this date is before another date.
 * @param {Date} date The date to compare to.
 * @param {Boolean} ignoreTime If true, ignores the time component of the date.
 * @return {Boolean} True if this date is before the other date, false otherwise.
 */
Date.prototype.isBefore = function(date, ignoreTime){
	return this.compareTo(date, ignoreTime) < 0;
};

/**
 * Returns true if this date is after another date.
 * @param {Date} date The date to compare to.
 * @param {Boolean} ignoreTime If true, ignores the time component of the date.
 * @return {Boolean} True if this date is after the other date, false otherwise.
 */
Date.prototype.isAfter = function(date, ignoreTime){
	return this.compareTo(date, ignoreTime) > 0;
};

/**
 * Returns true if this date is equal to another date.
 * @param {Date} date The date to compare to.
 * @param {Boolean} ignoreTime If true, ignores the time component of the date.
 * @return {Boolean} True if this date is equal to the other date, false otherwise.
 */
Date.prototype.equals = function(date, ignoreTime){
	return this.compareTo(date, ignoreTime) == 0;
};

/**
 * Return a date in the given format.  Use ^ to force the use of a literal character.
 *
 * Field		| Full Form	  | Short Form
 * -------------+----------------+-------------------
 * Year		 	| Y (4 digits)   | y (2 digits)
 * Month		| M (2 digits)   | m (1 or 2 digits)
 * Month Name   | N (full name)  | n (abbr)
 * Day of Month | D (2 digits)   | d (1 or 2 digits)
 * Day Name	 	| W (full name)  | w (abbr)
 * Hour (1-12)  | H (2 digits)   | h (1 or 2 digits)
 * Hour (0-23)  | R (2 digits)   | r (1 or 2 digits)
 * Minute	   	| I (2 digits)   | i (1 or 2 digits)
 * Second	   	| S (2 digits)   | s (1 or 2 digits)
 * AM/PM		| A (upper case) | a (lower case)
 * 
 * @param {String} format The format in which the date is to be returned.
 * @return {String} The formatted date.
 */
Date.prototype.toFormat = function(format){
	var pad = function(val) { 
		return (val > 9 ? '' : '0')+val; 
	};
	var result = '';
	for(var i = 0, len = format.length; i < len; i++) {
		var c = format.charAt(i);
		switch(c) {
			case 'Y': 
				result += this.getFullYear();
				break;
			case 'y': 
				result += (this.getFullYear()+'').substr(2,2);
				break;
			case 'M': 
				result += pad(this.getMonth()+1);
				break;
			case 'm': 
				result += this.getMonth()+1;
				break;
			case 'N':
				result += Date.monthNames[this.getMonth()];
				break;
			case 'n': 
				result += Date.shortMonthNames[this.getMonth()];
				break;
			case 'D': 
				result += pad(this.getDate());
				break;
			case 'd': 
				result += this.getDate();
				break;
			case 'W': 
				result += Date.dayNames[this.getDay()];
				break;
			case 'w': 
				result += Date.shortDayNames[this.getDay()];
				break;
			case 'H': 
				var hour = this.getHours() % 12;
				result += pad((hour ? hour : 12));
				break;
			case 'h': 
				var hour = this.getHours() % 12;
				result += (hour ? hour : 12);
				break;
			case 'R': 
				result += pad(this.getHours());
				break;
			case 'r': 
				result += this.getHours();
				break;
			case 'I': 
				result += pad(this.getMinutes());
				break;
			case 'i': 
				result += this.getMinutes().toString();
				break;
			case 'S': 
				result += pad(this.getSeconds());
				break;
			case 's':
				result += this.getSeconds().toString();
				break;
			case 'A': 
				result += (this.getHours() < 12 ? 'AM' : 'PM');
				break;
			case 'a': 
				result += (this.getHours() < 12 ? 'am' : 'pm');
				break;
			default:
				result += (c == '^' ? format.charAt(++i) : c);
		}
	}
	return result;
};


/**
 * Creates an object literal populated with date instance members.
 * @return {Object} Object literal with named properties for the date instance.
 */
Date.prototype.toObject = function(){
	return {
		month: 			this.getMonth()+1,
		monthName: 		Date.monthNames[this.getMonth()],
		day: 			this.getDate(),
		dayOfWeek: 		Date.dayNames[this.getDay()],
		year: 			this.getFullYear(),
		time:			this.toLocaleTimeString(),
		hours: 			this.getHours(),
		minutes: 		this.getMinutes(),
		seconds: 		this.getSeconds(),
		milliseconds: 	this.getMilliseconds(),
		offset: 		this.getTimezoneOffset(),
		value: 			this.getTime(),
		UTC: 			this.toUTCString()
	}
};

/**
 * Attempts to convert a string into a date based on a given format.  Fields will
 * match either the long or short form, except in the case of the year, where the 
 * string must match either a 2-digit or 4-digit format.  Ranges are checked.  Day
 * names are expected if they are included in the format string, but are otherwise
 * ignored.  Use ^ to force the use of a literal character. In other words, to
 * have the character Y appear insead of the actual year, use ^Y.
 * See formatting options in the comment for Date.prototype.toFormat
 * @param {String} format The basic format of the string.
 * @return {Date} The string as a date object.
 */
String.prototype.toDate = function(format){
	// Default values set to midnight Jan 1 of the current year.
	var year = new Date().getFullYear();
	var month = 0;
	var day = 1;
	var hours = 0;
	var minutes = 0;
	var seconds = 0;

	// Positions of each date element within the source string.  Use to know 
	// which backreference to check after a successful match.
	var yearPos = -1;
	var monthPos = -1;
	var dayPos = -1;
	var hoursPos = -1;
	var minutesPos = -1;
	var secondsPos = -1;
	var amPmPos = -1;

	var monthStyle = 'm';	   // How we interpret the month, digits (M/m) or names (N/n)
	var hoursStyle = 'h';	   // How we interpret the hours, 12-hour (h) or 24-hour (r)

	var position = 1;		   // Position of the current date element (year, month, day, etc.) in the source string
	var pattern = '';		   // Date pattern to be matched.

	// Remove extraneous whitespace from source string and format string.
	var str = this.replace(/\s+/g, ' ');
	format = format.replace(/\s+/g, ' ');

	// Loop throught the format string, and build the regex pattern
	// for extracting the date elements.
	for (var i = 0, len = format.length; i < len; i++) {
		var c = format.charAt(i)
		switch (c) {
			case 'Y' :
				pattern += '(\\d{4})';
				yearPos = position++;
				break;
			case 'y' :
				pattern += '(\\d{2})';
				yearPos = position++;
				break;
			case 'M' :
			case 'm' :
				pattern += '(\\d{1,2})';
				monthPos = position++;
				monthStyle = 'm'
				break;
			case 'N' :
				pattern += '(' + Date.monthNames.join('|') + ')';
				monthPos = position++;
				monthStyle = 'N';
				break;
			case 'n' :
				pattern += '(' + Date.shortMonthNames.join('|') + ')';
				monthPos = position++;
				monthStyle = 'n';
				break;
			case 'D' :
			case 'd' :
				pattern += '(\\d{1,2})';
				dayPos = position++;
				break;
			case 'W' : // We'll match W, but won't do anything with it.
				pattern += '(' + Date.dayNames.join('|') + ')';
				position++;
				break;
			case 'w' : // We'll match w, but won't do anything with it.
				pattern += '(' + Date.shortDayNames.join('|') + ')';
				position++;
				break;
			case 'H' :
			case 'h' :
				pattern += '(\\d{1,2})';
				hoursPos = position++;
				hoursStyle = 'h';
				break;
			case 'R' :
			case 'r' :
				pattern += '(\\d{1,2})';
				hoursPos = position++;
				hoursStyle = 'r';
				break;
			case 'I' :
			case 'i' :
				pattern += '(\\d{1,2})';
				minutesPos = position++;
				break;
			case 'S' :
			case 's' :
				pattern += '(\\d{1,2})';
				secondsPos = position++;
				break;
			case 'A' :
			case 'a' :
				pattern += '(AM|am|PM|pm)';
				amPmPos = position++;
				break;
			default :
				pattern += (c == '^' ? format.charAt(++i) : c);
		}
	}
	
	// Pull out the date elements from the input string
	var matches = str.match(new RegExp(pattern));
	if (!matches) {
		return null;	
	}
	// Now we have to interpret each of those parts...
	if (yearPos > -1) {
		year = parseInt(matches[yearPos], 10);
		year = (year < 50 ? year + 2000 : (year < 100 ? year + 1900 : year));
	}
	
	if (monthPos > -1) {
		switch (monthStyle) {
			case 'm':
				month = parseInt(matches[monthPos], 10) - 1;	// JavaScript months are zero based, user input generally is not.
				if (month > 11)
					return null;
				break;
			case 'N': 
				month = parseInt(Date.monthNumbers[matches[monthPos]], 10);
				if (isNaN(month))
					return null;
				break;
			case 'n':
				month = parseInt(Date.shortMonthNumbers[matches[monthPos]], 10);
				if (isNaN(month))
					return null;
				break;
		}
	}
	
	if (dayPos > -1) {
		day = parseInt(matches[dayPos], 10);
		if ((day < 1) || (day > Date.daysInMonth(month, year)))
			return null;
	}
	
	if (hoursPos > -1) {
		hours = parseInt(matches[hoursPos], 10);
		if (hoursStyle == 'h' && (hours == 0 || hours > 12))
			return null;
		else if (hours > 23)
			return null;
	}
	
	if (minutesPos > -1) {
		minutes = parseInt(matches[minutesPos], 10);
		if (minutes > 59)
			return null;
	}
	
	if (secondsPos > -1) {
		seconds = parseInt(matches[secondsPos], 10);
		if (seconds > 59)
			return null;
	}
	
	// Convert 12-hour time, if used, to 24-hour time.
	if (amPmPos > -1) {
		var amPm = matches[amPmPos];
		if ((amPm == 'pm' || amPm == 'PM') && (hours < 12))
			hours += 12;
	}
	
	return new Date(year, month, day, hours, minutes, seconds);
}


/**
 * Adds support for prototype's Range object.
 */
Date.prototype.succ = function(){
		var ret = new Date(this.getTime());
		ret.setDate(this.getDate() + 1);
		return ret;
};
/**
 * Month Range is a range of dates representing one month.
 */
var MonthRange = Class.create(ObjectRange, {
	initialize : function(year, month){
		this.date = this.buildDate(year, month);
		this.start = this.buildStart(this.date);
		this.end = this.buildEnd(this.date);
	},
	buildDate : function(year, month){
		var d = false;
		if(year instanceof Date){
			d = year;
		}
		else{
			d = new Date();
			d.setYear(year);
			d.setMonth(month);
		}							
							
		d.setDate(1);
		d.setHours(0);
		d.setMinutes(0);
		d.setSeconds(0);
		d.setMilliseconds(0);
		
		return d;
	},
	
	buildStart : function(date){
		return new Date(date.getTime());
	},

	buildEnd : function(date){
		var maxDays = Date.daysInMonth(date.getMonth(), date.getFullYear());		
		this.setMaxDays(maxDays);
		return new Date(date.getFullYear(), date.getMonth(), maxDays);
	},
	
	setMaxDays : function(num){
		this.maxDays = num;
	},
	
	getMaxDays : function(){
		return this.maxDays || 0;
	},
	
	getNextMonth : function(){
		return this.buildDate(this.date.getFullYear(), this.date.getMonth()+1);
	},
	
	getDate : function(){
		return this.date;
	},
	
	getPreviousMonth : function(){
		return this.buildDate(this.date.getFullYear(), this.date.getMonth()-1);
	},
	
	succ : function(){
		return new MonthRange(this.getNextMonth());
	}
});

