/**
 * @projectDescription A calendar picker
 * @author Jerome Wilson
 */


// *** Calendar ***

var Calendars = {
	"count": 0,
	
	"helpText": null,
	
	"add": function(calendarObj) {
		this[String(calendarObj.params.id)] = calendarObj;
	},
	
	"create": function(params) {
		return new Calendar(params);
	}
};

function Calendar(params) {
	var dateNow = new Date();

	this.calStore = [];
	this.startDate = null;
	this.endDate = null;
	this.calendarDays = 0;
	this.calendarWeeks = 0;
	this.bufferDays = 0;
	this.calIndex = Calendars.count;
	
	params = params || {};
	this.params = {
		"id":				params.id || "calendar_" + (++Calendars.count),
		"markedRanges":		params.markedRanges || [],
		"weekStartDay": 	params.startDay || 1,
		"labelLen": 		params.labelLen || 3,
		"months":			params.months || 6,
		"startMonth":		params.startMonth || null,
		"startYear":		params.startYear || null,
		"minMonth":			params.startMonth || null,
		"minYear":			params.startYear || null,
		"maxMonth":			params.startMonth || null,
		"maxYear":			params.startYear || null,
		"minRangeDays":		params.minRangeDays || 1,
		"maxRangeDays":		params.maxRangeDays || 30,
		"prePadding":		params.prePadding || 0,
		"postPadding":		params.postPadding || 0,
		"selectedRange":	params.selectedRange || new DateRange(null, null, null),
		"today":			params.today || dateNow,
		"baseDate":			params.baseDate || null,
		"selectFnc":		params.selectFnc || null,
		"clearFnc":			params.clearFnc || null,
		"baseUrl":			params.baseUrl || "",
		"noStatusLine":		params.noStatusLine || false,
		"selectExisting":	params.selectExisting || false,
		"disableSelection": params.disableSelection || false,
		"selectSingle":		params.selectSingle || false,
	
	// Text	
		"txtPrefix":		params.txtPrefix || "",
		"headingTxt":		params.headingTxt || "heading",
		"selectionTxt":		params.selectionTxt || "selection",
		"prePaddingTxt":	params.prePaddingTxt || "prePadding",
		"postPaddingTxt":	params.postPaddingTxt || "postPadding",
		"noRoomTxt":		params.noRoomTxt || "noRoom",
		"paddingTxt":		params.paddingTxt || "padding",
		"navPrevTxt":		params.navPrevTxt || "navPrev",
		"navResetTxt":		params.navResetTxt || "navReset",
		"navNextTxt":		params.navNextTxt || "navNext",
		"clearSelectionTxt":params.clearSelectionTxt || "clearSelection",
		"noSelectionTxt":	params.noSelectionTxt || "noSelection",
		"selectedDatesTxt":	params.selectedDatesTxt || "selectedDates",
		"datesFormatTxt":	params.datesFormatTxt || "datesFormat",
		"helpButtonTxt":	params.helpButtonTxt || "helpButton",
		"defaultStsMsgTxt":	params.defaultStsMsgTxt || "defaultStsMsg",
		"selectedStsMsgTxt":params.selectedStsMsgTxt || "selectedStsMsg",
		"selPadStsMsgTxt":	params.selPadStsMsgTxt || "selPadStsMsg",
		"helpFileTxt":		params.helpFileTxt || "helpFile"
	};
	
	this.params.today.setHours(0, 0, 0);
	
	// default month / year settings if necessary
	this.params.baseDate = this.params.baseDate || this.params.today;
	
	this.params.startMonth = this.params.startMonth || this.params.baseDate.getMonth() + 1;
	this.params.startYear = this.params.startYear || this.params.baseDate.getFullYear();
	this.params.minMonth = this.params.minMonth ||this.params.startMonth;
	this.params.minYear = this.params.minYear || this.params.startYear;
	this.params.maxMonth = this.params.maxMonth ||this.params.minMonth;
	this.params.maxYear = this.params.maxYear || this.params.minYear + 1;
	
	// the number of days to build on either side of the months that will actually be rendered
	this.bufferDays = Math.ceil((this.params.maxRangeDays - 1) / 7) * 7;
	
	// make sure the baseUrl ends with a '/' (if it was specified)
	if (this.params.baseUrl && !this.params.baseUrl.endsWith("/")) {
		this.params.baseUrl += "/";
	}
	
	// add a prefix to all text keys so that sets of text can be easily selected
	if (this.params.txtPrefix) {
		for (var arg in this.params) {
			if (arg.endsWith("Txt")) {
				this.params[arg] = this.params.txtPrefix + "__" + this.params[arg];
			}
		}
	}
	
	// make sure that the selectedRange is the first item in the markedRanges array
	if (this.params.markedRanges.length > 0 && this.params.markedRanges[0] !== this.params.selectedRange) {
		// shift array elements up one
		for (var i = this.params.markedRanges.length - 1; i >= 0; i--) {
			this.params.markedRanges[i + 1] = this.params.markedRanges[i];
		}
	}
	this.params.markedRanges[0] = this.params.selectedRange;

	// single day selection?
	if (this.params.selectSingle) {
		this.params.minRangeDays = 1;
		this.params.maxRangeDays = 1;
		this.params.prePadding = 0;
		this.params.postPadding = 0;
	} else if (this.params.minRangeDays <= 1 && this.params.maxRangeDays <= 1 && this.params.prePadding === 0 && this.params.postPadding === 0) {
		this.params.selectSingle = true;
	}
	
	// relative start year
	if (typeof(this.params.startYear) == "string") {
		switch (this.params.startYear.charAt(0)) {
			case "+":
				this.params.startYear = this.params.baseDate.getFullYear() + parseInt(this.params.startYear.substring(1), 10);
				break;
			case "-":
				this.params.startYear = (this.params.baseDate.getFullYear() - parseInt(this.params.startYear.substring(1), 10));
				break;
			default:
				this.params.startYear = parseInt(this.params.startYear, 10);
				break;
		}
	}

	// relative start month
	if (typeof(this.params.startMonth) == "string") {
		switch (this.params.startMonth.charAt(0)) {
			case "+":
				this.params.startMonth = baseDate.getMonth() + parseInt(this.params.startMonth.substring(1), 10) + 1;
				break;
			case "-":
				this.params.startMonth = (baseDate.getMonth() - parseInt(this.params.startMonth.substring(1), 10)) + 1;
				break;
			default:
				this.params.startMonth = parseInt(this.params.startMonth, 10);
				break;
		}
	}
	
	// fix any month overflow
	if (this.params.startMonth > 12) {
		this.params.startYear += Math.floor((this.params.startMonth - 1) / 12);
		this.params.startMonth = ((this.params.startMonth - 1) % 12) + 1;
	} else if (this.params.startMonth < 1) {
		this.params.startYear -= Math.ceil(Math.abs(this.params.startMonth - 1) / 12);
		this.params.startMonth = 12 - Math.abs(this.params.startMonth % 12);
	}

	this.currentMonth = this.params.startMonth;
	this.currentYear = this.params.startYear;
	
	// set some variables based on parameters
	this.recalculate();
	
	Calendars.add(this);
}

