/**
 * pad - Left-pads a number to a specified number of places. (Will maintain a negative)
 * @param	mixed		val		Number or String to pad
 * @param	integer		len		Number of places to pad the string to.(Default is 2. ie. 9 becomes 09)
 * @return	string				val padded to len places. (if val was negative then string is prepended with '-')
 */
function pad(val, len){
	var v = Number(val), vlen = Math.abs(v).toString().length, len = len || 2;
	if (isNaN(v) || v==0) {
		return new Array(len+1).join('0'); 
	}
	
	if ( (vlen) < len ) {
		return (val < 0?'-':'')+(new Array(len-vlen+1).join('0')+Math.abs(v));
	} else {
		return v.toString();
	}
}

/**
 * fixEvent - Returns an event object with common properties/methods normalized for easier cross browser usage.
 * @param e		object	optional;Event object to normalize
 * @return		object	Event object after normalization
 */
if(!fixEvent) {
function fixEvent(e){
	var evnt = e || window.event;
	if (!evnt.target) { 
		evnt.target = evnt.srcElement; 
	}
	evnt.preventDefault = (evnt.preventDefault)? evnt.preventDefault : function(){ this.returnValue = false; };
	evnt.stopPropagation = (evnt.stopPropagation)? evnt.stopPropagation : function(){ this.cancelBubble = true; }
	return evnt;
}
}

