// (C) Lee Davies www.fruitbatscode.com
//
// jQuery calendar - like the millions of others ;)
// version: 0.1.alpha 
// rev:		$Rev: 58 $
// svn:		$HeadURL: http://svn.fruitbatscode.com/cssdesigns/JQuery/Dates/jquery.fbis.Calender.js $
// date:	$Date: 2009-09-25 12:40:54 +0100 (Fri, 25 Sep 2009) $
// License
// This is licensed under the Creative Commons Attribution-Share Alike (http://creativecommons.org/licenses/by-sa/3.0/) license. 
// Use/abuse it as needed, within the scope of that license.

//Known Issues

//FIXED 18/09/2009: Drop down layout is off when you the year and month drop downs display (due to offset) FIX:css change and not using SlideOver when in dropdown mode
//When going from a month with 31 days to a month with 28 or 29, you end up on the 1st or second of the same month. (DateDiff error somewhere)

//Roadmap

// 0.1 - Bug fix this version, add onDateChanged event , implement year change by text box
// 0.2 - Add Array of appointments and highlight them when the relevant date is displayed and show tooltip or text in area (dayHoverSelector)
// 0.3 - tie into fbisGui and support different cultures
// 0.4 - Add range selection functionality

(function($){
	$.fn.fbisCalendar = function(options)
	{
		//init goes here I think
		
		// Extend our default options with those provided.
		var opts = $.extend({}, $.fn.fbisCalendar.defaults, options);
		
		return this.each(function() {
				var $$this = $(this);
				//bind the keyevents so we can update
				$$this.bind("keyup", textChange);
				//if mask plugin is available init it
				if($.mask){$$this.mask('99/99/9999');}
				
				var $$cal = $(opts.markup);
				
				//insert the calendar markup and drop item if required
				$$this.after($$cal);
				var $$drop = null;
				if(opts.drop!='')
				{
					$$drop = $('<span class="drop">' + opts.drop +'</span>');
					$$this.after($$drop);
				}
				if(opts.dropObj!=null)
				{
					$$drop = opts.dropObj;
				}
				
				if($$drop!=null)
				{
					$$cal.makeAbsolute(true); //move the calender into the body and make absolute
					//add drop down button
					$$drop.click(function() {
						$$dropUnder = opts.dropUnder != null ? opts.dropUnder : $$this
						$$cal.css('left', $$dropUnder.offset().left);
						$$cal.css('top', $$dropUnder.offset().top + $$dropUnder.outerHeight());
						$$cal.slideToggle('fast');
					});
					$$cal.addClass('dropwin');
					//$$cal.hide();//hide initially
					
				}else{
					if(opts.hideTextbox==true)
					{
						//hide the textbox
						$$this.hide();
					}
				}
				
				$$cal.addClass(opts.cssClass);
				//bind and initialise it
				$$cal.find('ul.months li').click(function(){monthClick(this)});  
				$$cal.find('ul.days li').click(function(){dayClick(this)});
				$$cal.find('ul.days li:lt(7)').addClass('dy');//add class to the day items
				$$cal.find('.years ul.dec li').click(function(){decadeClick(this)});
				$$cal.find('.years ul.unit li').click(function(){unitClick(this)});
				$$cal.find('.years ul.cent li').click(function(){centuryClick(this)});
				//set up browse items
				$$cal.find('ul.move li:eq(0)').click(function(){prevClick(this)});
				$$cal.find('ul.move li:eq(2)').click(function(){nextClick(this)});
				$$cal.find('ul.move li:eq(1) span.month').click(function(){selectMonth(this)});
				$$cal.find('ul.move li:eq(1) span.year').click(function(){selectYear(this)});

				$$cal.find('.slide').click(function(){$(this).parent().slideToggle();})
				renderDays();
				//render all the days and update the month display etc
				function renderDays()
				{
					$$cal.find('ul.days li.selected').removeClass('selected');
					//Get the first day of the month
					var theFirst = new Date();
					theFirst.setTime(opts.selectedDate.valueOf()); //copy selected date else we are using a ref
					theFirst.setDate(1);//first day
					var firstDayOfMonth = theFirst.getDay();//get first day of month
					var i=1;
					//ignore the first 7 as they are day titles
					var offset = 7 + firstDayOfMonth;
					var daysToRender = daysInMonth(opts.selectedDate.getMonth(), opts.selectedDate.getYear())
					//set up date cells
					$$cal.find('ul.days li').each(function(){
						if (i>offset && i<= offset + daysToRender)
						{
							$(this).html(i-offset).removeClass('empty');
						}else{
							//clear days
							if(i>7)
							{
								$(this).html('&nbsp;').addClass('empty');
							}
						}
						i++;
					});
					//Change display
					$$this.val(opts.selectedDate.format(opts.dateFormat));
					$$cal.find('ul.months li.selected').removeClass('selected');
					$$cal.find('ul.months li:eq('+(opts.selectedDate.getMonth())+')').addClass('selected');
					$$cal.find('ul.days li:eq('+(opts.selectedDate.getDate() + offset - 1)+')').addClass('selected');
					$$cal.find('ul.move li:eq(1) span.month').html(opts.selectedDate.format("mmmm"));
					$$cal.find('ul.move li:eq(1) span.year').html(opts.selectedDate.format("yyyy"));
				  
					if (opts.yearSelector)
					{
						//select the year in the 3 areas
						var yr = opts.selectedDate.getFullYear();// + 1900;
						var cn = yr - (yr % 100);
						var un = yr % 10;
						var dc = (yr % 100) - un;

						$$cal.find('.years ul li.selected').removeClass('selected');
						$$cal.find('.years ul.cent li:contains("'+ cn +'")').addClass('selected');
						$$cal.find('.years ul.unit li:contains("'+ un +'")').addClass('selected');
						if (dc==0)
						{
							//all decades have zero so we need to select the 00 one
							$$cal.find('.years ul.dec li:contains("00")').addClass('selected');
						}else $$cal.find('.years ul.dec li:contains("'+ dc +'")').addClass('selected');
					}
					//fire datechange event
					if(opts.onDateChange != undefined){
						opts.onDateChange(opts.selectedDate);
						}
				}
				//monitor keypress and change display if we have a valid date
				function textChange(e)
				{
					//console.log(ukDateToUs($$this.val()), new Date(ukDateToUs($$this.val())));
					if($$cal != null && isDate(ukDateToUs($$this.val())))
					{
						opts.selectedDate = new Date(ukDateToUs($$this.val()));
						renderDays();
					}
				}
				function dayClick(day)
				{
					if (!isNaN($(day).html()))
					{
						$(day).addClass('selected');
						opts.selectedDate.setDate($(day).html());
						renderDays();
						//if in drop mode hide the drop
						if($$drop!=null) 
							$$cal.slideToggle();
					}
				}
				function prevClick(obj)
				{
					opts.selectedDate = DateAdd(opts.selectedDate, 'M', -1);
					renderDays();
				}
				function nextClick(obj)
				{
					opts.selectedDate = DateAdd(opts.selectedDate, 'M', 1);
					renderDays();
				}
				function selectMonth(obj)
				{
					var yrs = $$cal.find('div.years:visible');
					if(yrs != null) yrs.slideToggle();
					if($$drop!=null)//don't use slideover if dropping
					{
						$$cal.find('div.monthswrap').slideToggle();
					}else{
						slideOver($$cal.find('div.monthswrap'), $$cal.find('ul.days'));
					}
				}
				function selectYear(obj)
				{
					if (opts.yearSelector==true) 
					{
						var mnths = $$cal.find('div.monthswrap:visible');
						if(mnths != null) mnths.slideToggle();
						if($$drop!=null)//don't use slideover if dropping
						{
							$$cal.find('div.years').slideToggle();
						}else{
							slideOver($$cal.find('div.years'), $$cal.find('ul.days'));
						}
					}else{
						//show text box
						$('#fbisYrEdit').val(opts.selectedDate.getFullYear()).toggle();
					}
				}
				function monthClick(month)
				{
					$$cal.find('ul.months li.selected').removeClass('selected');
					$(month).addClass('selected');
					opts.selectedDate.setMonth($$cal.find('ul.months li').index(month));
					renderDays();
					$$cal.find('div.monthswrap').slideToggle(); //hide me again
				}
				function decadeClick(dec)
				{
					if (!isNaN($(dec).html()))
					{
						$$cal.find('.years ul.dec li').removeClass('selected');
						$(dec).addClass('selected');
						//set unit to zero
						unitClick($$cal.find('.years ul.unit li:contains("0")'), true);
						opts.selectedDate.setYear(buildYear());
						renderDays();
					}
				}
				function unitClick(unit, suppressToggle)
				{
					if (!isNaN($(unit).html()))
					{
						$$cal.find('.years ul.unit li').removeClass('selected');
						$(unit).addClass('selected');
						opts.selectedDate.setYear(buildYear());
						renderDays();
						if(!suppressToggle)
							$$cal.find('div.years').slideToggle();
					}
				}
				function buildYear()
				{
					return parseInt($$cal.find('.years ul.cent li.selected').html()) +
						parseInt($$cal.find('.years ul.dec li.selected').html())+
						parseInt($$cal.find('.years ul.unit li.selected').html());
				}
				function centuryClick(cent)
				{
					if (!isNaN($(cent).html()))
					{
						var value = $(cent).html();
						$$cal.find('.years ul.cent li').removeClass('selected');
						$(cent).addClass('selected');
						opts.selectedDate.setYear(buildYear());
						renderDays();
					}
				}
		});
		
		


	};
	//default options -  - added as a property on our plugin function
	$.fn.fbisCalendar.defaults = 
	{
		selectedDate: new Date(),
		cssClass: '',
		dateFormat: 'dd/mm/yyyy',
		drop: '', //enable drop down behavior
		dropObj: null, //you can pass in a jquery object to use as the drop trigger
		dropUnder: null, //if set the drop down aligns with this object instead of the hosting textbox
		yearSelector: true, //hide the textbox
		hideTextbox: false,
		markup: '<div class="fbisCal">' +
			'<div class="years">' +
			'	<div class="slide">^</div>' +
			'	<ul class="cent"><li>1800</li><li>1900</li><li>2000</li></ul>' +
			'	<ul class="dec"><li>10</li><li>20</li><li>30</li><li>40</li><li>50</li><li>60</li><li>70</li><li>80</li><li>90</li><li>00</li></ul>' +
			'	<ul class="unit"><li>1</li><li>2</li><li>3</li><li>4</li><li>5</li><li>6</li><li>7</li><li>8</li><li>9</li><li>0</li></ul>' +
			'</div>' +
			'<div class="monthswrap">' +
			'	<div class="slide">^</div>' +
			'	<ul class="months">' +
			'		<li>Jan</li>' +
			'		<li>Feb</li>' +
			'		<li>Mar</li>' +
			'		<li>Apr</li>' +
			'		<li>May</li>' +
			'		<li>Jun</li>' +
			'		<li>July</li>' +
			'		<li>Aug</li>' +
			'		<li>Sep</li>' +
			'		<li>Oct</li>' +
			'		<li>Nov</li>' +
			'		<li>Dec</li>' +
			'	</ul>' +
			'</div>' +
			'<ul class="move">' +
			'	<li class="dir">Prev</li>' +
			'	<li><span class="month">month</span><span class="year">year</span><input id="fbisYrEdit" value="" style="display:none"/></li>' +
			'	<li class="dir">Next</li>' +
			'</ul>' +
			'<ul class="days">' +
			'	<li>S</li><li>M</li><li>T</li><li>W</li><li>T</li><li>F</li><li style="clear:right">S</li>' +
			'	<li style="clear:left;"></li><li></li><li></li><li></li><li></li><li></li><li></li>' +
			'	<li></li><li></li><li></li><li></li><li></li><li></li><li></li>' +
			'	<li></li><li></li><li></li><li></li><li></li><li></li><li></li>' +
			'	<li></li><li></li><li></li><li></li><li></li><li></li><li></li>' +
			'	<li></li><li></li><li></li><li></li><li></li><li></li><li></li>' +
			'	<li></li><li></li><li></li><li></li><li></li><li></li><li></li>' +
			'</ul>' +
		'</div>'
	};
})(jQuery);

