function TPCalendar(initialValue, selectHandler, closeHandler, startWithDay) {
	this.date = initialValue || new Date();
	this.selectHandler = selectHandler || Prototype.emptyFunction;
	this.closeHandler = closeHandler || Prototype.emptyFunction;
	this.startWithDay = startWithDay || 1;
	this.endWithDay = this.startWithDay - 1;
	if (this.endWithDay == -1) this.endWithDay = 6;

	this.minDate = new Date(this.date.getUTCFullYear(), 0, 1);
	this.maxDate = new Date(this.date.getUTCFullYear() + 1, 11, 31);

	this.isIE = navigator.userAgent.match(/MSIE/);
}

// add succ method to Date so we can use ObjectRange from prototype
Object.extend(Date, { monthNames: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'] });
Object.extend(Date.prototype, {
  succ: function() {
    return new Date(this.getTime() + (24 * 60 * 60 * 1000));
  },
	prev: function() {
    return new Date(this.getTime() - (24 * 60 * 60 * 1000));
	},
	toUTCDateString: function(withoutDay) {
		return (withoutDay ? '' : (this.getUTCDate() + " ")) + Date.monthNames[this.getUTCMonth()] + " " + this.getUTCFullYear();
	},
	dateEql: function(other) {
		return this.getUTCDate() == other.getUTCDate() && this.getUTCMonth() == other.getUTCMonth() && this.getUTCFullYear() == other.getUTCFullYear();
	}
});

TPCalendar.prototype = {
	setRange: function(min,max) {
		if (typeof min != 'undefined') this.minDate = min;
		if (typeof max != 'undefined') this.maxDate = max;

		if (this.date < this.minDate) this.date = this.minDate;
		if (this.date > this.maxDate) this.date = this.maxDate;

		if (this.currentPanel) {
			this.calPanels = new Array();
			var newPanel = this.getPanel();
			this.currentPanel.parentNode.replaceChild(newPanel, this.currentPanel);
			this.currentPanel = newPanel;
		}
	},
	getMonthRange: function(date) {
		// find the beginning and end of the month displayed for given date
		// will go outside the month to find how much of the neighbouring months to display to fill the calendar
		var startDate = new Date(date.getTime());

		startDate.setUTCDate(1);
		while (startDate.getUTCDay() != this.startWithDay) { startDate = startDate.prev(); }

		var endDate = new Date(date.getTime());
		endDate.setUTCDate(1);
		endDate.setUTCMonth(endDate.getUTCMonth() + 1);
		endDate.setTime(endDate.getTime() - (24 * 60 * 60 * 1000));
		while (endDate.getUTCDay() != this.endWithDay) { endDate = endDate.succ(); }

		return new ObjectRange(startDate, endDate, false);
	},
	dayNames: [ "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" ],
	getPanel: function() {
		var year = this.date.getUTCFullYear();
		var month = this.date.getUTCMonth();

		if (typeof this.calPanels[year] == 'undefined' || typeof this.calPanels[year][month] == 'undefined') {
			var panel = document.createElement('table');

			with (panel) {
				appendChild(document.createElement('thead'));
				firstChild.appendChild(document.createElement('tr'));
				firstChild.firstChild.className = 'daynames';
				setAttribute('cellpadding',0);
				setAttribute('cellspacing',0);
			}

			for (var i = this.startWithDay; ; i = (i + 1) % 7) {
				var th = document.createElement("th");
				th.appendChild(document.createTextNode(this.dayNames[i]));

				if ([0,6].include(i)) th.className = 'weekend';

				panel.firstChild.firstChild.appendChild(th);

				if (i == this.endWithDay) break;
			}

			panel.appendChild(document.createElement('tbody'));
			var monthRange = this.getMonthRange(this.date);
			var weekRow = false;
		
			monthRange.each(function(d) {
				if (d.getUTCDay() == this.startWithDay) {
					if (weekRow) panel.lastChild.appendChild(weekRow)

					weekRow = document.createElement("tr");
				}

				var dayA = document.createElement("a");
				dayA.appendChild(document.createTextNode(d.getUTCDate()));
				dayA.href = "#";
				Event.observe(dayA, 'mouseover', this.mouseOverHandler.bind(this, d, ''));
				Event.observe(dayA, 'mouseout', this.mouseOutHandler.bind(this, true));
				weekRow.appendChild(document.createElement("td"));

				if (d < this.minDate || d > this.maxDate) {
					Element.addClassName(weekRow.lastChild, "disabled");
					Event.observe(dayA, 'click', function(e) { Event.stop(e); });
				} else {
					Event.observe(dayA, 'click', this.clickHandler.bind(this, d));
				}
				if (d.getUTCMonth() != month) {
					Element.addClassName(dayA, "wrapmonth");
				}
				if ([0,6].include(d.getDay())) {
					Element.addClassName(weekRow.lastChild, 'weekend');
				}
				if (d.dateEql(this.date)) {
					Element.addClassName(weekRow.lastChild, 'selected');
				}
				if (d.dateEql(new Date())) {
					Element.addClassName(weekRow.lastChild, 'today');
				}
				weekRow.lastChild.appendChild(dayA);
			}.bind(this));

			if (weekRow) panel.lastChild.appendChild(weekRow)

			if (typeof this.calPanels[year] == 'undefined') {
				this.calPanels[year] = new Array();
			}
			this.calPanels[year][month] = panel;
		}
		return this.calPanels[year][month];
	},
	currentPanel: null,
	create: function() {
		this.calPanels = new Array();

		this.container = document.createElement('div');
		this.container.className = 'calendar';

		var closeHandle = document.createElement('p');
		var closeA = document.createElement('a');
		closeA.appendChild(document.createTextNode("x"));
		closeA.href = '#';
		Event.observe(closeA, 'mouseover', this.mouseOverHandler.bind(this, null, "Close calendar"));
		Event.observe(closeA, 'mouseout', this.mouseOutHandler.bind(this, false));
		Event.observe(closeA, 'click', this.closeClickHandler.bind(this));
		closeHandle.className = 'close'
		closeHandle.appendChild(closeA);
		this.container.appendChild(closeHandle);

		this.monthBox = document.createElement('h3');
		this.monthBox.appendChild(document.createTextNode(this.date.toUTCDateString(true)));
		this.container.appendChild(this.monthBox);

		var navList = document.createElement('ul');
		navList.className = 'clearfix control';

		this.prevYearLink = document.createElement('li');
		this.prevYearLink.title = "Previous year";
		this.prevYearLink.setAttribute("unselectable", true);
		this.prevYearLink.className = "prev_year";
		this.prevYearLink.appendChild(document.createTextNode('\u00ab Y'));
		Event.observe(this.prevYearLink, 'mouseover', this.mouseOverHandler.bind(this, null, "Previous year (hold for menu)"));
		Event.observe(this.prevYearLink, 'mouseout', this.mouseOutHandler.bind(this, false));
		Event.observe(this.prevYearLink, 'mousedown', this.clickOrHold.bind(this, -1, 'year'));
		Event.observe(this.prevYearLink, 'click', function(e) { Event.stop(e); });
		navList.appendChild(this.prevYearLink);

		this.prevMonthLink = document.createElement('li');
		this.prevMonthLink.title = "Previous month";
		this.prevMonthLink.setAttribute("unselectable", true);
		this.prevMonthLink.className = "prev_month";
		this.prevMonthLink.appendChild(document.createTextNode('< M'));
		Event.observe(this.prevMonthLink, 'mouseover', this.mouseOverHandler.bind(this, null, "Previous month (hold for menu)"));
		Event.observe(this.prevMonthLink, 'mouseout', this.mouseOutHandler.bind(this, false));
		Event.observe(this.prevMonthLink, 'mousedown', this.clickOrHold.bind(this, -1, 'month'));
		Event.observe(this.prevMonthLink, 'click', function(e) { Event.stop(e); });
		navList.appendChild(this.prevMonthLink);

		var todayLink = document.createElement('li');
		todayLink.className = 'today';
		todayLink.appendChild(document.createTextNode(' Today '));
		Event.observe(todayLink, 'mouseover', this.mouseOverHandler.bind(this, null, "Go to today"));
		Event.observe(todayLink, 'mouseout', this.mouseOutHandler.bind(this, false));
		Event.observe(todayLink, 'click', this.setDate.bind(this, new Date()));
		navList.appendChild(todayLink);

		this.nextMonthLink = document.createElement('li');
		this.nextMonthLink.title = "Next month";
		this.nextMonthLink.setAttribute("unselectable", true);
		this.nextMonthLink.className = "next_month";
		this.nextMonthLink.appendChild(document.createTextNode('M >'));
		Event.observe(this.nextMonthLink, 'mouseover', this.mouseOverHandler.bind(this, null, "Next month (hold for menu)"));
		Event.observe(this.nextMonthLink, 'mouseout', this.mouseOutHandler.bind(this, false));
		Event.observe(this.nextMonthLink, 'mousedown', this.clickOrHold.bind(this, 1, 'month'));
		Event.observe(this.nextMonthLink, 'click', function(e) { Event.stop(e); });
		navList.appendChild(this.nextMonthLink);

		this.nextYearLink = document.createElement('li');
		this.nextYearLink.title = "Next year";
		this.nextYearLink.setAttribute("unselectable", true);
		this.nextYearLink.className = "next_year";
		this.nextYearLink.appendChild(document.createTextNode('Y \u00bb'));
		Event.observe(this.nextYearLink, 'mouseover', this.mouseOverHandler.bind(this, null, "Next year (hold for menu)"));
		Event.observe(this.nextYearLink, 'mouseout', this.mouseOutHandler.bind(this, false));
		Event.observe(this.nextYearLink, 'mousedown', this.clickOrHold.bind(this, 1, 'year'));
		Event.observe(this.nextYearLink, 'click', function(e) { Event.stop(e); });
		navList.appendChild(this.nextYearLink);

		this.container.appendChild(navList);
		this.updateLinks();

		this.currentPanel = this.getPanel();
		this.container.appendChild(this.currentPanel);
			

		this.statusLine = document.createElement('div');
		this.statusLine.className = 'status';
		this.statusLine.appendChild(document.createTextNode("Select date"));
		this.container.appendChild(this.statusLine);
	},
	getDate: function() { return this.date; },
	setDatePart: function(what, value, direction) {
		var newDate = new Date(this.date.getTime());

		switch (what) {
			case 'year': newDate.setUTCFullYear(value); break;
			case 'month': 
				var newMonth = Date.monthNames.collect(function(e) { return e.substr(0,3); }).indexOf(value);
				if (direction == -1 && (newMonth >= newDate.getUTCMonth())) {
					newDate.setUTCFullYear(newDate.getUTCFullYear() - 1);
				} else if (direction == 1 && (newMonth <= newDate.getUTCMonth())) {
					newDate.setUTCFullYear(newDate.getUTCFullYear() + 1);
				}
				newDate.setUTCMonth(newMonth);
				break;
		}
		newDate = (newDate < this.minDate) ? this.minDate : (newDate > this.maxDate) ? this.maxDate : newDate;
		this.setDate(newDate);
	},
	setDate: function(date, evt) {
		if (evt) Event.stop(evt);

		if (date < this.minDate || date > this.maxDate) return;

		this.date = date;
		var newPanel = this.getPanel();

		if (newPanel != this.currentPanel) {
			this.container.replaceChild(newPanel, this.currentPanel);
			this.currentPanel = newPanel;
		}
		$A(this.currentPanel.getElementsByTagName('td')).each(function(day) {
			if (Element.hasClassName(day.firstChild, 'disabled') || Element.hasClassName(day.firstChild, 'wrapmonth')) return;

			var n = parseInt(day.textContent || day.innerText);
			if (!isNaN(n) && n == this.date.getUTCDate()) {
				Element.addClassName(day, 'selected');
			} else {
				Element.removeClassName(day, 'selected');
			}
		}.bind(this))
		this.updateLinks();
		this.monthBox.replaceChild(document.createTextNode(this.date.toUTCDateString(true)), this.monthBox.firstChild);
	},
	moveDate: function(num, what, evt) {
		var newDate = new Date(this.date.getTime());

		switch (what) {
			case 'year': 
				newDate.setUTCFullYear(newDate.getUTCFullYear() + num); 
				break;
			case 'month':
				var m = newDate.getUTCMonth();
				m = m + num;
				if (m < 0) { m += 12; newDate.setUTCFullYear(newDate.getUTCFullYear() - 1); }
				if (m > 11) { m -= 12; newDate.setUTCFullYear(newDate.getUTCFullYear() + 1); }

				// cope with months with < 31 days
				if ([1,3,5,8,10].include(m)) {
					if (newDate.getUTCDate() > (m == 1 ? 28 : 30)) {
						newDate.setDate(m == 1 ? 28 : 30);
					}
				}

				newDate.setUTCMonth(m);
				break;
		}
		if (num < 0) {
			newDate = (newDate > this.minDate) ? newDate : this.minDate;
		} else {
			newDate = (newDate < this.maxDate) ? newDate : this.maxDate;
		}
		this.setDate(newDate, evt);
	},
	clickHandler: function(d, evt) {
		this.setDate(d,evt);
		this.selectHandler(d);
	},
	clickOrHold: function(num, what, evt) {
		var el = Event.element(evt);
		Event.stop(evt);

		if (Element.hasClassName(el, 'disabled')) return;

		var mouseTimeout;
		var mup = function(e) { window.clearTimeout(mouseTimeout); Event.stopObserving(el, 'mouseup', mup); this.moveDate(num, what, e); }.bind(this);

		Event.observe(el, 'mouseup', mup);
		mouseTimeout = window.setTimeout(function() { Event.stopObserving(el, 'mouseup', mup); this.popupSelection(num, what, el) }.bind(this), 250);
	},
	popupSelection: function(num, what, el) {
		var items;

		if (what == 'month') {
			items = Date.monthNames.collect(function(month) { return month.substr(0,3); });
			items = items.reject(function(v, k) {
				var testYear = this.date.getUTCFullYear();

				if (num == -1) {
					if (k >= this.date.getUTCMonth()) 
						--testYear;

					return (
						(testYear < this.minDate.getUTCFullYear()) || 
						(testYear == this.minDate.getUTCFullYear() && k < this.minDate.getUTCMonth())
					);
				} else {
					if (k <= this.date.getUTCMonth()) 
						++testYear;

					return (
						(testYear > this.maxDate.getUTCFullYear()) || 
						(testYear == this.maxDate.getUTCFullYear() && k > this.maxDate.getUTCMonth())
					);
				}
			}.bind(this));
		} else {
			items = (num == -1) ? $R(this.minDate.getUTCFullYear(), this.date.getUTCFullYear() - 1) : $R(this.date.getUTCFullYear() + 1, this.maxDate.getUTCFullYear());
		}

		items = $A(items);
		if (items.length > 0) {
			var list = document.createElement('ul');
			list.className = "dropdown " + (num > 0 ? 'next' : 'prev') + "_" + what;
			items.each(function(i) {
				list.appendChild(document.createElement('li'));
				list.lastChild.appendChild(document.createElement('a'));
				list.lastChild.lastChild.appendChild(document.createTextNode(i));
				Event.observe(list.lastChild, 'mouseup', function() { this.setDatePart(what, i, num); }.bind(this));
				Event.observe(list.lastChild, 'mouseover', this.mouseOverHandler.bind(this, null, "Jump to " + i));
				Event.observe(list.lastChild, 'mouseout',  this.mouseOutHandler.bind(this, false));
			}.bind(this));

			this.container.appendChild(list);
			Event.observe(document, 'mouseup', function() { if (list.parentNode) list.parentNode.removeChild(list); });
		}
	},
	updateLinks: function() {
	    
		if (this.date.getUTCFullYear() > this.minDate.getUTCFullYear()) {
			Element.removeClassName(this.prevMonthLink, 'disabled');
			Element.removeClassName(this.prevYearLink, 'disabled');
		} else {
			Element.addClassName(this.prevYearLink, 'disabled');

			if (this.date.getUTCMonth() > this.minDate.getUTCMonth()) {
				Element.removeClassName(this.prevMonthLink, 'disabled');
			} else {
				Element.addClassName(this.prevMonthLink, 'disabled');
			}
		}
		if (this.date.getUTCFullYear() < this.maxDate.getUTCFullYear()) {
			Element.removeClassName(this.nextMonthLink, 'disabled');
			Element.removeClassName(this.nextYearLink, 'disabled');
		} else {
			Element.addClassName(this.nextYearLink, 'disabled');

			if (this.date.getUTCMonth() < this.maxDate.getUTCMonth()) {
				Element.removeClassName(this.nextMonthLink, 'disabled');
			} else {
				Element.addClassName(this.nextMonthLink, 'disabled');
			}
		}
	},
	closeClickHandler: function(evt) { this.closeHandler(); Event.stop(evt); },
	mouseOutHandler: function(isDate, evt) {
		this.setStatus("Select date", true);

		if (this.isIE) { // IE can't handle hover classes properly - even IE7 messes it up
			var element = isDate ? Event.findElement(evt, 'tr') : Event.element(evt);
			Element.removeClassName(element, "active");
		}
	},
	mouseOverHandler: function(date, message, evt) {
		if (typeof date != 'undefined' && date != null) {
			message = date.toUTCDateString();
			element = Event.findElement(evt, 'tr');
		} else {
			element = Event.element(evt);
			if (Element.hasClassName(element, 'disabled'))
				return;
		}
		this.setStatus(message, false);

		if (this.isIE) { // IE can't handle hover classes properly - even IE7 messes it up
			Element.addClassName(element, "active");
		}
	},
	statusTimeout: false,
	setStatus: function(msg, delayed) {
		if (this.statusTimeout) {
			window.clearTimeout(this.statusTimeout);
		}

		this.statusTimeout = window.setTimeout(function() {
			this.statusLine.replaceChild(document.createTextNode(msg), this.statusLine.firstChild);
			this.statusTimeout = false;
		}.bind(this), delayed ? 250 : 0);
	},
  showAt:  function(el) {
		var offset = Position.cumulativeOffset($(el));

		this.container.style.position = 'absolute';
		this.container.style.left = (offset[0] - el.offsetWidth) + "px";
		this.container.style.top = offset[1] + "px";
		this.container.style.display = 'block';
		document.getElementsByTagName('body').item(0).appendChild(this.container);
	},
	hide: function() { this.container.style.display='none';
	                   this.showDropDowns();
	                   DateSelection.update();
	},
	hideDropDowns: function() {
		$('monthfrom').style.visibility = 'hidden';
		$('dayfrom').style.visibility = 'hidden';
		$('monthto').style.visibility = 'hidden';
		$('dayto').style.visibility = 'hidden';
		$('numRooms').style.visibility = 'hidden';
		$('numNights').style.visibility = 'hidden';
		$('Guests').style.visibility = 'hidden';
	},
	showDropDowns: function() {
		$('monthfrom').style.visibility = 'visible';
		$('dayfrom').style.visibility = 'visible';
		$('monthto').style.visibility = 'visible';
		$('dayto').style.visibility = 'visible';
		$('numRooms').style.visibility = 'visible';
		$('numNights').style.visibility = 'visible';
		$('Guests').style.visibility = 'visible';
	}
};