Calendar.prototype.getMarkedRanges = function(refDate) {
	var ranges = [];
	
	if (this.params.markedRanges) {
		for (var i = 0; i < this.params.markedRanges.length; i++) {
			if (this.params.markedRanges[i].inRange(refDate)) {
				ranges[ranges.length] = this.params.markedRanges[i];
			}
		}
	}
	return ranges;
};

Calendar.prototype.getTextForDate = function(refDate) {
	var additionalInfoArray = [];
	var infoArray = this.getApplicableRangeInfo(refDate, additionalInfoArray);
	var textData;
	var html = "", footer = "";
	
	if (infoArray.length === 0) {
		textData = null;
	} else {
		textData = { "title": "", "html": "" };
		for (var i = 0; i < infoArray.length; i++) {
			html += (i > 0 ? "<hr size='1' />" : "") + infoArray[i].infoText;
			footer += (footer.length > 0 ? "<br />" : "") + additionalInfoArray[i].dateRange.toString("{0} - {1}", "d mmm yyyy");
		}
		textData.html = html;
		textData.footer = footer;
	}
	
	return textData;
};

Calendar.prototype.getApplicableRangeInfo = function(refDate, additionalInfoArray) {
	var infoArray = [];
	var ranges = this.getMarkedRanges(refDate);
	var infoCollection;
	var info;
	var foundDefault;
	var foundNonDefault;
	var temp;
	var num, num2;
	var additionalInfo;
	
	if (ranges.length > 0) {
		for (var i = 0; i < ranges.length; i++) {
			infoCollection = ranges[i].infoCollection;
			foundDefault = false;
			foundNonDefault = false;
			additionalInfo = null;
			for (var selector in infoCollection) {
				if (selector == "all") {
					infoArray[infoArray.length] = infoCollection[selector];
					additionalInfo = { "dateRange": ranges[i] };
					foundNonDefault = true;
				} else if (selector == "default" && !foundNonDefault) {
					infoArray[infoArray.length] = infoCollection[selector];
					additionalInfo = { "dateRange": ranges[i] };
					foundDefault = true;
				} else {
					temp = selector.split(":");
					switch (temp[0].toLowerCase()) {
						case "first":
							num = parseInt(temp[1], 10);
							if (ranges[i].dateFrom.diff(refDate) < num) {
								foundNonDefault = true;
								if (foundDefault) {
									infoArray[infoArray.length - 1] = infoCollection[selector];
								} else {
									infoArray[infoArray.length] = infoCollection[selector];
								}
								additionalInfo = { "dateRange": ranges[i].relativeRange("first", num) };
							}
							break;
						case "last":
							num = parseInt(temp[1], 10);
							if (refDate.diff(ranges[i].dateTo) < num) {
								foundNonDefault = true;
								if (foundDefault) {
									infoArray[infoArray.length - 1] = infoCollection[selector];
								} else {
									infoArray[infoArray.length] = infoCollection[selector];
								}
								additionalInfo = { "dateRange": ranges[i].relativeRange("last", num) };
							}							
							break;
						case "inner":
							temp = temp[1].split("-");
							num = parseInt(temp[0], 10);
							if (temp.length > 1 && temp[1].length > 0) {
								num2 = parseInt(temp[1], 10);
							} else {
								num2 = num;
							}
							additionalInfo = { "dateRange": ranges[i].relativeRange("inner", num, num2) };
							//if (ranges[i].dateFrom.diff(refDate) >= num && refDate.diff(ranges[i].dateTo) > num2) {
							if (additionalInfo.dateRange.inRange(refDate)) {
								foundNonDefault = true;
								if (foundDefault) {
									infoArray[infoArray.length - 1] = infoCollection[selector];
								} else {
									infoArray[infoArray.length] = infoCollection[selector];
								}
							}
							break;
					} // end swtich				
				} // end if
				if (additionalInfoArray) {
					additionalInfoArray[infoArray.length - 1] = additionalInfo;
				}
			} // end for: i
		} // end for: selector
	} // end if
	
	return infoArray;
};

