/**
 * Yet Another Calendar (YaCal)
 * A mini calendar for selecting an individual date.
 * @author pop-wad [tw]
 * TO IMPROVE:
 * 	- allow data population (monthDTO, JSON)
 */
var YaCal = Class.create();
// Static members
Object.extend(YaCal, {
	// Utility to convert a string to a date object and purge time information. Format is optional.
	parseDate: function(dt, format){
		// null needs to pass through untampered
		if(Object.isString(dt)){
			dt = (Object.isString(format)) ? dt.toDate(format) : new Date(dt);
		}
		if(dt instanceof Date){
			dt.clearTime();
		}
		return dt;
	}
});

// Instance members
Object.extend(YaCal.prototype, {
	_selectedDate: null,
	_availableDates: false,
	_minDate: null,
	_maxDate: null,
	
	initialize: function(container, options){
		this.options = Object.extend({
			initialDate: new Date(),
			titleFormat: false,
			footFormat: false
		}, options || {});
		
		if($(container)){
			this.container = $(container);
			this.containerID = this.container.identify();
		} else{return;}
		
		this.insertTable();
		this.table.observe('click', this.__tableClick.bindAsEventListener(this));
		this.today = new Date().clearTime();
		var dtToShow = new Date(this.today.getTime());
		
		// Check for setting available dates instead of minDate and maxDate options
		if(this.options.availableDates){
			this._availableDates = [];
			this.options.availableDates.each(function(date){this.addAvailableDates(date);}, this);
			this._availableDates.sort();
			var min = new Date();
			min.setTime(this._availableDates.first());
			this.options.minDate = min;
			var max = new Date();
			max.setTime(this._availableDates.last())
			this.options.maxDate = max;
		}		
	
		// check for minDate and maxDate options
		if(this.options.minDate){
			this.setMinDate(YaCal.parseDate(this.options.minDate));
		}
		if(this.options.maxDate){
			this.setMaxDate(YaCal.parseDate(this.options.maxDate));
		}
		
		this.options.initialDate = YaCal.parseDate(this.options.initialDate);
		dtToShow = new Date(this.options.initialDate.getTime());
		
		if(this.options.initialSelectedDate){
			var dtInitialSelected = YaCal.parseDate(this.options.initialSelectedDate);
			this.setSelectedDate(dtInitialSelected);
			dtToShow = new Date(dtInitialSelected.getTime());
		}		

		this.bindTable(dtToShow);
	},
	
	// Creates the table markup and inserts into the container.
	insertTable: function(){
		this.table = new Element('table', {'class': 'yacal', 'cellspacing':'0'});
		if(this.options.summary){
			this.table.writeAttribute('summary', this.options.summary);
		}
		if(this.options.caption){
			this.table.insert(new Element('caption').update(this.options.caption));
		}

		this.thead = new Element('thead');
		
		this.thead.insert(this.buildNavRow());
		this.thead.insert(this.buildDOWRow());
		this.table.insert(this.thead);
		this.tbody = new Element('tbody');
		
		this.table.insert(this.tbody);
		
		this.tfoot = new Element('tfoot');
		this.tfoot.insert(this.buildFootRow());
		this.table.insert(this.tfoot);
		
		this.container.insert(this.table);
		
	},
	
	// Binds the month table for the given date and adjusts navigation.
	bindTable: function(date){
		this.range = this.buildMonthRange(date);
		this.monthDTO = this.buildMonthDTO(this.range);
		var title = (this.options.titleFormat != false) ? 
				this.range.date.toFormat(this.options.titleFormat) : 
				this.range.date.getMonthName() + " " + this.range.date.getFullYear();
		
		this.thead.down('th.title').update(title);
		// add/remove disabled on next previous
		if(this._maxDate != null){
			var tdNext = this.thead.down('td.next');
			var nextMonth = this.getNextMonth(this.range.date);
			var maxMonth = new Date(this._maxDate.getTime());
			maxMonth.setDate(1);
			(nextMonth > maxMonth) ? tdNext.addClassName('disabled') : tdNext.removeClassName('disabled');
		}
		
		if(this._minDate != null){
			var tdPrevious = this.thead.down('td.previous');
			var previousMonth = this.getPreviousMonth(this.range.date);
			var minMonth = new Date(this._minDate.getTime());
			minMonth.setDate(1);
			(previousMonth < minMonth ) ? tdPrevious.addClassName('disabled') : tdPrevious.removeClassName('disabled');
		}
		
		this.tbody.update(); // clear out old day cells
		for(i = 0, len = this.monthDTO.size(); i < len; i++){
			this.tbody.insert(this.buildWeekRow(this.monthDTO[i]));
		}

	},
	
	rebindTable: function(){
		this.bindTable(new Date(this.range.date.getTime()));
	},
	
	// Creates elements for the month navigation row.
	buildNavRow: function(){
		/*
		<tr class="nav">
			<td class="previous"><a href="#previous">&#60;</a></td>
			<th class="title" scope="row" colspan="5">March 2010</th>
			<td class="next"><a href="#next">&#62;</a></td>
		</tr>
		*/
		var navRow = new Element('tr', {'class': 'nav'});
		navRow.insert(new Element('td', {'class': 'previous'}).update('<a href="#previous">Previous</a>'));
		navRow.insert(new Element('th', {'class': 'title', 'colspan': '5'}));
		navRow.insert(new Element('td', {'class': 'next'}).update('<a href="#next">Next</a>'));
		return navRow;
	},
	// Builds a range of months representing a month
	buildMonthRange: function(year, month){	
		return new MonthRange(year, month);
	},
	// Adds data to the month range (to be refined for passing date specific data)
	buildMonthDTO: function(range){
		var monthArr = [];
		var week = [];								

		// populate offset with undefined
		// I may need to find a better way for this. inGroupsOf()?
		range.start.getDay().times(function(itr){ week.push(undefined) });

		range.each(function(date){
			 week.push(date);
			 if(date.getDay() == 6){ // end of week
				monthArr.push(week);
				week = [];
			}
		}.bind(this));
		
		if(week.length > 0){
			while(week.length < 7){
				week.push(undefined);
			}
			monthArr.push(week);
		}
		return monthArr;
	
	},
	// Creates a weeks worth of cells and applies appropriate css classes.
	buildWeekRow: function(weekdays){
		var weekRow = new Element('tr');
		// classes: selected weekend today disabled other
		weekdays.each(function(day, index){
			var td = new Element('td');
			// weekend?
			if(index == 0 || index == 6){td.addClassName('weekend');}
			
			if(!Object.isUndefined(day)){
				var el;
				if(day.equals(this.today, true)){
					td.addClassName('today');
				}
				if(this._selectedDate && (day.equals(this._selectedDate, true))){
					td.addClassName('selected');
				}
				// date is between optional min and max dates, and is a member of optional available dates
				
				if(((this._minDate == null) ? true : (day >= this._minDate)) 
					&& ((this._maxDate == null) ? true : (day <= this._maxDate))
					&& ((this._availableDates == false) ? true : (this._availableDates.member(day.getTime())))
				){
					el = new Element('a', {'href': '#' + day.toDateString()}).update(day.getDate());
				}else{
					el = new Element('span').update(day.getDate());
					td.addClassName('disabled');
				}
				
				td._day = day; // store date obj as private member.
				
				td.update(el);
			}
			else{
				td.addClassName('other');
			}
			weekRow.insert(td);
		}.bind(this));
		
		return weekRow;
	},
	// Creates day of week header row.
	buildDOWRow: function(){
		/*
			<tr class="dow">
				<th scope="col" class="weekend"><abbr title="Sunday">S</abbr></th>
				<th scope="col"><abbr title="Monday">M</abbr></th>
				<th scope="col"><abbr title="Tuesday">T</abbr></th>
				<th scope="col"><abbr title="Wednesday">W</abbr></th>
				<th scope="col"><abbr title="Thursday">T</abbr></th>
				<th scope="col"><abbr title="Friday">F</abbr></th>
				<th scope="col" class="weekend"><abbr title="Saturday">S</abbr></th>
			</tr>
		*/
		var dowRow = new Element('tr', {'class': 'dow'});
		for(var i=0, len = Date.dayNames.size(); i < len; i++){
			var abbr = new Element('abbr', {title: Date.dayNames[i]}).update(Date.dayChars[i]);
			var th = new Element('th', {'scope': 'col'}).update(abbr);
			
			th.insert(abbr);
			if(i == 0 || i == 6){
				th.addClassName('weekend');
			}
			dowRow.insert(th);
		}
		return dowRow;
	},
	// Creates tfoot section with a cell for showing the selected date.
	buildFootRow: function(){
		/*
		<tfoot>
			<tr>
				<td class="selected" colspan="7">MM DD, YYYY</td>
			</tr>
		</tfoot>
		*/
		var footRow = new Element('tr');
		footRow.insert(new Element('td', {'class':'selected', 'colspan': '7'}));
		return footRow;
	},
	// Clears selected date and rebuilds the calendar with the initial date.
	reset: function(){
		this.setSelectedDate(null);
		this.bindTable(this.options.initialDate);
	},
	//Selects the date and optionally forces showing of the selected month.
	setSelectedDate: function(dt, bShowSelectedMonth){
		dt = YaCal.parseDate(dt);
		
		this._selectedDate = dt;

		var sDate = '';
		if(dt != null){
			sDate = (this.options.footFormat != false) ? dt.toFormat(this.options.footFormat): dt.toDateString() ;	
		}
		
		this.tbody.select('td').each(function(td){
			if((td._day) && (dt) && td._day.toDateString() == dt.toDateString()){
				td.addClassName('selected')
			}
			else{
				td.removeClassName('selected');
			}
		});
		this.tfoot.down('tr td.selected').update(sDate);
		// optional
		if(bShowSelectedMonth && bShowSelectedMonth == true){
			this.showSelectedMonth();
		}
	},
	// Gets the calendar's selected date, returns false if no date has been selected.
	getSelectedDate: function(){
		var dtSelected = false;
		if(this._selectedDate != null){
			dtSelected = new Date(this._selectedDate.getTime());// protect reference
		}
		return dtSelected;
	},
	// Gets the calendar's selected date, returns false if no date has been selected.
	toString: function(){
		return this.getSelectedDate();
	},	
	// Gets the currently showing month.
	getMonthShowing: function(){
		return new Date(this.range.date.getTime());
	},
	// Event handler for a click anywhere within the table; Dispatches next month, previous month, day selections.
	__tableClick: function(e){
		// examine e.element() and perform relevant action if any.
		e.stop();
		var element = e.element();
		var tdNext = this.thead.down('tr.nav td.next');
		var tdPrevious = this.thead.down('tr.nav td.previous');
		if(element.descendantOf(tdPrevious)){
			if(!tdPrevious.hasClassName('disabled')){
				this.showPreviousMonth();
			}
		}
		else if(element.descendantOf(tdNext)){
			if(!tdNext.hasClassName('disabled')){
				this.showNextMonth();
			}
		}
		else if(element.descendantOf(this.tbody) && element.nodeName == 'A'){
			var tdDay = element.up('td');
			this.setSelectedDate(tdDay._day);
			
			this._daySelected(this._selectedDate);
		}
	},
	// Shows the previous month.
	showPreviousMonth: function(){
		var date = new Date(this.range.start.getTime());
		date = this.getPreviousMonth(date);
		this.bindTable(date);
		this._monthSelected(date);
		this.container.fire('widget:onChange');
	},
	// Shows the next month
	showNextMonth: function(){
		var date = this.range.end.copy();//new Date(this.range.end.getTime());
		date = this.getNextMonth(date);
		this.bindTable(date);
		this._monthSelected(date);
		this.container.fire('widget:onChange');
	},
	// Shows the month of the given date.
	showMonth: function(dt){
		dt = YaCal.parseDate(dt);
		dt.setDate(1);
		
		this.bindTable(dt);
	},
	// Shows the NOW month.
	showCurrentMonth: function(){
		this.showMonth(new Date());
	},
	// Shows the month that contains the selected date.
	showSelectedMonth: function(){
		var dtSelected = this.getSelectedDate();
		if(dtSelected !== false){
			var isShowing = (
				(this.range.date.getMonth() == dtSelected.getMonth())
				&&
				(this.range.date.getFullYear() == dtSelected.getFullYear())
			);
			if(!isShowing){
				this.showMonth(dtSelected);
			}
		}
	},
	// Broadcasts a day selected event
	_daySelected: function(dt){
		this.container.fire(this.containerID + ':day_selected', {"date": dt});
	},
	// Broadcasts a month selected event
	_monthSelected: function(dt){
		this.container.fire(this.containerID + ':month_selected', {"date":dt});
	},
	// Internal utility function to get a previous month date time.
	getPreviousMonth: function(dt){
		var date = dt.copy().clearTime();
		date.setDate(1);
		date.addMonths(-1);
		return date;
	},
	// Internal utility function to get a next month date time.
	getNextMonth: function(dt){
		var date = dt.copy().clearTime();
		date.setDate(1);
		date.addMonths(1);
		return date;
	},
	// Sets a new minimum date to be available for selection.
	setMinDate: function(dt){
		this._minDate = YaCal.parseDate(dt);
	},
	// Sets a new maximum date to be available for selection.
	setMaxDate: function(dt){
		this._maxDate = YaCal.parseDate(dt);
	},
	// Makes dates available. dt can be a date range, a single date, or a date string
	addAvailableDates: function(dt){
		if(dt instanceof(ObjectRange)){
			var dates = $A(dt);
			for(var i=0, len = dates.size(); i < len ; i++){
				dates[i].clearTime();
				this._addAvailableDate(dates[i]);
			}
		}
		else{
			dt = YaCal.parseDate(dt);
			this._addAvailableDate(dt);
		}
		this._availableDates.sort();
	},
	// Private utility method to add a new date.
	_addAvailableDate: function(dt){
		var timestamp = dt.getTime();
		if(!(this._availableDates.member(timestamp))){
			this._availableDates.push(timestamp);
		}
	},
	//Gets an array of all available dates, if being used.
	getAvailableDates: function(){
		return this._availableDates.collect(function(timestamp){
			var dt = new Date();
			dt.setTime(timestamp);
			return dt;
		});
	}
});

/*** Widget On Change Event ***/

document.observe('widget:onChange', function(){
	Cufon.refresh();
});
