// Metodus: Internet Productivo, Usabilidad y Desarrollo Web http://www.metodus.com
// License: http://www.opensource.org/licenses/mit-license.php
// Calendar: v2.0 by Aeron Glemann http://www.electricprism.com/aeron

Metodus.calendar = Class.create();
Metodus.calendar.prototype = {
	initialize: function(input, props) {
		this.props = {
			debug: false,
			offset: 1,
			lang: 'en',
			past: false,
			future: true,
			format: 'mm/dd/yyyy',
			drag: true,
			classes: new Array('c-icon', 'c-icon-active', 'c-div', 'c-th-prev', 'c-th-next', 'c-td-heading', 'c-td-invalid', 'c-td-valid', 'c-td-hover', 'c-td-active')
		}
		Object.extend(this.props, props || {});	

		if (this.props.lang == 'en') {
			this.weekdays = new Array('S', 'S', 'M', 'T', 'W', 'T', 'F');
			this.months = new Array('January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December');
		}
		else {
			this.weekdays = new Array('S', 'D', 'L', 'M', 'M', 'J', 'V');
			this.months = new Array('Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre');
		}

		this.mDays = new Array(31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
		this.calendars = new Array();

		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 (i = 0; i < this.inputs.length; i++) {
			// create calendar button
			a = document.createElement('a');
			a.className = this.props.classes[0]; // css c-icon
			a.onclick = (function(z) { return function() { this.toggle(z); }})(i).bind(this);

			input = $(this.inputs[i]);
			// input may be select
			if (input.tagName == 'SELECT') {
				pn = input.parentNode;
				// in the case of selects there may be more than one
				selects = pn.getElementsByTagName('select');
				for (j = 0; j < selects.length; j++) {
					select = selects[j];
					if (!select.format) select.format = this.props.format;
					select.onchange = (function(z) { return function() { this.change(z); }})(i).bind(this);					
				}
			} else {
				selects = false;
				// if input is text than disable field
				input.readOnly = true;
				input.onfocus = (function(z) { return function() { this.toggle(z); }})(i).bind(this);
			}
			// append calendar button
			input.parentNode.insertBefore(a, input.nextSibling);

			x = a.offsetWidth + this.getStyle(a, 'marginRight');
			y = this.getStyle(input, 'width');

			// resize input to allow space for button	
			input.style.width = (y - x) + 'px';			

			// create calendar array
			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) { 
				v = calendar['value'].split(',');
				calendar['year'] = parseInt(v[2]);
				calendar['month'] = parseInt(v[1]);
			}
			// 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 = document.createElement('div');
		this.div.className = this.props.classes[2]; // css c-div
		this.div.style.position = 'absolute';
		this.div.style.zIndex = 1000;
		if (window.fx) {	
			this.fx = new fx.Opacity(this.div, { duration: 500 });
			this.fx.hide();
		}
		else this.div.style.display = 'none';
		
		// initialize drag method
		if (this.props.drag) {
			this.div.onmousedown = 	this.dragStart.bind(this);
			this.div.onDragStart = new Function();
			this.div.onDragEnd = new Function();
			this.div.onDrag = new Function();
		}

		// add calendar element to document
		document.body.appendChild(this.div);
	},

	// callable function to initialize calendar based on default values in inputs
	reset: function() {
		for (i = 0; i < this.inputs.length; i++) {			
			calendar = this.calendars[i];
			
			input = calendar['input'];
			selects = calendar['selects'];
			
			if (selects) calendar['value'] = this.getValue(selects);
			else calendar['value'] = this.unformat(input.value);
			if (calendar['value'].length) {
				v = calendar['value'].split(',');
				calendar['year'] = parseInt(v[2]);
				calendar['month'] = parseInt(v[1]);
			}
			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) { 
				if (!e) e = window.event;
				if (e.target) el = e.target;
				else if(e.srcElement) el = e.srcElement;
				if (el.nodeType == 3) el = el.parentNode; // defeat safari bug ??
				
				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 (i = 0; i < this.calendars.length; i++) {
				if (this.calendars[i] == calendar) {
					calendar['display'] = 'block';
					calendar['a'].className = this.props.classes[0] + ' ' + this.props.classes[1]; // css c-icon and c-icon-active
				}
				else {
					this.calendars[i]['display'] = 'none';
					this.calendars[i]['a'].className = this.props.classes[0]; // css c-icon
				}
			}
			
			p = Position.cumulativeOffset(calendar['a']);
			p[0] = p[0] + this.getStyle(calendar['a'], 'width');
	
			this.div.style.left = p[0] + 'px';
			this.div.style.top = p[1] + 'px';

			for (i = 0; i < this.calendars.length; i++) {
				if (i != n && this.calendars[i]['value'].length && !calendar['value'].length) {
					d = this.calendars[i]['value'].split(',');
						
					calendar['month'] = parseInt(d[1]);
					calendar['year'] = parseInt(d[2]);
				}
			}

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

			calendar['display'] = 'none';
			calendar['a'].className = this.props.classes[0]; // css c-icon
		}

		if (window.fx) {
			if (calendar['display'] == 'none' && this.fx.now > 0) this.fx.custom(1, 0);
			else if (this.fx.now == 0) this.fx.custom(0, 1);
		}
		else this.div.style.display = calendar['display'];		
	},

	change: function(cal, v) {
		if (typeof(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(',');
		d = parseInt(v[0]);
		m = parseInt(v[1]);
		y = parseInt(v[2]);		
		
		// 2. ensure that other calendars contain valid values
		for (c = 0; c < this.calendars.length; c++) {
			if (c != cal['debug']) {
				v = this.calendars[c]['value'].split(',');
				v[0] = parseInt(v[0]);
				v[1] = parseInt(v[1]);
				v[2] = parseInt(v[2]);
				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(','));
				}
			}
		}		
	},

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

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

			// set first selectable day of the month
			start = 1;
			if (v[1] == this.month && !this.props.past) start = this.today;
	
			// set last selectable day of the month
			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 (s = 0; s < cal['selects'].length; s++) {
				select = cal['selects'][s];
				select.value = this.format(v[0], v[1], v[2], select.format);
				// if day select
				if (select.format == 'd') {
					select.innerHTML = ''; // initialize select
					
					// for each day
					for (day = start; day <= stop; day++) {
						// create an option element
						option = document.createElement('option');
						option.value = String(day);
						if (day == (v[0])) option.selected = 'selected';
						option.appendChild(document.createTextNode(String(day)));
						
						select.appendChild(option);
					}
				}
			}
		}
		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) {
		cal['month']--;
		if (cal['month'] < 1) {
			cal['month'] = 12;
			cal['year']--;
		}
		
		this.create(cal);		
	},

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

		this.create(cal);
	},

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

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

			if (/d|m|y/.test(c)) {
				n = 0;
				while (f.substr(0, 1) == c) {
					n++;
					f = f.substr(1);
				}
				if (n > 0) {
					d = v.substr(u.length, (n + 1));
				}
				else if (f.length == 0) {
					d = v.substring(u.length, v.length);
				}
				else if (!/d|m|y/.test(f.substr(0, 1))) {
					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 (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
		x = new Array(d, m, y);
		// symbols for day, month, year
		y = new Array('d', 'm', 'y');

		// iterate for each date measure (d, m, y)
		for (k = 0; k < 3; k++) {
			num = 0; // number of times the symbol appears
			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;
	},

	getStyle: function(el, prop) {
		if (el.style[prop]) {
			v = el.style[prop];
		} 
		else if (el.currentStyle) {
			v = el.currentStyle[prop];
		} 
		else if (document.defaultView && document.defaultView.getComputedStyle) {
			re = /([A-Z])/g;
			prop = prop.replace(re, "-$1");
			prop = prop.toLowerCase();
			v = document.defaultView.getComputedStyle(el,"").getPropertyValue(prop);
		} 
		else return null;
		
		return parseInt(v);
	},

	getEvent: function(e) {
		if (typeof e == 'undefined') e = window.event;
		if (typeof e.layerX == 'undefined') e.layerX = e.offsetX;
		if (typeof e.layerY == 'undefined') e.layerY = e.offsetY;
		
		return e;
	},

	getWeekday: function(year, days) {
		d = days;
		if (year) d += (year - 1) * 365;
		for (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
		b = new Array(3);

		// iterate for each select
		for (s = 0; s < selects.length; s++) {
			select = selects[s];

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

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

		// return array as string
		return b.join(',');
	},

	// drag functions	
	dragStart: function(e) {
		e = this.getEvent(e);
		y = parseInt(this.div.style.top);
		x = parseInt(this.div.style.left);
		
		this.div.onDragStart(x, y);
		this.div.lastMouseX = e.clientX;
		this.div.lastMouseY = e.clientY;
		
		document.onmousemove = this.drag.bind(this);
		document.onmouseup = this.dragEnd.bind(this);
		
		return false;
	},
	
	drag: function(e) {
		e = this.getEvent(e);
		
		ey = e.clientY;
		ex = e.clientX;
		x = parseInt(this.div.style.left);
		y = parseInt(this.div.style.top);
		
		nx = x + ((ex - this.div.lastMouseX));
		ny = y + ((ey - this.div.lastMouseY));
		
		this.div.style.left = nx + "px";
		this.div.style.top = ny + "px";
		this.div.lastMouseX = ex;
		this.div.lastMouseY = ey;		
		this.div.onDrag(nx, ny);
		
		return false;
	},
	
	dragEnd: function() {
		document.onmousemove = null;
		document.onmouseup = null;
		
		this.div.onDragEnd(parseInt(this.div.style.left), parseInt(this.div.style.top));
	},
	
	// calendar create functions	
	create: function(cal) {
		n = cal['debug']; // index

		inLessMonth = inGreaterMonth = invalidMonth = activeDay = hoverDay = false;

		for (i = 0; i < this.calendars.length; i++) {
			v = this.calendars[i]['value'].split(',');
			v[0] = parseInt(v[0]);
			v[1] = parseInt(v[1]);
			v[2] = parseInt(v[2]);
			
			if (i != n && this.calendars[i]['value'].length) {
				activeDay = v[0];
					
				if (i < n) {
					if (cal['year'] < v[2] || (cal['year'] == v[2] && cal['month'] < v[1])) invalidMonth = true;
					if (cal['year'] == v[2] && cal['month'] == v[1]) inLessMonth = true;
				}
				if (i > n) {
					if (cal['year'] > v[2] || (cal['year'] == v[2] && cal['month'] > v[1])) invalidMonth = true;
					if (cal['year'] == v[2] && cal['month'] == v[1]) inGreaterMonth = true;
				}
			}
			else if (cal['year'] == v[2] && cal['month'] == v[1]) hoverDay = v[0];
		}
		
		(this.isLeapYear(cal['year'])) ? this.mDays[1] = 29 : this.mDays[1] = 28;
		for (i = days = 0; i < cal['month'] - 1; i++) days += this.mDays[i];

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

		start = days;
		if (cal['year']) start += (cal['year'] - 1) * 365;
		for (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;

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

		table = document.createElement('table');
		tbody = document.createElement('tbody');
		
		tbody.appendChild(this.th(cal, inThisMonth, inLessMonth, inGreaterMonth));

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

		daycount = 1;
		rowcount = 0;

		while(daycount <= stop || rowcount < 6) {
			tr = document.createElement('tr');
			rowcount++;			
			
			for (i = wdays = 0; i <= 6; i++) {
				// if ((inThisMonth && daycount < this.today && !this.props.past) || (inThisMonth && daycount > this.today && !this.props.future) || (inLessMonth && daycount < activeDay) || (inGreaterMonth && daycount > activeDay) || invalidMonth) cls = this.props.classes[6]; // css c-td-invalid
				if ((inThisMonth && daycount < this.today && !this.props.past) || (inThisMonth && daycount > this.today && !this.props.future)) cls = this.props.classes[6]; // css c-td-invalid
				else if ((inLessMonth && daycount == activeDay) || (inGreaterMonth && daycount == activeDay)) cls = this.props.classes[9]; // css c-td-active
				else if (daycount == hoverDay) cls = this.props.classes[8]; // css c-td-hover
				else cls = this.props.classes[7]; // css c-td-valid
				
				if ((daycount == 1 && i < start) || daycount > stop) txt = null;
				else {
					txt = daycount;
					daycount++;
					wdays++;
				}
				tr.appendChild(this.td(txt, cls, cal));
			}
			tbody.appendChild(tr);	
		}
		table.appendChild(tbody);

		if (this.div.childNodes.length) this.div.removeChild(this.div.firstChild);
		this.div.appendChild(table);

		if (this.props.debug) alert(this.div.innerHTML);
	},
	
	th: function(cal, inThisMonth, inLessMonth, inGreaterMonth) {
		tr = document.createElement('tr');
		
		th = document.createElement('th');
		th.className = this.props.classes[3]; // css c-th-prev
		// if ((!inThisMonth || this.props.past) && !inLessMonth) {
		if (!inThisMonth || this.props.past) {
			a = document.createElement('a');
			a.appendChild(document.createTextNode(unescape('%3C')));
			a.onclick = (function(z) { return function() { this.prev(z); }})(cal).bind(this);
			th.appendChild(a);
		}	
		tr.appendChild(th);
				
		th = document.createElement('th');
		th.colSpan = '5';
		th.appendChild(document.createTextNode(this.months[cal['month'] - 1] + ' ' + cal['year']));
		tr.appendChild(th);

		th = document.createElement('th');
		th.className = this.props.classes[4]; // css c-th-next
		// if ((!inThisMonth || this.props.future) && !inGreaterMonth) {
		if (!inThisMonth || this.props.future) {
			a = document.createElement('a');
			a.appendChild(document.createTextNode(unescape('%3E')));
			a.onclick = (function(z) { return function() { this.next(z); }})(cal).bind(this);
			th.appendChild(a);
		}
		tr.appendChild(th);
		
		return tr;
	},

	td: function(txt, cls, cal) {
		td = document.createElement('td');
		if (txt) td.className = cls;

		if (txt && cls == this.props.classes[7]) { // css c-td-valid
			n = cal['debug'];
			
			td.onmouseover = (function(z) { return function() { this.className = z; }})(this.props.classes[8]); // css c-td-hover
			td.onmouseout = function() { this.className = cls; };
			td.onclick = (function(z) { return function() {
				this.change(cal, txt + ',' + cal['month'] + ',' + cal['year']);
				this.toggle(z);
			}})(n).bind(this); 
		}
		text = (txt) ? document.createTextNode(txt) : document.createTextNode(unescape('%20')); // nbsp
		td.appendChild(text);

		return td;
	}
}