Calendar.prototype.recalculate = function() {
	// set date of the first day of the first month
	this.startDate = new Date(this.currentYear, this.currentMonth - 1, 1);
	
	// calculate last day of the last month 
	this.endDate = new Date(this.startDate);
	this.endDate.setDate(1);
	this.endDate.setMonth(this.endDate.getMonth() + this.params.months);
	this.endDate.setDate(0);

	// setup the CalInfo items to describe the selected range
	this.params.selectedRange.infoCollection = {};
	this.params.selectedRange.infoCollection["inner:" + (this.params.prePadding) + "-" + (this.params.postPadding)] = new CalInfo("selection", LocalText.get(this.params.selectionTxt, CalendarText));
	if (this.params.prePadding > 0) {
		this.params.selectedRange.infoCollection["first:" + this.params.prePadding] = new CalInfo("prepadding", LocalText.get(this.params.prePaddingTxt, CalendarText));
	}
	if (this.params.postPadding > 0) {
		this.params.selectedRange.infoCollection["last:" + this.params.postPadding] = new CalInfo("postpadding", LocalText.get(this.params.postPaddingTxt, CalendarText));
	}
};

Calendar.prototype.back = function() {
	this.currentMonth--;
	if (this.currentMonth === 0) {
		this.currentYear--;
		this.currentMonth = 12;
	}
	this.refresh();
};

Calendar.prototype.reset = function() {
	this.currentMonth = this.params.startMonth;
	this.currentYear = this.params.startYear;
	this.refresh();
};

Calendar.prototype.forward = function() {
	this.currentMonth++;
	if (this.currentMonth === 13) {
		this.currentYear++;
		this.currentMonth = 1;
	}
	this.refresh();
};

Calendar.prototype.select = function(yyyymmdd) {
	var dateSplit = yyyymmdd.split(" ");
	var selectedDate = new Date(parseInt(dateSplit[0],10), parseInt(dateSplit[1],10)-1, parseInt(dateSplit[2],10));
	var newDateFrom = null, newDateTo = null;
	var selectedDay = this.getDay(this.getOffsetFromDate(selectedDate));
	var newRange;
	var commitSelection = true;
	
	if (this.params.disableSelection || (!this.params.selectExisting && selectedDay.unselectable)) {
		return false; // *** EXITS HERE >>>	
	}

	if (this.params.selectedRange.dateFrom && this.params.selectedRange.dateTo) {

		// extend existing selection?		
		if (!this.params.selectSingle && selectedDate.valueOf() > this.params.selectedRange.dateFrom.valueOf()) {
			if (this.params.selectExisting || this.isAvailable(this.params.selectedRange.dateFrom, selectedDate)) {
				if (this.params.selectedRange.dateFrom.diff(selectedDate) < this.params.maxRangeDays + this.params.prePadding) {
					if (this.params.selectedRange.dateFrom.diff(selectedDate) >= this.params.minRangeDays + this.params.prePadding) {
						selectedDate.setDate(selectedDate.getDate() + this.params.postPadding);
						newDateTo = new Date(selectedDate);
					} else {
						// >>> range would be too short
						return false; // *** EXITS HERE >>>
					}
				} else {
					// >>> range would be too long
				}
				
			} else {
				// >>> range would span an existing unavailable range
			}
		}
	}
	if (!newDateFrom && !newDateTo) {
		// reset range to the selected day (+ min days)	
		newDateFrom = new Date(selectedDate);
		newDateFrom.setDate(newDateFrom.getDate() - this.params.prePadding);
		newDateTo = new Date(selectedDate);
		newDateTo.setDate(newDateTo.getDate() + (this.params.minRangeDays - 1) + this.params.postPadding);
		while (!this.params.selectExisting && this.getDay(this.getOffsetFromDate(newDateTo)).unavailable) {
			newDateFrom.setDate(newDateFrom.getDate() - 1);
			newDateTo.setDate(newDateTo.getDate() - 1);
		}
	}
	
	if (newDateFrom || newDateTo) {
		newRange = new DateRange(newDateFrom || this.params.selectedRange.dateFrom, newDateTo || this.params.selectedRange.dateTo, this.params.selectedRange.infoCollection);
		if (!this.params.selectExisting && !this.isAvailable(newRange.dateFrom, newRange.dateTo)) {
			// we should never get here!
			alert("Invalid selection");
		} else {
			if ((newDateFrom && (!this.params.selectedRange.dateFrom || newDateFrom.valueOf() != this.params.selectedRange.dateFrom.valueOf()))
				|| (newDateTo && (!this.params.selectedRange.dateTo || newDateTo.valueOf() != this.params.selectedRange.dateTo.valueOf()))) {
				// call specified function
				
				if (this.params.selectFnc) {
					commitSelection = this.params.selectFnc(newRange, this.getInnerSelectedRange(newRange.dateFrom, newRange.dateTo), selectedDay);
				}
				if (commitSelection) {
					if (newDateFrom) {
						this.params.selectedRange.dateFrom = newDateFrom;
					}
					if (newDateTo) {
						this.params.selectedRange.dateTo = newDateTo;
					}
					this.refresh();
				}
			}
		}
	}
};

