/**
 *
 * inspired by Simon Willison, this is a date object that accepts three types of
 * input
 *	1) Simon's magic date
 * 2) separate inputs for month, day, year
 * 3) a pop-up calendar, hopefully from egroupware
 *
 * @param swd "FORM INPUT"	 the free text magic date input
 *
 *	@param puc string	 id of the html element that is the trigger for our popup clendar
 *
 *	@param err "HTML SPAN"	where to report errors
 *
 *	@param separates	ARRAY  an array with three inputs, year, month day, in that order, keyed by the leters 	Y, m, d
 *
 **/
function willisonDate (swd, puc, separates, err){

	this.freedate = document.getElementById(swd);
	this.popper = document.getElementById(puc);

	this.yearfinal = document.getElementById(separates["Y"]);
	this.monthfinal = document.getElementById(separates["m"]);
	this.dayfinal = document.getElementById(separates["d"]);

	this.errorreport = document.getElementById(err);

	// add an event listener to the freedate
	// to run parseDateString on change

	// {{{ our popup calendar
	this.pucal = Calendar.setup(
                {
                inputField     : swd,         // ID of the input field
                ifFormat       : "%m/%d/%Y",    // the date format
                button         : puc,       // ID of the button
                /* displayArea : err, */
                yearInput      : separates["Y"],
                monthInput     : separates["m"],
                dayInput       : separates["d"],
                separateInputs : true
                }
                );
	// }}}

	// straight from SW
	this.monthNames = "January February March April May June July August September October November December".split(" ");
	this.weekdayNames = "Sunday Monday Tuesday Wednesday Thursday Friday Saturday".split(" ");
	this.numberWords = "zero one two three four five six seven eight nine ten eleven twelve".split(" ");

	// {{{ function parseMonth
	/**
	 *
	 **/
	this.parseMonth = function (month) {
		var matches = this.monthNames.filter(function(item) { 
                        return new RegExp("^" + month, "i").test(item);
                        });
		if (matches.length == 0) {
			throw new Error("Invalid month string");
		}
		if (matches.length > 1) {
			throw new Error("Ambiguous month");
		}
		return this.monthNames.indexOf(matches[0]);
	}
// }}}
// {{{ function parseWeekday
/* Same as parseMonth but for days of the week */
this.parseWeekday = function (weekday) {
	var matches = this.weekdayNames.filter(function(item) {
                        return new RegExp("^" + weekday, "i").test(item);
                        });
	if (matches.length == 0) {
		throw new Error("Invalid day string");
	}
	if (matches.length > 1) {
		throw new Error("Ambiguous weekday");
	}
	return this.weekdayNames.indexOf(matches[0]);
}
// }}}
// {{{ function parseNumberWord
this.parseNumberWord = function (num) {
	var matches = this.numberWords.filter(function(item) { 
                        return new RegExp("^" + num, "i").test(item);
                        });
	if (matches.length == 0) {
		throw new Error("Invalid number word");
	}
	if (matches.length > 1) {
		throw new Error("Ambiguous number");
	}
	return this.numberWords.indexOf(matches[0]);
}
// }}}
// {{{ dateParsePatterns
this.dateParsePatterns = [
        // Today
        {   re: /^tod/i,
            handler: function() { 
            	return new Date();
            } 
        },
        // Tomorrow
        {   re: /^tom/i,
            handler: function() {
            	var d = new Date(); 
            	d.setDate(d.getDate() + 1); 
            	return d;
            }
        },
        // Yesterday
        {   re: /^yes/i,
            handler: function() {
            	var d = new Date();
            	d.setDate(d.getDate() - 1);
            	return d;
            }
        },
        // 4th
        {   re: /^(\d{1,2})(st|nd|rd|th)?$/i, 
            handler: function(bits) {
            	var d = new Date();
            	d.setDate(parseInt(bits[1], 10));
            	return d;
            }
        },
        // 4th Jan
        {   re: /^(\d{1,2})(?:st|nd|rd|th)? (\w+)$/i, 
            handler: function(bits, caller) {
            	var d = new Date();
            	d.setDate(parseInt(bits[1], 10));
            	d.setMonth(caller.parseMonth(bits[2]));
            	return d;
            }
        },
        // 4th Jan 2003
        {   re: /^(\d{1,2})(?:st|nd|rd|th)? (\w+),? (\d{4})$/i,
            handler: function(bits, caller) {
            	var d = new Date();
            	d.setDate(parseInt(bits[1], 10));
            	d.setMonth(caller.parseMonth(bits[2]));
            	d.setYear(bits[3]);
            	return d;
            }
        },
        // Jan 4th
        {   re: /^(\w+) (\d{1,2})(?:st|nd|rd|th)?$/i, 
            handler: function(bits, caller) {
            	var d = new Date();
            	d.setDate(parseInt(bits[2], 10));
            	d.setMonth(caller.parseMonth(bits[1]));
            	return d;
            }
        },
        // Jan 4th 2003
        {   re: /^(\w+) (\d{1,2})(?:st|nd|rd|th)?,? (\d{4})$/i,
            handler: function(bits, caller) {
            	var d = new Date();
            	d.setDate(parseInt(bits[2], 10));
            	d.setMonth(caller.parseMonth(bits[1]));
            	d.setYear(bits[3]);
            	return d;
            }
        },
        // xxx/## weeks ago
        {   re: /^(\w+) weeks?\s*ago$/i,
            handler: function(bits, caller) {
            	var d = new Date();
            	if(bits[1]==="a"){
            		bits[1] = "one";
            	}
            	if(isNaN(parseInt(bits[1]))) {
            		d.setDate(d.getDate()-(parseInt(caller.parseNumberWord(bits[1]), 10)*7));
            	}
            	else {
            		d.setDate(d.getDate()-(parseInt(bits[1], 10)*7));
            	}
            	return d;
            }
        },
        // xxx/## months ago
        {   re: /^(\w+) months?\s*ago$/i,
            handler: function(bits, caller) {
            	var d = new Date();
            	if(bits[1]==="a"){
            		bits[1] = "one";
            	}
            	if(isNaN(parseInt(bits[1]))) {
            		d.setMonth(d.getMonth()-parseInt(caller.parseNumberWord(bits[1]), 10));
            	}
            	else {
            		d.setMonth(d.getMonth()-parseInt(bits[1], 10));
            	}
            	return d;
            }
        },
        // in xxx/## weeks from tomorrow|today|yesterday
        {   re: /^(in)?\s*(\w+) weeks?\s*from\s*(tomorrow|today|yesterday)$/i,
            handler: function(bits, caller) {
            	var d = new Date();
            	var day = d.getDay();
            	var dayoffset = 0;
            	if(bits[2]==="a"){
            		bits[2] = "one";
            	}
            	switch(bits[bits.length-1]){
            		case "tomorrow" :
            			dayoffset = 1;
            			break;
            		case "yesterday" :
            			dayoffset = -1;
            			break;
            		default :
            			dayoffset = 0;
            	}
            	if(isNaN(parseInt(bits[2]))) {
            		d.setDate(d.getDate()+parseInt(caller.parseNumberWord(bits[2]), 10)*7 + dayoffset);
            	}
            	else {
            		d.setDate(d.getDate()+parseInt(bits[2], 10)*7 + dayoffset);
            	}
            	return d;
            }
        },
        // in xxx/## weeks
        {   re: /^(in)?\s*(\w+) weeks?\s*(from (last)?\s*(\w+))?$/i,
            handler: function(bits, caller) {
            	var d = new Date();
            	var day = d.getDay();
            	var dayoffset = 0;
            	if(bits[2]==="a"){
            		bits[2] = "one";
            	}
            	// we will also try to handle "from DAY" to guess
            	// when they want to start from
            	try{
            		if(bits.length && caller.parseWeekday(bits[bits.length-1])!==null){
            			// then maybe they named a date
            			var startday = caller.parseWeekday(bits[bits.length-1]);
            			if(startday<day){
            				dayoffset = 7 - day + startday;
            			}
            			else {
            				dayoffset = startday - day;
            			}
            			if(bits[bits.length-2]==="last"){
            				dayoffset -= 7;
            			}
            		}
            	}
            	catch(e){
            		// this error just means we didn't have a valid weekday
            		//alert("just a little off");
            	}
            	if(isNaN(parseInt(bits[2]))) {
            		d.setDate(d.getDate()+parseInt(caller.parseNumberWord(bits[2]), 10)*7 + dayoffset);
            	}
            	else {
            		d.setDate(d.getDate()+parseInt(bits[2], 10)*7 + dayoffset);
            	}
            	return d;
            }
        },
        // in xxx/## months
        {   re: /^(in)?\s*(\w+) months?\s*(from now)?$/i,
            handler: function(bits, caller) {
            	var d = new Date();
            	if(bits[2]==="a"){
            		bits[2] = "one";
            	}
            	if(isNaN(parseInt(bits[2]))) {
            		d.setMonth(d.getMonth()+parseInt(caller.parseNumberWord(bits[2]), 10));
            	}
            	else {
            		d.setMonth(d.getMonth()+parseInt(bits[2], 10));
            	}
            	return d;
            }
        },
        // next Tuesday - this is suspect due to weird meaning of "next"
        {   re: /^next (\w+)$/i,
            handler: function(bits, caller) {
            	var d = new Date();
            	var day = d.getDay();
            	var newDay = caller.parseWeekday(bits[1]);
            	var addDays = newDay - day;
            	if (newDay <= day) {
            		addDays += 7;
            	}
            	d.setDate(d.getDate() + addDays);
            	return d;
            }
        },
        // last Tuesday
        // if last DAY was yesterday then make it 8 days ago
        // otherwise last DAY is the closest last day
        {   re: /^last (\w+)$/i,
            handler: function(bits, caller) {
            	//throw new Error("Not yet implemented");
            	var d = new Date();
            	var day = d.getDay();
            	var newDay = caller.parseWeekday(bits[1]);
            	var minusDays = day - newDay;
            	if(minusDays==-7){ // day before || Sunday to Saturday
            		minusDays = 8;
            	}
            	else if(minusDays<2){
            		// day is yesterday or today or any day coming up
            		minusDays+=7;
            	}
            	d.setDate(d.getDate()-minusDays);
            	return d;
            }
        },
        // mm/dd/yyyy (American style)
        {   re: /(\d{1,2})\/(\d{1,2})\/(\d{4})/,
            handler: function(bits) {
            	var d = new Date();
            	d.setYear(bits[3]);
            	d.setDate(parseInt(bits[2], 10));
            	d.setMonth(parseInt(bits[1], 10) - 1); // Because months indexed from 0
            	return d;
            }
        },
        // yyyy-mm-dd (ISO style)
        {   re: /(\d{4})-(\d{1,2})-(\d{1,2})/,
            handler: function(bits) {
            	var d = new Date();
            	d.setYear(parseInt(bits[1]));
            	d.setDate(parseInt(bits[3], 10));
            	d.setMonth(parseInt(bits[2], 10) - 1);
            	return d;
            }
        },
        // mark: yyyymmdd (Like ISO style, but no dashes)
        {   re: /(\d{4})(\d{2})(\d{2})/,
            handler: function(bits) {
            	var d = new Date();
            	d.setYear(parseInt(bits[1]));
            	d.setDate(parseInt(bits[3]));
            	d.setMonth(parseInt(bits[2]) - 1);
            	return d;
            }
        },
        // just any old day of the week
        {   re: /^(\w+)$/i,
            handler: function(bits, caller) {
            	//throw new Error("Not yet implemented");
            	var d = new Date();
            	var day = d.getDay();
            	var newDay = caller.parseWeekday(bits[1]);
            	var daysHence = newDay - day ;
            	if(daysHence<0){
            		daysHence +=7;
            	}
            	d.setDate(d.getDate()+daysHence);
            	return d;
            }
        },
	];
// }}}

// {{{ function parseDateString
this.parseDateString = function(s) {
	for (var i = 0; i < this.dateParsePatterns.length; i++) {
		var re = this.dateParsePatterns[i].re;
		var handler = this.dateParsePatterns[i].handler;
		var bits = re.exec(s);
		if (bits) {
			return handler(bits, this);
		}
	}
	throw new Error("Invalid date string");
}
// }}}
// {{{ function magicDate
this.magicDate  = function(evt) {
	this.errorreport.firstChild.innerHTML = "";
	this.errorreport.className = "";
	try {
		var d = this.parseDateString(this.freedate.value);
		this.yearfinal.value=d.getFullYear();
		this.monthfinal.value = d.getMonth()+1;
		this.dayfinal.value = d.getDate();
		//input.value = (d.getMonth() + 1) + '/' + d.getDate() + '/' + d.getFullYear();
		//alert((d.getMonth() + 1) + '/' + d.getDate() + '/' + d.getFullYear());
		//input.className = '';
		// Human readable date
		//document.getElementById(messagespan).firstChild.nodeValue = d.toDateString();
		//document.getElementById(messagespan).className = 'normal';
	}
	catch (e) {
		this.freedate.className = 'error';
		var message = e.message;
		// Fix for IE6 bug
		if (message.indexOf('is null or not an object') > -1) {
			message = 'Invalid date string';
		}
		this.errorreport.firstChild.innerHTML = message;
		this.errorreport.className = 'error';
	}
}
addObjectEventFunction(this.freedate, "change", this, "magicDate", true);
this.freedate.addEventListener("focus", function(e) {e.target.select(); }, true);
// }}}

} // end of willisonDate declaration

