/** 
 * @projectDescription jQuery extension for generating tooltip/overlay style message with a variety of behaviors.
 * This is a complete rewrite from version 3.x
 *
 * @author Luke Cuthbertson
 * @version 4.2
 *
 * Updated
 * @version 4.3
 * @author Matthew Langley
 * Opts added to stop the tool tip from going outside the document.
 */
(function($){
	
	/*
	 * because the options are quite complex and not all are comatible with each other
	 * these are provided for convienience. Pass in the preset name as an optional
	 * first parameter
	 */
	var presets = {
		menuStyle: {
			fixed: true,
			offset: ['center', 'bottom'],
			autoClose: 6000,
			fadeTime: 150,
			sensitivity: 600,
			src: 'html',
			tipClass: 'tip',
			hoverClass: 'hover',
			events: 'mouseenter',
			documentClose: false,
			className: 'menuStyle',
			reEnterPreventsClose: true,
			mouseOverTipPreventsClose: true,
			ieTransparency: true,
			transitions: 'fade',
			slideDistance: 15,
			format: function(text){return text;}
		},
		HTMLhelp: {
			fixed: true,
			offset: ['top', 'right'],
			autoClose: false,
			fadeTime: 150,
			sensitivity: 600,
			src: 'html',
			tipClass: 'tip',
			hoverClass: 'hover',
			events: 'mouseenter',
			documentClose: false,
			className: 'HTMLStyle',
			reEnterPreventsClose: true,
			mouseOverTipPreventsClose: false,
			ieTransparency: true,
			transitions: 'fade',
			slideDistance: 15,
			format: function(text){return text;}
		},
		nativeStyle: {
			fixed: false,//fixed position relative to element or position relative to mouse
			offset: [5, 10],//offset relative to fixed setting (element or mouse)
			autoClose: 5000,//tip closes after a period of time
			fadeTime: 150,//time spend fading tip inand out
			sensitivity: 600,//delay before tip begins to fade in or out
			src: 'title',//contents of tooltip from an html element or the title attribute
			tipClass: 'tip',//class to look for on element if src setting is 'html'
			hoverClass: 'hover',//class is placed on target element when mouse is over it
			events: 'mouseenter',//what events should trigger the tooltip - String or Array for combination
			documentClose: true,//should clicking the page close tool tip. Use with events 'click'
			className: 'nativeStyle',//class name to place in tooltip wrapper element
			reEnterPreventsClose: false,//should tip close and reopen or stay open when mouse reenters target
			mouseOverTipPreventsClose: false,//should hovering over the tip itself prevent it from closing
			ieTransparency: true,//apply AlphaImageLoader hack in IE
			transitions: 'fade',//transition style - 'slide' or 'fade' String or Array for combination
			slideDistance: 15,//how far to slide when using 'slide' transition	
			format: function(text){return text;},//supply function that gets passed the tip text - use in 'title' mode
			stopOverlap: true //enables checking to keep the tooltip inside the document (makes sure it doesnt overlap outside)
		}
	}
	
	
	$.fn.tooltip = function(presetName, opts){
		var obj = this;

		//if a preset String is not supplied, first argument is opts
		if(!presetName instanceof String){
			var opts = presetName;
		}
		
		var opts = $.extend({}, presets['nativeStyle']/*default*/, presets[presetName] || {}, opts || {});//opts override presets
		
		//pack single events into array so they can be handled the same way
		opts.events = $.makeArray(opts.events);
		opts.transitions = $.makeArray(opts.transitions);

		var evt;
		//track mouse position continuously
		$(document).mousemove(function(e){
			evt = e;
		});
		
		/**
		 * @param {Object} elem target element
		 * @return {Boolean} true of the mouse is over the target element, otherwise false
		 */
		function testProximity(elem){
			var offset = elem.offset();
			if((evt.pageX >= offset.left && evt.pageX <= offset.left + elem.outerWidth())
				&& (evt.pageY >= offset.top && evt.pageY <= offset.top + elem.outerHeight())){
				return true;
			};
			return false;
		};
	
		/**
		 * calculates the position that the tooltip should be placed
		 * @param {Collection} elem target element
		 * @param {Collection} container tooltip element
		 * @return {Array} array of coordinates relative to document
		 */
		function getPosition(elem, container) {
			//if we are using fixed positioning the tool tip is placed at a fixed distance from
			//the top left corner of the target element
			if (opts.fixed) {
				var coords = [(elem.offset().left + opts.offset[0]), (elem.offset().top + opts.offset[1])];
				if(opts.offset[0] == 'center'){
					coords[0] = elem.offset().left - (container.outerWidth() / 2) + (elem.outerWidth() / 2);
				}
				if (opts.offset[0] == 'left') {
					coords[0] = elem.offset().left;
				}
				if (opts.offset[0] == 'right') {
					coords[0] = elem.offset().left - (container.outerWidth() - elem.outerWidth());
				}
				if (opts.offset[0] == 'outLeft') {
					coords[0] = elem.offset().left - container.outerWidth();
				}
				if (opts.offset[0] == 'outerRight') {
					coords[0] = elem.offset().left + elem.outerWidth();
				}								
				
				if(opts.offset[1] == 'bottom'){
					coords[1] = elem.offset().top + elem.outerHeight();
				}
				if(opts.offset[1] == 'top'){
					coords[1] = elem.offset().top - container.outerHeight();
				}
				if (opts.stopOverlap){
					//check that the tool tip resides within the document and does not overlap the edges
					if (coords[0] < 0) {
						coords[0] = 0;	
					}else if (coords[0] > (document.width - elem.outerWidth())) {
						coords = (document.width - elem.outerWidth());
					}
				}
				return coords;
			}
			//otherwise we use the mouse coordinates from the last move event like a native tooltip does
			else {
				var coords = [evt.pageX + opts.offset[0], evt.pageY + opts.offset[1]];

				if (opts.stopOverlap){
					//check that the tool tip resides within the document and does not overlap the edges
					if (coords[0] < 0) {
						coords[0] = 0;	
					}else if (coords[0] > (document.width - elem.outerWidth())) {
						coords = (document.width - elem.outerWidth());
					}
				}				
				return coords;
			};
		};
		
		this.each(function(){

			var container;//the tool tip itself
			var enterTimers = [];//timer for mouse enter
			var exitTimers = [];//timer for mouse leave
			var autoCloseTimers = [];//timer for auto close after delay
			var event;//mouse move events on the hit box stored here		
			var elem = $(this);//convenience reference to target element
			var closeIntervals = [];//polling for mouseleave
			var locked = false;//open has been triggered or already open
			var stop = false;//used by stop handler
			
			if (opts.src == 'title') {
				var tipText = elem.attr('title');//the text for the tooltip extracted from title attribute			
			}
			else{
				if(opts.src == 'html'){
					//search up and down the tree to find the closest html tooltip
					var parent = $(this).parent();
					var tip = parent.find('.'+opts.tipClass);
					while(tip.size() == 0 && parent.parents().length != 0){
						parent = parent.parent();	
						tip = parent.find('.'+opts.tipClass);
					}
					if(tip[0] == document){
						return obj;//return collection if not found
					}
					//the tipText is then the html for the discovered tooltip
					var tipText = $('<div></div>').append(tip).html(); 
				}
				else{
					throw new Error('invalid mode given');
				}
			}

			//moving the mouse clears the title attribute temporarily so it does not display
			//this seems to work more reliably in IE than mouseenter at removing the title attribute in time
			elem.mousemove(function(e){
				if (opts.src == 'title') {
					elem.attr('title', '');
				}
				evt = e;
			})
			for (var i = 0; i < opts.events.length; i++) {
				switch (opts.events[i]) {
					case 'mouseover':
					case 'mouseenter':
						elem.mouseenter(function(){
							if (opts.reEnterPreventsClose) {
								clearExitTimers();//is this dangerous
							}
							//the handlers will apply the initial delay themselves passing 0 to the enterHanlder function
							//this prevents the initial call being cancelled by other activities that occur after the ecucution
							//of the handler
							enterTimers.push(setTimeout(function(){
								//we must test that the mouse hasn't strayed of the target again durring the delay
								if (testProximity(elem)) {
									enterHandler(0);
								}
							}, opts.sensitivity));
						});
						elem.mouseleave(function(){
							//this ensures that the tooltip is unlocked in situations where the mouse is left hovering over
							//the target until an autoclose kicks in.
							locked = false;
						});
						break;
					case 'click':
						elem.click(function(e){
							enterTimers.push(setTimeout(function(){
								if (testProximity(elem)) {
									enterHandler(0);
								}
							}, opts.sensitivity));
							return false;
						});
						if (opts.documentClose) {
							//in this mode clicking the document can be made to make the tooltip close
							$(document).click(function(){
								leaveHandler();
							});
						}
						;						break;
					case 'load':
						enterHandler(0);
						var done;
						//listen for autocloses and release the lock
						elem.bind('tooltip.autoclosed', function(){
							if(!done){
								locked = false;
							}
							done = true;
						})
						//once the user has interacted with the tool tip, or it
						//closes automatically, this event handler does nothing
						elem.mouseenter(function(){
							if (!done) {
								closeIntervals.push(setInterval(function(){
									try {
										if (!testProximity(elem)) {
											leaveHandler();
										}
									} 
									catch (e) {
										clearCloseIntervals();
									}
								}, 50));
								enterTimers.push(setTimeout(function(){
									if (testProximity(elem)) {
										enterHandler(0);
									}
								}, opts.sensitivity));
								done = true;
							}
						});
						break;
					default:
						throw new Error('unknown event option \'' + opts.events[i] + "'");
				}
			}
			
			/**
			 * manages the timing of operations when the mouse leaves
			 */
			function leaveHandler(delay){
				locked = false;
				elem.removeClass('hover');
				
				exitTimers.push(setTimeout(function(){
					close();
					//reinstate title attribute
					if (opts.src == 'title') {
						elem.attr('title', tipText);
					}
					clearExitTimers();
					clearCloseIntervals();//stop polling
				}, typeof delay != 'undefined' ? delay : (opts.sensitivity == 0) ? 0 : opts.sensitivity/2));
				//clear other timers
				clearAutoCloseTimers()		
				clearEnterTimers()
			};
			
			/**
			 * manages the timing of operations when the mouse enters
			 */
			function enterHandler(delay){
					
						if(stop){
							return;
						}
					
						elem.addClass('hover');
						
						if (opts.reEnterPreventsClose) {
							clearExitTimers()
						}
						
						startPolling();
						
						if (!isOpen() && !locked) {
							locked = true;
							enterTimers.push(setTimeout(function(){
								open();
							}, delay));
						}
						
						if (opts.autoClose) {
							autoClose();
						};
			};
			
			/**
			 * handles the automatic closing of the tooltip after a period of time. Is only called from enterHandler
			 */
			function autoClose(){
				//Autoclose does not release the lock  like leaveHandler as this would allow the tooltip to 
				//immediately reopen again. The mouse must leave and reenter.
				autoCloseTimers.push(setTimeout(function(){					
					close();
					exitTimer = null;
					if (opts.src == 'title') {
						elem.attr('title', tipText);
					}
					clearCloseIntervals()
					clearAutoCloseTimers();
					elem.trigger('tooltip.autoclosed')
				}, opts.autoClose));
			}
			
			function startPolling(){
				if (evt) {
					//start polling for mouse coordinates leaving target
					clearCloseIntervals()
					closeIntervals.push(setInterval(function(){
						if (!testProximity(elem)) {
							leaveHandler();
						}
						else {
							enterHandler(opts.sensitivity);//could this be 0;
						}
					}, 50));
				}
			}
			
			function isOpen(){
				return (container && container.parents().size() != 0);
			}
			
			function clearExitTimers(){
				for (var i = 0, j = exitTimers.length; i < j; i++) {
					clearTimeout(exitTimers.pop());
				}	
			}
			function clearAutoCloseTimers(){
				for (var i = 0, j = autoCloseTimers.length; i < j ; i++) {
					clearTimeout(autoCloseTimers.pop());
				}
			}
			function clearEnterTimers(){
				for (var i = 0, j = enterTimers.length; i < j; i++) {
					clearTimeout(enterTimers.pop());
				}
			}
			function clearCloseIntervals(){
				for (var i = 0, j = closeIntervals.length; i < j; i++) {
					clearTimeout(closeIntervals.pop());
				}
			}
			
			/**
			 * opens the tool tip
			 * @param {Event} e
			 */
			function open(e){
				try {
					createToolTip(tipText);
					if (!e) {
						elem.trigger('tooltip.onopen')
					};
				}
				catch(e){ throw e};
			}
			
			/**
			 * closes the tooltip
			 * @param {Event} e
			 */
			function close(e){
				if (container) {
					container.fadeOut(opts.fadeTime, function(){
						container.remove();
					});
					if (!e) {
						elem.trigger('tooltip.onclose')
					};
				}
			}	
			
			function openHandler(){
				enterTimer = setTimeout(function(){
					var previousSetting = opts.fixed;
					opts.fixed = true;
					open(e);
					opts.fixed = previousSetting;
				}, 0);
			}
			
			function closeHandler(){ 
				leaveHandler(0)
			}
			
			function stopHandler(){
				stop = true;
			}
			
			function startHandler(){
				stop = false;
			}
			
			elem.bind('tip.close', closeHandler);
			elem.bind('tip.open', openHandler);
			elem.bind('tip.stop', stopHandler);
			elem.bind('tip.start', startHandler);

			/**
			 * creates the tool tip itself at the specified coordinates.
			 * @param {String} text
			 * @return {Collection} the outermost element of the tooltip
			 */
			function createToolTip(text){
				var html = 	'<div id="tooltip" class="tooltipWrapper1 '+opts.className+'">\
						 		<div class="tooltipWrapper2">\
								<div class="tooltipWrapper3">\
								<div class="tooltipWrapper4">\
								<div class="tooltipWrapper5">\
								<div class="tooltipWrapper6">\
								<div class="tooltipWrapper7">\
						  		<a href="#" class="close" >&nbsp;</a>'
									+opts.format(text)+
								'</div>\
								</div>\
								</div>\
								</div>\
								</div>\
								<div class="tooltipWrapper8"></div>\
								<div class="tooltipWrapper9"></div>\
								<div class="tooltipWrapper10"></div>\
								</div>\
							</div>';
							
				container = $(html).css({
					position: 'absolute',
					zIndex: 999
				});
				//add in AlphaImageLoader hack for IE
				if(opts.ieTransparency && !jQuery.support.opacity){
					container.find('*').css('filter', 'progid:DXImageTransform.Microsoft.AlphaImageLoader()')
				} 
				
				$(document.body).append(container);
				
				var coords = getPosition(elem, container);
				container.css({
					top: ((coords[1]) + 'px'),
					left: ((coords[0])+'px'),
					display: 'none'
				});
				
				//attach event handlers to tooltip if required
				if (opts.mouseOverTipPreventsClose) {
					container.mouseenter(function(){
						enterHandler();
						//stop polling as the the tooltip is likely not directly over the target
						//and mouse leaving would be detected
						clearCloseIntervals();
						//I have decided autoclosing should be disabled while the mouse is over
						//the tooltip as this suggests the user is interested in it and still reading it
						clearAutoCloseTimers();
					});
					container.mouseleave(function(){
						//fire the leave handler
						leaveHandler();
						//but restart the polling that it will have turned off
						startPolling();				
					});
				}				
				
				//events are bound to both the trigger and the tooltip
				elem.bind('tip.close', closeHandler);
				elem.bind('tip.open', openHandler);
				elem.bind('tip.stop', stopHandler);
				elem.bind('tip.start', startHandler);

				transition(container);
	
				return container;
			};
			
			function transition(container){
				for (var i = 0; i < opts.transitions.length; i++) {
					switch(opts.transitions[i]){
						case 'fade':
							container.fadeIn(opts.fadeTime);
							break;
						case 'slideDown':
							container.animate({
								top: container.offset().top + opts.slideDistance
							}, {
								queue: false,
								duration: opts.fadeTime
							});
							break;
						case 'slideUp':
							container.animate({
								top: container.offset().top - opts.slideDistance
							}, {
								queue: false,
								duration: opts.fadeTime
							});	
							break;					
					}
				}
			}

		})
		return this;
	}
	
})(jQuery);