Calendar.prototype.isAvailable = function(dateFrom, dateTo) {
	return (this.findNextUnavailable(this.getOffsetFromDate(dateFrom)) > this.getOffsetFromDate(dateTo));
};

Calendar.prototype.isSelectable = function(dateFrom, dateTo) {
	return (this.findNextUnselectable(this.getOffsetFromDate(dateFrom)) > this.getOffsetFromDate(dateTo));
};

Calendar.prototype.getOffsetFromDate = function(date) {
	return this.calStore[0][0].date.diff(date);
};

Calendar.prototype.clear = function() {
	if (!this.params.clearFnc || this.params.clearFnc()) {
		this.params.selectedRange.clearDates();
		this.refresh();
	}
};

Calendar.prototype.help = function() {
	var calTable = document.getElementById(this.params.id);
	var overlay = document.getElementById(this.params.id + "_help_overlay");
	var shadow = document.getElementById(this.params.id + "_help_shadow");
	var container = document.getElementById(this.params.id + "_help");
	var pos;
	var widthDivisor = 0.8, heightDivisor = 0.9;
	var shadowOffsetX = 5, shadowOffsetY = 5;
	var helpWidth, helpHeight;
	var url, loader;
	var that = this;
	
	if (!Calendars.helpText) {
		container.innerHTML = "<div style='margin: 100px 0px 0px 220px;'><a href='#' onclick='Calendars[\"" + this.params.id + "\"].closeHelp();return false;'><img src='" + this.params.baseUrl + "ajax-loader.gif' border='0' /></a></div>";
		url = LocalText.get(this.params.helpFileTxt, CalendarText);
		if (!url.startsWith("http://") && !url.startsWith("/")) {
			url = this.params.baseUrl + url;
		}
		loader = new net.ContentLoader(
			url, 
			null,
			function() {
				that.helpFill(this.getResult());
			}
		);
	} else {
		container.innerHTML = Calendars.helpText;
	}
	
	pos = Browser.findPos(calTable);
	overlay.style.left = pos.left + "px";
	overlay.style.top = pos.top + "px";
	overlay.style.width = calTable.offsetWidth + "px";
	overlay.style.height = calTable.offsetHeight + "px";
	overlay.style.display = "block";

	helpWidth = Math.round(calTable.offsetWidth * widthDivisor);
	helpHeight = Math.round(calTable.offsetHeight * heightDivisor);

	shadow.style.left = pos.left + Math.round((calTable.offsetWidth - helpWidth) / 2) + shadowOffsetX + "px";
	shadow.style.top = pos.top + Math.round((calTable.offsetHeight - helpHeight) / 2) + shadowOffsetY + "px";
	shadow.style.width = helpWidth + "px";
	shadow.style.height = helpHeight + "px";
	shadow.style.display = "block";

	container.style.left = pos.left + Math.round((calTable.offsetWidth - helpWidth) / 2) + "px";
	container.style.top = pos.top + Math.round((calTable.offsetHeight - helpHeight) / 2) + "px";
	container.style.width = helpWidth + "px";
	container.style.height = helpHeight + "px";
	container.style.display = "block";
};

Calendar.prototype.helpFill = function(helpHtml) {
	var container = document.getElementById(this.params.id + "_help");
	
	Calendars.helpText = helpHtml.replace(/\{id\}/gi, this.params.id);
	container.innerHTML = Calendars.helpText;
};

