/*
SCRIPT: calendar.js
	Creates a calendar table element and attaches a user specified function to each day
	Built to be very flexible. Use it as the base for a date picker or insert it directly into the page.

LICENSE:
	MIT-style license

COPYRIGHT:
	Copyright (c) 2008 [Darren Kovalchik](http://ellipsisentity.com)


USAGE:
	- All options are optional
	- Date defaults to current month and year (setting a day value does not set the current day to this date)
	- Today defaults to current date
	- Includes methods to change and increment the day, month, and year
		- Does not include any built in events to change day or month, but it's easy to add your own
	- The day function:
		- "this" refers to day table cell
		- Does not set any html attributes for the day cell unless you specify
		- You have access to:
			- A date object for the day
			- The calendar class object
	
	var calendar = new Calendar({
		date: new Date(2008, 9),
		today: new Date(2008, 9, 31),
		dayLabels: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
		shortYear: true,
		dayFunction: function(date, calendar) {
			this.set({
				html: date.getDate(),
				events: {
					click: function() {
						calendar.changeDay(date);
					}
				}
			});
		}
	});
	
	
TODO:

*/


/*
Adjust the margins of a caption element so it lines up correctly accross browsers
This is kind of absurd, but it has always driven me crazy that different browsers place the caption differently
*/
Element.implement({
	
	fixCaption: function() {
		// Adjust the left margin for firefox
		if (Browser.Engine.gecko)
			this.setStyle('margin-left', -1);
		
		// Adjust the right margin for webkit
		if (Browser.Engine.webkit)
			this.setStyle('margin-right', -1);
		
		// Adjust the bottom margin for webkit and IE. Does not work in IE7 or IE6, but whatever.
		if (Browser.Engine.webkit || Browser.Engine.trident)
			this.setStyle('margin-bottom', -1);
	}
	
});


/*
Finds the number of days in a month
By incrementing the month by one and setting the day to zero we default the date object to the last day of this month
Then we can simply retrieve the day number
*/
Date.implement({
	
	getMonthLength: function() {
		return new Date(this.getFullYear(), this.getMonth()+1, 0).getDate();
	}
	
});