if(!Date.format){
	/**
	 * Date.format - Emulates PHP's date() function by accepting a string to format the date and time.
	 * 					Unrecognized characters will be returned inline. To print the literal version of
	 * 					a formatting character, the character can be escaped with double backslashes (ie. \\d)
	 * Note:
	 *	The following formatting characters are currently unsupported and will be replaced with an empty string:
	 * 		W - ISO-8601 week number of year, weeks starting on Monday
	 * 		o - ISO-8601 year number. This has the same value as Y, except that if the ISO week number (W) belongs to the previous or next year, that year is used instead.
	 * 		e - Timezone identifier
	 */
	Date.prototype.format = function(fs){
		fs = ((typeof fs=='string' || fs instanceof String)?fs:null) || null;
		if (!fs) {
			return ''; 
		}
		fs = fs.split("");
		var ret='';
		var cmn = {'date':this.getDate(),'day':this.getDay(),'month':this.getMonth()+1,'year':this.getFullYear(),
			'hour':this.getHours(),'minute':this.getMinutes(),'second':this.getSeconds(),'time':this.getTime(),
			'ordinals':{1:'st',2:'nd',3:'rd','else':'th'}, 'offset':pad(this.getTimezoneOffset()/60*-100,4),
			'daynames':['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
			'monthnames':['January','February','March','April','May','June','July','August','September','October','November','December']
		};
		cmn['dayone'] = new Date(cmn['year'],0,1); //january 1st of the year
		cmn['suffix'] = (cmn['hour'] < 12 ? 'am' : 'pm');
		var lastDigit = cmn['date'].toString().substr(-1);
		var skip = false;
		for (var i=0;i < fs.length;i++) {
			if (skip) {
				ret+=fs[i];
				skip=false; 
				continue;
			}
			switch (fs[i]) {
				case 'd': ret+=pad(cmn['date']); break; 
				case 'D': ret+=cmn['daynames'][cmn['day']].substr(0,3); break;
 				case 'j': ret+=cmn['date']; break;
				case 'l': ret+=cmn['daynames'][cmn['day']]; break;
				case 'N': ret+=cmn['day']+1; break;
				case 'S': ret+=(cmn['ordinals'][((lastDigit > 3 || lastDigit==0) ? 'else':lastDigit)]); break;
				case 'w': ret+=cmn['day']; break;												//86400000 (864e5) = milliseconds per day
				case 'z': ret+=Math.floor((cmn['time'] - cmn['dayone'].getTime()) / 864e5); break;
				case 'W': ret+=''; break; //todo
				case 'F': ret+=cmn['monthnames'][cmn['month']-1]; break;
				case 'm': ret+=pad(cmn['month']); break;
				case 'M': ret+=cmn['monthnames'][cmn['month']].substr(0,3); break;
				case 'n': ret+=cmn['month']; break;
				case 't': ret+=new Date(cmn['year'],cmn['month'],0).getDate(); break;
				case 'L': ret+=(new Date(cmn['year'],2,0).getDate()==29 ? 1 : 0); break;
				case 'o': ret+=''; break; //todo
				case 'Y': ret+=cmn['year']; break;
				case 'y': ret+=cmn['year'].toString().substr(-2); break;
				case 'a': ret+=cmn['suffix']; break;
				case 'A': ret+=cmn['suffix'].toUpperCase(); break;
				case 'g': ret+=(cmn['hour']==0 ? 12 : (cmn['hour'] > 12 ? cmn['hour']-12: cmn['hour'])); break;
				case 'G': ret+=cmn['hour']; break;
				case 'h': ret+=pad(cmn['hour']==0 ? 12 : (cmn['hour'] > 12 ? cmn['hour']-12: cmn['hour'])); break;
				case 'H': ret+=pad(cmn['hour']); break;
				case 'i': ret+=pad(cmn['minute']); break;
				case 's': ret+=pad(cmn['second']); break;
				case 'u': ret+=this.getMilliseconds()*1000; break;
				case 'e': ret+=''; break; //todo
				case 'I': ret+=((this.getTimezoneOffset() - cmn['dayone'].getTimezoneOffset()) ? 1 : 0); break;
				case 'O': ret+=cmn['offset']; break;
				case 'P': ret+=cmn['offset'].replace(/^([+-]\d{2})(\d{2})/i,'$1:$2'); break;
				case 'T': ret+=this.toTimeString().replace(/.*\((\w+)\)$/,'$1'); break;
				case 'Z': ret+=(this.getTimezoneOffset()*60); break;
				case 'c': ret+=this.format('Y-m-d')+'T'+this.format('H:i:s'+(this.getTimezoneOffset() < 60 ? '\\Z':'P')); break;
				case 'r': ret+=this.format('D, d M Y H:i:s O'); break;
				case 'U': ret+=cmn['time']/1000; break;
				case '\\': skip=true; break;
				default:
					ret+=fs[i]; break;
			}
		}
		return ret;
	};
}
/**
 * CalendarManager ( Static Class )
 * Properties and methods declared statically
 */
function CalendarManager(){}
/* Array of Calendar objects */
CalendarManager.calendars = [];
/* Reference table so a calendar can be referred to by target element id */
CalendarManager.calRef = {};
/**
 * Returns a member variable (or a calander instance if value is a valid target element id)
 * @param mvar mixed	Name of member variable or (string)ID of calendar target element or (int)Index of calendar object in array. 
 * @return 				The Calendar object instance (or null if not found)
 */
CalendarManager.get = function(mvar){
	if (typeof mvar =='string' || mvar instanceof String) {
		if (mvar in this.calRef) { 
			return this.calendars[this.calRef[mvar]];
		} else if (mvar in this) { 
			return this[mvar]; 
		}
	} else if (typeof mvar=='number' || mvar instanceof Number) {
		if (this.calendars.indexOf(mvar) != -1) {
			return this.calendars[mvar]; 
		}
	}
	return null;
};
/**
 * Sets an existing Calendar object into the calendars member array.
 * @param target string	ID of the element that will hold the datetime string
 * @param cal Calendar		Calendar object to set into the array.
 * @return					The static CalenderManager object
 */
CalendarManager.set = function(target,cal){
	if ((target in this.calRef)||(!cal)||(cal.constructor != Calendar)) {
		return null; 
	}
	this.calRef[target] = this.calendars.length;
	this.calendars.push(cal);
	return this;
}
/**
 * Returns either an existing calendar for the target if one exists, or a new calendar for the target.
 * @param target string	ID of element that will hold datetime string
 * @return					Either the pre-existing Calendar object or the new Calendar object.
 */
CalendarManager.add = function(target){
	var cal = this.get(target);
	if (cal) { 
		return cal; 
	}
	this.calRef[target]=this.calendars.length;
	this.calendars.push(new Calendar(target));
	return this.calendars[this.calendars.length-1];
};
/**
 * Removes a Calendar object from the array and returns it.
 * @param target string	ID of the element for the calendar to remove
 * @return					The Calendar object removed from the manager.
 */
CalendarManager.remove = function(target){
	if (!target || !(target in this.calRef)){
		return null; 
	}
	return this.calendars.splice(this.calRef[target],1);
};

/**
 * Calendar ( Class )
 * @param target	mixed	Element object or string ID of the element that will hold the datetime string.
 * @param button	mixed	Element object or string ID of the element that will popup the calendar.
 * @param options	object	Key/value pairs in an object specifying options
 *
 * Member Variables (publicly accessible, but use 'set' method)
 * target			element		Element that will receive date/time
 * button			element		Element that will activate the popup calendar
 * timeRequired		boolean		True - popup will show Time selects, False - popup will not show Time selects
 * yearScroll		boolean		Allow the years to be scrolled through in the popup
 * autoUpdate		boolean		When using the 'set' method and setting 'date', 'time', or 'datetime' the target element's value will be changed automatically
 * initialized		boolean		(Read-only please)Specifies whether calendar has been attempted to read the target element's value and parse it into a date
 * calPath			string		Path to the popup calendar file.
 * targetFormat		string		Format string to target element to receive on update. Format characters match PHP's date() function. See: Date.prototype.format
 * onUpdate			function	Function to be called when date is picked in popup. Overrides default behaviour of assigning value into target element
 * datetime			Date		Object that holds the chosen date/time
 * popWin			object		(Read-only please)Holds reference to the popup window
 */
function Calendar(target, button, options){
	 /* if we have a string treat it as an id and get the element */
	this.set('target',target);
	this.set('button',button);
	if(!options['calFrame']) {
		this.set('calFrame','calendar');
	}
	
	//Settings
	this.timeRequired = false;
	this.yearScroll = true;
	this.autoUpdate = false;
	this.initialized = false;
	this.calPath = 'minical.php';
	this.targetFormat = 'm/d/Y'+(this.timeRequired ? 'h:i:s':'');
	this.onUpdate = null;
	
	for(var key in options){
		this.set(key,options[key]);
	}
	
	//Data members
	this.datetime = new Date();
	this.popWin = null;
	
	var self = this;
	function popCal(e) {
		fixEvent(e).preventDefault();
		self.popup(e.clientX,e.clientY);
	}
	
	if (!button || !target) {
		return null;
	}

	if (CalendarManager && CalendarManager.get(target) == null) {
		CalendarManager.set(this.target.id, this);
	}
}
/**
 * Prototype (applies to all instances - not available statically)
 */

/**
 * set - sets member variables
 * @param string mvar	The name of the member variable (or 'date', or 'time')
 * @param mixed value	The value to assign to the member variable
 * @return				The instance of the Calendar object (or null on error)
 */
Calendar.prototype.set = function (mvar, value) {
	switch(mvar){
		case 'timeRequired': 
		case 'yearScroll': 
		case 'autoUpdate': 
			this[mvar] = (value ? true : false); /* cast to boolean */
			break;
		case 'datetime':
			if (typeof value =='string' || value instanceof String) {  /* if we have a string, parse it first */
				value = this.parse(value); 
			}
			/* test for getTime because for some reason instanceof and constructor don't work right when coming from a popup window */
			if (!value.getTime) {
				this.showError('member','set',mvar,value);
				return null; 
			}
			var tmp = new Date(value.getTime()); /* make a new object so we aren't using a reference */
			this[mvar] = tmp;
			this.initialized = true;
			if (this.autoUpdate) { 
				this.updateTarget(); 
			}
			break;
		case 'target': /* if we have a string treat it as an id and get the element */
		case 'button':
		case 'calFrame':
			var tmp = (value != null) ? ((typeof value=='object') ? value : did(value)) : null;
			if (tmp==null) {
				Calendar.showError(mvar, value); 
				return null; 
			}
			this[mvar] = tmp;
			break;
		case 'date':
			if (typeof value =='string'|| value instanceof String) {
				value = this.parseDate(value); 
			} /* if we have a string, parse it first */
			/* copy the values (so we don't overwrite any time settings) */
			this.datetime.setFullYear(value.getFullYear(),value.getMonth(),value.getDate());
			if (this.autoUpdate && this.initialized){ 
				this.updateTarget(); 
			}
			break;
		case 'time':
			if (typeof value =='string'|| value instanceof String) {
				value = this.parseTime(value); 
			} /* if we have a string, parse it first */
			/* copy the values (so we don't overwrite any date settings) */
			this.datetime.setHours(value.getHours(),value.getMinutes(),value.getSeconds(),value.getMilliseconds());
			if (this.autoUpdate && this.initialized) {
				this.updateTarget(); 
			}
			break;
		case 'displayErrors':
			Calendar.displayErrors = (value ? true : false);
			break;
		case 'onUpdate':
			if (typeof(value) == 'function') {
				this[mvar] = value; 
			}
			break;
		case 'calPath':
		case 'targetFormat':
			this[mvar] = value; break;
		default:
			if (!(mvar in this)) { 
				this.showError('member','set',mvar,value);
				return null; 
			}
			break;
	}
	return this;
}

/**
 * get - gets the value of a member variable
 * @param string mvar	The name of the member variable who's value to retrieve (or 'date', or 'time', or 'dateandtime')
 * @return				The value of the member variable (or null on error)
 */
Calendar.prototype.get = function(mvar) {
	if(this.initialized){
		switch(mvar){ /* Get full date/time in this format: mm-dd-yyyy dd:dd (AM or PM) */
			case 'dateandtime': return this.datetime.format('m-d-Y h:i A');break;
			case 'date': /* return date in this format: mm-dd-yyyy */
				return this.datetime.format('m-d-Y');
				break;
			case 'time': /* return time in this format: dd:dd (AM or PM) */
				return this.datetime.format('h:i A');
				break;
		}
	}
	if (!(mvar in this)) { 
		return null; /* for everything else make sure it's a real member var */ 
	}
	return this[mvar];
}

/**
 * popup - Opens the pop-up with the mini calendar in it.
 * @param integer x		optional;Distance from left side of screen to position popup
 * @param integer y		optional;Distnace from top side of screen to position popup
 * @return	The instance of the Calendar object
 */
Calendar.prototype.popup = function(x,y) {
	if (!this.initialized) {
		this.datetime = this.parse(); 
		this.initialized=true; 
	}
	var argstr='',args = {'datetime':this.get('datetime').getTime(),'id':this.target.id,'yscroll':this.get('yearScroll'),'timereq':this.get('timeRequired')};
	for (var key in args) {
		argstr+= key+'='+encodeURIComponent(args[key])+'&';
	}
	argstr = argstr.replace(/&$/,''); /* remove trailing & */
	x = x || 200; y = y || 200;
	this.popWin = window.open(this.calPath+'?'+argstr,'Calendar','width=210,height='+(this.timeRequired ? 228 : 200)+',status=no,resizable=no,top='+y+',left='+x+',dependent=yes,alwaysRaised=yes');
	this.popWin.opener = window;
	this.popWin.focus();
	return this;
}

/**
 * popin - Opens the mini calendar in a pop-in
 * @param integer x		Left position of calendar
 * @param integer y		Top position of calendar
 * @return	The instance of the Calendar object
 */
Calendar.prototype.popin = function(x,y) {
	if (!this.initialized) {
		this.datetime = this.parse();
		this.initialized=true; 
	}
	var argstr='',args = {'datetime':this.get('datetime').getTime(),'id':this.target.id,'yscroll':this.get('yearScroll'),'timereq':this.get('timeRequired')};
	for (var key in args) {
		argstr+= key+'='+encodeURIComponent(args[key])+'&';
	}
	argstr = argstr.replace(/&$/,''); /* remove trailing & */
	this.calFrame.src = this.calPath+'?'+argstr;
	this.calFrame.style.left = x+'px';
	this.calFrame.style.top = y+'px';
	this.calFrame.style.display = '';
	return this;
}

/**
 * popout - Closes the mini calendar pop-in
 * @return	The instance of the Calendar object
 */
Calendar.prototype.popout = function() {
	this.calFrame.style.display = 'none';
	return this;
}

/**
 * updateTarget - Updates the targeted element with the current date (and if required time)
 * @return	The instance of the Calendar object (or null if not initialized)
 */
Calendar.prototype.updateTarget = function() {
	if (!this.initialized) {
		return null; 
	}
	if (typeof(this.onUpdate) == 'function') {
		this.onUpdate.call(this,this.target,this.datetime);
	} else {
		this.target.value = this.get('datetime').format(this.targetFormat);
	}
	return this;
}

/* Fix the constructor property for all instances */
Calendar.prototype.constructor = Calendar;


/* A means to call the static methods from a Calendar object */
Calendar.prototype.parse = function(datetimestr) {
	return Calendar.parse.call(this,datetimestr || this.target.value);
}

Calendar.prototype.parseDate = function(datestr) {
	return Calendar.parseDate.call(this,datestr || this.target.value); 
}

Calendar.prototype.parseTime = function(timestr) {
	return Calendar.parseTime.call(this,timestr || this.target.value); 
}

Calendar.prototype.formatTime = function(timestr) {
	return Calendar.formatTime.call(this,timestr || this.target.value); 
}

Calendar.prototype.showError = function(type,source,data) {
	return Calendar.showError.apply(this,arguments); 
}

/**
 * parse - Attempts to get a valid time an date from a string
 * @param string datetimestr	The string to parse
 * @return 						Date object with corresponding date and time values
 */
Calendar.parse = function(datetimestr){
	var value = datetimestr || '';
	if(/^\s*$/i.test(value)){ return new Date(); } /* test for empty string */ 
	else if(/^\d+$/.test(value)){ return new Date(value); } /* if it's all digits treat it as milliseconds since unix epoch */

	var d=null,t=null, tmp = Calendar.displayErrors; /* store old setting */
	Calendar.displayErrors=false; /* disable error messages so we can accept one or the other */
	var d = Calendar.parseDate(value); /* parse the date portion and receive Date object */
	var t = Calendar.parseTime(value); /* parse the time portion and receive Date object */
	if(d==null && t==null){ return null; } /* if we got errors on both then return */
	if(t && d){
		/* Copy time values into our 'd' Date object */
		d.setHours(t.getHours(),t.getMinutes(),t.getSeconds(),t.getMilliseconds());
	}
	Calendar.displayErrors=tmp; /* restore error message setting */
	return d || t; /* return date if we have it (will have adjusted time if time was valid), otherwise return time */
};
/**
 * parseDate - Attempts to get a valid date from a string
 * @param string datestr	The string to parse
 * @return					A Date object with the corresponding date (or null on error)
 */
Calendar.parseDate = function(datestr) {
	if (!datestr) { 
		Calendar.showError('param','parseDate',datestr);
		return null; 
	}
	var parts = null;
	parts = /^([1-9]|0[1-9]|1[0-2])[\/-]([1-9]|0[1-9]|[1-2][0-9]|3[0-1])[\/-](\d{4})/.exec(datestr);
	if (!parts) {
		parts = /^(\d{4})[\/-]([1-9]|0[1-9]|1[0-2])[\/-]([1-9]|0[1-9]|[1-2][0-9]|3[0-1])/.exec(datestr);
		parts.shift();
	} else if(parts){
		parts.shift(); /* shift the full match off since we don't need it */
		parts.unshift(parts.pop()); /* stick the year value onto the beginning */
	}
	if(parts){
		return new Date(Date.parse(parts.join('/')));
	}else{
		Calendar.showError('date_format','parseDate',datestr); return null;
	}
};
/**
 * parseTime - Attempts to get a valid time from a string
 * @param string timestr	The string to parse
 * @return					A Date object with the corresponding time (or null on error)
 */
Calendar.parseTime = function(timestr){
	if (!timestr) {
		Calendar.showError('param','parseTime',timestr);
		return null; 
	}
	var t = new Date(), parts=null;
	/* match range of 00:00:00 - 23:59:59 (Input format allows single digit hours, optional minutes and seconds, and use of AM/PM)  */
	if ((parts = /^([0-9]|0[0-9]|[1-2][0-4])(:[0-5][0-9])?(:[0-5][0-9])?\s?([AP]M)?$/i.exec(timestr)) != null) {
		parts.shift(); /* shift the full match off since we don't need it */
		parts = Calendar.formatTime(timestr).split(':'); /* See formatTime static method */
		t.setHours.apply(t,parts);
		return t;
	} else {
		Calendar.showError('time_format','parseTime',timestr); return null;
	}
};
/**
 * formatTime - Transforms a time string with optional minutes/seconds and AM/PM into a 24hour time in the format (hh:mm:ss)
 * @param string timestr	The string to parse and transform
 * @return					A time string in standard 24 hour format with minutes and seconds (hh:mm:ss)
 */
Calendar.formatTime = function(timestr){
	var timeCheck = /(\d{1,2})(:\d{1,2})?(:\d{1,2})?\s?([AP]M)?$/i, parts=null, suffx='';
	if ((parts= timeCheck.exec(timestr)) != null) {		/* correct the hour based on AM/PM. If PM 1-11 become 13-23 and 12 stays 12. If AM 1-11 stay the same, 12 becomes 00. */
		parts.shift();
		suffix = parts.pop();
		suffix = (suffix!=null)? suffix.toUpperCase() : (parts[0] == 12 ? 'PM':'AM'); /* Assume Noon or Morning if not specified */
		parts[0] = pad(suffix=='PM' ? (parts[0] < 12 ? Number(parts[0])+12 : parts[0]) : (parts[0] < 12 ? parts[0] : '00'));
		for (var i=1;i < 3;i++) {
			parts[i] = pad((parts[i]!=null) ? parts[i].replace(/[:\s]/,'') : '00'); 
		}
		return parts.join(':');
	}
	return '00:00:00';
};

/* Setting causing alerts to be shown or hidden */
Calendar.displayErrors = true;
/* Holds data for the last error trigged */
Calendar.lastError = {'type':'','source':null,'data':null,'message':''};
/**
 * showError - Displays an alert with a message based on the error type
 * @param string type	The type of the error (corresponding to a value in err object)
 * @param string source	The source of the error (ie. function name where error was triggered)
 * @param mixed data	The data that caused the error
 * @return				null
 */
Calendar.showError = function(type,source,data){
	type = type || 'param';
	source = source || 'showError';
	data = (data === undefined) ? 'undefined': (data===null ? 'null' : data);
	var err = {
		'error':'Invalid error type {data} sent to {source}',
		'member':'You may not set the {data} member variable to {1}',
		'target':'Invalid target element: {data}.',
		'button':'Invalid button element: {data}.',
		'param':"Missing or invalid paramter\nSent: {data} to {source}.",
		'date_format':"Invalid date format: {data} supplied to {source}.\nAccepted formats are mm-dd-yyyy and mm/dd/yyyy.",
		'date_part':"Invalid date: {data} supplied to {source}.\n{1} cannot be found or is invalid.",
		'time_format':"Invalid time format: {data} supplied to {source}.",
		'time_part':"Invalid time: {data} supplied to {source}.\n{1} cannot be found or is invalid."
	};
	if (!(type in err)) {
		Calendar.showError('error','showError',type); 
		return null; 
	}
	var msg = err[type].replace(/\{data\}/ig,data).replace(/\{source\}/ig,source);
	for (var i=2;i < arguments.length;i++) {
		msg = msg.replace('{'+(i-2)+'}',arguments[i]);
	}
	Calendar.lastError = {'type':type,'source':source,'data':data,'message':msg};
	if (Calendar.displayErrors) {
		alert(msg); 
	}
	return null;
};