$.fn.makeAbsolute = function(rebase) {
    return this.each(function() {
        var el = $(this);
        var pos = el.position();
        el.css({ position: "absolute",
            marginLeft: 0, marginTop: 0,
            top: pos.top, left: pos.left });
        if (rebase)
            el.remove().appendTo("body");
    });
}

function DateAdd(objDate, strInterval, intIncrement)
    {
        if(typeof(objDate) == "string")
        {
            objDate = new Date(objDate);
 
            if (isNaN(objDate))
            {
                throw("DateAdd: Date is not a valid date");
            }
        }
        else if(typeof(objDate) != "object" || objDate.constructor.toString().indexOf("Date()") == -1)
        {
            throw("DateAdd: First parameter must be a date object");
        }
 
        if(
        strInterval != "M"
        && strInterval != "D"
        && strInterval != "Y"
        && strInterval != "h"
        && strInterval != "m"
        && strInterval != "uM"
        && strInterval != "uD"
        && strInterval != "uY"
        && strInterval != "uh"
        && strInterval != "um"
        && strInterval != "us"
        )
        {
            throw("DateAdd: Second parameter must be M, D, Y, h, m, uM, uD, uY, uh, um or us");
        }
 
        if(typeof(intIncrement) != "number")
        {
            throw("DateAdd: Third parameter must be a number");
        }
 
        switch(strInterval)
        {
            case "M":
				objDate.setMonth(parseInt(objDate.getMonth()) + parseInt(intIncrement));
				break;
            case "D":
				objDate.setDate(parseInt(objDate.getDate()) + parseInt(intIncrement));
				break;
            case "Y":
				objDate.setYear(parseInt(objDate.getYear()) + parseInt(intIncrement));
				break;
            case "h":
				objDate.setHours(parseInt(objDate.getHours()) + parseInt(intIncrement));
				break;
            case "m":
				objDate.setMinutes(parseInt(objDate.getMinutes()) + parseInt(intIncrement));
				break;
            case "s":
				objDate.setSeconds(parseInt(objDate.getSeconds()) + parseInt(intIncrement));
				break;
            case "uM":
				objDate.setUTCMonth(parseInt(objDate.getUTCMonth()) + parseInt(intIncrement));
				break;
            case "uD":
				objDate.setUTCDate(parseInt(objDate.getUTCDate()) + parseInt(intIncrement));
				break;
            case "uY":
				objDate.setUTCFullYear(parseInt(objDate.getUTCFullYear()) + parseInt(intIncrement));
				break;
            case "uh":
				objDate.setUTCHours(parseInt(objDate.getUTCHours()) + parseInt(intIncrement));
				break;
            case "um":
				objDate.setUTCMinutes(parseInt(objDate.getUTCMinutes()) + parseInt(intIncrement));
				break;
            case "us":
				objDate.setUTCSeconds(parseInt(objDate.getUTCSeconds()) + parseInt(intIncrement));
				break;
        }
        return objDate;
    }

