/**
 * @author Marco Alionso Ramirez, marco@onemarco.com
 * @url http://onemarco.com
 * This code is public domain
 */

/**
 * Utility function for DatePicker
 * @param {HTMLElement} child
 * @param {HTMLElement} parent
 */
function isDescendant(child, parent){
	if (child === document) {
		return false;
	}
	if(child.parentNode === parent){
		return true;
	}
	return isDescendant(child.parentNode, parent);
}

/**
 * Utility function for DatePicker
 * @param {Function} func
 * @param {Object} opts
 * opts is an  object literal with options:
 * scope [Object],
 * args [Array] arguments to pass to callback,
 * suppressArgs [Boolean] if suppress args is true, arguments given to the callback
 * function by the caller will be ignored, otherwise they are prepended before the args
 * array supplied
 */

function callback(func,opts){	
	var cb = function(){
		var args = opts.args ? opts.args : [];
		var scope = opts.scope ? opts.scope : this;
		var fargs = (opts.supressArgs === true) ?
			[] : $.makeArray(arguments);
		func.apply(scope,fargs.concat(args));
	};
	return cb;
}

/**
 * Constructor for DatePicker class
 * @param {HTMLElement} input
 * @param {Date} minDate
 * @param {Date} maxDate
 */
function DatePicker(input,minDate,maxDate){

	this.input = input;
	$(input).addClass('calendarInput');

	//today
	this.minDate = minDate.getDate();
	this.minMonth = minDate.getMonth();
	this.minYear = minDate.getFullYear();
	
	this.maxDate = maxDate.getDate();
	this.maxMonth = maxDate.getMonth();
	this.maxYear = maxDate.getFullYear();	

	//currently selected date
	this.selected = new Date();
	this.selected.setFullYear(minDate.getFullYear(),minDate.getMonth(),minDate.getDate());
	this.date = minDate;
	this.date.setDate(1);
	
	//calendar arrays
	this.years = {};
	this.months = [];
	this.dates = [];	
	this.createCalendar();
	
	this.inputAssist();
}

/**
 * @private
 */
DatePicker.prototype.createCalendar = function(){

	var i;
	
	//create container
	this.container = $('<div/>').attr('class','datePickerContainer');
	var closeButton = $('<span/>').attr('class','closeButton').append('Close [x]').click(callback(this.hideContainer,{scope:this}));
	var container = $('<div/>').attr('class','datePicker');
	this.container[0].style.display = 'none';
	this.container.hidden = true;
	this.container.append(closeButton).append(container);
	
	//create years
	var yearList = $('<ul/>').attr('class','yearList');
	for(i = this.minYear; i <= this.maxYear; i++){
		var year = $('<li/>').append(i);
		this.years[i] = year;
		yearList.append(year.mouseup(callback(this.setYear,{scope:this,args:[i],supressArgs:true})));
	}
	container.append(yearList);	
	
	//create months
	var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
	var monthList = $('<ul/>').attr('class','monthList');
	for(i = 0; i < 12; i++){
		var month = $('<li/>').append(months[i]);
		this.months[i] = month;
		monthList.append(month.mouseup(callback(this.setMonth,{scope:this,args:[i],supressArgs:true})));
	}
	container.append(monthList);
	
	//create dates
	var days = ['S','M','T','W','T','F','S'];
	var dateList = $('<ul/>').attr('class','dateList');
	
	//weekday headings
	for(i = 0; i < days.length; i ++){
		dateList.append($('<li/>').attr('class','weekday').append((days[i])));
	}
	
	//add dates
	for(i = -5; i <= 31; i++){
		var date = $('<li/>');
		var text = '&nbsp;';
		var className = 'empty';
		if (i >= 1 && i <= 31) {
			text = i;
			className = 'date';
			date.mouseup(callback(this.selectDate,{scope:this,args:[i],supressArgs:true}));
		}
		this.dates[i] = date.attr('class',className).append(text);
		$(dateList).append(date);
	}
	container.append(dateList);
	this.setMonth(this.minMonth,this.minYear);
};