Calendar.prototype.closeHelp = function() {
	var overlay = document.getElementById(this.params.id + "_help_overlay");
	var shadow = document.getElementById(this.params.id + "_help_shadow");
	var container = document.getElementById(this.params.id + "_help");
	
	overlay.style.display = "none";
	shadow.style.display = "none";
	container.style.display = "none";
};

Calendar.prototype.refresh = function() {
	this.recalculate();
	this.render(document.getElementById(this.params.id).parentNode);
};

Calendar.prototype.build = function() {
	var weekOffset = 0;
	var dayOfWeekOffset = 0;
	var ok = true;
	var currentDate;
	var infoArray;
	var day;
	
	this.calStore = [];
	
	// calculate first date on calendar
	var startDate = new Date(this.startDate);
	// move startDate back into previous month based on specified weekStartDay
	while (startDate.getDay() != this.params.weekStartDay) {
		startDate.setDate(startDate.getDate() - 1);
	}
	
	this.calendarWeeks = Math.ceil(startDate.diff(this.endDate) / 7);
	this.calendarDays = this.calendarWeeks * 7;
	
	var totalDays = this.calendarDays + (this.bufferDays * 2);
	
	startDate.setDate(startDate.getDate() - this.bufferDays);
	
	// build calStore - a two dimensional array of Day objects [week][day] to represent the calendar to render
	// need to build more than we're going to display (totalDays), depending on the minimum range days
	for (var dayOffset = 0; dayOffset < totalDays; dayOffset++) {
		currentDate = new Date(startDate);
		currentDate.setDate(currentDate.getDate() + dayOffset);
		weekOffset = Math.floor(dayOffset / 7);
		dayOfWeekOffset = dayOffset % 7;
		
		// add an array for a new week to the calendar
		if (this.calStore.length <= weekOffset) {
			this.calStore[this.calStore.length] = [ null, null, null, null, null, null, null];
		}
		
		// add this day to the calendar
		day = new Day(currentDate, this.getApplicableRangeInfo(currentDate));
		this.calStore[weekOffset][dayOfWeekOffset] = day;
	}
	
	// look for and mark ranges where there isn't enough space to fit a range of the minimum days
	var lastUnavailable = -100;
	var nextUnavailable = -100;
	for (var i = 0; i < totalDays; i++) {
		day = this.getDay(i);
		
		if (day.unavailable) {
			lastUnavailable = i; 
		}
		if (nextUnavailable < i) {
			nextUnavailable = this.findNextUnavailable(i);
		}

		if (!day.unavailable && (nextUnavailable - lastUnavailable) - 1 < this.params.minRangeDays) {
			day.addInfo(new CalInfo("noroom", LocalText.get(this.params.noRoomTxt, CalendarText), false, true));
		} else if (!day.unavailable 
			&& ((i - lastUnavailable <= this.params.prePadding)
			|| (nextUnavailable - i <= this.params.postPadding))
			) {
			day.addInfo(new CalInfo("padding", LocalText.get(this.params.paddingTxt, CalendarText), false, true));
		}
	}
};

Calendar.prototype.getDay = function(offset) {
	if (offset < 0 || offset >= this.calStore.length * 7) {
		var baseDate = new Date(this.calStore[0][0].date);
		baseDate.setDate(baseDate.getDate() + offset);
		return new Day(baseDate);
	} else {
		return this.calStore[Math.floor(offset / 7)][offset % 7];
	}
};

Calendar.prototype.setDay = function(offset, day) {
	this.calStore[Math.floor(offset / 7)][offset % 7] = day;
};

Calendar.prototype.findNextUnavailable = function(startOffset) {
	var offset = startOffset;
	var day = this.getDay(offset++);
	
	while (offset < this.calStore.length * 7) {
		if (day.unavailable) {
			break;
		} else {
			day = this.getDay(offset++);
		}
	}
	
	return (day ? offset - 1 : 99999);
};

Calendar.prototype.findNextUnselectable = function(startOffset) {
	var offset = startOffset;
	var day = this.getDay(offset++);
	
	while (offset < this.calStore.length * 7) {
		if (day.unselectable) {
			break;
		} else {
			day = this.getDay(offset++);
		}
	}	
	return (day ? offset - 1 : 99999);
};

Calendar.prototype.getInnerSelectedRange = function(dateFrom, dateTo) {
	innerDateFrom = new Date(dateFrom || this.params.selectedRange.dateFrom);
	innerDateTo = new Date(dateTo || this.params.selectedRange.dateTo);
	innerDateFrom.setDate(innerDateFrom.getDate() + this.params.prePadding);
	innerDateTo.setDate(innerDateTo.getDate() - this.params.postPadding);
	return new DateRange(innerDateFrom, innerDateTo);
};

