// Calendar: v2.1 by Aeron Glemann http://www.electricprism.com/aeron

Metodus.calendar = new Class({
	initialize: function(input, props) {
		this.props = Object.extend({
			classes: ['c-icon', 'c-icon-active', 'c-div', 'c-a-prev', 'c-a-next', 'c-td-heading', 'c-td-invalid', 'c-td-valid', 'c-td-hover', 'c-td-active'],
			drag: true, 
			emptycells: true,
			format: 'yyyy-mm-dd',
			future: true,
			lang: 'en', 
			navigation: 1,
			offset: 1,
			past: false,
			debug: false
		}, props || {});
		
		this.props.emptycells = (this.props.emptycells) ? 6 : 5;
		
		if (this.props.lang == 'en') {
			this.weekdays = ['S', 'S', 'M', 'T', 'W', 'T', 'F'];
			this.months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
		}
		else {
			this.weekdays = ['S', 'D', 'L', 'M', 'M', 'J', 'V'];
			this.months = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'];
		}

		this.mDays = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
		this.calendars = [];

		var d = new Date();
		this.year = d.getYear();
		if (this.year < 1900) { this.year += 1900; }
		this.month = d.getMonth() + 1;
		this.today = d.getDate();

		this.inputs = input.replace(/ /g, '').split(',');

		for (var i = 0; i < this.inputs.length; i++) {
			var input = $(this.inputs[i]);
			
			if (!input) { continue };
			
			// input may be select
			if (input.getTag() == 'select') {
				var selects = $ES('select', input.parentNode);
				selects.each(function(select) {
					if (!select.format) select.format = this.props.format;
					select.addEvent('change', function(i) { this.change(i); }.pass(i, this));
				}, this);
			} else {
				// if input is text than disable field
				var selects = null;
				input.readOnly = true;
				input.addEvent('focus', function(i) { this.toggle(i); }.pass(i, this));
			}
			
			// create calendar button
			var a = new Element('a');
			a.addClass(this.props.classes[0]).addEvent('click', function(i) { this.toggle(i); }.pass(i, this)).injectAfter(input);

			var x = a.offsetWidth + a.getStyle('margin-right').toInt();
			var y = input.getStyle('width').toInt();

			// resize input to allow space for button	
			input.setStyle('width', (y - x) + 'px');	
					
			// create calendar array
			var calendar = new Array();
			calendar['debug'] = i;
			calendar['a'] = a; // button reference
			calendar['input'] = input; // input reference
			calendar['selects'] = selects; // if selects
			if (selects) calendar['value'] = this.getValue(selects);
			else calendar['value'] = this.unformat(input.value);
			// set starting values based on calendar value
			if (calendar['value'].length) { 
				var v = calendar['value'].split(',');
				calendar['year'] = v[2].toInt();
				calendar['month'] = v[1].toInt();
			}
			// if no value set starting values based on today
			else {
				calendar['year'] = this.year;
				calendar['month'] = this.month;
			}
			calendar['display'] = 'none'; // hidden by default

			this.calendars.push(calendar); // add to calendars array
			if (selects) this.click(calendar, calendar['value']); // update select options
		}

		// create calendar element
		// there may be multiple inputs but only one calendar element per instance
		this.div = new Element('div');
		this.div.addClass(this.props.classes[2]).setStyles({ opacity: 0, position: 'absolute', zIndex: 1000 }).injectInside(document.body);

		this.fx = new Fx.Style(this.div, 'opacity');
		
		// initialize drag method
		if (this.props.drag) { new Drag.Move(this.div); }
	},

	// callable function to initialize calendar based on default values in inputs
	reset: function() {
		for (var i = 0; i < this.inputs.length; i++) {			
			var calendar = this.calendars[i];
			
			var input = calendar['input'];
			var selects = calendar['selects'];
			
			if (selects) calendar['value'] = this.getValue(selects);
			else calendar['value'] = this.unformat(input.value);
			if (calendar['value'].length) {
				var v = calendar['value'].split(',');
				calendar['year'] = v[2].toInt();
				calendar['month'] = v[1].toInt();
			}
			else {
				calendar['year'] = this.year;
				calendar['month'] = this.month;
			}
		}
	},

	toggle: function(n) {
		calendar = this.calendars[n];

		if (calendar['display'] == 'none') { // show calendar
			// hide calendar on out-of-bounds click
			document.onmousedown = (function(z) { return function(e) { 
				var e = new Event(e);
				
				var el = e.target;
				
				while (el != document.body) {
					if (el == this.div) return;
					for (i = 0; i < this.calendars.length; i++) {
						if (el == this.calendars[i]['a'] || el == this.calendars[i]['input']) return;
					}
					el = el.parentNode;
				}
				
				this.toggle(z);
			}})(n).bind(this);
			
			for (var i = 0; i < this.calendars.length; i++) {
				if (this.calendars[i] == calendar) {
					calendar['display'] = 'block';
					calendar['a'].addClass(this.props.classes[1]); // css c-icon-active
				}
				else {
					this.calendars[i]['display'] = 'none';
					this.calendars[i]['a'].removeClass(this.props.classes[1]); // css c-icon-active
				}
			}
			
			var c = calendar['a'].getCoordinates();
			
			this.div.setStyles({ left: (c['left'] + c['width']) + 'px', top: c['top'] + 'px' });

			for (var i = 0; i < this.calendars.length; i++) {
				// if another calendar has already a value set, default to that calendars value for the initial month / year
				if (i != n && this.calendars[i]['value'].length && !calendar['value'].length) {
					var v = this.calendars[i]['value'].split(',');
						
					calendar['month'] = v[1].toInt();
					calendar['year'] = v[2].toInt();
				}
			}

			this.create(calendar);
		}
		else { // hide calendar
			document.onmousedown = null;

			calendar['display'] = 'none';
			calendar['a'].removeClass(this.props.classes[1]); // css c-icon-active
		}

		if (calendar['display'] == 'none' && this.div.getStyle('opacity') > 0) this.fx.custom(1, 0);
		else if (this.div.getStyle('opacity') == 0) this.fx.custom(0, 1);
	},

	// when a select or input is changed (onclick or onchange) adjusts other selects / inputs accordingly
	// makes sure first dates do not come after last dates, also adjust option values, etc..

	change: function(cal, v) {
		if ($type(cal) == 'number') cal = this.calendars[cal];
		
		if (!v) {
			if (cal['selects']) v = this.getValue(cal['selects']);
			else v = this.unformat(cal['input'].value);
		}

		this.click(cal, v);

		v = cal['value'].split(',');
		var d = v[0].toInt();
		var m = v[1].toInt();
		var y = v[2].toInt();	
		
		// 2. ensure that other calendars contain valid values
		for (var c = 0; c < this.calendars.length; c++) {
			if (c != cal['debug'] && this.calendars[c]['value'].length) {
				v = this.calendars[c]['value'].split(',');
				v[0] = v[0].toInt();
				v[1] = v[1].toInt();
				v[2] = v[2].toInt();
				var changed = false;
					
				if (c < cal['debug']) { // less
					// if this has a year less than the lesser year
					if (y < v[2]) {
						changed = true;
						v[2] = y; // set lesser year to this year
						v[1] = m; // set lesser month to this month
						v[0] = d - 1; // set lesser day to yesterday
					}
					// if this has a month less than the lesser month
					if (y == v[2] && m < v[1]) {
						changed = true;
						v[1] = m; // set lesser month to this month
						v[0] = d - 1; // set lesser day to yesterday
					}
					// if this has a day less than the lesser day
					if (y == v[2] && m == v[1] && d < v[0]) {
						changed = true;
						v[0] = d - 1; // set lesser day to yesterday
					}
					// if yesterday is invalid day
					if (v[0] < 1) {
						v[1] = v[1] - 1;
						if (v[1] < 1) {
							v[2] = v[2] - 1;
							v[1] = 12;
						}
						v[0] = (this.isLeapYear(v[2]) && v[1] == 2) ? 29 : this.mDays[v[1] - 1];
					}
					
					if (changed) this.click(this.calendars[c], v.join(','));
				}
				
				if (c > cal['debug']) { // greater				
					// if this has a year greater than the greater year
					if (y > v[2]) {
						changed = true;
						v[2] = y; // set greater year to this year
						v[1] = m; // set greater month to this month
						v[0] = d + 1; // set greater day to tomorrow
					}
					// if this has a month greater than the greater month
					if (y == v[2] && m > v[1]) {
						changed = true;
						v[1] = m; // set greater month to this month
						v[0] = d + 1; // set greater day to tomorrow
					}
					// if this has a day greater than the greater day
					if (y == v[2] && m == v[1] && d > v[0]) {
						changed = true;
						v[0] = d + 1; // set greater day to tomorrow
					}
					// if tomorrow is invalid day
					if (v[0] > ((this.isLeapYear(v[2]) && v[1] == 2) ? 29 : this.mDays[v[1] - 1])) {
						v[1] = v[1] + 1;
						if (v[1] > 12) {
							v[2] = v[2] + 1;
							v[1] = 1;
						}
						v[0] = 1;
					}
					
					if (changed) this.click(this.calendars[c], v.join(','));
				}
			}
		}		
	},

    // just formats the incoming value for the calendar
	// in the case of selects based calendar, must adjust options

   	click: function(cal, v) {
		v = v.split(',');

		if (cal['selects']) {
			v[0] = v[0].toInt(); // day
			v[1] = v[1].toInt(); // month
			v[2] = v[2].toInt(); // year

			// set first selectable day of the month
			var start = 1;
			if (v[1] == this.month && !this.props.past) start = this.today;
	
			// set last selectable day of the month
			var stop = (this.isLeapYear(v[2]) && v[1] == 2) ? 29 : this.mDays[v[1] - 1]; // account for leap year
			if (v[1] == this.month && !this.props.future) stop = this.today;
	
			// adjust day value if not in range of allowed days
			if (v[0] < start) v[0] = start;
			if (v[0] > stop) v[0] = stop;
	
			for (var s = 0; s < cal['selects'].length; s++) {
				var select = cal['selects'][s];
				select.value = this.format(v[0], v[1], v[2], select.format);
				// if day select
				if (select.format == 'd') {
					select.setHTML(''); // initialize select
					
					// for each day
					for (var day = start; day <= stop; day++) {
						// create an option element
						var option = new Element('option');
						if (day == (v[0])) option.selected = true;
						option.setProperty('value', String(day)).appendText(String(day)).injectInside(select);
					}
				}
			}
		}
		else cal['input'].value = this.format(v[0], v[1], v[2]);

		cal['value'] = v[0] + ',' + v[1] + ',' + v[2];
		cal['year'] = v[2];
		cal['month'] = v[1];
	},

	// navigation functions	
	prev: function(cal, d) {
		if (d == 'm') {
			cal['month']--;
			if (cal['month'] < 1) {
				cal['month'] = 12;
				cal['year']--;
			}
		}
		else { cal['year']--; }
		
		this.create(cal);		
	},

	next: function(cal, d) {
		if (d == 'm') {
			cal['month']++;
			if (cal['month'] > 12) {
				cal['month'] = 1;
				cal['year']++;			
			}
		}
		else { cal['year']++; }

		this.create(cal);
	},

	// formatting functions	
	unformat: function(v, f) {
		if (!v || !v.length || v == this.props.format) return '';
		
		f = (f) ? f : this.props.format;
		var u = '';

		var x = new Array();
		var y = new Array();		
		
		while (f.length) {
			var c = f.substr(0, 1);
			f = f.substr(1);

			if (/d|m|y/.test(c)) {
				var n = 0;
				while (f.substr(0, 1) == c) {
					n++;
					f = f.substr(1);
				}
				if (n > 0) {
					var d = v.substr(u.length, (n + 1));
				}
				else if (f.length == 0) {
					var d = v.substring(u.length, v.length);
				}
				else if (!/d|m|y/.test(f.substr(0, 1))) {
					var d = v.substring(u.length, v.indexOf(f.substr(0, 1), u.length));
				}
				else {
					if (this.props.lang == "en") return alert("Parameter 'format' must contain exact units (mm, dd, etc) or separator characters (/, -, etc)");
					else return alert("El parametro 'format' debe contener unidades exactas (mm, dd, etc) o caracteres tipo separador (/, -, etc)");
				}

				u = u + d;

				x.push(d);
				y.push(c);
			}
			else u = u + c;
		}

		f = "d,m,y";

		for (var k = 0; k < x.length; k++) {
			f = f.replace(y[k], x[k]);		
		}

		f = f.replace(/d|m|y/g,'');

		return f
	},

	// format can be:
	// d (1 - 31), dd (01 - 31), ddd (001 - 031) etc..
	// m (1 - 12), mm (01 - 12), mmm (001 - 012) etc..
	// y or yyyy (1999, 2000, etc..), yy (99, 00, etc..), yyy (999, 000, etc..) etc..
	// differentiated by seperator characters (/- etc..) such as 'dd/mm/yyyy' or 'ddmmyyyy' or 'd-m-y' or 'mm,y' or whatever
	format: function(d, m, y, f) {
		// if no format param is passed use the default value
		f = (f) ? f : this.props.format;

		// values for day, month, year
		var x = new Array(d, m, y);
		// symbols for day, month, year
		var y = new Array('d', 'm', 'y');

		// iterate for each date measure (d, m, y)
		for (var k = 0; k < 3; k++) {
			var num = 0; // number of times the symbol appears
			var p = f.indexOf(y[k]);
			while (p != -1) {
				num++;
				p = f.indexOf(y[k], p + 1);
			}
			if (num > 1) { // exact specification - ie dd, mm, yyyy etc..
				x[k] = String(x[k]); 
				while (x[k].length < num) x[k] = '0' + x[k]; // pad the value if needed
				re = new RegExp(y[k] + '+');
				f = f.replace(re, x[k].substring(x[k].length - num, x[k].length)); // replace instance of symbol in format
			}
			else f = f.replace(y[k], x[k]); // simple specifications - ie d, m, y
		}

	  return f; //  return format with values replaced
	},

	// helper functions
	isLeapYear: function(year) {
		return (!(year % 4) && (year < 1582 || year % 100 || !(year % 400))) ? true : false;
	},

	getWeekday: function(year, days) {
		var d = days;
		if (year) d += (year - 1) * 365;
		for (var i = 1; i < year; i++) if (this.isLeapYear(i)) d++;
		if (year > 1582 || (year == 1582 && days >= 277)) d -= 10;
		if (d) d = (d - this.props.offset) % 7;
		else if (this.props.offset) d += 7 - this.props.offset;

		return d;
	},

	getValue: function(selects) {
		// day, month, year
		var b = new Array(3);

		selects.each(function(select) {
			// get select format (d, m, y)
			var f = select.format;	
			// returns an array [d, m, y] may contain empty values
			var a = this.unformat(select.value, f).split(',');

			// combine arrays to overwrite empty values
			for (var i = 0; i < 3; i++) if (a[i].length) b[i] = a[i];			
		}, this);

		// return array as string
		return b.join(',');
	},
	
	// calendar create functions	
	create: function(cal) {
		var n = cal['debug']; // index

		var inThisYear = false;
		var inThisMonth = false;
		var thisDay = false;
		var inLessMonth = false;
		var lessDay = false;
		var inGreaterMonth = false;
		var greaterDay = false;

		for (var i = 0; i < this.calendars.length; i++) {
			if (this.calendars[i]['value'].length) {
				var v = this.calendars[i]['value'].split(',');
				v[0] = v[0].toInt();
				v[1] = v[1].toInt();
				v[2] = v[2].toInt();

				if (i != n) {
					if (i < n && cal['year'] == v[2] && cal['month'] == v[1]) {
						inLessMonth = true;  
						lessDay = v[0];
					}
					if (i > n && cal['year'] == v[2] && cal['month'] == v[1]) {
						inGreaterMonth = true; 
						greaterDay = v[0];
					}
				}
				else {
					if (cal['year'] == this.year) { 
						inThisYear = true;
						if (cal['month'] == this.month) inThisMonth = true; 
					}
					
					if (cal['year'] == v[2] && cal['month'] == v[1]) thisDay = v[0]; 
				}
			}
		}
		
		this.mDays[1] = (this.isLeapYear(cal['year'])) ? 29 : 28;
		var days = 0;
		for (var i = 0; i < cal['month'] - 1; i++) days += this.mDays[i];

		// if ((cal['year'] == this.year) && (cal['month'] == this.month)) inThisMonth = true;

		// this bit here calculates what day of the week the days for the month start at
		var start = days;
		if (cal['year']) start += (cal['year'] - 1) * 365;
		for (var i = 1; i < cal['year']; i++) if (this.isLeapYear(i)) start++;
		if (cal['year'] > 1582 || (cal['year'] == 1582 && days >= 277)) start -= 10;
		if (start) start = (start - this.props.offset) % 7;
		else if (this.props.offset) start += 7 - this.props.offset;

		var stop = this.mDays[cal['month'] - 1];

		var table = new Element('table');
		
		var tbody = new Element('tbody');
		tbody.injectInside(table);
		
		var tr = new Element('tr');
		tr.injectInside(tbody);
		
		var th = new Element('th');
		if (!inThisMonth || this.props.past) {
			var a = new Element('a');
			a.onclick = (function (kal) { return function() { this.prev(kal, 'm'); }})(cal).bind(this);   
			a.addClass(this.props.classes[3]).appendText(unescape('%3C')).injectInside(th);
		}	
		if (!inThisMonth || this.props.future) {
			var a = new Element('a');
			a.onclick = (function (kal) { return function() { this.next(kal, 'm'); }})(cal).bind(this);   
			a.addClass(this.props.classes[4]).appendText(unescape('%3E')).injectInside(th);
		}		

		if (this.props.navigation == 1) { // old style navigation
			th.setProperty('colSpan', '7').appendText(this.months[cal['month'] - 1] + ' ' + cal['year']).injectInside(tr);
		} 
		if (this.props.navigation == 2) { // funky new style navigation
			th.setProperty('colSpan', '4').appendText(this.months[cal['month'] - 1]).injectInside(tr); 
			
			var th = new Element('th');
			if (!inThisYear || this.props.past) {
				var a = new Element('a');
				a.onclick = (function (kal) { return function() { this.prev(kal, 'y'); }})(cal).bind(this);   
				a.addClass(this.props.classes[3]).appendText(unescape('%3C')).injectInside(th);
			}	
			if (!inThisYear || this.props.future) {
				var a = new Element('a');
				a.onclick = (function (kal) { return function() { this.next(kal, 'y'); }})(cal).bind(this);   
				a.addClass(this.props.classes[4]).appendText(unescape('%3E')).injectInside(th);
			}		
			th.setProperty('colSpan', '3').appendText(cal['year']).injectInside(tr);
		}

		var tr = new Element('tr');
		tr.injectInside(tbody);
		
		for (var i = 0; i <= 6; i++) {
			var d = (i + this.props.offset) % 7;
			this.td(this.weekdays[d], this.props.classes[5], cal, tr); // css c-td-heading
		}

		var daycount = 1;
		var rowcount = 0;

		while(daycount <= stop || rowcount < this.props.emptycells) {
			var tr = new Element('tr');
			tr.injectInside(tbody);
			
			rowcount++;	
			var wdays = 0;		
			
			for (var i = 0; i <= 6; i++) {
				if ((inThisMonth && daycount < this.today && !this.props.past) || (inThisMonth && daycount > this.today && !this.props.future)) cls = this.props.classes[6]; // invalid
				else if ((inLessMonth && daycount == lessDay) || (inGreaterMonth && daycount == greaterDay)) cls = this.props.classes[9]; // active
				else if (daycount == thisDay) cls = this.props.classes[8]; // hover
				else cls = this.props.classes[7]; // valid
				
				if ((daycount == 1 && i < start) || daycount > stop) txt = null;
				else {
					txt = daycount;
					daycount++;
					wdays++;
				}
				this.td(txt, cls, cal, tr);
			}
		}

		this.div.setHTML('');
		
		table.injectInside(this.div);

		if (this.props.debug) alert('<div class="' + this.div.className + '">' + this.div.innerHTML + '</div>');
	},
	
	td: function(txt, cls, cal, tr) {
		var td = new Element('td');
		td.appendText(((txt) ? txt : unescape('%20'))).injectInside(tr);
		
		if (txt) td.addClass(cls);

		if (txt && cls == this.props.classes[7]) { // css c-td-valid
			td.addEvent('mouseover', function(td, cls) { td.addClass(cls); }.pass([td, this.props.classes[8]]));
			td.addEvent('mouseout', function(td, cls) { td.removeClass(cls); }.pass([td, this.props.classes[8]]));
			
			fn = function(txt, cal) {
				this.change(cal, txt + ',' + cal['month'] + ',' + cal['year']);
				this.toggle(cal['debug']);
			}.pass([txt, cal], this);
			
			td.addEvent('click', fn);
		}
	}
});