function daysInMonth(iMonth, iYear)
  {
    //How does this function work? It is quite simple. When the Date() function is given 
	//a day number that is greater than the number of days in the given month of the given year, 
	//it wraps the date into the next month. The getDate() function returns the day of the month, 
	//starting from the beginning of the month that the date is in. So, day 32 of March is 
	//considered to be day 1 of April. Subtracting 1 from 32 gives the correct number of days in March!
    return 32 - new Date(iYear, iMonth, 32).getDate();
  }
  
  	/*
	 * Date Format 1.2.3
	 * (c) 2007-2009 Steven Levithan <stevenlevithan.com>
	 * MIT license
	 *
	 * Includes enhancements by Scott Trenda <scott.trenda.net>
	 * and Kris Kowal <cixar.com/~kris.kowal/>
	 *
	 * Accepts a date, a mask, or a date and a mask.
	 * Returns a formatted version of the given date.
	 * The date defaults to the current date/time.
	 * The mask defaults to dateFormat.masks.default.
	 */

	var dateFormat = function () {
		var	token = /d{1,4}|m{1,4}|yy(?:yy)?|([HhMsTt])\1?|[LloSZ]|"[^"]*"|'[^']*'/g,
			timezone = /\b(?:[PMCEA][SDP]T|(?:Pacific|Mountain|Central|Eastern|Atlantic) (?:Standard|Daylight|Prevailing) Time|(?:GMT|UTC)(?:[-+]\d{4})?)\b/g,
			timezoneClip = /[^-+\dA-Z]/g,
			pad = function (val, len) {
				val = String(val);
				len = len || 2;
				while (val.length < len) val = "0" + val;
				return val;
			};

		// Regexes and supporting functions are cached through closure
		return function (date, mask, utc) {
			var dF = dateFormat;

			// You can't provide utc if you skip other args (use the "UTC:" mask prefix)
			if (arguments.length == 1 && Object.prototype.toString.call(date) == "[object String]" && !/\d/.test(date)) {
				mask = date;
				date = undefined;
			}

			// Passing date through Date applies Date.parse, if necessary
			date = date ? new Date(date) : new Date;
			if (isNaN(date)) throw SyntaxError("invalid date");

			mask = String(dF.masks[mask] || mask || dF.masks["default"]);

			// Allow setting the utc argument via the mask
			if (mask.slice(0, 4) == "UTC:") {
				mask = mask.slice(4);
				utc = true;
			}

			var	_ = utc ? "getUTC" : "get",
				d = date[_ + "Date"](),
				D = date[_ + "Day"](),
				m = date[_ + "Month"](),
				y = date[_ + "FullYear"](),
				H = date[_ + "Hours"](),
				M = date[_ + "Minutes"](),
				s = date[_ + "Seconds"](),
				L = date[_ + "Milliseconds"](),
				o = utc ? 0 : date.getTimezoneOffset(),
				flags = {
					d:    d,
					dd:   pad(d),
					ddd:  dF.i18n.dayNames[D],
					dddd: dF.i18n.dayNames[D + 7],
					m:    m + 1,
					mm:   pad(m + 1),
					mmm:  dF.i18n.monthNames[m],
					mmmm: dF.i18n.monthNames[m + 12],
					yy:   String(y).slice(2),
					yyyy: y,
					h:    H % 12 || 12,
					hh:   pad(H % 12 || 12),
					H:    H,
					HH:   pad(H),
					M:    M,
					MM:   pad(M),
					s:    s,
					ss:   pad(s),
					l:    pad(L, 3),
					L:    pad(L > 99 ? Math.round(L / 10) : L),
					t:    H < 12 ? "a"  : "p",
					tt:   H < 12 ? "am" : "pm",
					T:    H < 12 ? "A"  : "P",
					TT:   H < 12 ? "AM" : "PM",
					Z:    utc ? "UTC" : (String(date).match(timezone) || [""]).pop().replace(timezoneClip, ""),
					o:    (o > 0 ? "-" : "+") + pad(Math.floor(Math.abs(o) / 60) * 100 + Math.abs(o) % 60, 4),
					S:    ["th", "st", "nd", "rd"][d % 10 > 3 ? 0 : (d % 100 - d % 10 != 10) * d % 10]
				};

			return mask.replace(token, function ($0) {
				return $0 in flags ? flags[$0] : $0.slice(1, $0.length - 1);
			});
		};
	}();

	// Some common format strings
	dateFormat.masks = {
		"default":      "ddd mmm dd yyyy HH:MM:ss",
		shortDate:      "m/d/yy",
		mediumDate:     "mmm d, yyyy",
		longDate:       "mmmm d, yyyy",
		fullDate:       "dddd, mmmm d, yyyy",
		shortTime:      "h:MM TT",
		mediumTime:     "h:MM:ss TT",
		longTime:       "h:MM:ss TT Z",
		isoDate:        "yyyy-mm-dd",
		isoTime:        "HH:MM:ss",
		isoDateTime:    "yyyy-mm-dd'T'HH:MM:ss",
		isoUtcDateTime: "UTC:yyyy-mm-dd'T'HH:MM:ss'Z'"
	};

	// Internationalization strings
	dateFormat.i18n = {
		dayNames: [
			"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
			"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
		],
		monthNames: [
			"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
			"January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"
		]
	};

	// For convenience...
	Date.prototype.format = function (mask, utc) {
		return dateFormat(this, mask, utc);
	};
	function ukDateToUs(suk)
	{
		var A = String(suk).split(/[\\\/]/); 
		A = [A[1],A[0],A[2]]; 
		return A.join('/');
	}