Calendar.prototype.render = function(renderTarget, resizeContainer) {
	var html = new StringBuilder();
	var week;
	var day;
	var dayOfWeek;
	var dayName, classNames;
	var monthNo = 0, monthNoPrev = -1, yearNo = 0, yearNoPrev = 0;
	var monthLabel = "", monthLabelPrev = "";
	var monthLabels = "";
	var monthLabelsSpan = 1;
	var startWeek = this.bufferDays / 7;

	// build the calendar array	
	this.build();
	
	// build the html to render the calendar table
	html.appendFormat("<table class='calendar' id='{0}' cellspacing='0'>", this.params.id);
	html.appendFormat("<tr><th class='header' colspan='{0}'>", this.calStore.length + 1);
	
	html.append("<table><tr><th class='left'>");
	
	// navigation buttons
	if (this.startDate.getTime() <= (new Date(this.params.minYear, this.params.minMonth - 1, 1)).getTime()) {
		html.appendFormat("<a class='button_inactive' href='#' onclick=\"return false;\" title=\"{1}\" />", this.params.id, LocalText.get(this.params.navPrevTxt, CalendarText));
		html.appendFormat("<img src='{0}back_inactive.gif' width='16' height='16' border='0' alt=\"{1}\" /></a>", this.params.baseUrl, LocalText.get(this.params.navPrevTxt, CalendarText));
	} else {
		html.appendFormat("<a class='button' href='#' onclick=\"Calendars['{0}'].back();return false;\" title=\"{1}\" />", this.params.id, LocalText.get(this.params.navPrevTxt, CalendarText));
		html.appendFormat("<img src='{0}back.gif' width='16' height='16' border='0' alt=\"{1}\" /></a>", this.params.baseUrl, LocalText.get(this.params.navPrevTxt, CalendarText));
	}
	
	html.appendFormat("<a class='button' href='#' onclick=\"Calendars['{0}'].reset();return false;\" title=\"{1}\" />", this.params.id, LocalText.get(this.params.navResetTxt, CalendarText));
	html.appendFormat("<img src='{0}reset.gif' width='16' height='16' border='0' alt=\"{1}\" /></a>", this.params.baseUrl, LocalText.get(this.params.navResetTxt, CalendarText));
	
	if (this.endDate.getTime() >= (new Date(this.params.maxYear, this.params.maxMonth, 0)).getTime()) {
		html.appendFormat("<a class='button_inactive' href='#' onclick=\"return false;\" title=\"{1}\" />", this.params.id, LocalText.get(this.params.navNextTxt, CalendarText));
		html.appendFormat("<img src='{0}forward_inactive.gif' width='16' height='16' border='0' alt=\"{1}\" /></a>", this.params.baseUrl, LocalText.get(this.params.navNextTxt, CalendarText));
	} else {
		html.appendFormat("<a class='button' href='#' onclick=\"Calendars['{0}'].forward();return false;\" title=\"{1}\" />", this.params.id, LocalText.get(this.params.navNextTxt, CalendarText));
		html.appendFormat("<img src='{0}forward.gif' width='16' height='16' border='0' alt=\"{1}\" /></a>", this.params.baseUrl, LocalText.get(this.params.navNextTxt, CalendarText));
	}
	
	// calendar heading
	html.appendFormat("</th><th class='centre'><h1>{0}</h1></th><th class='right'>&nbsp;</th></tr></table></tr>", LocalText.get(this.params.headingTxt, CalendarText));
		
	html.append("<tr class='monthLabels'>#month_labels#</tr>");
	
	monthLabelPrev = "&nbsp;";
	
	// loop each day in the week (row)
	for (var dayOffset = 0; dayOffset < 7; dayOffset++) {
		
		// loop each week (column)
		for (var weekOffset = startWeek; weekOffset < this.calendarWeeks + startWeek; weekOffset++) {
			day = this.calStore[weekOffset][dayOffset];
			dayOfWeek = day.date.getDay();
			dayName = DateHelper.dayName(dayOfWeek);
			monthNo = day.date.getMonth();
			yearNo = day.date.getFullYear();
			
			// render the TR and the row label (Mon, Tues, etc.) if this is the first column
			if (weekOffset === startWeek) {
				html.appendFormat("<tr class='weekday{0}'><th class='label'>{1}</th>", dayOfWeek, dayName.substr(0, this.params.labelLen));
			}
			
			// determine class names for the date table cell
			if (this.startDate.diff(day.date, "m") < 0 || this.endDate.diff(day.date, "m") > 0) {
				classNames = "outofbounds";
				
				if (dayOffset === 0) {
					monthLabel = "&nbsp;";
				}
			} else {
				classNames = "month" + String(this.startDate.diff(day.date, "m") + 1);
				
				if (dayOffset === 0) {
					monthLabel = DateHelper.monthName(monthNo).substr(0, 3) + " " + String(yearNo);
				}
			}

			// today?
			if (day.date.diff(this.params.today) === 0) {
				classNames += " today";
			}
			// add class names for borders between months
			if (weekOffset > startWeek && day.date.getMonth() != this.calStore[weekOffset - 1][dayOffset].date.getMonth()) {
				classNames += " leftBorder";
			}
			if (dayOffset > 0 && day.date.getMonth() != this.calStore[weekOffset][dayOffset - 1].date.getMonth()) {
				classNames += " topBorder";
			}
			// other class names
			classNames += (day.unavailable ? " unavailable" : (day.unselectable ? " unselectable" : " available"));
			classNames += (day.infoTypes ? " " + day.infoTypes : "");
			
			// append the HTML for the current day
			html.appendFormat("<td class='{0}'><span>", classNames);
			html.append(day.render(this), "</span></td>");
			
			// close TR if this is the end of a row
			if (weekOffset == this.calendarWeeks + startWeek) {
				html.append("</tr>");
			}

			if (dayOffset === 0) {
				if (monthLabel != monthLabelPrev) {
					monthLabels += "<th colspan='" + monthLabelsSpan + "'>" + monthLabelPrev + "</th>";
					monthLabelPrev = monthLabel;
					monthLabelsSpan = 1;
				} else {
					monthLabelsSpan++;
				}
			}
			
			monthNoPrev = monthNo;
			yearNoPrev = yearNo;
			
		}
	}
	
	monthLabels += "<th colspan='" + monthLabelsSpan + "'>" + monthLabel + "</th>";
	
	if (!this.params.disableSelection && !this.params.noStatusLine) {
		html.appendFormat("<tr class='status'><th colspan='{0}' id='{1}_status'>{2}</th></tr>", this.calStore.length + 1, this.params.id, this.renderStatus());
	}
			
	html.append("</table>");
	
	html.appendFormat("<div class='calendarHelpOverlay' id='{0}_help_overlay' style='display: none'>&nbsp;</div>", this.params.id);
	html.appendFormat("<div class='calendarHelpShadow' id='{0}_help_shadow' style='display: none'>&nbsp;</div>", this.params.id);
	html.appendFormat("<div class='calendarHelp' id='{0}_help' style='display: none'></div>", this.params.id);
		
	html = html.replace("#month_labels#", monthLabels);

	if (renderTarget) {
		renderTarget.innerHTML = html.toString();
		
		// make the container fit the rendered calendar table?
		if (resizeContainer) {
			var container = document.getElementById(this.params.id).parentNode;
			container.style.width = document.getElementById(this.params.id).clientWidth + "px";
			container.style.height = document.getElementById(this.params.id).clientHeight + "px";
		}
	}
	
	return html.toString();
};

