/**
 * FlumCalendar class, creates a flexible javascript calendar.
 *
 * @param mixed element Either an ID or a DOM-Node to use as the root for the calendar.
 * @param int year The year to initialize with, leave null to use current year.
 * @param int month The month to initialize with, leave null to use current month.
 * @param array config The main config array.
 *	{
 *		renderOnLoad: true, // automatically render the calendar once the DOM is loaded
 *		showMonth: true, // shows the month+year above the calendar
 *		showHeader: true, // shows Mo/Tu/... in the top row
 *		showFooter: true, // shows Mo/Tu/... in the footer row
 *		menu: {
 *			prevNextAtTop: true, // shows the previous/next buttons at the top
 *			prevNextAtBottom: true, // shows the previous/next buttons at the bottom
 *			prevNextInline: true // shows the previous/next buttons in the top/bottom line if possible
 *		},
 *		specialDays: { // additional classes for special days
 *			week: {
 *				0: 'weekend', // 0 = Su, 1 = Mo, ... 6 = Sa
 *				6: 'weekend'
 *			}
 *		},
 *		events: {
 *			init: { // called after the calendar is initialized
 *				0: function () { ... }
 *				1: function () { ... }
 *			},
 *			rendered: {}, // called after the calendar got rendered
 *			previousMonth: {}, // called when the month got changed backwards
 *			nextMonth: {}, // called when the month got changed forwards
 *			hoverActive: { 0: function (node) { ... } }, // when the mouse hovers over an item
 *			hoverInactive: { 0: function (node) { ... } }, // when the mouse hovers away from an item
 *		}
 *	}
 * @param array events If a list of events already exists you can use this, but the preferred way is to use the addEventForDay function.
 * @return Object The new Calendar object.
 */
