// scal.js v0.1_beta8, 2007-08-28
//
//   - A Calendar based on the fabulous script.aculo.us and prototype js libraries, which means script.aculo.us and
//     Prototype.js are required.
//
//   - Turns any element into a calendar, though for practical reasons, DIVs are best suited.
//
// Copyright (c) 2007 Jamie Grove with regards to:
//        Sam Stephenson @ http://www.prototype.js
//        Thomas Fuchs @ http://script.aculo.us, http://mir.aculo.us
//        ... and lots o' folks who worked on these great libraries!
// 
// Permission is hereby granted, free of charge, to any person obtaining
// a copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to
// permit persons to whom the Software is furnished to do so, subject to
// the following conditions:
// 
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

// Extending Date to support next().  This will be useful for quickly generating ranges.
Date.prototype.next = function() {
		var sd = new Date(this.getFullYear(),this.getMonth(),this.getDate()+1);
		sd.setHours(this.getHours(),this.getMinutes(),this.getSeconds(),this.getMilliseconds());
		return sd;
	};
// Extending Date to support prev().  This will be useful for quickly generating ranges.
Date.prototype.prev = function() {
		var sd = new Date(this.getFullYear(),this.getMonth(),this.getDate()-1);
		sd.setHours(this.getHours(),this.getMinutes(),this.getSeconds(),this.getMilliseconds());
		return sd;
	};
// Extending Date to calculate the first and last of the month.  Use for obvious reasons.
Date.prototype.firstofmonth = function () {
		var rd = new Date(this.getFullYear(),this.getMonth(),1);
		return rd;
	};
Date.prototype.lastofmonth = function() {
		var rd = new Date(this.getFullYear(),this.getMonth()+1,0);
		return rd;
	};

scalmonthnames = new Array(
	'January',
	'February',
	'March',
	'April',
	'May',
	'June',
	'July',
	'August',
	'September',
	'October',
	'November',
	'December'
	);

scaldaynames = new Array(
	'Sunday',
	'Monday',
	'Tuesday',
	'Wednesday',
	'Thursday',
	'Friday',
	'Saturday'
	);

// borrowed from http://www.codeproject.com/jscript/dateformat.asp
Date.prototype.format = function(f)
	{
	    if (!this.valueOf())
	        return '&nbsp;';

	    var d = this;

	    return f.replace(/(yyyy|mmmm|mmm|mm|dddd|ddd|dd|hh|nn|ss|a\/p)/gi,
	        function($1)
	        {
	            switch ($1.toLowerCase())
	            {
	            case 'yyyy': return d.getFullYear();
	            case 'mmmm': return scalmonthnames[d.getMonth()];
	            case 'mmm':  return (scalmonthnames[d.getMonth()]).substr(0, 3);
	            case 'mm':   return (d.getMonth() + 1);
	            case 'dddd': return scaldaynames[d.getDay()];
	            case 'ddd':  return scaldaynames[d.getDay()].substr(0, 3);
	            case 'dd':   return d.getDate();
	            case 'hh':   return ((h = d.getHours() % 12) ? h : 12);
	            case 'nn':   return d.getMinutes();
	            case 'ss':   return d.getSeconds();
	            case 'a/p':  return d.getHours() < 12 ? 'a' : 'p';
	            }
	        }
	    );
	};

// borrowed from http://www.quirksmode.org/js/week.html
Date.prototype.weeknumber = function(){
	Year = takeYear(this);
	Month = this.getMonth();
	Day = this.getDate();
	now = Date.UTC(Year,Month,Day+1,0,0,0);
	var Firstday = new Date();
	Firstday.setYear(Year);
	Firstday.setMonth(0);
	Firstday.setDate(1);
	then = Date.UTC(Year,0,1,0,0,0);
	var Compensation = Firstday.getDay();
	if (Compensation > 3) Compensation -= 4;
	else Compensation += 3;
	NumberOfWeek =  Math.round((((now-then)/86400000)+Compensation)/7);
	return NumberOfWeek;
	function takeYear(theDate)
	{
		x = theDate.getYear();
		var y = x % 100;
		y += (y < 38) ? 2000 : 1900;
		return y;
	}
}