Calendar.prototype.renderStatus = function(renderTarget, statusMsg) {
	var html = new StringBuilder();
	var selectedText;
	var paddingMsg = "";

	html.append("<table><tr><th class='left'>");
	
	if (!this.params.selectedRange.dateFrom && !this.params.selectedRange.dateTo) {
		html.appendFormat("<a class='button_inactive' href='#' onclick='return false;' title=\"{1}\" />", this.params.id, LocalText.get(this.params.clearSelectionTxt, CalendarText));
		html.appendFormat("<img src='{0}clear_inactive.gif' width='16' height='16' border='0' alt=\"{1}\" /></a>", this.params.baseUrl, LocalText.get(this.params.clearSelectionTxt, CalendarText));

		html.appendFormat("</th><th class='centre noSelection'>{0}</th>", LocalText.get(this.params.noSelectionTxt, CalendarText));
	} else {
		html.appendFormat("<a class='button' href='#' onclick=\"Calendars['{0}'].clear();return false;\" title=\"{1}\" />", this.params.id, LocalText.get(this.params.clearSelectionTxt, CalendarText));
		html.appendFormat("<img src='{0}clear.gif' width='16' height='16' border='0' alt=\"{1}\" /></a>", this.params.baseUrl, LocalText.get(this.params.clearSelectionTxt, CalendarText));

		html.appendFormat("</th><th class='centre'>{0}</th>", this.getInnerSelectedRange());
	}
	html.appendFormat("<th class='right'><a class='button' href='#' onclick=\"Calendars['{0}'].help();return false;\" title=\"{1}\" />", this.params.id, LocalText.get(this.params.helpButtonTxt, CalendarText));
	html.appendFormat("<img src='{0}help.gif' width='16' height='16' border='0' alt=\"{1}\" /></a></th></tr>", this.params.baseUrl, LocalText.get(this.params.helpButtonTxt, CalendarText));
	
	if (!statusMsg) {
		if (this.params.selectedRange.dateFrom && this.params.selectedRange.dateTo) {
			if (this.params.prePadding || this.params.postPadding) {
				paddingMsg = LocalText.get(this.params.selPadStsMsgTxt, CalendarText);
				paddingMsg = paddingMsg.format(this.params.prePadding, this.params.postPadding);
			}
			statusMsg = LocalText.get(this.params.selectedStsMsgTxt, CalendarText).format(this.getInnerSelectedRange().dayCount(), paddingMsg);			
		} else {
			statusMsg = LocalText.get(this.params.defaultStsMsgTxt, CalendarText);
		}
	}
	html.appendFormat("<tr><th colspan='3'><div class='statusMsg'>{0}</div></th></tr></table>", statusMsg);
	
	if (renderTarget) {
		renderTarget.innerHTML = html.toString();
	}
	
	return html.toString();
};