var Calendar = new Class({
	
	Implements: Options,

	options: {
		date: new Date(),
		dayLabels: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
		monthLabels: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
		calendarClass: 'calendar',
		dayClass: 'day',
		todayClass: 'today'
		/*
		today: $empty   (date object)   // Set in the class, but can be specified by the user
		yearFormat: $empty  (string)  // 'long|short|none'
		dayFunction: $empty  (function)   // User specified function to fire on each calendar day
		todayFunction: $empty  (function)   // User specified function to fire on today
		*/
	},
	
	/*
	Sets options and generates a calendar object
	Does not inject the calendar
	*/
	initialize: function(options){
		// Set the today date object
		// I have to create it like this because a new date also includes a time, which won't match with the day class 
		if (!options.today) {
			var today = new Date();
			options.today = new Date(today.getFullYear(), today.getMonth(), today.getDate());
		}
		
		// Set the options
		this.setOptions(options);
		
		// Create the calendar element
		this.calendar = new Element('table', {
			'class': this.options.calendarClass
		});
		
		this.generateCalendar();
    },
	
	/*
	Returns the calendar element
	*/
	element: function() {
		return this.calendar;
	},
	
	/*
	Sets the today class onto a new day without rebuilding the calendar
	*/
	changeDay: function(day) {
		// Set the today option to the passed date object
		if ($type(day) == 'date')
			this.options.today = day;
		// Else, set the day of the today object with the passed number
		else if ($type(day) == 'number')
			this.options.today.setDate(day);
		// Otherwise, just use the current today object
		
		// Try to remove the today class from the current today element
		var oldToday = this.calendar.getElement('.today');
		if (oldToday) oldToday.removeClass('today');
		
		// if the passed date's class is found on the current calendar, apply the today class
		var newToday = this.calendar.getElement('.'+this.options.dayClass+'-'+this.options.today.getTime());
		if (newToday) newToday.addClass('today');
		
		return this;
	},
	
	/*
	Changes options and rebuilds calendar based on new month
	*/
	changeMonth: function(month) {
		// Change the current month
		this.options.date.setMonth(month);
		
		// Replace the current calendar
		this.generateCalendar();
		
		return this;
	},
	
	/*
	Changes options and rebuilds calendar based on new year
	*/
	changeYear: function(year, month, day) {
		// Change the current year
		this.options.date.setFullYear(year);
		
		// Replace the current calendar
		this.generateCalendar();
		
		return this;
	},
	
	/*
	Increment today by one or increment the month
	*/
	incrementDay: function() {
		this.changeDay(this.options.today.getDate() + 1);
		return this;
	},
	
	/*
	Deincrement the day by one or deincrement the month
	*/
	deincrementDay: function() {
		this.changeDay(this.options.today.getDate() - 1);
		return this;
	},
	
	/*
	Calls change month with the current month + 1 and the first day of the month
	*/
	incrementMonth: function() {
		this.changeMonth(this.options.date.getMonth() + 1);
		return this;
	},
	
	/*
	Calls change month with the current month and zero as the day
	*/
	deincrementMonth: function() {
		this.changeMonth(this.options.date.getMonth() - 1);
		return this;
	},
	
	/*
	Calls change year with the current year + 1 and the first day of the first month
	*/
	incrementYear: function() {
		this.changeYear(this.options.date.getFullYear() + 1);
		return this;
	},
	
	/*
	Calls change year with the current year - 1 and the first day of the first month
	*/
	deincrementYear: function() {
		this.changeYear(this.options.date.getFullYear() - 1);
		return this;
	},
	
	/*
	Generates and returns a calendar table
	*/
	generateCalendar: function(options) {
		// Set year and month variables
		var year = this.options.date.getFullYear();
		var month = this.options.date.getMonth();
				
		// Get the day of the week of the first day of month
		var firstDay = new Date(year, month, 1);
		var startingDay = firstDay.getDay();
			
		// Find number of days in the current month
		var monthLength = this.options.date.getMonthLength();
		
		// Get the date object for the current date
		var today = $pick(this.options.today, this.options.date);
		
		// Empty the calendar
		this.calendar.empty();
		
		// Create the caption, add the year and month, and inject into the calendar
		// The year is displayed in two digit format if necessary
		var caption = new Element('caption', {
			'html': this.options.monthLabels[month] + ((this.options.yearFormat == 'none') ? '' : (this.options.shortYear) ? "&nbsp;'"+year.toString().substring(2, 4) : '&nbsp;'+year)
		}).inject(this.calendar);
		
		// Fix the caption margins
		caption.fixCaption();
		
		// Create the thead and a row
		var thead = new Element('thead').inject(this.calendar);
		var headRow = new Element('tr').inject(thead);
		
		// Inject the day labels as table heading cells
		for(var i = 0; i < 7; i++ ){
			var head = new Element('th', {
				'html': this.options.dayLabels[i]
			}).inject(headRow);
		}
		
		// Create the tbody and inject
		var tbody = new Element('tbody').inject(this.calendar);
				
		// Set the start day
		var day = 1;
		
		// Determine the number of rows needed
		var rows = Math.ceil((monthLength + startingDay) / 7);
		
		// This loop is for is weeks (rows)
		for (var i = 0; i < rows; i++) {
			// Inject a table row
			var bodyRow = new Element('tr').inject(tbody);
			
			// This loop is for weekdays (cells)
			for (var j = 0; j < 7; j++) { 
				// Create a table cell for the day
				var tDay = new Element('td');
				
				// Add the day class if the cell is a numbered day
				if (day <= monthLength && (i > 0 || j >= startingDay)) {
					// Add the day class and the day class with a number appended
					tDay.addClass(this.options.dayClass).addClass(this.options.dayClass+'-'+new Date(year, month, day).getTime());
					
					// Add the today function if necessary
					if (year.toString()+month+day == today.getFullYear().toString()+today.getMonth()+today.getDate() && this.options.todayFunction) {
						this.options.todayFunction.bind(tDay, [
							new Date(year, month, day),
							this
						])();
					}
					// If the dayFunction is set, bind the current table cell, and fire the function
					else if (this.options.dayFunction) {
						this.options.dayFunction.bind(tDay, [
							new Date(year, month, day),
							this
						])();
					}
					// Else set the html of the cell to the current day
					else {
						tDay.set('html', day);
					}
					
					// Increment the day
					day++;
				}
				// Inject the day table cell into the current row
				tDay.inject(bodyRow);
			}
		}
		
		// Add the today class to the current day
		this.changeDay();
		
		// Return the calendar table object
		return this.calendar;
	}
});