jQuery.fn.calendar = function(date, options) {

	var dayNames = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
	var monthNames = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
	var template = "<table><caption><span class=\"previous\"/><span class=\"month\"/><span class=\"next\"/></caption><thead><tr><th/><th/><th/><th/><th/><th/><th/></tr></thead><tbody><tr><td/><td/><td/><td/><td/><td/><td/></tr><tr><td/><td/><td/><td/><td/><td/><td/></tr><tr><td/><td/><td/><td/><td/><td/><td/></tr><tr><td/><td/><td/><td/><td/><td/><td/></tr><tr><td/><td/><td/><td/><td/><td/><td/></tr><tr><td/><td/><td/><td/><td/><td/><td/></tr></tbody></table>";

	return this.each(function () {
		var target = jQuery(this);
		var isNew = !target.data("date.active");
		
		if (!target.is("table") || isNew) {
			var table = createTable(target, options); 
			populateHeader(table);
			setSelectedDate(table, date);
		} else {
			setActiveDate(target, date);
		}
	});
	
	function createActiveDate(date) {
		var firstDate = new Date(date.getTime());
		firstDate.setDate(2);
		
		return firstDate;
	}
	
	function getFirstDateInCalendar(date, firstDayOfWeek) {	
		var dayOfWeek = date.getUTCDay();
		
		if (dayOfWeek != firstDayOfWeek) {
			date.setUTCDate(date.getUTCDate() - 1);
			date = getFirstDateInCalendar(date, firstDayOfWeek);
		}
		
		return date;
	}
	
	function createTable(predecessor, options) {
		predecessor.after(template);
		var table = predecessor.next();
		
		if (options && options.className)
			table.addClass(options.className);
			
		table.data("firstDay", (options && options.firstDayOfWeek) ? options.firstDayOfWeek : 0);
		table.data("days", (options && options.days) ? options.days : dayNames);
		table.data("months", (options && options.months) ? options.months : monthNames);
		table.data("alwaysShowLast", (options && options.alwaysShowLastRow == true) ? true : false);
		
		if (options && options.onDateChange)
			table.data("onDateChange", options.onDateChange);
		
		if (options && options.onDisplayChange)		
			table.data("onDisplayMonthChanged", options.onDisplayChange);
			
		// Attach event handlers to the next and previous controls in the caption.
		enableChangeMonth(table, "caption span.previous", "&laquo;", moveToPreviousMonth);
		enableChangeMonth(table, "caption span.next", "&raquo;", moveToNextMonth);
		
		return table;
	}
	
	function enableChangeMonth(table, selector, text, callback) {
		jQuery(selector, table)
			.html(text)
			.click(callback);
	}
	
	function changeMonth(table, offset) {
		
		var date = new Date (table.data("date.active").getTime());
		date.setUTCMonth(date.getUTCMonth() + offset);
		setActiveDate(table, date);
	}
	
	function moveToPreviousMonth() {
		var table = jQuery(this).parents("table");
		changeMonth(table, -1);
	}	
	
	function moveToNextMonth() {
		var table = jQuery(this).parents("table");
		changeMonth(table, 1);
	}
	
	function populateHeader(table) {
		var days = table.data("days");
		var firstDay = table.data("firstDay");
		
		var headerIndex = firstDay;
		
		jQuery("th", table).each(function() {
			var cell = jQuery(this);
			cell.text(days[headerIndex++]);
			
			if (headerIndex > 6)
				headerIndex = 0;
		});
	}
	
	function populateCaption(table) {
		var date = table.data("date.active");
		jQuery("caption span.month", table).text(table.data("months")[date.getUTCMonth()] + ", " + date.getUTCFullYear());	
	}
	
	function populateBody(table) {
	
		var date = new Date(table.data("date.active").getTime());
		var firstDayOfWeek = table.data("firstDay");
		
		var currentDate = getFirstDateInCalendar(date, firstDayOfWeek);
				
		jQuery("td", table).each(function() {
			jQuery(this)
				.unbind("click")
				.removeClass()
				.data("date", new Date(currentDate.getTime()))
				.text(currentDate.getUTCDate())
				.addClass("dw" + currentDate.getUTCDay())
				.addClass("d" + currentDate.getUTCDate())
				.addClass("m" + currentDate.getUTCMonth())
				.addClass("y" + currentDate.getUTCFullYear());
					
			currentDate.setUTCDate(currentDate.getUTCDate() + 1);
		});	
	}
	
	function decorate(table) {
		var date = table.data("date");
		var activeDate = table.data("date.active");
		
		var currentDayStyle = ".d" + date.getUTCDate();
		var currentMonthStyle = ".m" + date.getUTCMonth();
		var currentYearStyle = ".y" + date.getUTCFullYear();	
		
		var activeMonthStyle = ".m" + activeDate.getUTCMonth();	
		var activeYearStyle = ".y" + activeDate.getUTCFullYear();	
		
		// Mark all months which are not part of the current month or year.
		jQuery("td:not(" + activeMonthStyle + ")", table)
			.add("td:not(" + activeYearStyle + ")")
			.addClass("other");
		
		// If a day is already marked, unmark it.
		jQuery("td.selected", table).removeClass("selected");
		
		// Mark the current date.
		jQuery("td" + currentDayStyle, table)
			.filter(currentMonthStyle)
			.filter(currentYearStyle)
			.addClass("selected");
		
		// If the last row is not being used, hide it unless instructed otherwise.
		// The last row is unused if its first cell is empty.
		var lastRow = jQuery("tr:last", table);	
		if (table.data("alwaysShowLast") || !jQuery("td:first", lastRow).is(".other"))
			lastRow.show();	
		else
			lastRow.hide();	
	}
	
	function bindCellBehaviour(table) {
		jQuery("td", table).click(selectCell);
	}
	
	function selectCell() {
		var cell = $(this);
		var table = cell.parents("table");		
		var date = cell.data("date");
				
		setSelectedDate(table, date);
	}
	
	function setSelectedDate(table, date) {
		var oldDate = table.data("date");
		table.data("date", new Date(date.getTime()));	

		table.data("date.active", createActiveDate(date));

		var isNewCalendar = !oldDate;
		var monthOrYearChanged = 
			!isNewCalendar &&
			(oldDate.getUTCMonth() != date.getUTCMonth() || 
			 oldDate.getUTCFullYear() != date.getUTCFullYear());
	
		update(table, (isNewCalendar || monthOrYearChanged));
		
		// Call the date changed callback.
		if (table.data("onSelectedDateChange"))
			table.data("onSelectedDateChange")(table.data("date"));
			
		// call the display changed callback if needed.
		if ((isNewCalendar || monthOrYearChanged) && table.data("onDisplayMonthChanged"))
			table.data("onDisplayMonthChanged")(table.data("date.active"));
	}
	
	function setActiveDate(table, date) {
		table.data("date.active", createActiveDate(date));
		update(table, true);
		
		if (table.data("onDisplayMonthChanged"))
			table.data("onDisplayMonthChanged")(table.data("date.active"));
	}
	
	function update(table, repopulate) {
		if (repopulate)
			populateBody(table);
			
		populateCaption(table);
		
		decorate(table);
		bindCellBehaviour(table);
	}
};