// *** Day ***

function Day(date, infoArray) {
	this.date = new Date(date);
	this.info = (infoArray ? (typeof(infoArray) == "CalInfo" ? [ infoArray ] : infoArray) : null);
	this.unavailable = false;
	this.unselectable = false;
	this.infoTypes = "";
	this.infoText = "";
	
	this.init();
}

Day.prototype.addInfo = function(info) {
	this.info = this.info || [];
	this.info[this.info.length] = info;
	this.init();
};

Day.prototype.init = function() {
	this.unavailable = false;
	this.unselectable = false;
	this.infoTypes = "";
	this.infoText = "";
	
	if (this.info) {
		for (var i = 0; i < this.info.length; i++) {
			if (this.info[i].unavailable) {
				this.unavailable = true;
			}
			if (this.info[i].unselectable) {
				this.unselectable = true;
			}			
			if (this.info[i].infoType) {
				this.infoTypes += (this.infoTypes ? " " : "") + this.info[i].infoType;
			}
			if (this.info[i].infoText) {
				this.infoText += (this.infoText ? "\t" : "") + this.info[i].infoText;
			}
		}
	}
};

Day.prototype.toString = function() {
	return this.date.format("dd mmmm yyyy");
};

Day.prototype.render = function(calendar) {
	var onclickJs = "Calendars['{0}'].select('{1}');return false;".format(calendar.params.id, this.date.format("yyyy mm dd"));
	return buildPopupLink(null, null, null, this.date.getDate(), "onclick=\"" + onclickJs + "\"", function(refDate) { return calendar.getTextForDate(refDate); }, this.date);
};

// *** DateRange ***

function DateRange(dateFrom, dateTo, infoCollection) {
	this.dateFrom = (dateFrom ? dateFrom : null);
	this.dateTo = (dateTo ? dateTo : null);
	this.infoCollection = (infoCollection ? (typeof(infoCollection.infoType) != "undefined" ? { "default": infoCollection } : infoCollection) : null);
}

DateRange.prototype.inRange = function(checkDate) {
	return (
		(this.dateFrom && this.dateTo)
		&& (this.dateFrom.valueOf() <= checkDate.valueOf())
		&& (this.dateTo.valueOf() >= checkDate.valueOf())
	);
};

DateRange.prototype.clearDates = function() {
	this.dateFrom = null;
	this.dateTo = null;
};

DateRange.prototype.dayCount = function(infoTypes) {
	var count = 0;
	
	if (!infoTypes) {
		count = this.dateFrom.diff(this.dateTo) + 1;		
	} else {
		if (typeof(infoTypes) == "string") {
			infoTypes = [ infoTypes ];
		}
		// TODO: count only days of specified types
	}
	return count;
};

DateRange.prototype.relativeRange = function(relType, param1, param2) {
	
	var newDateFrom = new Date(this.dateFrom);
	var newDateTo = new Date(this.dateTo);

	switch (relType) {
		case "first": // first x days
			newDateTo = newDateTo.earliest(newDateFrom.add(param1 - 1));
			break;
		case "last": // last x days
			newDateFrom = newDateFrom.latest(newDateTo.add((param1 - 1) * -1));
			break;
		case "inner": // from day x to the nth day before the end
			newDateFrom = newDateTo.earliest(newDateFrom.add(param1));
			newDateTo = newDateFrom.latest(newDateTo.add(param2 * -1));
			break;
		default:
			// defaults to a copy of the existing range
			break;
	}
		
	return new DateRange(newDateFrom, newDateTo);
};

DateRange.prototype.toString = function(pattern, dateFormat) {
	if (!dateFormat) {
		dateFormat = LocalText.get("datesFormat", CalendarText);
	}
	if (!pattern) {
		pattern = LocalText.get("selectedDates", CalendarText);
	}
	return pattern.format(this.dateFrom.format(dateFormat), this.dateTo.format(dateFormat));
};


// *** CalInfo ***

function CalInfo(infoType, infoText, unavailable, unselectable) {
	this.infoType = infoType || "";
	this.infoText = infoText || "";
	this.unavailable = unavailable || false;
	this.unselectable = unselectable || this.unavailable;
}

CalInfo.prototype.toString = function() {
	return "CalInfo[Type:{0}, Text:{1}, Available:{2}]".format(this.infoType, this.infoText, this.unavailable);
};

CalInfo.prototype.copy = function() {
	var targetCalInfo = new CalInfo();
	
	targetCalInfo.infoType = (this.infoType ? String(this.infoType) : "");
	targetCalInfo.infoText = (this.infoText ? String(this.infoText) : "");
	targetCalInfo.unavailable = (this.unavailable ? true : false);
	
	return targetCalInfo;
};










