/*
 * A time picker for jQuery
 * Based on original timePicker by Sam Collet (http://www.texotela.co.uk) -
 * copyright (c) 2006 Sam Collett (http://www.texotela.co.uk)
 *
 * Dual licensed under the MIT and GPL licenses.
 * Copyright (c) 2009 Anders Fajerson
 * @name     timePicker
 * @version  0.2
 * @author   Anders Fajerson (http://perifer.se)
 * @example  $("#mytime").timePicker();
 * @example  $("#mytime").timePicker({step:30, startTime:"15:00", endTime:"18:00"});
 */

(function($){
	$.fn.timePicker = function(options) {
		// Build main options before element iteration
		var settings = $.extend({}, $.fn.timePicker.defaults, options);

		return this.each(function() {
			$.timePicker(this, settings);
		});
	};

	$.timePicker = function (elm, settings) {
		var e = $(elm)[0];
		return e.timePicker || (e.timePicker = new jQuery._timePicker(e, settings));
	};

	$._timePicker = function(elm, settings) {

		var tpOver = false;
		var keyDown = true;
		var startTime = timeToDate(settings.startTime, settings);
		var endTime = timeToDate(settings.endTime, settings);

		$(elm).attr('autocomplete', 'OFF'); // Disable browser autocomplete

		var times = [];
		var time = new Date(startTime); // Create a new date object.
		while(time <= endTime) {
			times[times.length] = formatTime(time, settings);
			time = new Date(time.setMinutes(time.getMinutes() + settings.step));
		}

		var $tpDiv = $('<div class="time-picker'+ (settings.show24Hours ? '' : ' time-picker-12hours') +'"></div>');
		var $tpList = $('<ul></ul>');

		// Build the list.
		for(var i = 0; i < times.length; i++) {
			$tpList.append("<li>" + times[i] + "</li>");
		}
		$tpDiv.append($tpList);
		// Append the timPicker to the body and position it.
		var elmOffset = $(elm).offset();
		$tpDiv.appendTo('body').css({'top':elmOffset.top, 'left':elmOffset.left}).hide();

		// Store the mouse state, used by the blur event. Use mouseover instead of
		// mousedown since Opera fires blur before mousedown.
		$tpDiv.mouseover(function() {
			tpOver = true;
		}).mouseout(function() {
			tpOver = false;
		});

		$("li", $tpList).mouseover(function() {
			if (!keyDown) {
				$("li.selected", $tpDiv).removeClass("selected");
				$(this).addClass("selected");
			}
		}).mousedown(function() {
			tpOver = true;
		}).click(function() {
			setTimeVal(elm, this, $tpDiv, settings);
			tpOver = false;
		});

		var showPicker = function() {
			if ($tpDiv.is(":visible")) {
				return false;
			}
			$("li", $tpDiv).removeClass("selected");

			// Show picker. This has to be done before scrollTop is set since that
			// can't be done on hidden elements.
			$tpDiv.show();

			// Try to find a time in the list that matches the entered time.
			var time = elm.value ? timeStringToDate(elm.value, settings) : startTime;
			var startMin = startTime.getHours() * 60 + startTime.getMinutes();
			var min = (time.getHours() * 60 + time.getMinutes()) - startMin;
			var steps = Math.round(min / settings.step);
			var roundTime = normaliseTime(new Date(0, 0, 0, 0, (steps * settings.step + startMin), 0));
			roundTime = (startTime < roundTime && roundTime <= endTime) ? roundTime : startTime;
			var $matchedTime = $("li:contains(" + formatTime(roundTime, settings) + ")", $tpDiv);
	
			if ($matchedTime.length) {
				$matchedTime.addClass("selected");
				// Scroll to matched time.
				$tpDiv[0].scrollTop = $matchedTime[0].offsetTop;
			}
			return true;
		};
		// Attach to click as well as focus so timePicker can be shown again when
		// clicking on the input when it already has focus.
		$(elm).focus(showPicker).click(showPicker);
		// Hide timepicker on blur
		$(elm).blur(function() {
			if (!tpOver) {
				$tpDiv.hide();
			}
		});
		// Keypress doesn't repeat on Safari for non-text keys.
		// Keydown doesn't repeat on Firefox and Opera on Mac.
		// Using kepress for Opera and Firefox and keydown for the rest seems to
		// work with up/down/enter/esc.
		var event = ($.browser.opera || $.browser.mozilla) ? 'keypress' : 'keydown';
		$(elm)[event](function(e) {
			var $selected;
			keyDown = true;
			var top = $tpDiv[0].scrollTop;
			switch (e.keyCode) {
				case 38: // Up arrow.
				// Just show picker if it's hidden.
				if (showPicker()) {
					return false;
				};
				$selected = $("li.selected", $tpList);
				var prev = $selected.prev().addClass("selected")[0];
				if (prev) {
					$selected.removeClass("selected");
					// Scroll item into view.
					if (prev.offsetTop < top) {
						$tpDiv[0].scrollTop = top - prev.offsetHeight;
					}
				}else {
					// Loop to next item.
					$selected.removeClass("selected");
					prev = $("li:last", $tpList).addClass("selected")[0];
					$tpDiv[0].scrollTop = prev.offsetTop - prev.offsetHeight;
				}
				return false;
				break;
				case 40: // Down arrow, similar in behaviour to up arrow.
					if (showPicker()) {
						return false;
					};
					$selected = $("li.selected", $tpList);
					var next = $selected.next().addClass("selected")[0];
					if (next) {
						$selected.removeClass("selected");
						if (next.offsetTop + next.offsetHeight > top + $tpDiv[0].offsetHeight) {
						$tpDiv[0].scrollTop = top + next.offsetHeight;
					}
				}else {
					$selected.removeClass("selected");
					next = $("li:first", $tpList).addClass("selected")[0];
					$tpDiv[0].scrollTop = 0;
				}
				return false;
				break;
				case 13: // Enter
					if ($tpDiv.is(":visible")) {
						var sel = $("li.selected", $tpList)[0];
						setTimeVal(elm, sel, $tpDiv, settings);
					}
				return false;
				break;
				case 27: // Esc
					$tpDiv.hide();
					return false;
					break;
			}
			return true;
		});
		$(elm).keyup(function(e) {
			keyDown = false;
		});
		// Helper function to get an inputs current time as Date object.
		// Returns a Date object.
		this.getTime = function() {
			return timeStringToDate(elm.value, settings);
		};
		// Helper function to set a time input.
		// Takes a Date object.
		this.setTime = function(time) {
			elm.value = formatTime(normaliseTime(time), settings);
			// Trigger element's change events.
			$(elm).change();
		};

	}; // End fn;

	// Plugin defaults.
	$.fn.timePicker.defaults = {
		step:15,
		startTime: new Date(0, 0, 0, 08, 0, 0),
		endTime: new Date(0, 0, 0, 22, 0, 0),
		separator: ':',
		show24Hours: true
	};

	// Private functions.

	function setTimeVal(elm, sel, $tpDiv, settings) {
		// Update input field
		elm.value = $(sel).text();
		// Trigger element's change events.
		$(elm).change();
		// Keep focus for all but IE (which doesn't like it)
		if (!$.browser.msie) {
			elm.focus();
		}
		// Hide picker
		$tpDiv.hide();
	}

	function formatTime(time, settings) {
		var h = time.getHours();
		var hours = settings.show24Hours ? h : (((h + 11) % 12) + 1);
		var minutes = time.getMinutes();
		return formatNumber(hours) + settings.separator + formatNumber(minutes) + (settings.show24Hours ? '' : ((h < 12) ? ' AM' : ' PM'));
	}

	function formatNumber(value) {
		return (value < 10 ? '0' : '') + value;
	}

	function timeToDate(input, settings) {
		return (typeof input == 'object') ? normaliseTime(input) : timeStringToDate(input, settings);
	}

	function timeStringToDate(input, settings) {
		if (input) {
			var array = input.split(settings.separator);
			var hours = parseFloat(array[0]);
			var minutes = parseFloat(array[1]);

			// Convert AM/PM hour to 24-hour format.
			if (!settings.show24Hours) {
				if (hours === 12 && input.substr('AM') !== -1) {
					hours = 0;
				}else if (hours !== 12 && input.indexOf('PM') !== -1) {
					hours += 12;
				}
			}
			var time = new Date(0, 0, 0, hours, minutes, 0);
			return normaliseTime(time);
		}
		return null;
	}

	/* Normalise time object to a common date. */
	function normaliseTime(time) {
		time.setFullYear(2001);
		time.setMonth(0);
		time.setDate(0);
		return time;
	}

})(jQuery);