/**
 * Adds calendar icon to input, calendar pops up
 */
DatePicker.prototype.inputAssist = function(){
	
	this.blurCheckFunction = callback(this.blurCheck,{scope:this});
	this.escCheckFunction = callback(this.escCheck,{scope:this});
	
	//hook into mouse click on input
	$(this.input).mouseup(callback(this.showContainer,{scope:this}));
	
	//create and append caldar icon and append container
	this.icon = $('<span></span>').addClass('calendarIcon').mouseup(callback(this.showContainer,{scope:this}));
	$(this.input).after($('<span/>').addClass('calendarIconContainer').append(this.icon));
	$(this.input).after(this.container);
	this.setYear(this.date.getFullYear(), true);
	this.setMonth(this.date.getMonth(), true);
};

/**
 * Changes the currently displayed year
 * @param {Integer} year
 */
DatePicker.prototype.setYear = function(year){
	var force = arguments.length > 1 ? arguments[1] : false;
	if(!force && year === this.date.getFullYear()){return;}
	
	var i;
	
	//set year to a valid year
	if(year > this.maxYear){
		year = this.maxYear;
	}
	if(year < this.minYear){
		year = this.minYear;
	}
	
	this.date.setFullYear(year);
	
	//highlight current year
	for(i in this.years){
		if(parseInt(i,10) === year){
			this.years[i].addClass('current');
		}else{
			this.years[i].removeClass('current');
		}
	}

	//reset months	
	for(i = 0; i < this.months.length; i++){
		this.months[i].removeClass('inactive').removeClass('current');
	}
	
	//deactivate unselectable months
	if(year === this.minYear){
		for(i = 0; i < this.minMonth; i++){
			this.months[i].addClass('inactive');
		}
	}

	if(year === this.maxYear){
		for(i = this.months.length - 1; i > this.maxMonth; i--){
			this.months[i].addClass('inactive');
		}
	}	

	//update month
	var month = this.date.getMonth();
	if(year === this.maxYear && month > this.maxMonth){month = this.maxMonth;}
	if(year === this.minYear && month < this.minMonth){month = this.minMonth;}
	this.setMonth(month,true);
};

/**
 * Changes the currently displayed month
 * @param {Integer} month
 * @param {Integer} year
 */
DatePicker.prototype.setMonth = function(month){
	var force = arguments.length > 1 ? arguments[1] : false;
	if(!force && month === this.date.getMonth()){return;}
	
	var i;

	var year = this.date.getFullYear();

	//set month to a valid month
	if(year === this.maxYear && month > this.maxMonth){
		month = this.maxMonth;
	}
	if(year === this.minYear && month < this.minMonth){
		month = this.minMonth;
	}	

	this.date.setMonth(month);
	
	//highlight current month
	for(i = 0; i < 12; i++){
		if(i !== month){
			this.months[i].removeClass('current');
		}else{
			this.months[i].addClass('current');
		}
	}
	
	//pad days to set the first day to the correct weekday
	var firstDay = this.firstDayOfMonth(month,this.date.getFullYear());
	for(i = -5; i < 1; i++){
		if((firstDay - i) >= 6){
			this.dates[i].show();
		}else{
			this.dates[i].hide();
		}
	}

	//hide last days of month
	var lastDate = this.lastDateOfMonth(month,this.date.getFullYear());	
	for(i = 29; i <= 31; i++){
		if(i > lastDate){
			this.dates[i].hide();
		}else{
			this.dates[i].show();
		}
	}	
	
	//reset dates
	for(i = 1; i <= 31; i++){
		this.dates[i].removeClass('inactive').removeClass('current');
	}
	
	//deactivate unselectable dates
	if(month === this.minMonth && year === this.minYear){
		for(i = 1; i < this.minDate; i++){
			this.dates[i].addClass('inactive');
		}
	}

	if(month === this.maxMonth && year === this.maxYear){
		for(i = 31; i > this.maxDate; i--){
			this.dates[i].addClass('inactive');
		}
	}
	
	//highlight selected date
	if(this.selected.getMonth() === this.date.getMonth() &&
		this.selected.getFullYear() === this.date.getFullYear() &&
		!this.dates[this.selected.getDate()].hasClass('inactive')){
		this.dates[this.selected.getDate()].addClass('current');
	}
};