// The scal class definition proper.
var scal = Class.create();
scal.prototype = {
	initialize: function (element,update,options){

		this.startdate = new Date();
		this.startdate.setHours(0,0,0,0)

		this.updateelement = update;
		this.baseelement = element;

		this.options = options
		
		this.titleformat = this.options.titleformat || 'mmmm yyyy';
		this.closebutton = this.options.closebutton || 'X';
		this.prevbutton = this.options.prevbutton || '&laquo;';
		this.nextbutton = this.options.nextbutton || '&raquo;';
		this.exactweeks = this.options.exactweeks || false;
		this.dayheadlength = this.options.dayheadlength || 2;
		this.weekdaystart = this.options.weekdaystart || 0;
		
		this.approvaldays = this.options.approvaldays || 0;
		this.productiondays = this.options.productiondays || 5;
		this.shippingdays = this.options.shippingdays || 2;
		
		year = this.options.year || this.startdate.getFullYear();
		month = this.options.month-1 || 0;
		day = this.options.day || 1;

		// if defaults set for year, month, and day then assume nothing passed and keep current date
		if (year != this.startdate.getFullYear() && month != 0 && day != 1){
			this.startdate.setYear(year);
			this.startdate.setMonth(month);
			this.startdate.setDate(day);
		};

		this.displayed = false;
		this.setCurrentDate(this.startdate);
		this.setestimates(this.startdate);
		this.selecteddate = new Date(this.currentdate.getFullYear(),this.currentdate.getMonth(),this.currentdate.getDate());
	},
	setCurrentDate: function(date){
		this.currentdate = new Date(date.getFullYear(),date.getMonth(),date.getDate());
		this.firstofmonth = this.currentdate.firstofmonth();
		this.lastofmonth = this.currentdate.lastofmonth();
	},
	buildCalendar: function(){
		// Set local variables to house the first and last date to be used on the calendar
		// Actually, one really only needs to set the first date and then just increment while
		// building the array.  However, if the control will support multiple month displays
		// side by side, it might be convenient to have this built in.
		firstdaycal = new Date(this.firstofmonth.getFullYear(),this.firstofmonth.getMonth(),this.firstofmonth.getDate());
		lastdaycal = new Date(this.lastofmonth.getFullYear(),this.lastofmonth.getMonth(),this.lastofmonth.getDate());
		firstdaycal.setDate(firstdaycal.getDate() - firstdaycal.getDay() + this.weekdaystart);
		lastdaycal.setDate(lastdaycal.getDate() + (6-lastdaycal.getDay()));
		this.calendar = null;
		
		// determine the number of weeks to build out
		if (this.exactweeks){
			weeks = this.lastofmonth.weeknumber()-this.firstofmonth.weeknumber()+1;
		}else{
			weeks = 6;
		}
		this.calendar = new Array(weeks); for (i = 0; i < this.calendar.length; ++ i)
			this.calendar[i] = new Array(7);

		tempdate = new Date(firstdaycal.getFullYear(),firstdaycal.getMonth(),firstdaycal.getDate());

		for (week=0; week < this.calendar.length; ++ week){
			for (day=0; day < this.calendar[week].length; ++ day){
				this.calendar[week][day] = tempdate;
				tempdate = new Date(tempdate.getFullYear(),tempdate.getMonth(),tempdate.getDate()+1);
			};
		};
	},
	showCalendar: function(){
		if (typeof this.calendar == 'undefined'){
			this.buildCalendar();
		}
		this.getCalendar();
	},
	closeCalendar: function(){
		if (this.displayed){
			Element.hide(this.baseelement);
			this.displayed = false;
		}
	},
	openCalendar: function(){
		if (!this.displayed){
			Element.show(this.baseelement);
			this.displayed = true;
		}
	},
	getCalendar: function(){
		//new Draggable(this.baseelement);
		$(this.baseelement).update(); // zap the current calendar
		$(this.baseelement).calendar = this.calendar; // convenience handle to the calendar array
		
		cal_header = Builder.node('div',{id:'cal_header'});
		$(cal_header).addClassName('calheader');

		// Calendar navigation controls
	
		// Previous Month Button
		prevmonth = Builder.node('div',{id:'cal_prevmonth'});
		// This bit of code allows you to pass an HTML element to be the button instead of text.
		if (typeof this.prevbutton == 'object'){
			$(prevmonth).appendChild(this.prevbutton);
		}else{
			$(prevmonth).update(this.prevbutton);
		}
		$(prevmonth).addClassName('calcontrol');
		$(prevmonth).addClassName('calprevmonth');
		$(prevmonth).observe('click',this.prevmonth.bind(this));
		$(cal_header).appendChild(prevmonth);
		prevmonth = null;

		// Next Month Button
		nextmonth = Builder.node('div',{id:'cal_nextmonth'});
		// This bit of code allows you to pass an HTML element to be the button instead of text.
		if (typeof this.nextbutton == 'object'){
			$(nextmonth).appendChild(this.nextbutton);
		}else{
			$(nextmonth).update(this.nextbutton);
		}
		$(nextmonth).addClassName('calcontrol');
		$(nextmonth).addClassName('calnextmonth');
		$(nextmonth).observe('click',this.nextmonth.bind(this));
		$(cal_header).appendChild(nextmonth);
		nextmonth = null;

		// Calendar Title
		cal_title = Builder.node('div',{id:'cal_title'});
		$(cal_title).update(this.currentdate.format(this.titleformat));
		$(cal_title).addClassName('caltitle');
		$(cal_title).observe('click',this.returntocurrentselecteddate.bind(this));
		$(cal_header).appendChild(cal_title);
		cal_title = null;
		
		$(this.baseelement).appendChild(cal_header);
		cal_header = null;
		
		cal_wrapper = Builder.node('div',{id:'cal_wrapper'});
		$(cal_wrapper).addClassName('calwrapper');
		$(this.baseelement).appendChild(cal_wrapper);

		// Add a day of the week header

		row = Builder.node('div',{id:'cal_week_name'});
		$(row).addClassName('weekbox');
		$(row).addClassName('weekboxname');
		for (day=0; day < this.calendar[0].length-1; ++ day){
			cell = Builder.node('div',{id:'cal_day_name_'+day});
			$(cell).addClassName('daybox');
			$(cell).addClassName('dayboxname');
			$(cell).update(this.calendar[0][day].format('dddd').substr(0,this.dayheadlength));
			$(row).appendChild(cell);
			cell = null;
		}
		day = this.calendar[0].length-1;
		cell = Builder.node('div',{id:'cal_day_name_'+day});
		$(cell).addClassName('daybox');
		$(cell).addClassName('dayboxname');
		$(cell).update(this.calendar[0][day].format('dddd').substr(0,this.dayheadlength));
		cell.addClassName('endweek');
		$(row).appendChild(cell);
		$(cal_wrapper).appendChild(row);
		row = null;

		cal_weeks_wrapper = Builder.node('div',{id:'cal_weeks_wrapper'});
		$(cal_weeks_wrapper).addClassName('calweekswrapper');
		$(cal_wrapper).appendChild(cal_weeks_wrapper);
		
		// Loop through the calendar array and create DOM elements to display calendar.
		// We begin with a loop through the 'weeks' and then process each day in the following loop
		for (week=0; week < this.calendar.length; ++ week){
			row = Builder.node('div',{id:'cal_week_'+week});
			$(row).addClassName('weekbox'); // This might be a good place to add an onclick listener if you want to style rows individually
			for (day=0; day < this.calendar[week].length-1; ++ day){
				if (day==0) {
					weeknumber = this.calendar[week][0].weeknumber() - 1;
				}
				cell = this.buildday(week,day,weeknumber);
				$(row).appendChild(cell);
				cell = null;
			};
			cell = this.buildday(week,this.calendar[week].length-1,weeknumber);
			cell.addClassName('endweek');
			$(row).appendChild(cell);
			cell = null;
			$(cal_weeks_wrapper).appendChild(row);
			row = null;
		};

		cal_wrapper = null;
		cal_weeks_wrapper = null;
		// all done building the calendar.  Now, display it if necessary.
		this.openCalendar();
		this.showestimates();
	},
	buildday: function(week,day,weeknumber){
		tempdate = this.calendar[week][day];
		cellid = 'cal_day_'+weeknumber+'_'+day;
		cell = Builder.node('div',{id:cellid});
		// add convenience values to the cell object
		$(cell).week = week;
		$(cell).day = day;
		$(cell).date = tempdate;
		celldate = Builder.node('div',{id:cellid+'_date'});
		$(celldate).addClassName('dayboxdate');
		$(celldate).update(tempdate.getDate()); // update what appears in the calendar date cell

		// observe clicks
		$(cell).observe('click',this.cellclick.bind(this)); // set observation of clicks in the calendar object

		$(cell).datefield = $(celldate);

		$(cell).appendChild(celldate);

		// apply styles
		$(cell).addClassName('daybox');
		$(cell).addClassName('daybox'+tempdate.format('dddd').toLowerCase());
		// if we are on the currently selected date, set the class to dayselected (i.e. highlight it).
		if (tempdate.getMonth() == this.selecteddate.getMonth() && tempdate.getDate() == this.selecteddate.getDate() && tempdate.getFullYear() == this.selecteddate.getFullYear()){
			$(cell).addClassName('dayselected');
			this.selecteddatecell=$(cell);		
		}
		// if we are outside the current month set the day style to 'deactivated'
		if (tempdate.getMonth() != this.currentdate.getMonth()){
			$(cell).addClassName('dayoutmonth');
			$(cell).removeClassName('dayinmonth');
		}else{
			$(cell).addClassName('dayinmonth');
			$(cell).removeClassName('dayoutmonth');
		};
		$(cell).addClassName(cellid);
		return cell;
	},
	prevmonth: function(event){
		clicked = Event.element(event);
		backyear = this.currentdate.getFullYear();
		backmonth = this.currentdate.getMonth()-1;
		if (backmonth == -1){
			backmonth = 11;
			backyear = backyear-1;
		}
		tempdate = new Date(backyear,backmonth,this.currentdate.getDate());
		this.setCurrentDate(tempdate);
		this.buildCalendar();
		this.getCalendar();
		Event.stop(event);
	},
	nextmonth: function(event){
		clicked = Event.element(event);
		tempdate = new Date(this.currentdate.getFullYear(),this.currentdate.getMonth()+1,this.currentdate.getDate());
		this.setCurrentDate(tempdate);
		this.buildCalendar();
		this.getCalendar();
		Event.stop(event);
	},
	returntocurrentselecteddate: function(event){
		clicked = Event.element(event);
		this.setCurrentDate(this.selecteddate);
		this.buildCalendar();
		this.getCalendar();
	},
	opencalendar: function(event){
		clicked = Event.element(event);
		if (!this.displayed){
			this.displayed = true;
		}
	},
	closecalendar: function(event){
		clicked = Event.element(event);
		if (this.displayed){
			this.displayed = false;
		}
	},
	cellclick: function(event){
		clicked = Event.element(event);
		// if a sub element of the date cell was clicked (which is likely) work with the parent object (i.e the date cell)
		if(clicked.id.indexOf('_date') != -1 || clicked.id.indexOf('_value') != -1) clicked = clicked.up();
		if (clicked.id == this.selecteddatecell.id) return;
		clicked.addClassName('dayselected');
		this.selecteddatecell.removeClassName('dayselected');
		this.selecteddate=clicked.date;
		this.selecteddatecell=clicked;
		this.hideestimates();
		this.resetestimates();
		this.setestimates(this.selecteddate);
		this.showestimates();
		this.updateexternal();
		Event.stop(event);
	},
	updateexternal: function(){
		if (typeof this.updateelement == 'string'){
			// update the defined update element with the currently selected date
			$(this.updateelement).update(this.selecteddate);
		}else if (typeof this.updateelement == 'function'){
			this.updateelement();
		};
	},
	getdatecell: function(week,day) {
		week -= 1;
		cellid = 'cal_day_'+week+'_'+day;
		return cellid;
	},
	addclassnametodates: function(dates, classname) {
		dates.each(function(d) {
			cellid = this.getdatecell(d.weeknumber(), d.getDay());
			Element.addClassName(cellid, classname);
		}.bind(this))
	},
	removeclassnamefromdates: function(dates, classname) {
		dates.each(function(d) {
			cellid = this.getdatecell(d.weeknumber(), d.getDay());
			Element.removeClassName(cellid, classname);
		}.bind(this))
	},
	setestimates: function(date) {
		if (this.approvaldays == 0) {
			this.approvaldates = [this.skipweekend(date)];
		} else {
			this.approvaldates = this.getdaterange(date, this.approvaldays);
		}
		this.productiondates = this.getdaterange(this.approvaldates[this.approvaldates.length - 1], this.productiondays);
		this.shippingdates = this.getdaterange(this.productiondates[this.productiondates.length - 1], this.shippingdays)
	},
	resetestimates: function() {
		this.approvaldates = [];
		this.productiondates = [];
		this.shippingdates = [];
	},
	hideestimates: function() {
		this.removeclassnamefromdates(this.approvaldates, 'approval');
		this.removeclassnamefromdates(this.productiondates, 'production');
		this.removeclassnamefromdates(this.shippingdates, 'shipping');
	},
	showestimates: function() {
		this.addclassnametodates(this.approvaldates, 'approval');
		this.addclassnametodates(this.productiondates, 'production');
		this.addclassnametodates(this.shippingdates, 'shipping');
	},
	getdaterange: function(startdate, days) {
		var result = [];
		result[0] = this.skipweekend(startdate.next());
		for (var i = 1; i <= (days - 1); i++) {
			result[i] = this.skipweekend(result[result.length - 1].next());
		}
		return result;
	},
	skipweekend: function(date) {
		if (date.getDay() == 0) {
			date = date.next();
		} else if (date.getDay() == 6) {
			date = date.next();
			date = date.next();
		}
		return date;
	},
	skipweekendbackwards: function(date) {
		if (date.getDay() == 0) {
			date = date.prev();
		} else if (date.getDay() == 6) {
			date = date.prev();
			date = date.prev();
		}
		return date;
	},
	refresh: function(date) {
		this.hideestimates();
		this.resetestimates();
		this.setestimates(this.selecteddate);
		this.showestimates();
	}
};