function FlumCalendar(element, year, month, config, events) {
	// if events should be preset
	if (!events) {
		this.events = new Array();
	} else {
		this.events = events;
	}
	
	// config array holding some configuration variables
	if (!config) {
		this.config = new Array();
	} else {
		this.config = config;
	}
	
	/**
	 * Sets the year and month.
	 *
	 * @param int year The year to display, may be left null for the current year.
	 * @param int month The month to display, may be left null for the current month.
	 * @return Object Self-reference to chain functions.
	 */
	this.setMonth = function (year, month) {
		var today = new Date();
		if (year != undefined && year != null) {
			this.activeYear = year;
		} else {
			this.activeYear = today.getUTCFullYear();
		}
		if (month != undefined && month != null) {
			this.activeMonth = month-1;
		} else {
			this.activeMonth = today.getUTCMonth();
		}
		return this;
	};
	
	/**
	 * Initialize the DOM-Element. Before this is called the object cannot render!
	 * Must not be called before the DOM is ready.
	 *
	 * @return Object Self-reference to chain functions.
	 */
	this.initElement = function () {
		this.element = $(element);
		this.inactive = false;
		try {
			// walks the rootline back to the body tag and removes any overflow: hidden
			this.element.ancestors().each(
				function (n) {
					if (n.tagName == 'HTML' || n.tagName == 'BODY') return;
					if (n.getStyle('overflow').match(/hidden/) || n.getStyle('overflow-x').match(/hidden/) || n.getStyle('overflow-y').match(/hidden/)) {
						n.addClassName('overflowVisible');
					}
				}
			);
		} catch (e) {
			/* alert(e); */
		}
		if (this.config['renderOnLoad']) {
			this.render();
		}
		
		this.raiseEvent('init');
		return this;
	};
	
	/**
	 * Returns the amount of days that are in that month.
	 *
	 * @param int month The month to check for.
	 * @param int year The year of the month.
	 * @return int The amount of days in the given month.
	 */
	this.daysOfTheMonth = function (month, year) {
		// calculate the amount of days the current month has
		var daysOfMonth = 31;
		if (month == 1) {
			daysOfMonth = 28;
			if (this.isLeapYear(year)) daysOfMonth++;
		} else {
			// subtract 1 day if not a 31 day month
			daysOfMonth -= ((month + (month > 6)) % 2);
		}
		return daysOfMonth;
	};
	
	/**
	 * Starts the rendering process and adds the event handlers.
	 *
	 * @return Object Self-reference to chain functions.
	 */
	this.render = function () {
		if (this.inactive) {
			throw 'DOM not initialized!';
		}
		// renders the html for the calendar
		this.element.innerHTML = this.renderCalendar();
		
		// event handler to jump to the previous month
		this.element.select('.previousMonth').each(
			(function (n) {
				n.observe('click',
					(function (e) {
						this.activeMonth--;
						if (this.activeMonth < 0) {
							this.activeYear--;
							this.activeMonth = 11;
						}
						this.render();
						this.raiseEvent('previousMonth');
					}).bind(this)
				);
			}).bind(this)
		);
		
		// event handler to jump to the next month
		this.element.select('.nextMonth').each(
			(function (n) {
				n.observe('click',
					(function (e) {
						this.activeMonth++;
						if (this.activeMonth > 11) {
							this.activeYear++;
							this.activeMonth = 0;
						}
						this.render();
						this.raiseEvent('nextMonth');
					}).bind(this)
				);
			}).bind(this)
		);
		
		// event handler for the hovers
		this.element.select('td.event').each(
			(function (n) {
				n.observe('mouseover',
					(function (e) {
						this.element.select('.hover').each(
							function (nd) {
								nd.removeClassName('hover');
							}
						);
						n.addClassName('hover');
						this.raiseEvent('hoverActive', n);
					}).bind(this)
				);
				
				n.observe('mouseout',
					(function (e) {
						n.removeClassName('hover');
						this.raiseEvent('hoverInactive', n);
					}).bind(this)
				);
			}).bind(this)
		);
		
		this.element.select('td.event a.dayLink').each(
			(function (n) {
				n.observe('click',
					(function (e) {
						e.element().blur();
						e.stop();
					}).bind(this)
				);
			}).bind(this)
		);
		
		this.raiseEvent('rendered');
		return this;
	};
	
	/**
	 * Renders the html for the calendar.
	 *
	 * @return string The HTML for the calendar.
	 */
	this.renderCalendar = function() {
		// calculate the offset of the day
		var activeDate = new Date();
		
		activeDate.setUTCFullYear(this.activeYear);
		activeDate.setUTCMonth(this.activeMonth);
		activeDate.setUTCDate(1);
		activeDate.setUTCHours(2);
		
		var dayOffset = activeDate.getUTCDay() - this.firstDay;
		
		// calculate the amount of days the current month has
		var daysOfMonth = 31;
		if (this.activeMonth == 1) {
			daysOfMonth = 28;
			if (this.isLeapYear(this.activeYear)) daysOfMonth++;
		} else {
			// subtract 1 day if not a 31 day month
			daysOfMonth -= ((this.activeMonth + (this.activeMonth > 6)) % 2);
		}
		
		var html = '';
		
		// render the month/year
		if (this.config['showMonth']) {
			html += '<span class="month">' + this.monthList[this.activeMonth] + '</span> <span class="year">' + this.activeYear + '</span>';
		}
		
		// render the table
		html += '<table class="flumCalendar">';
		
		// only show header if necessary
		if (this.config['showHeader']) {
			html += '<thead><tr>';
			
			for (var c = 0; c < 7; c++) {
				html += '<th>' + this.dayList[((c + this.firstDay) % 7)] + '</th>';
			}
			
			html += '</tr></thead>';
		}
		
		html += '<tbody>';
		
		var menuConfig;
		if (isArray(this.config['menu'])) {
			menuConfig = this.config['menu'];
		} else {
			menuConfig = new Array();
		}
		
		if (dayOffset < 0) dayOffset += 7;
		
		// if the previous/next buttons can fit on the last line
		var menuFits = ((daysOfMonth + dayOffset - 1) % 7) < 5;
		var menuFitsTop = dayOffset > 1;
		
		// dont show the prev/next inline
		if (!menuConfig['prevNextInline']) {
			menuFits = false;
			menuFitsTop = false;
		}
		
		// dont show the prev/next
		if (!menuConfig['prevNextAtBottom']) menuFits = false;
		if (!menuConfig['prevNextAtTop']) menuFitsTop = false;
		
		if (!menuFitsTop && menuConfig['prevNextAtTop']) {
			html += '<tr><td class="previousMonth">&nbsp;</td><td class="nextMonth">&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td></tr>';
		}
		
		var today = new Date();
		this.today = today;
		this.checkToday = activeDate.getUTCMonth() == today.getUTCMonth() && activeDate.getUTCFullYear() == today.getUTCFullYear();
		
		var currentDay = 1;
		for (var c = 0; currentDay <= daysOfMonth; c += 7) {
			// creates a line for each week
			html += '<tr>';
			
			for (var i = c; i < (c + 7); i++) {
				// goes through a week
				var elValue = '<td>&nbsp;</td>';
				if (i >= dayOffset) {
					if (currentDay <= daysOfMonth) {
						activeDate.setUTCDate(currentDay);
						elValue = this.renderElement(currentDay, activeDate.getUTCDay());
						currentDay++;
					} else if (menuFits) {
						if (i == (c + 5)) {
							elValue = '<td class="previousMonth">&nbsp;</td>';
						} else if (i == (c + 6)) {
							elValue = '<td class="nextMonth">&nbsp;</td>';
						}
					}
				} else if (menuFitsTop) {
					if (i == 0) {
						elValue = '<td class="previousMonth">&nbsp;</td>';
					} else if (i == 1) {
						elValue = '<td class="nextMonth">&nbsp;</td>';
					}
				}
				html += elValue;
			}
			
			html += '</tr>';
		}
		
		if (!menuFits && menuConfig['prevNextAtBottom']) {
			html += '<tr><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td>&nbsp;</td><td class="previousMonth">&nbsp;</td><td class="nextMonth">&nbsp;</td></tr>';
		}
		
		html += '</tbody>';
		
		// only show footer if necessary
		if (this.config['showFooter']) {
			html += '<tfoot><tr>';
			
			for (var c = 0; c < 7; c++) {
				html += '<th>' + this.dayList[((c + this.firstDay) % 7)] + '</th>';
			}
			
			html += '</tr></tfoot>';
		}
		
		html += '</table>';
		
		return html;
	};
	
	/**
	 * Renders a single day.
	 * 
	 * @param int day The day to render.
	 * @param int dayOfTheWeek The day of the week.
	 * @return string The html representation of the day.
	 */
	this.renderElement = function (day, dayOfTheWeek) {
		var classes = 'day';
		var value = day;
		var events = this.getEventsForDay(day);
		
		if (events.length > 0) {
			classes += ' event';
			value = this.renderElementLink(day, value) + '<div class="eventsContainer"><ul class="events">';
			
			for (var c = 0; c < events.length; c++) {
				value += '<li class="event">' + events[c]['teaser'] + '</li>';
			}
			
			value += '</ul></div>';
		}
		
		if (this.checkToday && day == this.today.getUTCDate()) {
			classes += ' today';
		}
		
		if (isArray(this.config['specialDays'])) {
			if (isArray(this.config['specialDays']['week'])) {
				if (this.config['specialDays']['week'][dayOfTheWeek]) {
					classes += ' ' + this.config['specialDays']['week'][dayOfTheWeek];
				}
			}
		}
		
		return '<td class="' + classes + '">' + value + '</td>';
	};
	
	/**
	 * Renders a link around the day.
	 *
	 * @param int day The day that is getting rendered.
	 * @param string value The rendered day.
	 * @return string The rendered day with a link around.
	 */
	this.renderElementLink = function (day, value) {
		return '<a class="dayLink" href="#">' + value + '</a>';
	}
	
	/**
	 * Finds the events for a given day.
	 *
	 * @param int day The day to get the events for.
	 * @param int month The month to get the events for, if empty the currently stored month is used.
	 * @param int year The year to get the events for, if empty the currently stored year is used.
	 * @return array The events for the specified day.
	 */
	this.getEventsForDay = function (day, month, year) {
		if (month == undefined) month = this.activeMonth;
		else month--;
		if (year == undefined) year = this.activeYear;
		
		if (!isArray(this.events)) return new Array();
		if (!isArray(this.events[year])) return new Array();
		if (!isArray(this.events[year][month])) return new Array();
		if (!isArray(this.events[year][month][day])) return new Array();
		return this.events[year][month][day];
	}
	
	/**
	 * Adds an event for a specific day.
	 *
	 * @param int day The day to add the event for.
	 * @param int month The month to add the event for, if empty the currently stored month is used.
	 * @param int year The year to add the event for, if empty the currently stored year is used.
	 * @param array event An event object/array.
	 * @return object Self reference to chain functions.
	 */
	this.addEventForDay = function (day, month, year, event) {
		if (month == undefined) month = this.activeMonth;
		else month--;
		if (year == undefined) year = this.activeYear;
		
		if (!isArray(this.events[year])) {
			this.events[year] = new Array();
		}
		
		if (!isArray(this.events[year][month])) {
			this.events[year][month] = new Array();
		}
		
		if (!isArray(this.events[year][month][day])) {
			this.events[year][month][day] = new Array();
		}
		
		this.events[year][month][day][this.events[year][month][day].length] = event;
		return this;
	}
	
	/**
	 * Adds an array of events.
	 *
	 * @param int day The day to add the event for.
	 * @param int month The month to add the event for, if empty the currently stored month is used.
	 * @param int year The year to add the event for, if empty the currently stored year is used.
	 * @param array event An event object/array.
	 * @return object Self reference to chain functions.
	 */
	this.addEventForDays = function (day_begin, month_begin, year_begin, event ,day_end, month_end, year_end) {
		
		/** Make sure that there is no 0X with X > 7 */
		/* day_begin = parseInt_(day_begin+"");
		month_begin = parseInt_(month_begin+"");
		year_begin = parseInt_(year_begin+"");
		day_end = parseInt_(day_end+"");
		month_end = parseInt_(month_end+"");
		year_end = parseInt_(year_end+""); */
		
		if (month_begin == undefined) return;
		if(day_end == undefined) {
			this.addEventForDay(day_begin, month_begin, year_begin, event);
		} else {
			month_end--;
			month_begin--;
			
			// Check if the second date is in the future
			if (year_end > year_begin || (year_end == year_begin && (month_end > month_begin || (month_end == month_begin && day_end >= day_begin)))) {
			//if(day_end > day_begin && ((month_end >= month_begin && (year_end >= year_begin)) || month_end <= month_begin && year_end > year_begin)) {
				var c = 0, td = day_begin, tm = month_begin, ty = year_begin;
				var daysOfMonth = this.daysOfTheMonth(tm, ty);
				while (true) {
					if (td > daysOfMonth) {
						td = 0;
						tm++;
						
						if (tm >= 12) {
							tm = 0;
							ty++;
						}
						daysOfMonth = this.daysOfTheMonth(tm, ty);
					}
					
					this.addEventForDay(td, tm+1, ty, event);
					
					if (td == day_end && tm == month_end && ty == year_end) {
						break;
					}
					td++;
					c++;
				}
			} else {
				this.addEventForDay(day_begin, month_begin, year_begin, event);
			}
		}
		return this;
	}
	
	/**
	 * Overwrites the events for a specific day.
	 *
	 * @param int day The day to set the events for.
	 * @param int month The month to set the events for, if empty the currently stored month is used.
	 * @param int year The year to set the events for, if empty the currently stored year is used.
	 * @param array events An array of event object/array.
	 * @return object Self reference to chain functions.
	 */
	this.setEventsForDay = function (day, month, year, events) {
		if (month == undefined) month = this.activeMonth;
		else month--;
		if (year == undefined) year = this.activeYear;
		
		if (!isArray(this.events[year])) {
			this.events[year] = new Array();
		}
		
		if (!isArray(this.events[year][month])) {
			this.events[year][month] = new Array();
		}
		
		if (!isArray(this.events[year][month][day])) {
			this.events[year][month][day] = new Array();
		}
		
		this.events[year][month][day] = events;
		return this;
	}
	
	/**
	 * Finds out if a given year is a leap year (e.g. if February has 29 days).
	 *
	 * @param int year The year to check.
	 * @return boolean Whether or not the year is a leap year.
	 */
	this.isLeapYear = function (year) {
		if ((year % 4 == 0) && (year % 100 != 0) || (year % 400 == 0)) return true;
		return false;
	};
	
	/**
	 * Raises an event.
	 *
	 * @param string type Event type.
	 * @param mixed arguments Function arguments.
	 * @return array Accumulated errors.
	 */
	this.raiseEvent = function (type, arguments) {
		var events = new Array();
		var errors = new Array();
		if (isArray(this.config['events'])) {
			events = this.config['events'];
		}
		if (isArray(events[type])) {
			events = events[type];
		}
		
		for (var key in events) {
			if (isFunction(events[key])) {
				try {
					(events[key].bind(this))(arguments);
				} catch (e) {
					errors.push(e);
				}
			}
		}
		
		return errors;
	}
	
	/**
	 * Registers an event function.
	 *
	 * @param string type Event type.
	 * @param function func The function.
	 * @return object Self reference to chain functions.
	 */
	this.registerEvent = function (type, func) {
		if (!isFunction(func)) throw "Function required!";
		if (!isArray(this.config['events'])) {
			this.config['events'] = new Array();
		}
		if (!isArray(this.config['events'][type])) {
			this.config['events'][type] = new Array();
		}
		
		this.config['events'][type].push(func);
		
		return this;
	}
	
	/**
	 * @var int firstDay Which day starts the week (So = 0 ... Sa = 6)
	 */
	this.firstDay = 1;
	
	/**
	 * @var int activeYear The currently displayed year.
	 */
	this.activeYear = (new Date()).getFullYear();
	
	/**
	 * @var int activeMonth The currently displayed month.
	 */
	this.activeMonth = (new Date()).getMonth();
	
	// init the year/month combination as specified in the constructor
	this.setMonth(year, month);
	
	/**
	 * @var array dayList A list of labels for the days of the week. Should be overwritten to localize the calendar.
	 */
	this.dayList = {0: 'Su', 1: 'Mo', 2: 'Tu', 3: 'We', 4: 'Th', 5: 'Fr', 6: 'Sa'};
	
	/**
	 * @var array monthList A list of labels for the months. Should be overwritten to localize the calendar.
	 */
	this.monthList = {0: 'January', 1: 'February', 2: 'March', 3: 'April', 4: 'May', 5: 'June', 6: 'July', 7: 'August', 8: 'September', 9: 'October', 10: 'November', 11: 'December'};
	
	/**
	 * @var boolean inactive When the calendar has been anchored in the DOM this will be set to false.
	 */
	this.inactive = true;
	
	// make sure that the calendar is only rendered after the DOM is fully ready
	if (!$(element)) {
		document.observe('dom:loaded', 
			this.initElement.bind(this)
		);
	} else {
		this.initElement();
	}
}

/**
 * Checks if a variable is an array (or an object, which can be accessed as an array).
 *
 * @param mixed obj The variable to probe.
 * @return boolean Whether or not the variable can be accessed like an array.
 */
function isArray(obj) {
	if (!obj) return false;
	if (obj.constructor.toString().indexOf("Array") == -1 && obj.constructor.toString().indexOf("Object") == -1) return false;
	else return true;
}

/**
 * Checks if a variable is a function.
 *
 * @param mixed obj The variable to probe.
 * @return boolean Whether or not the variable is a function.
 */
function isFunction(obj) {
	if (!obj) return false;
	if (obj.constructor.toString().indexOf("Function") == -1) return false;
	else return true;
}

/**
* Alternate parseInt to make sure 001 is parsed as decimal and no NaN is returned.
*
* @param string text Text to parse as int.
* @return int Integer value of the text.
*/
function parseInt_(text) {
	text = text.replace(/^(0+)[1-9]/, '');
	if (text == null || text == '' || isNaN(text)) return 0;
	
	var cleared = parseInt(text);
	if (cleared < 0) return 0;
		return cleared;
}

