jQuery.fn.timepicker = (function () {
	var
	mktime = function (mo,dy,yr,hr,mn,sc) {
		var _ret = new Date(),und;
		mo!=und?_ret.setMonth(mo-1):false;
		dy!=und?_ret.setDate(dy):false;
		yr!=und?_ret.setFullYear(yr):false;
		hr!=und?_ret.setHours(hr):false;
		mn!=und?_ret.setMinutes(mn):false;
		sc!=und?_ret.setSeconds(sc):false;
		return Math.floor(_ret.getTime()/1000);
	},
	fromUNIX = function(ts) {
		var d = new Date();
		d.setTime(ts*1000);
		return d;
	},
	$=jQuery,
		_formatDate = function (d) {
			var o = this.data('options'),
				ap = '',
				hr = d.getHours(),
				mn = d.getMinutes(),
				sc = d.getSeconds();
			if (!o.military) {
				ap = hr>=12?' pm':' am';
				hr = hr % 12;
				if (hr==0) hr=12;
			}
			if (mn==0) mn='';
			else {
				if (mn<10) mn='0'+mn;
				mn=':'+mn;
			}
			if (sc==0) sc='';
			else {
				if (sc<10) sc='0'+sc;
				sc=':'+sc;
			}
			return hr+mn+sc+ap;
		},
		addOptions = function(obj,opts) {
			var tmp,options = obj.data('options');
			opts = $.extend(options,opts);
			if (opts.startTime instanceof Array) {	
				tmp=opts.startTime;
				opts.startTime = mktime(0,0,0,tmp[0],tmp[1],0);
			}
			if (opts.endTime instanceof Array) {	
				tmp=opts.endTime;
				opts.endTime = mktime(0,0,0,tmp[0],tmp[1],0);
			}
			if (opts.endTime < opts.startTime) 
				opts.endTime+=86400;			
			obj.data('options',opts);
			return opts;
		},
		timepicker = function (options) {
			var _main=this;
			if (options == 'settings') {
				addOptions(this,arguments[1]);
				return;
			}
			
			if (!(options instanceof Object)) return;
			this.data('options',{
				startTime: [9,0], //hours, minutes
				endTime: [17,0],
				timeStep: 15, //in minutes
				military: false,
				format: _formatDate, //takes a Javascript Date object
				rootClass: 'ui-timepicker', //for inheritance; do not style this class.
				callback: function (){}, //called with (dateText, UNIX Timestamp, Date object)
				timeOK: function () { return [true]; } //similar to datepicker's beforeShowDay
			});
			addOptions(this,options);
			
			_main.each(function () {
				var 
					_main = $(this),
					_div = '<div></div>',
					_link = '<a></a>',
					hrs = null,
					mns = null,
					_onElemClick = function (e) {
						_hours.hide();
						_main.attr('disabled',true);
						if (hrs!==null)
							hrs.remove();
						hrs=null;
						
						var 
							o = _main.data('options'),
							i,d,lh=-1,h,ft,ld,t,ok;
						for (i=o.startTime; i<=o.endTime; i+=o.timeStep*60) {
							d = fromUNIX(i);
							h = d.getHours();
							ok = o.timeOK(i);
							if (h != lh && (ok[0] || !!ok[1])) {
								t = _hour.clone(true);
								t.text(_formatDate.apply(_main,[d]))
								t.data('stamp',i)
								t.appendTo(_hours);
								if (!!ok[1])
									t.addClass(t[1]);
							}
							lh = h;
						}
						hrs = _hours.find('.ui-timepicker-hr');
						_positionHours(e);
						_hours.show();
						return false;
					},
					_onHourHover = function (e) {
						_minutes.hide();
						if (mns!==null)
							mns.remove();
						mns = null;
						var _mainHour = $(this),
							o = _main.data('options'),
							st = _mainHour.data('stamp'),
							ed = Math.min(o.endTime,Math.floor(st/3600)*3600+3600),
							m,lm=-1,d,ld,i,t,ok;
						for (i=st+o.timeStep*60; i<ed; i+=o.timeStep*60) {
							d = fromUNIX(i);
							m = d.getMinutes();
							ok = o.timeOK(i);
							if (m!=lm && (ok[0] || !!ok[1])) {
								t = _minute
									.clone(true)
									.appendTo(_minutes)
									.text(_formatDate.apply(_main,[d]))
									.data('stamp',i);
								if (!!ok[1])
									t.addClass(t[1]);
							}
							lm = m;
						}
						mns = _minutes.find('.ui-timepicker-mn');
						if (mns.length>0) {
							
							hrs = _hours.find('.ui-timepicker-hr');
							_positionMinutes(e,mns.index(_mainHour),hrs.length);
							_minutes.show();
						}
						return false;
					},
					_positionHours = function (e) {
						var _body = $(document.body),
							mx=e.pageX,
							my=e.pageY,
							sl=_body.scrollLeft(),
							st=_body.scrollTop(),
							cw=_body.attr('clientWidth'),
							ch=_body.attr('clientHeight'),
							ew=_hours.width(),
							eh=_hours.height();
						_hours.css({
							left: sl+(mx-sl)/cw*(cw-ew),
							top: st+(my-st)/ch*(ch-eh),
							position: 'absolute',
							whiteSpace: 'nowrap'
						});
						
					},
					_positionMinutes = function (e,idx,len) {
						var _body = $(document.body),
							mx=e.pageX,
							my=e.pageY,
							sl=_body.scrollLeft(),
							st=_body.scrollTop(),
							cw=_body.attr('clientWidth'),
							ch=_body.attr('clientHeight'),
							ew=_minutes.width(),
							eh=_minutes.height(),
							pt=parseInt(_hours.css('top')),
							pl=parseInt(_hours.css('left')),
							pw=_hours.width(),
							ph=_hours.height();
						_minutes.css({
							left: (mx-sl)>(cw/2)?-ew:pw,
							top: (ph-eh)*(idx/(len-1)),
							position: 'absolute'
						});
					},
					_chooseTime = function () {
						var _this = $(this),
							txt = _this.text(),
							stamp = _this.data('stamp'),
							d = new Date();
						d.setTime(stamp*1000);
						_main
							.attr('disabled',false)
							.val(txt)
							.change()
							.data('options')
							.callback(txt,stamp,d);
						_hours.hide();
						if (hrs!=null)
							hrs.remove();
						if (mns!=null)
							mns.remove();
						return false;
					},
					_cancelEvent = function () {return false;},
					_trxp = /(\d?\d)(?:\:(\d{2}))?\s?(a|p)m/i,
					_wrap = $(_div)
						.addClass($(this).data('options').rootClass)
						.appendTo(document.body),
					_hours = $(_div)
						.addClass('ui-timepicker-hrbox')
						.hide()
						.mouseover(_cancelEvent)
						.appendTo(_wrap),
					_hour = $(_link)
						.addClass('ui-timepicker-hr')
						.mouseover(_onHourHover)
						.attr('href','#')						
						.click(_chooseTime),
					_minutes = $(_div)
						.addClass('ui-timepicker-mnbox')
						.appendTo(_hours)
						,
					_minute = $(_link)
						.addClass('ui-timepicker-mn')
						.attr('href','#')
						.click(_chooseTime),
					_cancel = function () { return false; };
					
				_main
					.addClass('.ui-timepicker-input')
					.click(_onElemClick)
					.keydown(_cancel)
					.keyup(_cancel)
					.keypress(_cancel);
				$(window).mouseover(function () { 
					_hours.hide();
					_main[0].removeAttribute('disabled');					
				});
				if (_trxp.test(_main.val())) {
					var cur = _main.val().match(_trxp);
					cur.shift();
					if (cur[0]==12) cur[0]=0;
					if (cur[2]=='p') cur[0]=parseInt(cur[0])+12;	
					if (typeof cur[1] == 'undefined') cur[1]=0;
					var 
						stamp = mktime(0,0,0,cur[0],cur[1],0),
						d = new Date(stamp*1000);
					
					_main
						.change()
						.data('options')
						.callback(_main.val(), stamp, d);
				}
			});
		return this;
	}
	return timepicker;
})();