// Checks if a given date string is in 
	// one of the valid formats:
	// a) M/D/YYYY format
	// b) M-D-YYYY format
	// c) M.D.YYYY format
	// d) M_D_YYYY format
	function isDate(s)
	{   
		// make sure it is in the expected format
		if (s.search(/^\d{1,2}[\/|\-|\.|_]\d{1,2}[\/|\-|\.|_]\d{4}/g) != 0)
			return false;

		// remove other separators that are not valid with the Date class 			
		s = s.replace(/[\-|\.|_]/g, "/");
				
		// convert it into a date instance
	    var dt = new Date(Date.parse(s));	    

	    // check the components of the date
	    // since Date instance automatically rolls over each component
	    var arrDateParts = s.split("/");
	    return (
	        dt.getMonth() == arrDateParts[0]-1 &&
	        dt.getDate() == arrDateParts[1] &&
	        dt.getFullYear() == arrDateParts[2]
		);			
	}
	
	function slideOver(obj, target)
	{
		//get the position of the placeholder element  
		var pos = $(target).offset();    
		var eWidth = $(target).outerWidth();
		var mWidth = $(obj).outerWidth();
		var left = (pos.left + eWidth - mWidth) + "px";
		var top = pos.top + "px";
		//show the menu directly over the placeholder  
		$(obj).css({ 
				position: 'absolute',
				zIndex: 5000,
				left: left, 
				top: top
				});

		$(obj).slideToggle();
	}
