// Calendar: a Javascript class for Mootools that adds accessible and unobtrusive date pickers to your form elements <http://electricprism.com/aeron/calendar>

// Calendar RC4, Copyright (c) 2007 Aeron Glemann <http://electricprism.com/aeron>, MIT Style License.

// Mootools 1.2 compatibility by Davorin ?ego



var Calendar = new Class({	



  Implements: Options,



	options: {

		blocked: [], // blocked dates 

		classes: [], // ['calendar', 'prev', 'next', 'month', 'year', 'today', 'invalid', 'valid', 'inactive', 'active', 'hover', 'hilite']

		days: ['Неделя', 'Понеделник', 'Вторник', 'Сряда', 'Четвъртък', 'Петък', 'Събота'], // days of the week starting at sunday

		direction: 0, // -1 past, 0 past + future, 1 future

		draggable: true,

		months: ['Януари', 'Февуари', 'Март', 'Април', 'Май', 'Юни', 'Юли', 'Август', 'Септември', 'Октомври', 'Ноември', 'Декември'],

		navigation: 1, // 0 = no nav; 1 = single nav for month; 2 = dual nav for month and year

		offset: 0, // first day of the week: 0 = sunday, 1 = monday, etc..

		onHideStart: Class.empty,

		onHideComplete: Class.empty,

		onShowStart: Class.empty,

		onShowComplete: Class.empty,

		pad: 1, // padding between multiple calendars

		tweak: {x: 0, y: 0} // tweak calendar positioning

	},



	// initialize: calendar constructor

	// @param obj (obj) a js object containing the form elements and format strings { id: 'format', id: 'format' etc }

	// @param props (obj) optional properties



	initialize: function(obj, options) {

		// basic error checking

		if (!obj) { return false; }



		this.setOptions(options);



		// create our classes array

		var keys = ['calendar', 'prev', 'next', 'month', 'year', 'today', 'invalid', 'valid', 'inactive', 'active', 'hover', 'hilite'];



		var values = keys.map(function(key, i) {

			if (this.options.classes[i]) {

				if (this.options.classes[i].length) { key = this.options.classes[i]; }

			}

			return key;

		}, this);



		this.classes = values.associate(keys);



		// create cal element with css styles required for proper cal functioning

		this.calendar = new Element('div', { 

			'styles': { left: '-1000px', opacity: 0, position: 'absolute', top: '-1000px', zIndex: 1000 }

		}).addClass(this.classes.calendar).injectInside(document.body);



		// iex 6 needs a transparent iframe underneath the calendar in order to not allow select elements to render through

		if (window.ie6) {

			this.iframe = new Element('iframe', { 

				'styles': { left: '-1000px', position: 'absolute', top: '-1000px', zIndex: 999 }

			}).injectInside(document.body);

			this.iframe.style.filter = 'progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0)';

		}



		// initialize fade method

		this.fx = new Fx.Tween(this.calendar, {

			onStart: function() { 

				if (this.calendar.getStyle('opacity') == 0) { // show

					if (window.ie6) { this.iframe.setStyle('display', 'block'); }

					this.calendar.setStyle('display', 'block');

					this.fireEvent('onShowStart', this.element);

				}

				else { // hide

					this.fireEvent('onHideStart', this.element);

				}

			}.bind(this),

			onComplete: function() { 

				if (this.calendar.getStyle('opacity') == 0) { // hidden

					this.calendar.setStyle('display', 'none');

					if (window.ie6) { this.iframe.setStyle('display', 'none'); }

					this.fireEvent('onHideComplete', this.element);

				}

				else { // shown

					this.fireEvent('onShowComplete', this.element);

				}

			}.bind(this)

		});



		// initialize drag method

		if (window.Drag && this.options.draggable) {

			this.drag = new Drag.Move(this.calendar, { 

				onDrag: function() {

					if (window.ie6) { this.iframe.setStyles({ left: this.calendar.style.left, top: this.calendar.style.top }); } 

				}.bind(this) 

			}); 

		}

		

		// create calendars array

		this.calendars = [];



		var id = 0;

		var d = new Date(); // today



		d.setDate(d.getDate() + this.options.direction.toInt()); // correct today for directional offset



		for (var i in obj) {

			var cal = { 

				button: new Element('button', { 'type': 'button' }),

				el: $(i),

				els: [],

				id: id++,

				month: d.getMonth(),

				visible: false,

				year: d.getFullYear()

			};



			// fix for bad element (naughty, naughty element!)

			if (!this.element(i, obj[i], cal)) { continue; }

			

			cal.el.addClass(this.classes.calendar);



			// create cal button

			cal.button.addClass(this.classes.calendar).addEvent('click', function(cal) { this.toggle(cal); }.pass(cal, this)).injectAfter(cal.el);



			// read in default value

			cal.val = this.read(cal);



			$extend(cal, this.bounds(cal)); // abs bounds of calendar



			$extend(cal, this.values(cal)); // valid days, months, years



			this.rebuild(cal);



			this.calendars.push(cal); // add to cals array		

		}	

	},





	// blocked: returns an array of blocked days for the month / year

	// @param cal (obj)

	// @returns blocked days (array)



	blocked: function(cal) {

		var blocked = [];

		var offset = new Date(cal.year, cal.month, 1).getDay(); // day of the week (offset)

		var last = new Date(cal.year, cal.month + 1, 0).getDate(); // last day of this month

		

		this.options.blocked.each(function(date){

			var values = date.split(' ');

			

			// preparation

			for (var i = 0; i <= 3; i++){ 

				if (!values[i]){ values[i] = (i == 3) ? '' : '*'; } // make sure blocked date contains values for at least d, m and y

				values[i] = values[i].contains(',') ? values[i].split(',') : new Array(values[i]); // split multiple values

				var count = values[i].length - 1;

				for (var j = count; j >= 0; j--){

					if (values[i][j].contains('-')){ // a range

						var val = values[i][j].split('-');

						for (var k = val[0]; k <= val[1]; k++){

							if (!values[i].contains(k)){ values[i].push(k + ''); }

						}

						values[i].splice(j, 1);

					}

				}

			}



			// execution

			if (values[2].contains(cal.year + '') || values[2].contains('*')){

				if (values[1].contains(cal.month + 1 + '') || values[1].contains('*')){

					values[0].each(function(val){ // if blocked value indicates this month / year

						if (val > 0){ blocked.push(val.toInt()); } // add date to blocked array

					});



					if (values[3]){ // optional value for day of week

						for (var i = 0; i < last; i++){

								var day = (i + offset) % 7;

	

								if (values[3].contains(day + '')){ 

									blocked.push(i + 1); // add every date that corresponds to the blocked day of the week to the blocked array

								}

						}

					}

				}

			}

		}, this);



		return blocked;

	},





	// bounds: returns the start / end bounds of the calendar

	// @param cal (obj)

	// @returns obj	



	bounds: function(cal) {

		// 1. first we assume the calendar has no bounds (or a thousand years in either direction)

		

		// by default the calendar will accept a millennium in either direction

		var start = new Date(1000, 0, 1); // jan 1, 1000

		var end = new Date(2999, 11, 31); // dec 31, 2999



		// 2. but if the cal is one directional we adjust accordingly

		var date = new Date().getDate() + this.options.direction.toInt();


if(this.options.direction == 2){
			var date = new Date().getDate();
				start = new Date();
				start.setDate(date + this.options.pad * cal.id);
		}else{
			var date = new Date().getDate() + this.options.direction.toInt();
			if (this.options.direction > 0) {
				start = new Date();
				start.setDate(date + this.options.pad * cal.id);
			}
			
			if (this.options.direction < 0) {
				end = new Date();
				end.setDate(date - this.options.pad * (this.calendars.length - cal.id - 1));
			}
		}


		// 3. then we can further filter the limits by using the pre-existing values in the selects

		cal.els.each(function(el) {	

			if (el.get('tag') == 'select') {		

				if (el.format.test('(y|Y)')) { // search for a year select

					var years = [];



					el.getChildren().each(function(option) { // get options

						var values = this.unformat(option.value, el.format);

	

						if (!years.contains(values[0])) { years.push(values[0]); } // add to years array

					}, this);

	

					years.sort(this.sort);

			

					if (years[0] > start.getFullYear()) { 

						d = new Date(years[0], start.getMonth() + 1, 0); // last day of new month

					

						if (start.getDate() > d.getDate()) { start.setDate(d.getDate()); }

	

						start.setYear(years[0]); 

					}

					

					if (years.getLast() < end.getFullYear()) { 

						d = new Date(years.getLast(), end.getMonth() + 1, 0); // last day of new month

					

						if (end.getDate() > d.getDate()) { end.setDate(d.getDate()); }

	

						end.setYear(years.getLast());

					}		

				}

	

				if (el.format.test('(F|m|M|n)')) { // search for a month select

					var months_start = [];

					var months_end = [];



					el.getChildren().each(function(option) { // get options

						var values = this.unformat(option.value, el.format);

	

						if ($type(values[0]) != 'number' || values[0] == years[0]) { // if it's a year / month combo for curr year, or simply a month select

							if (!months_start.contains(values[1])) { months_start.push(values[1]); } // add to months array

						}

	

						if ($type(values[0]) != 'number' || values[0] == years.getLast()) { // if it's a year / month combo for curr year, or simply a month select

							if (!months_end.contains(values[1])) { months_end.push(values[1]); } // add to months array

						}

					}, this);

	

					months_start.sort(this.sort);

					months_end.sort(this.sort);

					

					if (months_start[0] > start.getMonth()) { 

						d = new Date(start.getFullYear(), months_start[0] + 1, 0); // last day of new month

					

						if (start.getDate() > d.getDate()) { start.setDate(d.getDate()); }

	

						start.setMonth(months_start[0]); 

					}

					

					if (months_end.getLast() < end.getMonth()) { 

						d = new Date(start.getFullYear(), months_end.getLast() + 1, 0); // last day of new month

					

						if (end.getDate() > d.getDate()) { end.setDate(d.getDate()); }

	

						end.setMonth(months_end.getLast());

					}		

				}

			}

		}, this);

		

		return { 'start': start, 'end': end };

	},





	// caption: returns the caption element with header and navigation

	// @param cal (obj)

	// @returns caption (element)



	caption: function(cal) {

		// start by assuming navigation is allowed

		var navigation = {

			prev: { 'month': true, 'year': true },

			next: { 'month': true, 'year': true }

		};

		

		// if we're in an out of bounds year

		if (cal.year == cal.start.getFullYear()) { 

			navigation.prev.year = false; 

			if (cal.month == cal.start.getMonth() && this.options.navigation == 1) { 

				navigation.prev.month = false;

			}		

		}		

		if (cal.year == cal.end.getFullYear()) { 

			navigation.next.year = false; 

			if (cal.month == cal.end.getMonth() && this.options.navigation == 1) { 

				navigation.next.month = false;

			}

		}



		// special case of improved navigation but months array with only 1 month we can disable all month navigation

		if ($type(cal.months) == 'array') {

			if (cal.months.length == 1 && this.options.navigation == 2) {

				navigation.prev.month = navigation.next.month = false;

			}

		}



		var caption = new Element('caption');



		var prev = new Element('a').addClass(this.classes.prev).appendText('\x3c'); // <		

		var next = new Element('a').addClass(this.classes.next).appendText('\x3e'); // >



		if (this.options.navigation == 2) {

			var month = new Element('span').addClass(this.classes.month).injectInside(caption);

			

			if (navigation.prev.month) { prev.clone().addEvent('click', function(cal) { this.navigate(cal, 'm', -1); }.pass(cal, this)).injectInside(month); }

			

			month.adopt(new Element('span').appendText(this.options.months[cal.month]));



			if (navigation.next.month) { next.clone().addEvent('click', function(cal) { this.navigate(cal, 'm', 1); }.pass(cal, this)).injectInside(month); }



			var year = new Element('span').addClass(this.classes.year).injectInside(caption);



			if (navigation.prev.year) { prev.clone().addEvent('click', function(cal) { this.navigate(cal, 'y', -1); }.pass(cal, this)).injectInside(year); }

			

			year.adopt(new Element('span').appendText(cal.year));



			if (navigation.next.year) { next.clone().addEvent('click', function(cal) { this.navigate(cal, 'y', 1); }.pass(cal, this)).injectInside(year); }

		}

		else { // 1 or 0

			if (navigation.prev.month && this.options.navigation) { prev.clone().addEvent('click', function(cal) { this.navigate(cal, 'm', -1); }.pass(cal, this)).injectInside(caption); }



			caption.adopt(new Element('span').addClass(this.classes.month).appendText(this.options.months[cal.month]));

			

			caption.adopt(new Element('span').addClass(this.classes.year).appendText(cal.year));

			

			if (navigation.next.month && this.options.navigation) { next.clone().addEvent('click', function(cal) { this.navigate(cal, 'm', 1); }.pass(cal, this)).injectInside(caption); }



		}



		return caption;

	},





	// changed: run when a select value is changed

	// @param cal (obj)



	changed: function(cal) {

		cal.val = this.read(cal); // update calendar val from inputs	



		$extend(cal, this.values(cal)); // update bounds - based on curr month



		this.rebuild(cal); // rebuild days select



		if (!cal.val) { return; } // in case the same date was clicked the cal has no set date we should exit		



		if (cal.val.getDate() < cal.days[0]) { cal.val.setDate(cal.days[0]); }

		if (cal.val.getDate() > cal.days.getLast()) { cal.val.setDate(cal.days.getLast()); }

		

		cal.els.each(function(el) {	// then we can set the value to the field

			el.value = this.format(cal.val, el.format); 		

		}, this);

		

		this.check(cal); // checks other cals



		this.calendars.each(function(kal) { // update cal graphic if visible

			if (kal.visible) { this.display(kal); }

		}, this);

	},





	// check: checks other calendars to make sure no overlapping values

	// @param cal (obj)



	check: function(cal) {

		this.calendars.each(function(kal, i) {

			if (kal.val) { // if calendar has value set

				var change = false;

			

				if (i < cal.id) { // preceding calendar

					var bound = new Date(Date.parse(cal.val));

					

					bound.setDate(bound.getDate() - (this.options.pad * (cal.id - i)));



					if (bound < kal.val) { change = true; }

				}

				if (i > cal.id) { // following calendar

					var bound = new Date(Date.parse(cal.val));

					

					bound.setDate(bound.getDate() + (this.options.pad * (i - cal.id)));

					

					if (bound > kal.val) { change = true; }

				}



				if (change) {

					if (kal.start > bound) { bound = kal.start; }

					if (kal.end < bound) { bound = kal.end; }



					kal.month = bound.getMonth();

					kal.year = bound.getFullYear();		



					$extend(kal, this.values(kal));			



					// TODO - IN THE CASE OF SELECT MOVE TO NEAREST VALID VALUE

					// IN THE CASE OF INPUT DISABLE



					// if new date is not valid better unset cal value

					// otherwise it would mean incrementally checking to find the nearest valid date which could be months / years away

					kal.val = kal.days.contains(bound.getDate()) ? bound : null;



					this.write(kal);



					if (kal.visible) { this.display(kal); } // update cal graphic if visible

				}

			}

			else {

				kal.month = cal.month;

				kal.year = cal.year;

			}

		}, this);

	},

	



	// clicked: run when a valid day is clicked in the calendar

	// @param cal (obj)



	clicked: function(td, day, cal) {

		cal.val = (this.value(cal) == day) ? null : new Date(cal.year, cal.month, day); // set new value - if same then disable



		this.write(cal); 



		// ok - in the special case that it's all selects and there's always a date no matter what (at least as far as the form is concerned)

		// we can't let the calendar undo a date selection - it's just not possible!!

		if (!cal.val) { cal.val = this.read(cal); }



		if (cal.val) {

			this.check(cal); // checks other cals						

			this.toggle(cal); // hide cal

		} 

		else { // remove active class and replace with valid

			td.addClass(this.classes.valid);

			td.removeClass(this.classes.active);

		}

	},

	



	// display: create calendar element

	// @param cal (obj)



	display: function(cal) {

		// 1. header and navigation

		this.calendar.empty(); // init div



		this.calendar.className = this.classes.calendar + ' ' + this.options.months[cal.month].toLowerCase();



		var div = new Element('div').injectInside(this.calendar); // a wrapper div to help correct browser css problems with the caption element



		var table = new Element('table').injectInside(div).adopt(this.caption(cal));

				

		// 2. day names		

		var thead = new Element('thead').injectInside(table);



		var tr = new Element('tr').injectInside(thead);

		

		for (var i = 0; i <= 6; i++) {

			var th = this.options.days[(i + this.options.offset) % 7];

			

			tr.adopt(new Element('th', { 'title': th }).appendText(th.substr(0, 1)));

		}



		// 3. day numbers

		var tbody = new Element('tbody').injectInside(table);

		var tr = new Element('tr').injectInside(tbody);



		var d = new Date(cal.year, cal.month, 1);

		var offset = ((d.getDay() - this.options.offset) + 7) % 7; // day of the week (offset)

		var last = new Date(cal.year, cal.month + 1, 0).getDate(); // last day of this month

		var prev = new Date(cal.year, cal.month, 0).getDate(); // last day of previous month

		var active = this.value(cal); // active date (if set and within curr month)

		var valid = cal.days; // valid days for curr month

		var inactive = []; // active dates set by other calendars

		var hilited = [];

		this.calendars.each(function(kal, i) {

			if (kal != cal && kal.val) {

				if (cal.year == kal.val.getFullYear() && cal.month == kal.val.getMonth()) { inactive.push(kal.val.getDate()); }



				if (cal.val) {

					for (var day = 1; day <= last; day++) {

						d.setDate(day);

						

						if ((i < cal.id && d > kal.val && d < cal.val) || (i > cal.id && d > cal.val && d < kal.val)) { 

							if (!hilited.contains(day)) { hilited.push(day); }

						}

					}

				}

			}

		}, this);

		var d = new Date();

		var today = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime(); // today obv 

		

		for (var i = 1; i < 43; i++) { // 1 to 42 (6 x 7 or 6 weeks)

			if ((i - 1) % 7 == 0) { tr = new Element('tr').injectInside(tbody); } // each week is it's own table row



			var td = new Element('td').injectInside(tr);

						

			var day = i - offset;

			var date = new Date(cal.year, cal.month, day);

			

			var cls = '';

			

			if (day === active) { cls = this.classes.active; } // active

			else if (inactive.contains(day)) { cls = this.classes.inactive; } // inactive

			else if (valid.contains(day)) { cls = this.classes.valid; } // valid

			else if (day >= 1 && day <= last) { cls = this.classes.invalid; } // invalid



			if (date.getTime() == today) { cls = cls + ' ' + this.classes.today; } // adds class for today



			if (hilited.contains(day)) { cls = cls + ' ' + this.classes.hilite; } // adds class if hilited



			td.addClass(cls);



			if (valid.contains(day)) { // if it's a valid - clickable - day we add interaction

				td.setProperty('title', this.format(date, 'D M jS Y'));

				

				td.addEvents({

					'click': function(td, day, cal) { 

						this.clicked(td, day, cal); 

					}.pass([td, day, cal], this),

					'mouseover': function(td, cls) { 

						td.addClass(cls); 

					}.pass([td, this.classes.hover]),

					'mouseout': function(td, cls) { 

						td.removeClass(cls); 

					}.pass([td, this.classes.hover])

				});

			}



			// pad calendar with last days of prev month and first days of next month

			if (day < 1) { day = prev + day; }

			else if (day > last) { day = day - last; }



			td.appendText(day);

		}

	},





	// element: helper function

	// @param el (string) element id

	// @param f (string) format string

	// @param cal (obj)



	element: function(el, f, cal) {

		if ($type(f) == 'object') { // in the case of multiple inputs per calendar

			for (var i in f) { 

				if (!this.element(i, f[i], cal)) { return false; }		

			}

			

			return true;

		}



		el = $(el);



		if (!el) { return false; }

		

		el.format = f;

		

		if (el.get('tag') == 'select') { // select elements allow the user to manually set the date via select option

			el.addEvent('change', function(cal) { this.changed(cal); }.pass(cal, this));

		}

		else { // input (type text) elements restrict the user to only setting the date via the calendar

			el.readOnly = true;

			el.addEvent('focus', function(cal) { this.toggle(cal); }.pass(cal, this));

		}



		cal.els.push(el);



		return true;

	},





	// format: formats a date object according to passed in instructions

	// @param date (obj)

	// @param f (string) any combination of punctuation / separators and d, j, D, l, S, m, n, F, M, y, Y

	// @returns string



	format: function(date, format) {

		var str = '';

		

		if (date) {

			var j = date.getDate(); // 1 - 31

      var w = date.getDay(); // 0 - 6

			var l = this.options.days[w]; // Sunday - Saturday

			var n = date.getMonth() + 1; // 1 - 12

			var f = this.options.months[n - 1]; // January - December

			var y = date.getFullYear() + ''; // 19xx - 20xx

			

			for (var i = 0, len = format.length; i < len; i++) {

				var cha = format.charAt(i); // format char

				

				switch(cha) {

					// year cases

					case 'y': // xx - xx

						y = y.substr(2);

					case 'Y': // 19xx - 20xx

						str += y;

						break;

	

					// month cases

					case 'm': // 01 - 12

						if (n < 10) { n = '0' + n; }

					case 'n': // 1 - 12

						str += n;

						break;

	

					case 'M': // Jan - Dec

						f = f.substr(0, 3);

					case 'F': // January - December

						str += f;

						break;

	

					// day cases

					case 'd': // 01 - 31

						if (j < 10) { j = '0' + j; }

					case 'j': // 1 - 31

						str += j;

						break;

	

					case 'D': // Sun - Sat

						l = l.substr(0, 3);

					case 'l': // Sunday - Saturday

						str += l;

						break;

	

					case 'N': // 1 - 7

						w += 1;

					case 'w': // 0 - 6

						str += w;

						break;



					case 'S': // st, nd, rd or th (works well with j)

						if (j % 10 == 1 && j != '11') { str += 'st'; }

						else if (j % 10 == 2 && j != '12') { str += 'nd'; }

						else if (j % 10 == 3 && j != '13') { str += 'rd'; }

						else { str += 'th'; }

						break;

	

					default:

						str += cha;

				}

			}

		}



	  return str; //  return format with values replaced

	},





	// navigate: calendar navigation

	// @param cal (obj)

	// @param type (str) m or y for month or year

	// @param n (int) + or - for next or prev



	navigate: function(cal, type, n) {

		switch (type) {

			case 'm': // month

					if ($type(cal.months) == 'array') {

						var i = cal.months.indexOf(cal.month) + n; // index of current month

						

						if (i < 0 || i == cal.months.length) { // out of range

							if (this.options.navigation == 1) { // if type 1 nav we'll need to increment the year

								this.navigate(cal, 'y', n);		

							}

		

							i = (i < 0) ? cal.months.length - 1 : 0;

						}



						cal.month = cal.months[i];

					}

					else { 

						var i = cal.month + n;

		

						if (i < 0 || i == 12) {

							if (this.options.navigation == 1) {

								this.navigate(cal, 'y', n);	

							}

		

							i = (i < 0) ? 11 : 0;

						}

						

						cal.month = i;

					}		

					break;



				case 'y': // year

					if ($type(cal.years) == 'array') {

						var i = cal.years.indexOf(cal.year) + n;



						cal.year = cal.years[i]; 

					}

					else { 

						cal.year += n;

					}						

					break;		

		}



		$extend(cal, this.values(cal));



		if ($type(cal.months) == 'array') { // if the calendar has a months select

			var i = cal.months.indexOf(cal.month); // and make sure the curr months exists for the new year



			if (i < 0) { cal.month = cal.months[0]; } // otherwise we'll reset the month

		}





		this.display(cal);

	},





	// read: compiles cal value based on array of inputs passed in

	// @param cal (obj)

	// @returns date (obj) or (null)



	read: function(cal) {

		var arr = [null, null, null];



		cal.els.each(function(el) {

			// returns an array which may contain empty values

			var values = this.unformat(el.value, el.format);

			

			values.each(function(val, i) { 

				if ($type(val) == 'number') { arr[i] = val; }

			}); 

		}, this);



		// we can update the cals month and year values

		if ($type(arr[0]) == 'number') { cal.year = arr[0]; }

		if ($type(arr[1]) == 'number') { cal.month = arr[1]; }



		var val = null;



		if (arr.every(function(i) { return $type(i) == 'number'; })) { // if valid date

			var last = new Date(arr[0], arr[1] + 1, 0).getDate(); // last day of month



			if (arr[2] > last) { arr[2] = last; } // make sure we stay within the month (ex in case default day of select is 31 and month is feb)

			

			val = new Date(arr[0], arr[1], arr[2]);

		}



		return (cal.val == val) ? null : val; // if new date matches old return null (same date clicked twice = disable)

	},



	

	// rebuild: rebuilds days + months selects

	// @param cal (obj)



	rebuild: function(cal) {

		cal.els.each(function(el) {			

			/*

			if (el.get('tag') == 'select' && el.format.test('^(F|m|M|n)$')) { // special case for months-only select

				if (!cal.options) { cal.options = el.clone(); } // clone a copy of months select

			

				var val = (cal.val) ? cal.val.getMonth() : el.value.toInt();



				el.empty(); // initialize select



				cal.months.each(function(month) {

					// create an option element

					var option = new Element('option', {

						'selected': (val == month),

						'value': this.format(new Date(1, month, 1), el.format);

					}).appendText(day).injectInside(el);

				}, this);

			}

			*/



			if (el.get('tag') == 'select' && el.format.test('^(d|j)$')) { // special case for days-only select

				var d = this.value(cal);



				if (!d) { d = el.value.toInt(); } // if the calendar doesn't have a set value, try to use value from select



				el.empty(); // initialize select



				cal.days.each(function(day) {

					// create an option element

					var option = new Element('option', {

						'selected': (d == day),

						'value': ((el.format == 'd' && day < 10) ? '0' + day : day)

					}).appendText(day).injectInside(el);

				}, this);

			}

		}, this); 

	},





	// sort: helper function for numerical sorting



	sort: function(a, b) {

		return a - b;

	},





	// toggle: show / hide calendar 

	// @param cal (obj)



	toggle: function(cal) {

		document.removeEvent('mousedown', this.fn); // always remove the current mousedown script first

			

		if (cal.visible) { // simply hide curr cal						

			cal.visible = false;

			cal.button.removeClass(this.classes.active); // active

			

			this.fx.start('opacity', 1, 0);

		}

		else { // otherwise show (may have to hide others)

			// hide cal on out-of-bounds click

			this.fn = function(e, cal) { 

				var e = new Event(e);

			

				var el = e.target;



				var stop = false;

				

				while (el != document.body && el.nodeType == 1) {

					if (el == this.calendar) { stop = true; }

					this.calendars.each(function(kal) {

						if (kal.button == el || kal.els.contains(el)) { stop = true; }

					});



					if (stop) { 

						e.stop();

						return false;

					}

					else { el = el.parentNode; }

				}

				

				this.toggle(cal);

			}.create({ 'arguments': cal, 'bind': this, 'event': true });				



			document.addEvent('mousedown', this.fn);



			this.calendars.each(function(kal) {

				if (kal == cal) {

					kal.visible = true;

					kal.button.addClass(this.classes.active); // css c-icon-active

				}

				else {

					kal.visible = false;

					kal.button.removeClass(this.classes.active); // css c-icon-active

				}

			}, this);

			

			var size = window.getScrollSize();

			

			var coord = cal.button.getCoordinates();



			var x = coord.right + this.options.tweak.x;

			var y = coord.top + this.options.tweak.y;



			// make sure the calendar doesn't open off screen

			if (!this.calendar.coord) { this.calendar.coord = this.calendar.getCoordinates(); }



			if (x + this.calendar.coord.width > size.x) { x -= (x + this.calendar.coord.width - size.x); }

			if (y + this.calendar.coord.height > size.y) { y -= (y + this.calendar.coord.height - size.y); }

			

			this.calendar.setStyles({ left: x + 'px', top: y + 'px' });



			if (window.ie6) { 

				this.iframe.setStyles({ height: this.calendar.coord.height + 'px', left: x + 'px', top: y + 'px', width: this.calendar.coord.width + 'px' }); 

			}



			this.display(cal);

			

			this.fx.start('opacity', 0, 1);

		}

	},





	// unformat: takes a value from an input and parses the d, m and y elements

	// @param val (string)

	// @param f (string) any combination of punctuation / separators and d, j, D, l, S, m, n, F, M, y, Y

	// @returns array

	

	unformat: function(val, f) {

		f = f.escapeRegExp();

		

		var re = {

			d: '([0-9]{2})',

			j: '([0-9]{1,2})',

			D: '(' + this.options.days.map(function(day) { return day.substr(0, 3); }).join('|') + ')',					

			l: '(' + this.options.days.join('|') + ')',

			S: '(st|nd|rd|th)',

			F: '(' + this.options.months.join('|') + ')',

			m: '([0-9]{2})',

			M: '(' + this.options.months.map(function(month) { return month.substr(0, 3); }).join('|') + ')',					

			n: '([0-9]{1,2})',

			Y: '([0-9]{4})',

			y: '([0-9]{2})'

		}



		var arr = []; // array of indexes



		var g = '';



		// convert our format string to regexp

		for (var i = 0; i < f.length; i++) {

			var c = f.charAt(i);

			

			if (re[c]) {

				arr.push(c);



				g += re[c];

			}

			else {

				g += c;

			}

		}



		// match against date

		var matches = val.match('^' + g + '$');

		

		var dates = new Array(3);



		if (matches) {

			matches = matches.slice(1); // remove first match which is the date



			arr.each(function(c, i) {

				i = matches[i];

				

				switch(c) {

					// year cases

					case 'y':

						i = '19' + i; // 2 digit year assumes 19th century (same as JS)

					case 'Y':

						dates[0] = i.toInt();

						break;



					// month cases

					case 'F':

						i = i.substr(0, 3);

					case 'M':

						i = this.options.months.map(function(month) { return month.substr(0, 3); }).indexOf(i) + 1;

					case 'm':

					case 'n':

						dates[1] = i.toInt() - 1;

						break;



					// day cases

					case 'd':

					case 'j':

						dates[2] = i.toInt();

						break;

				}

			}, this);

		}



		return dates;

	},





	// value: returns day value of calendar if set

	// @param cal (obj)

	// @returns day (int) or null



	value: function(cal) {

		var day = null;



		if (cal.val) {

			if (cal.year == cal.val.getFullYear() && cal.month == cal.val.getMonth()) { day = cal.val.getDate(); }

		}



		return day;

	},

	



	// values: returns the years, months (for curr year) and days (for curr month and year) for the calendar

	// @param cal (obj)

	// @returns obj	



	values: function(cal) {

		var years, months, days;



		cal.els.each(function(el) {	

			if (el.get('tag') == 'select') {		

				if (el.format.test('(y|Y)')) { // search for a year select

					years = [];



					el.getChildren().each(function(option) { // get options

						var values = this.unformat(option.value, el.format);

	

						if (!years.contains(values[0])) { years.push(values[0]); } // add to years array

					}, this);

	

					years.sort(this.sort);

				}

	

				if (el.format.test('(F|m|M|n)')) { // search for a month select

					months = []; // 0 - 11 should be



					el.getChildren().each(function(option) { // get options

						var values = this.unformat(option.value, el.format);

	

						if ($type(values[0]) != 'number' || values[0] == cal.year) { // if it's a year / month combo for curr year, or simply a month select

							if (!months.contains(values[1])) { months.push(values[1]); } // add to months array

						}

					}, this);

	

					months.sort(this.sort);

				}

				

				if (el.format.test('(d|j)') && !el.format.test('^(d|j)$')) { // search for a day select, but NOT a days only select

					days = []; // 1 - 31

					

					el.getChildren().each(function(option) { // get options

						var values = this.unformat(option.value, el.format);



						// in the special case of days we dont want the value if its a days only select

						// otherwise that will screw up the options rebuilding

						// we will take the values if they are exact dates though

						if (values[0] == cal.year && values[1] == cal.month) {

							if (!days.contains(values[2])) { days.push(values[2]); } // add to days array

						}

					}, this);

				}

			}

		}, this);

		

		// we start with what would be the first and last days were there no restrictions

		var first = 1;

		var last = new Date(cal.year, cal.month + 1, 0).getDate(); // last day of the month

		

		// if we're in an out of bounds year

		if (cal.year == cal.start.getFullYear()) {

			// in the special case of improved navigation but no months array, we'll need to construct one

			if (months == null && this.options.navigation == 2) {

				months = [];

				

				for (var i = 0; i < 12; i ++) { 

					if (i >= cal.start.getMonth()) { months.push(i); } 

				}

			}

			

			// if we're in an out of bounds month

			if (cal.month == cal.start.getMonth()) { 

				first = cal.start.getDate(); // first day equals day of bound

			}

		}		

		if (cal.year == cal.end.getFullYear()) {

			// in the special case of improved navigation but no months array, we'll need to construct one

			if (months == null && this.options.navigation == 2) {

				months = [];

				

				for (var i = 0; i < 12; i ++) { 

					if (i <= cal.end.getMonth()) { months.push(i); } 

				}

			}



			if (cal.month == cal.end.getMonth()) { 

				last = cal.end.getDate(); // last day equals day of bound

			}

		}



		// let's get our invalid days

		var blocked = this.blocked(cal);



		// finally we can prepare all the valid days in a neat little array

		if ($type(days) == 'array') { // somewhere there was a days select

			days = days.filter(function(day) {

				if (day >= first && day <= last && !blocked.contains(day)) { return day; }

			});

		}

		else { // no days select we'll need to construct a valid days array

			days = [];

			

			for (var i = first; i <= last; i++) { 

				if (!blocked.contains(i)) { days.push(i); }

			}

		}		



		days.sort(this.sort); // sorting our days will give us first and last of month



		return { 'days': days, 'months': months, 'years': years };

	},





	// write: sets calendars value to form elements

	// @param cal (obj)



	write: function(cal) {

		this.rebuild(cal);	 // in the case of options, we'll need to make sure we have the correct number of days available

		

		cal.els.each(function(el) {	// then we can set the value to the field

			el.value = this.format(cal.val, el.format); 		

		}, this);

	}

});



Calendar.implement(new Events, new Options);
