function ncCalendar(pDateName, pDefaultDate){
	var c = this;
	c.Name = pDateName;
	c.DefaultDate = pDefaultDate;
	c.ShowTime = 3;
	c.CellWidth = 20;
	c.CellHeight = 16;
	c.ImgBase = '../v/images/';
	c.CalImg = 'ncCalendar.gif';
	c.PrevImg = 'ncCalendarPrevious.gif';
	c.NextImg = 'ncCalendarNext.gif';
	c.ZIndex = 100;
	
	c.ObjName = c.Name + '_Object';
	
	c.WeekDays = new Array('S','M','T','W','T','F','S');
	c.MonthDays = new Array(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
	c.MonthNames = new Array('January','February','March','April','May','June','July','August','September','October','November','December');

	// return various html elements
	c.GetDayList = new Function('return document.getElementById("' + c.Name + '_Day_ID")');
	c.GetMonthList = new Function('return document.getElementById("' + c.Name + '_Month_ID")');
	c.GetYearField = new Function('return document.getElementById("' + c.Name + '_Year_ID")');
	c.GetHourField = new Function('return document.getElementById("' + c.Name + '_Hour_ID")');
	c.GetMinuteField = new Function('return document.getElementById("' + c.Name + '_Minute_ID")');
	c.GetCalIcon = new Function('return document.getElementById("' + c.Name + '_CalIcon")');
	c.GetCal = new Function('return document.getElementById("' + c.Name + '_ID")');
	c.GetCurrentDisplay = new Function('return document.getElementById("' + c.Name + '_Current_ID")');
	c.GetDayTable = new Function('return document.getElementById("' + c.Name + '_DayTable_ID")');
	c.IsVisible = new Function('return !(this.GetCal().style.visibility != \'visible\')');
	
	c.__Disabled = false;
	c.DisabledCls = c.EnabledCls = '';
}
ncCalendar.prototype.SetDisabled = function(p_bDisabled){
	var c = this;
	c.__Disabled = p_bDisabled;
	
	c.__SetFieldDisabled(c.GetYearField());
	c.__SetFieldDisabled(c.GetMonthList());
	c.__SetFieldDisabled(c.GetDayList());
	if(c.GetHourField()){
		c.__SetFieldDisabled(c.GetHourField());
		c.__SetFieldDisabled(c.GetMinuteField());
	}
	ncBwHlpr.SetOpacity(c.GetCalIcon(), p_bDisabled ? 30 : 100);
	c.Hide(); 
};
ncCalendar.prototype.__SetFieldDisabled = function(p_oInput){
	p_oInput.disabled = this.__Disabled;
	p_oInput.className = this.__Disabled ? this.DisabledCls : this.EnabledCls;
};
// creates initial html for calendar object
ncCalendar.prototype.Create = function(){
	var c = this;
	var temp = '<img id="' + c.Name + '_CalIcon" src="' + c.ImgBase + c.CalImg + '" style="margin-left: 2px; vertical-align: text-bottom; border: none; cursor: pointer; width: 16px; height: 15px;" title="Calendar" onclick="' + c.Name + '_Object.Show()">';
	/*
	temp += '<div class="ncCalendar" id="' + c.Name + '_ID" style="width:' + (c.CellWidth * 7) + 'px; z-index:' + c.ZIndex + '" onmouseover="' + c.Name + '_Object.CancelTimer();" onmouseout="' + c.Name + '_Object.ResetTimer();">';
	temp += '<table class="ncCalendarHeaderTable"><tr>';
	temp += '<td id="' + c.Name + '_Previous_ID" class="calendarDateInput" style="height:' + c.CellHeight + 'px" onclick="' + c.Name + '_Object.IncrementMonth(-1);">' +
			'<img src="' + c.ImgBase + c.PrevImg + '"></td>';
	temp += '<td id="' + c.Name + '_Current_ID" style="height:' + c.CellHeight + 'px" colspan="5"></td>';
	temp += '<td id="' + c.Name + '_Next_ID" style="height:' + c.CellHeight + 'px" onclick="' + c.Name + '_Object.IncrementMonth(1);">' + 
			'<img src="' + c.ImgBase + c.NextImg + '"></td></tr><tr>';		
	for (var w=0;w<7;w++) temp += '<td class="ncDayTitleCell" style="height:' + c.CellHeight + 'px;width:' + c.CellWidth + 'px;">' + c.WeekDays[w] + '</td>';
	temp += '</tr></table><div id="' + c.Name + '_DayTable_ID"></div></div>';
	*/
	return temp;
};
ncCalendar.prototype.Build = function(){
	var c = this, d = document;
	var calId = c.Name + "_ID";
	var cal = d.getElementById(calId);
	if(!cal){
		// create calendar for the first time
		cal = d.body.appendChild(d.createElement("div"));
		with(cal){
			id = calId;
			className = "ncCalendar"; 
			style.width = (c.CellWidth * 7) + "px";
			style.zIndex = c.ZIndex;
			onmouseover = new Function(c.Name + "_Object.CancelTimer();");
			onmouseout = new Function(c.Name + "_Object.ResetTimer();");
			
			var temp = '<table class="ncCalendarHeaderTable"><tr>';
			temp += '<td id="' + c.Name + '_Previous_ID" class="calendarDateInput" style="height:' + c.CellHeight + 'px" onclick="' + c.Name + '_Object.IncrementMonth(-1);">' +
					'<img src="' + c.ImgBase + c.PrevImg + '"></td>';
			temp += '<td id="' + c.Name + '_Current_ID" style="height:' + c.CellHeight + 'px" colspan="5"></td>';
			temp += '<td id="' + c.Name + '_Next_ID" style="height:' + c.CellHeight + 'px" onclick="' + c.Name + '_Object.IncrementMonth(1);">' + 
					'<img src="' + c.ImgBase + c.NextImg + '"></td></tr><tr>';		
			for (var w=0;w<7;w++) temp += '<td class="ncDayTitleCell" style="height:' + c.CellHeight + 'px;width:' + c.CellWidth + 'px;">' + c.WeekDays[w] + '</td>';
			temp += '</tr></table><div id="' + c.Name + '_DayTable_ID"></div></div>';
			innerHTML = temp;
		}
	}
	return cal;
};
// attaches behaviour to various events of the form fields 
ncCalendar.prototype.WireUpFormElements = function(){
	var c = this;
	c.GetDayList().onchange = new Function(c.ObjName + ".UpdateDisplayWithFormFields()");
	c.GetMonthList().onchange = new Function(c.ObjName + ".CheckDayListOptions();" + c.ObjName + ".UpdateDisplayWithFormFields();");
	c.GetYearField().onkeyup = new Function("ncValidation.ForceInteger(this, false); "  +
				"if(this.value.length == 4){" + c.ObjName + ".CheckDayListOptions();" + c.ObjName + ".UpdateDisplayWithFormFields();};");
	c.GetYearField().onblur = new Function(c.ObjName + ".CheckDayListOptions();" + c.ObjName + ".UpdateDisplayWithFormFields();");
	var minFld = c.GetMinuteField();
	if(minFld){
		minFld.onkeyup = new Function("ncValidation.ForceInteger(this, false, 0, 59); " + c.ObjName + ".RegisterChange();");
		c.GetHourField().onkeyup = new Function("ncValidation.ForceInteger(this, false, 0, 23); " + c.ObjName + ".RegisterChange();");
	}
	c.CheckDayListOptions();
};
ncCalendar.prototype.RegisterChange = function(){
	if(this.ChangeHandler){
		this.ChangeHandler(this);
	}
};
// updates day ddl options to match those allowed according to month / year
ncCalendar.prototype.CheckDayListOptions = function(){
	var c = this;
	var dayList = c.GetDayList(); 
	var allowBlank = dayList.options[0].value == "-1";
	var optionCount = dayList.options.length;
	
	var ml = c.GetMonthList();
	var m = parseInt(ml[ml.selectedIndex].value, 10);
	if(m != -1){
		// adjust to match array indexes
		m--;
	}
	var y = parseInt(c.GetYearField().value, 10);
	if(isNaN(y)){y = -1};
	var requiredCount = c.GetDayCount(y, m);
	var dayOffset = 0;
	if(allowBlank){
		requiredCount++;
		dayOffset++;
	}
	var selectedIndex = dayList.selectedIndex;
	if(optionCount < requiredCount){
		for(var i = optionCount + 1; i <= requiredCount; i++){
			var newOption = new Option(i - dayOffset, i - dayOffset);
			try{
				dayList.add(newOption, null);
			}catch(e){
				dayList.add(newOption);
			}
		}
	}else if(optionCount > requiredCount){
		for(var i = optionCount - 1; i > requiredCount - 1; i--){
			dayList.remove(i);
		}
	}
	if(selectedIndex > dayList.length - 1){
		dayList.selectedIndex = dayList.length - 1;
	}
};
ncCalendar.prototype.ResetTimer = function(){
	var c = this;
	c.CancelTimer();
	c.Timer = setTimeout(c.ObjName + ".Hide();", 1000 * c.ShowTime);
};
ncCalendar.prototype.CancelTimer = function(){
	var c = this;
	if(c.Timer){clearTimeout(c.Timer);}
	c.Timer = null;
};
// returns date selected by lists
ncCalendar.prototype.GetSelectedDate = function(){
	var daySubstituted = false;
	var c = this;
	var dl = c.GetDayList();
	var d = parseInt(dl[dl.selectedIndex].value, 10);
	if(d == -1){
		d = c.DefaultDate.getDate();
		daySubstituted = true;
	}
	
	var ml = c.GetMonthList();
	var m = parseInt(ml[ml.selectedIndex].value, 10);
	if(m == -1){
		m = c.DefaultDate.getMonth();
	}else{
		// adjust to match zero based month structure
		m = m - 1;
	}
	
	var y = c.GetYearField().value;
	if(isNaN(y) || y == ""){y = c.DefaultDate.getFullYear();}
	var dt = new Date();
	dt.setFullYear(y);
	dt.setMonth(m);
	dt.setDate(d);
	if(c.GetHourField()){
		var lHour = Math.max(c.GetHourField().value, 0);
		dt.setHours(lHour);
		var lMin = Math.max(c.GetMinuteField().value, 0);
		dt.setMinutes(lMin);
	}
	return [dt, daySubstituted]; 	
};
ncCalendar.prototype.GetSelectedDateObj = function(){
	return this.GetSelectedDate()[0];
};
// ncGetDateFromIso() can be handy in conjunction with this
ncCalendar.prototype.SetSelectedDate = function(p_dDate){
	var c = this;
	var lMonth = p_dDate.getMonth() + 1;
	var monthList = c.GetMonthList();
	for(var i = 0;i < monthList.length; i++) {
		if(monthList.options[i].value - lMonth == 0){
			monthList.selectedIndex = i;
		}
	}
	var yearField = c.GetYearField();
	yearField.defaultValue = yearField.value = p_dDate.getFullYear();
	
	// check options before we attempt to set the day - may need to add it to the display first as month changed
	var lDate = p_dDate.getDate();
	c.CheckDayListOptions();
	var dayList = c.GetDayList();
	for(var j = 0; j < dayList.length; j++) {
		if(dayList.options[j].value - lDate == 0){
			dayList.selectedIndex = j;
		}
	}
	
	if(c.GetHourField()){
		c.GetHourField().value = (p_dDate.getHours() < 10 ? '0' : '') + p_dDate.getHours();
		c.GetMinuteField().value = (p_dDate.getMinutes() < 10 ? '0' : '') + p_dDate.getMinutes();
	}
};
ncCalendar.prototype.GetIsoDate = function(){
	var c = this;
	var oDate = c.GetSelectedDate()[0];
	var sTemp = oDate.getFullYear() + "-";
	sTemp += (oDate.getMonth() < 9 ? '0' : '') + (oDate.getMonth() + 1) + "-";
	sTemp += (oDate.getDate() < 10 ? '0' : '') + oDate.getDate();
	
	if(c.GetHourField()){
		sTemp += ' ' + (oDate.getHours() < 10 ? '0' : '') + oDate.getHours();
		sTemp += ':' + (oDate.getMinutes() < 10 ? '0' : '') + oDate.getMinutes();
	}
	
	return sTemp;
};
ncCalendar.prototype.BuildDayTable = function(){
	var c = this;
	var firstDay = new Date(c.DisplayYear, c.DisplayMonth, 1).getDay();
	var dayCount = c.GetDayCount(c.DisplayYear, c.DisplayMonth);
	var selDateObj = c.GetSelectedDate();
	var selectedDate = selDateObj[0];
	
	// if date substituted, do not show selected
	var selectedDay = selDateObj[1] ? -1 : selectedDate.getFullYear() == c.DisplayYear && selectedDate.getMonth() == c.DisplayMonth ? selectedDate.getDate() : -1;
	var rows = 5;
	if((dayCount == 31 && firstDay > 4) || (dayCount == 30 && firstDay == 6)) rows = 6;
	else if(dayCount == 28 && firstDay == 0) rows = 4;
	var HTML = '<table class="ncCalendarDayTable">';
	for (var j = 0; j < rows; j++) {
		HTML += '<tr>';
		for (var i = 1; i <= 7; i++) {
			var day = (j * 7) + (i - firstDay);
			if ((day >= 1) && (day <= dayCount)) {
				var className, hoverClassName;
				if(day == selectedDay) {
					className = hoverClassName = "ncDayCellSelected";
				}
				else {
					className = "";
					hoverClassName = "ncDayCellHover";
				}
				
				// TODO: highlight current day (not selected)
				//if ((this.displayed.yearValue == Today.getFullYear()) && (this.displayed.monthIndex == Today.getMonth()) && (day == Today.getDate())) TextStyle += 'border:1px solid darkred;padding:0px;';
				HTML += '<td style="height:' + c.CellHeight + 'px;width:' + c.CellWidth + 'px;" class="' + className + '" ' + 
						'onclick="' + c.ObjName + '.PickDay(' + day + ')" onmouseover="this.className=\'' + hoverClassName + '\';" ' + 
						'onmouseout="this.className = \'' + className + '\';">' + day + '</td>';
			}
			else HTML += '<td style="height:' + c.CellHeight + 'px;">&nbsp;</td>';
		}
		HTML += '</tr>';
	}
	return HTML += '</table>';
};
// returns number of days in month according to month / year (allows for leap years)
ncCalendar.prototype.GetDayCount = function(pYear, pMonth){
	if(pMonth == 1){
		if(pYear == -1){
			return 29;
		}else{
			return !(pYear % 4) && ( (pYear % 100) || !(pYear % 400) ) ? 29 : 28;
		}
	}else{
		if(pMonth != -1){
			return this.MonthDays[pMonth];
		}else{
			return 31;
		}
	}
};
// show / hide
ncCalendar.prototype.Show = function(){
	var c = this;
	if(!c.__Disabled){
		if(c.IsVisible()){
			// calendar icon clicked again
			c.Hide();
		}else{
			// display calendar in correct position
			var icon = c.GetCalIcon();
			var coords = c.GetCoords(icon);
			var cal = c.GetCal();
			cal.style.left = (coords.x + icon.offsetWidth) + "px";
			cal.style.top = coords.y + "px";
			
			// populate according to selected values
			c.UpdateDisplayWithFormFields();
			cal.style.visibility = 'visible';	
			if(document.all && !window.opera){
				var iFrName = c.Name + "_IFr";
				var ifr = document.getElementById(iFrName);
				if(ifr){
					ifr.style.visibility = "visible";
				}
			}
			
			// check page not overflowed
			var oFirstRow = c.GetDayTable().getElementsByTagName("tr")[0];
			var lRows = oFirstRow.parentNode.childNodes.length;
			var lMaxHeight = cal.offsetHeight + ((6 - lRows) * oFirstRow.offsetHeight);
			if(coords.y + lMaxHeight > ncBwHlpr.Body.innerHeight() + ncBwHlpr.Body.pageYOffset()){
				cal.style.top = (ncBwHlpr.Body.innerHeight() + ncBwHlpr.Body.pageYOffset() - lMaxHeight) + 'px';
			}
			if(coords.x + icon.offsetWidth + cal.offsetWidth > ncBwHlpr.Body.innerWidth()){
				cal.style.left = (coords.x - cal.offsetWidth) + 'px';
			}
			c.ResetTimer();
		}
	}
};
ncCalendar.prototype.Hide = function(){
	var c = this;
	c.GetCal().style.visibility = 'hidden';
	
	if(document.all && !window.opera){
		var iFrName = c.Name + "_IFr";
		var ifr = document.getElementById(iFrName);
		if(ifr){
			ifr.style.visibility = "hidden";
		}
	}
	
	c.CancelTimer();
};
ncCalendar.prototype.UpdateDisplayWithFormFields = function(){
	var c = this;
	var selectedDate = c.GetSelectedDate()[0];
	c.DisplayMonth = selectedDate.getMonth();
	c.DisplayYear = selectedDate.getFullYear();
	c.UpdateDisplay();
	c.ResetTimer();
	c.RegisterChange();
};
// updates month displayed in calendar according to .DisplayMonth / .DisplayYear properties
ncCalendar.prototype.UpdateDisplay = function(){
	var c = this;
	
	// update month year above the table
	var yearText = "000" + c.DisplayYear;
	yearText = yearText.substr(yearText.length - 4);
	c.GetCurrentDisplay().innerHTML = c.MonthNames[c.DisplayMonth] + " " + yearText;
	
	// update table
	c.GetDayTable().innerHTML = c.BuildDayTable();
	
	// if this is an ie browser, create iframe to obscure any select elements under the calendar
	if(document.all && !window.opera){
		var iFrName = c.Name + "_IFr";
		var cal = c.GetCal();
		var ifr = document.getElementById(iFrName);
		if(!ifr){
			ifr = document.body.appendChild(document.createElement("iframe"));
			ifr.id = iFrName;
			ifr.style.position = "absolute";
			ifr.style.filter = "progid:DXImageTransform.Microsoft.Alpha(style=0, opacity=0)";
			ifr.style.zIndex = c.ZIndex - 1;
		}
		ifr.style.left = cal.style.left;
		ifr.style.top = cal.style.top;
		ifr.style.width = cal.offsetWidth + "px";
		ifr.style.height = cal.offsetHeight + "px";
	}
};
ncCalendar.prototype.IncrementMonth = function(pOffset){
	var c = this;
	c.DisplayMonth = (c.DisplayMonth + pOffset + 12) % 12;
	if(pOffset == 1){
		if(c.DisplayMonth == 0){c.DisplayYear++;}
	}else{
		// pOffset == -1
		if(c.DisplayMonth == 11){c.DisplayYear--;}
	}
	c.UpdateDisplay();
};
ncCalendar.prototype.PickDay = function(pDay){
	var c = this;
	c.Hide();
	var monthList = c.GetMonthList();
	for(var i = 0;i < monthList.length; i++) {
		if(monthList.options[i].value - (c.DisplayMonth + 1) == 0){
			monthList.selectedIndex = i;
		}
	}

	var dayList = c.GetDayList();
	for(var j = 0; j < dayList.length; j++) {
		if(dayList.options[j].value - pDay == 0){
			dayList.selectedIndex = j;
		}
	}

	var yearText = "000" + c.DisplayYear;
	yearText = yearText.substr(yearText.length - 4);
	var yearField = c.GetYearField();
	yearField.defaultValue = yearField.value = yearText;
	c.CheckDayListOptions();
	c.RegisterChange();
};
ncCalendar.prototype.GetCoords = function(pEl){
	var x = pEl.offsetLeft, y = pEl.offsetTop;
	var el = pEl;	
	while(el.tagName.toLowerCase() != "body" && el.tagName.toLowerCase() != "html"){
		el = el.offsetParent;
		x += el.offsetLeft;
		y += el.offsetTop;
	}	
	return {x: x, y: y};
};
// adds calendar to a date field group already written to the page (e.g. server side) allowing
// non js browsers to still enter dates
// supply default date in suitable form for javascript date constructor
// supply buildnow=true and parentel if building fields client side
function ncDateInput(pDateName, pDefaultDateStr, pImgBase, p_bBuildNow, p_oParentEl){
	// check date object allows pre 1901 dates - safari currently does not
	var myDate = new Date();
	myDate.setFullYear(1000);
	if(myDate.getFullYear() == 1000){
		var calObj = window[pDateName + '_Object'] = new ncCalendar(pDateName, new Date(pDefaultDateStr));
		calObj.ImgBase = pImgBase;	
		if(!p_bBuildNow){
			document.write(calObj.Create());
		}else{
			p_oParentEl.innerHTML += calObj.Create();
		}
		
		calObj.WireUpFormElements();
		
		if(!p_bBuildNow){
			// add build to onload event
			var w = window;
			if(typeof(w.onload) == 'function'){
				var extraOnload = window.onload;
				w.onload = function(){calObj.Build(); extraOnload();};
			}else{
				w.onload = function(){calObj.Build();};
			}
		}else{
			calObj.Build();
		}
	}
}
// creates input fields and sets up calendar client side
function ncCreateDateInput(p_oParentEl, p_sDateName, p_bRequired, p_sCssClass, p_bShowTime, p_oSelectedDate, 
					p_oDefaultDate, p_sImgBase){
	var sTemp = '';
	
	var oSelectedDate = p_oSelectedDate ? p_oSelectedDate : p_bRequired ? p_oDefaultDate : null;
	var lYear, lMonth, lDay, lHour, lMin;
	if(oSelectedDate){
		lYear = oSelectedDate.getFullYear();
		lMonth = oSelectedDate.getMonth() + 1;
		lDay = oSelectedDate.getDate();
		lHour = "0" + oSelectedDate.getHours();
		lHour = lHour.substr(lHour.length - 2);
		lMin = "0" + oSelectedDate.getMinutes();
		lMin = lMin.substr(lMin.length - 2);
	}
	
	// ----- create hidden field to tell the update code the field exists in parts -----
	sTemp += '<input type="hidden" name="' + p_sDateName + '_Present" value="1" />';
	
	// ----- day input -----
    sTemp += '<select name="' + p_sDateName + '_Day_ID" id="' + p_sDateName + '_Day_ID" class="' + p_sCssClass + '">';
    if(!p_bRequired){
		sTemp += '<option value="-1">';
	}
    for(var i = 1; i < 32; i++){
		var sSelected = i == lDay ? ' selected="selected"' : '';
		sTemp += '<option value="' + i + '"' + sSelected + '>' + i + '</option>';    
	}
    sTemp += '</select>';
    
	// ----- month input -----
	sTemp += '<select name="' + p_sDateName + '_Month_ID" id="' + p_sDateName + '_Month_ID" class="' + p_sCssClass + '">';
    if(!p_bRequired){
		sTemp += '<option value="-1">';
	}
	var sMonthArr = ['January','February','March','April','May','June','July','August','September','October','November','December'];
    for(var i = 0; i < sMonthArr.length; i++){
		var sSelected = (i + 1) == lMonth ? ' selected="selected"' : '';
		sTemp += '<option value="' + (i + 1) + '"' + sSelected + '>' + sMonthArr[i] + '</option>';
	}
	sTemp += "</select>";
    
    // ----- year input -----
    sTemp += '<input class="' + p_sCssClass + '" type="text" name="' + p_sDateName + '_Year_ID" id="' + p_sDateName + '_Year_ID" ' + 
			'size="4" maxlength="4" value="' + lYear + '" />';
    // ----- time inputs -----
    if(p_bShowTime){
		sTemp += '&nbsp;<input class="' + p_sCssClass + '" type="text" name="' + p_sDateName + '_Hour_ID" id="' + p_sDateName + '_Hour_ID"' + 
				'size="2" maxlength="2" value="' + lHour + '" />:';
		sTemp += '<input class="' + p_sCssClass + '" type="text" name="' + p_sDateName + '_Minute_ID" id="' + p_sDateName + '_Minute_ID"' +
				'size=2" maxlength="2" value="' + lMin + '" />';
	}
	p_oParentEl.innerHTML = sTemp;
	ncDateInput(p_sDateName, p_oDefaultDate.toString(), p_sImgBase, true, p_oParentEl);
	return window[p_sDateName + '_Object'];
}
