if((typeof Prototype=='undefined') || 
   (typeof Element == 'undefined') || 
   (typeof Element.Methods=='undefined') ) 
{
   throw("Ajax.JsonRpc requires including the Prototype JavaScript Framework");
}

if(typeof Builder == 'undefined')
  throw("jsonrpc.js requires including script.aculo.us' builder.js library");


/**
 * Some classes for making calendar widgets. 
 * By Jay. 
 * Requires: prototype.js.
 * Example Usage: new Calendar.Simple(); //somewhere in the body or after body has loaded
 * 
 * Detailed Usage: new Calendar.Simple('calendarId', new Date(2004,12,25), 'parentElementId', {})
 **/
Calendar = { };


Calendar.monthNames = ['january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december'];
Calendar.dayNames = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; //for the index returned by new Date().getDay() which starts with sunday.

Calendar.getDaysInMonth = function(d){
	return 32 - new Date(d.getFullYear(), d.getMonth(), 32).getDate();
};

Calendar.toCfDate = function(d) {
	return d.getDate() 
		+ "-" 
		+ Calendar.monthNames[d.getMonth() - 1].substring(0,3) 
		+ "-" 
		+ d.getFullYear() 
		+ " " 
		+ d.getHours() 
		+ ":" 
		+ d.getMinutes() 
		+ ":" 
		+ d.getSeconds();
}


Calendar.counter = 0; //how many cals created on this page

