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)
			setOptions(table, options)
			
		// 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 setOptions(table, options) {
		if (options.className) table.addClass(options.className);
		
		table.data("firstDay", (options.firstDayOfWeek) ? options.firstDayOfWeek : 0);
		table.data("days", (options.days) ? options.days : dayNames);
		table.data("months", (options.months) ? options.months : monthNames);
		table.data("alwaysShowLast", (options.alwaysShowLastRow == true) ? true : false);
		table.data("allowWeekends", (options.allowWeekends == false) ? false : true);
		
		if (options.onDateChange) table.data("onSelectedDateChange", options.onDateChange);		
		if (options.onDisplayChange) table.data("onDisplayMonthChanged", options.onDisplayChange);
			
		if (options.minimumDate) table.data("minimumDate", options.minimumDate);			
		if (options.maximumDate) table.data("maximumDate", options.maximumDate);
			
		if (options.eventSource) {
			table.data("eventSource", options.eventSource);
			table.data("eventFetchMode", (options.eventFetchMode) ? options.eventFetchMode : "POST");	
			table.data("eventSelectionHandler", (options.eventSelectionHandler) ? options.eventSelectionHandler : eventSelectionHandler);
		}
	}
	
	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);
		});

		// If an event source has been defined, request information for the month.
		if (table.data("eventSource"))
			fetchEvents(table);
	}
	
	function fetchEvents(table) {
	
		var date = table.data("date.active");
	
		jQuery.ajax({
			dataType: "json",
			type: table.data("eventFetchMode"),
			url: table.data("eventSource"),
			data: {
				month:date.getUTCMonth(), 
				year:date.getUTCFullYear(), 
				calendar: table.prev().attr("id")				
			},
			cache: false,
			success: displayCalendarEvents
		});
	}
	
	function displayCalendarEvents(data) {
		
		if (data.status != "OK") return;
		
		var table = jQuery("#" + data.calendar).next();
		
		for (var index in data.events) {
			var event = data.events[index];
			var eventCell = jQuery(".d" + event.date, table)
				.filter(".m" + event.month)
				.filter(".y" + event.year);
				
			eventCell.addClass("event");
			eventCell.data("event.data", event);
			eventCell
				.unbind("click")
				.click(table.data("eventSelectionHandler"))
				.click(selectCell);
		} 
	}
	
	function eventSelectionHandler() {
		var eventCell = jQuery(this);
		alert(eventCell.data("event.data").content);
	}
	
	function setLimit(table, date, type) {
	
		jQuery("td.d" + date.getUTCDate(), table)
			.filter(".m" + date.getUTCMonth())
			.filter(".y" + date.getUTCFullYear())
			.addClass(type);
	}
	
	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 minimum date or the maximum date are set and appear in the current view, mark them as disabled.
		if (table.data("minimumDate")) {
			var selectableDateRangeStart = table.data("minimumDate");
			setLimit(table, selectableDateRangeStart, "start");
			
			// Disable all cells before the start cell.
			var startCell = jQuery("td.start", table);
			
			startCell
				.prevAll()											// All cells in the row containing the start cell, and preceding the start cell.
				.add(startCell.parent().prevAll().children("td"))	// Add all cells in all rows preceding row in which the start cell is present.
				.addClass("disabled");
				
			var lastCell = jQuery("td:last", table);
			if (lastCell.data("date") < selectableDateRangeStart)
				jQuery("td", table).addClass("disabled");
		}
		
		if (table.data("maximumDate")) {
			var selectableDateRangeEnd = table.data("maximumDate");
			setLimit(table, selectableDateRangeEnd, "end");
			
			// Disable all cells before the end cell.
			var endCell = jQuery("td.end", table);
			
			endCell
				.nextAll()										// All cells in the row containing the start cell, and preceding the start cell.
				.add(endCell.parent().nextAll().children("td"))	// Add all cells in all rows preceding row in which the start cell is present.
				.addClass("disabled");
				
			var firstCell = jQuery("td:first", table);
			if (firstCell.data("date") > selectableDateRangeEnd)
				jQuery("td", table).addClass("disabled");
		}
		
		// If weekends cannot be selected, mark them so.
		if (!table.data("allowWeekends"))
			jQuery("td.dw0, td.dw6").addClass("disabled");
			
		// 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:not(.disabled)", 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);
	}
};