/**
 * Selects date in calendar and changes input's value to reflect current date
 * @param {Date} date
 */
DatePicker.prototype.selectDate = function(date){
	if(this.dates[date].hasClass('inactive')){return;}
	this.select(this.date.getFullYear(),this.date.getMonth(),date);
	this.dates[date].addClass('current');
	this.outputSelected();	
	this.hideContainer();
};

/**
 * @private
 * @param {Integer} year
 * @param {Integer} month
 * @param {Integer} date
 */
DatePicker.prototype.select = function(year,month,date){
	this.selected = new Date();
	this.selected.setFullYear(year,month,date);
};

/**
 * @private
 * @param {Integer} month
 * @param {Integer} year
 */
DatePicker.prototype.lastDateOfMonth = function(month,year){
	var d = new Date();
	var lastDate = 28;
	d.setFullYear(year, month, lastDate);
	for (var i = 29; i <= 31; i++) {
		d.setDate(i);
		if (d.getMonth() !== month) {
			break;
		}
		lastDate = i;
	}
	return lastDate;
};

/**
 * @private
 * @param {Integer} month
 * @param {Integer} year
 */
DatePicker.prototype.firstDayOfMonth = function(month,year){
	var d = new Date();
	d.setFullYear(year, month, 1);
	return d.getDay();
};

/**
 * show calendar
 */
DatePicker.prototype.showContainer = function(){
	if(this.container.hidden === false){return;}
	this.loadFromInput();
	this.container[0].style.display = 'inline';
	this.container.hidden = false;
	$(document).mouseup(this.blurCheckFunction).keyup(this.escCheckFunction);
};

/**
 * @private
 */
DatePicker.prototype.hideContainer = function(){
	this.container[0].style.display = 'none';
	this.container.hidden = true;
	$(document).unbind('mouseup',this.blurCheckFunction).unbind('keyup',this.escCheckFunction);
};


/**
 * @private
 * Check mouseclicks to see if they occur outside of calendar or input
 * @param {Event} e
 */
DatePicker.prototype.blurCheck = function(e){
	if(e.target === this.input || e.target === this.container[0] || e.target === this.icon[0] || isDescendant(e.target, this.container[0])){
		return;
	}
	this.hideContainer();
};

/**
 * @private
 * If user presses Esc key while calendar is up, calendar is closed
 * @param {Event} e
 */
DatePicker.prototype.escCheck = function(e){
	if(e.keyCode === 27){
		this.hideContainer();
	}
};

/**
 * @private
 * load values from input into calendar
 */
DatePicker.prototype.loadFromInput = function(){
	this.fixDate();

	var dateVal = this.input.value;
	var items = dateVal.split('/');
	if(items.length < 3){return;}
	var month = parseInt(items[0],10) - 1;
	var year = parseInt(items[2],10);
	var date = parseInt(items[1],10);

	this.select(year,month,date);
	this.outputSelected();
	
	this.setYear(year, true);
	this.setMonth(month, true);
};

/**
 * @private
 * format date
 */
DatePicker.prototype.fixDate = function(){
	var clean = this.input.value.replace(/-/g,'/').replace(
		/[^\d\/]/g,'').replace(/\/(\d{2})$/,'/20$1');
	var valid = '';
	if(/^(\d{1,2}\/){2}\d{4}$/.test(clean)){
		valid = clean;
	}
	this.input.value = valid;
};

/**
 * @private output selected date to input
 */
DatePicker.prototype.outputSelected = function(){
	this.input.value = (this.selected.getMonth() + 1) + '/' +
			this.selected.getDate() + '/' + this.selected.getFullYear();
};