Calendar.Base = Class.create({
	baseInitialize: function(id,  date, parent, options){
		this.parent = $(parent) || document.body;
		Calendar.counter++;
		this.id = id || 'calendar' + Calendar.counter;
		this.date = date || new Date();
		
		//setups options and default options:
		this.options = {
			view: 'month' // month | day | week | year
		};
		
		Object.extend(this.options, options || { }); //gem
		
		
		//draw
		this.markup();
		this.fill();
		this.setSelectedCell(this.getIndexFromDate(this.date));
		
		
	},
	
	fdom: function(d){
		return new Date(d.getFullYear(), d.getMonth(), 1);
	},
	
	
	onPrev: function(e){
		//if (this.options.onPrev) this.options.onPrev(this);
		if (this.options.view == 'month'){
			this.prevMonth();
		} else if (this.options.view == 'day'){
			this.prevDay();
		}
		e.cal = this;
		(this.options.onPrev || Prototype.emptyFunction)(e);
	},
	onNext: function(e){
		//if (this.options.onNext) this.options.onNext(this);
		if (this.options.view == 'month'){
			this.nextMonth();
		} else if (this.options.view == 'day'){
			this.nextDay();
		}
		e.cal = this;
		(this.options.onNext || Prototype.emptyFunction)(e);
	},
	
	onClickCell: function(e){
		var dateClicked = this.getDateFromIndex(e.target.id.split(/cell/).pop());
		this.setSelectedCell(this.getIndexFromDate(dateClicked));
		this.setDate(dateClicked);
		e.cal = this;
		e.date = dateClicked;
		
		(this.options.onClickCell || Prototype.emptyFunction)(e);
	},
	
	onMouseDownCell: function(e){
		e.cal = this;
		(this.options.onMouseDownCell || Prototype.emptyFunction)(e);
	},
	
	onMouseUpCell: function(e){
		e.cal = this;
		(this.options.onMouseUpCell || Prototype.emptyFunction)(e);
	},
	
	onMouseOverCell: function(e){
		e.cal = this;
		if (e.target.className == 'ThisMonth'
			|| e.target.className == 'PrevMonth'
			|| e.target.className == 'NextMonth'
		){
			var i = e.target.id.split(/_cell/)[1];
			this.highlightCell(this.getCell(i));
		}
		(this.options.onMouseOverCell || Prototype.emptyFunction)(e);
	},
	onMouseOutCell: function(e){
		e.cal = this;
		var i = e.target.id.split(/_cell/)[1];
		this.unHighlightCell(this.getCell(i));
		(this.options.onMouseOutCell || Prototype.emptyFunction)(e);
	},
	
	highlightCell: function(cell){
		var cell = $(cell);
		if (cell){
			cell.savedBg = cell.style.background;
			classes = cell.className.split(/\s+/);
			cell.className = classes.without('Highlighted').join(' ') + ' Highlighted';
		}
	},
	
	unHighlightCell: function(cell){
		var cell = $(cell);
		if (cell){
			//cell.style.background = cell.savedBg;
			var classes = cell.className.split(/\s+/);
			cell.className = classes.without('Highlighted').join(' ');
		}
	},
	selectCell: function(index){
		var cell = this.getCell(index);
		if (cell){
			//cell.style.background = cell.savedBg;
			var classes = cell.className.split(/\s+/);
			cell.className = classes.without('Selected').join(' ') + ' Selected';
		}
	},
	unselectCell: function(index){
		var cell = this.getCell(index);
		if (cell){
			//cell.style.background = cell.savedBg;
			var classes = cell.className.split(/\s+/);
			cell.className = classes.without('Selected').join(' ');
		}
	},
	setSelectedCell: function (index){
		this.unselectCell(this.getIndexFromDate(this.date));
		this.selectCell(index);
	},
	
	//markup the thing just about the month-calendar sheet
	//by default, we put in a "prevMonth" button, date-label, and "nextMonth" button
	//lib user can override by providing this.options.markupControls function
	//container is a div. and is also: $(this.id);
	markupControlsMonth: function(container){
		if (this.options.markupControls){
			this.options.markupControls(container);
		} else {
			var controls = Builder.node("table", { className: 'Controls'});
			var tb = Builder.node('tbody');
			controls.appendChild(tb);
			var tr = Builder.node('tr');
			tb.appendChild(tr);
			var l = Builder.node('a', {href: 'javascript:void(0);', className: 'PrevButton'});
			//l.innerHTML = '&larr;';
			l.innerHTML = '&#9668;';
			Event.observe(l, 'click', this.onPrev.bindAsEventListener(this));
			var r = Builder.node('a', {href: 'javascript:void(0);',  className: 'NextButton'});
			Event.observe(r, 'click', this.onNext.bindAsEventListener(this));
			
			//r.innerHTML = '&rarr;';
			r.innerHTML = '&#9658;';
			var left = Builder.node( 
				'td',
				{ id: this.id + '_left', className: 'Left' },
				l
			);
			var mid = Builder.node( 
				'td',
				{ id: this.id + '_dateLabel', className: 'Mid' },
				'nothing'
			);
			var right= Builder.node( 
				'td',
				{ id: this.id + '_right', className: 'Right' },
				r
			);
			
			tr.appendChild(left);
			tr.appendChild(mid);
			tr.appendChild(right);
			
			this.container.appendChild(controls);
		}
	},
	markup: function(){
		(this['markup' + this.options.view] || this['markupmonth']).bind(this)();
	},
	markupday: function(){
		this.container = Builder.node("div", {className: 'Calendar', id: this.id});
		this.markupControlsMonth();
		var days = Builder.node(
			'table', 
			{
				className: 'Days DayView'
			}
		);
		var tb = Builder.node(
			'tbody', 
			{ }
		);
		days.appendChild(tb);
		for (var r = 0; r < 24; r++){
			var tr = Builder.node('tr');
			tb.appendChild(tr);
			var td = Builder.node(
				"td",
				{
					id: this.id + '_cell' + r
				}
			);
			Event.observe(td, 'click', this.onClickCell.bindAsEventListener(this));
			Event.observe(td, 'mousedown', this.onMouseDownCell.bindAsEventListener(this));
			Event.observe(td, 'mouseup', this.onMouseUpCell.bindAsEventListener(this));
			Event.observe(td, 'mouseover', this.onMouseOverCell.bindAsEventListener(this));
			Event.observe(td, 'mouseout', this.onMouseOutCell.bindAsEventListener(this));
			tr.appendChild(td);
		}
		this.container.appendChild(days);
		this.parent.appendChild(this.container);
		
	},
	markupmonth: function(){
		this.container = Builder.node("div", {className: 'Calendar', id: this.id});
		this.markupControlsMonth(); //see comment
		var days = Builder.node(
			'table', 
			{
				className: 'Days MonthView'
			}
		);
		var tb = Builder.node(
			'tbody', 
			{ }
		);
		days.appendChild(tb);
		
		//day col headings M T W T etc
		var dayHeadings = Builder.node('tr', {id: this.id + '_dayHeadings', className: 'DayHeadings'});
		'mtwtfss'.toUpperCase().split('').each(function(item) {
			dayHeadings.appendChild(Builder.node('td', item));
		});
		
		tb.appendChild(dayHeadings);
		
		var i = 0;
		for( var r = 0; r < 7; r++){
			var tr = Builder.node('tr');
			tb.appendChild(tr);
			for (var c = 0; c < 7; c++){
				var td = Builder.node(
					'td', 
					{
						id: this.id + '_cell' + i
					}
				);
				Event.observe(td, 'click', this.onClickCell.bindAsEventListener(this));
				Event.observe(td, 'mousedown', this.onMouseDownCell.bindAsEventListener(this));
				Event.observe(td, 'mouseup', this.onMouseUpCell.bindAsEventListener(this));
				Event.observe(td, 'mouseover', this.onMouseOverCell.bindAsEventListener(this));
				Event.observe(td, 'mouseout', this.onMouseOutCell.bindAsEventListener(this));
				tr.appendChild(td);
				i++;
			}
		}
		this.container.appendChild(days);
		this.parent.appendChild(this.container);
	},
	
	
	getCell: function(index){
		return $(this.id + '_cell' + index);
	},
	
	
	/**
	 * @arg date: must be a first-day-of-month type of date
	 * @return: the index of the days-array where this date will 
	 * 			start (e.g. input: 1/1/2006 output: an int  0-49)
	 */
	getStartIndex: function(date){
		// decided to hard-code the 'rules' about how to pad the start 
		// and end of the calendar with the days from the prev and next 
		// month into the following lookup. Scratched my head thinking 
		// some algo, then gave up and made a simple lookup.
		d = new Date(date);
		if (d.getDate() != 1) {
			d.setDate(1);
		}
		var startingPositions = {}
		startingPositions[0] = 6;
		startingPositions[1] = 7;
		startingPositions[2] = 8;
		startingPositions[3] = 9;
		startingPositions[4] = 10;
		startingPositions[5] = 11;
		startingPositions[6] = 12;
		return startingPositions[d.getDay()];
	},
	
	/**
	 * calculate the date from the input cell index. 
	 * note that the cell index means different things for different 
	 * calendar view (this.options.view)
	 **/
	getDateFromIndex: function(index) {
		var returnValue = null;
		if (this.options.view == 'month') {
			var fdom = this.fdom(this.date);
			returnValue =  new Date(
				fdom.getFullYear(), 
				fdom.getMonth(), 
				index - this.getStartIndex(this.date) + 1,
				this.date.getHours(),
				this.date.getMinutes(),
				this.date.getSeconds(),
				this.date.getMilliseconds()
			);
		} else if (this.options.view == 'day') {
			returnValue = new Date(
				this.date.getFullYear(), 
				this.date.getMonth(),
				this.date.getDate(),
				index,
				0,
				0,
				0
			);
		} else if (this.options.view == 'week'){
			throw new Error(
				"getDateFromIndex not yet " + 
				"implemented for week view. get coding!"
			);
		}
		return returnValue;
	},
	
	//note that this can return negative indexes and indexes > 48
	//i.e. dates not on this cal
	getIndexFromDate: function(d){
		var returnValue = null;
		if (this.options.view == 'month'){
			var deltaDays = Math.ceil((this.fdom(this.date) - d) / (1000 * 60 * 60 * 24));
			return this.getStartIndex(this.date) - deltaDays;
		} else if (this.options.view == 'day') {
			//fixme: do we just assume that d has the same 
			//year,month,day as this.date?
			returnValue = d.getHours();
		} else if (this.options.view == 'week'){
			throw new Error(
				"getDateFromIndex not yet " + 
				"implemented for week view. get coding!"
			);
		}
		return returnValue;
	},
	
	renderCell: function(td, index, date){
		var tdClass = 'ThisMonth';
		if (date.getMonth() < this.date.getMonth()){
			tdClass = 'PrevMonth';
		} else if (date.getMonth() > this.date.getMonth()){
			tdClass = 'NextMonth';
		}
		td.className = tdClass;
		td.innerHTML = date.getDate();
		//td.innerHTML = '<a class="Day" href="javascript:void(0);">' + date.getDate() + '</a>';
		if (this.options.afterRenderCell){
			this.options.afterRenderCell(td, index, date);
		}
	},
	fill: function(){
		(this['fill' + this.options.view] || this['markupmonth']).bind(this)();
		//tell user that i'm filled 
		if (this.options.onFill){
			this.options.onFill(this);
		}
	},
	fillday: function(){
		for (var i = 0; i < 24; i++){
			this.getCell(i).innerHTML = (i % 12 == 0 ? 12 : i % 12)  + (i < 12 ? 'am' : 'pm');
		}
		//set date label properly. 
		$(this.id + '_dateLabel').innerHTML = Calendar.dayNames[this.date.getDay()].capitalize() + 
			" " + Calendar.monthNames[this.date.getMonth()].capitalize().slice(0,3) +
			" " + this.date.getDate() + 
			", " + this.date.getFullYear();
	},
	
	fillmonth: function(){
		var fdom = new Date(this.date.getFullYear(), this.date.getMonth(), 1);
		var index = this.getStartIndex(fdom);
		var cell = null;
		
		//fill head space from prev month
		var d = new Date(fdom - (index * 24 * 60 * 60 * 1000));
		for (var i = 0; i < index; i++){
			cell = this.getCell(i);
			this.renderCell(cell, i, d);
			d.setDate(d.getDate() + 1);
		}
		
		var keepGoing = true;
		while (keepGoing){
			cell = this.getCell(index);
			this.renderCell(cell, index, fdom);
			fdom.setDate(fdom.getDate() + 1); //Check this in IE. FF understands setDate(32) for example...
			keepGoing = (fdom.getDate() > 1);
			index++;
		}
		
		//fill tail space from prev month
		var d = new Date(this.date);
		d.setMonth(d.getMonth() + 1);
		d.setDate(1);
		for (var i = index; i < 49; i++){
			cell = this.getCell(i);
			this.renderCell(cell, i, d);
			d.setDate(d.getDate() + 1);
		}
		
		//set date label properly. 
		$(this.id + '_dateLabel').innerHTML = Calendar.monthNames[this.date.getMonth()].capitalize() + " " + this.date.getFullYear();
		
		
	},
	setDate: function(d){
		var tmp = new Date(this.date);
		//this.setSelectedCell(this.getIndexFromDate(d));
		this.unselectCell(this.getIndexFromDate(this.date));
		this.date = new Date(d);
		if (
			(this.options.view == 'month' && (
				this.date.getMonth() != tmp.getMonth() 
				|| this.date.getFullYear() != tmp.getFullYear()
			))
			|| (this.options.view == 'day' && (
				this.date.getMonth() != tmp.getMonth() 
				|| this.date.getFullYear() != tmp.getFullYear()
				|| this.date.getDate() != tmp.getDate()
			))
				
		){
			this.fill();
		}
		this.selectCell(this.getIndexFromDate(d));
	},
	
	nextDay: function(){
		var d = new Date(this.date);
		d.setDate(d.getDate() + 1);
		this.setDate(d);
	},
	prevDay: function(){
		var d = new Date(this.date);
		d.setDate(d.getDate() - 1);
		this.setDate(d);
	},
	nextMonth: function(){
		var d = new Date(this.date);
		d.setMonth(d.getMonth() + 1);
		this.setDate(d);
	}, 
	prevMonth: function(){
		var d = new Date(this.date);
		d.setMonth(d.getMonth() - 1);
		this.setDate(d);
	}
});

Calendar.Simple = Class.create(Calendar.Base, {
	initialize: function(id, date, parentElem, options){
		this.baseInitialize(id, date, parentElem, options);
	}
});

