/**
 * PowerTip
 *
 * @fileoverview  jQuery plugin that creates hover tooltips.
 * @link          http://stevenbenner.github.com/jquery-powertip/
 * @author        Steven Benner (http://stevenbenner.com/)
 * @version       1.1.0
 * @requires      jQuery 1.7+
 *
 * @license jQuery PowerTip Plugin v1.1.0
 * http://stevenbenner.github.com/jquery-powertip/
 * Copyright 2012 Steven Benner (http://stevenbenner.com/)
 * Released under the MIT license.
 * <https://raw.github.com/stevenbenner/jquery-powertip/master/LICENSE.txt>
 */

//Add custom i18n.js

(function($) {
	'use strict';

	// useful private variables
	var $document = $(document),
		$window = $(window),
		$body = $('body');

	/**
	 * Session data
	 * Private properties global to all powerTip instances
	 * @type Object
	 */
	var session = {
		isPopOpen: false,
		isFixedPopOpen: false,
		isClosing: false,
		popOpenImminent: false,
		activeHover: null,
		currentX: 0,
		currentY: 0,
		previousX: 0,
		previousY: 0,
		desyncTimeout: null,
		mouseTrackingActive: false
	};

	/**
	 * Display hover tooltips on the matched elements.
	 * @param {Object} opts The options object to use for the plugin.
	 * @return {Object} jQuery object for the matched selectors.
	 */
	$.fn.powerTip = function(opts) {

		// don't do any work if there were no matched elements
		if (!this.length) {
			return this;
		}

		// extend options
		var options = $.extend({}, $.fn.powerTip.defaults, opts),
			tipController = new TooltipController(options);

		// hook mouse tracking
		initMouseTracking();

		// setup the elements
		this.each(function() {
			var $this = $(this),
				dataPowertip = $this.data('powertip'),
				dataElem = $this.data('powertipjq'),
				dataTarget = $this.data('powertiptarget'),
				//title = $this.attr('title');
				title = I18N.i18nText($this.attr('lang'));


			// attempt to use title attribute text if there is no data-powertip,
			// data-powertipjq or data-powertiptarget. If we do use the title
			// attribute, delete the attribute so the browser will not show it
			if (!dataPowertip && !dataTarget && !dataElem && title) {
				$this.data('powertip', title);
				$this.removeAttr('title');
			}

			// create hover controllers for each element
			$this.data(
				'displayController',
				new DisplayController($this, options, tipController)
			);
		});

		// attach hover events to all matched elements
		return this.on({
			// mouse events
			mouseenter: function(event) {
				trackMouse(event);
				session.previousX = event.pageX;
				session.previousY = event.pageY;
				$(this).data('displayController').show();
			},
			mouseleave: function() {
				$(this).data('displayController').hide();
			},

			// keyboard events
			focus: function() {
				var element = $(this);
				if (!isMouseOver(element)) {
					element.data('displayController').show(true);
				}
			},
			blur: function() {
				$(this).data('displayController').hide(true);
			}
		});

	};

	/**
	 * Default options for the powerTip plugin.
	 * @type Object
	 */
	$.fn.powerTip.defaults = {
		fadeInTime: 200,
		fadeOutTime: 100,
		followMouse: false,
		popupId: 'powerTip',
		intentSensitivity: 7,
		intentPollInterval: 100,
		closeDelay: 100,
		placement: 'n',
		smartPlacement: false,
		offset: 7,
		mouseOnToPopup: false
	};

	/**
	 * Default smart placement priority lists.
	 * The first item in the array is the highest priority, the last is the
	 * lowest. The last item is also the default, which will be used if all
	 * previous options do not fit.
	 * @type Object
	 */
	$.fn.powerTip.smartPlacementLists = {
		n: ['n', 'ne', 'nw', 's'],
		e: ['e', 'ne', 'se', 'w', 'nw', 'sw', 'n', 's', 'e'],
		s: ['s', 'se', 'sw', 'n'],
		w: ['w', 'nw', 'sw', 'e', 'ne', 'se', 'n', 's', 'w'],
		nw: ['nw', 'w', 'sw', 'n', 's', 'se', 'nw'],
		ne: ['ne', 'e', 'se', 'n', 's', 'sw', 'ne'],
		sw: ['sw', 'w', 'nw', 's', 'n', 'ne', 'sw'],
		se: ['se', 'e', 'ne', 's', 'n', 'nw', 'se']
	};

	/**
	 * Public API
	 * @type Object
	 */
	$.powerTip = {

		/**
		 * Attempts to show the tooltip for the specified element.
		 * @public
		 * @param {Object} element The element that the tooltip should for.
		 */
		showTip: function(element) {
			// close any open tooltip
			$.powerTip.closeTip();
			// grab only the first matched element and ask it to show its tip
			element = element.first();
			if (!isMouseOver(element)) {
				element.data('displayController').show(true, true);
			}
		},

		/**
		 * Attempts to close any open tooltips.
		 * @public
		 */
		closeTip: function() {
			$document.triggerHandler('closePowerTip');
		}

	};

	/**
	 * Creates a new tooltip display controller.
	 * @private
	 * @constructor
	 * @param {Object} element The element that this controller will handle.
	 * @param {Object} options Options object containing settings.
	 * @param {TooltipController} tipController The TooltipController for this instance.
	 */
	function DisplayController(element, options, tipController) {
		var hoverTimer = null;

		/**
		 * Begins the process of showing a tooltip.
		 * @private
		 * @param {Boolean=} immediate Skip intent testing (optional).
		 * @param {Boolean=} forceOpen Ignore cursor position and force tooltip to open (optional).
		 */
		function openTooltip(immediate, forceOpen) {
			cancelTimer();
			if (!element.data('hasActiveHover')) {
				if (!immediate) {
					session.popOpenImminent = true;
					hoverTimer = setTimeout(
						function() {
							hoverTimer = null;
							checkForIntent(element);
						},
						options.intentPollInterval
					);
				} else {
					if (forceOpen) {
						element.data('forcedOpen', true);
					}
					tipController.showTip(element);
				}
			}
		}

		/**
		 * Begins the process of closing a tooltip.
		 * @private
		 * @param {Boolean=} disableDelay Disable close delay (optional).
		 */
		function closeTooltip(disableDelay) {
			cancelTimer();
			if (element.data('hasActiveHover')) {
				session.popOpenImminent = false;
				element.data('forcedOpen', false);
				if (!disableDelay) {
					hoverTimer = setTimeout(
						function() {
							hoverTimer = null;
							tipController.hideTip(element);
						},
						options.closeDelay
					);
				} else {
					tipController.hideTip(element);
				}
			}
		}

		/**
		 * Checks mouse position to make sure that the user intended to hover
		 * on the specified element before showing the tooltip.
		 * @private
		 */
		function checkForIntent() {
			// calculate mouse position difference
			var xDifference = Math.abs(session.previousX - session.currentX),
				yDifference = Math.abs(session.previousY - session.currentY),
				totalDifference = xDifference + yDifference;

			// check if difference has passed the sensitivity threshold
			if (totalDifference < options.intentSensitivity) {
				tipController.showTip(element);
			} else {
				// try again
				session.previousX = session.currentX;
				session.previousY = session.currentY;
				openTooltip();
			}
		}

		/**
		 * Cancels active hover timer.
		 * @private
		 */
		function cancelTimer() {
			hoverTimer = clearTimeout(hoverTimer);
		}

		// expose the methods
		return {
			show: openTooltip,
			hide: closeTooltip,
			cancel: cancelTimer
		};
	}

	/**
	 * Creates a new tooltip controller.
	 * @private
	 * @constructor
	 * @param {Object} options Options object containing settings.
	 */
	function TooltipController(options) {

		// build and append popup div if it does not already exist
		var tipElement = $('#' + options.popupId);
		if (tipElement.length === 0) {
			tipElement = $('<div></div>', { id: options.popupId });
			// grab body element if it was not populated when the script loaded
			// this hack exists solely for jsfiddle support
			if ($body.length === 0) {
				$body = $('body');
			}
			$body.append(tipElement);
		}

		// hook mousemove for cursor follow tooltips
		if (options.followMouse) {
			// only one positionTipOnCursor hook per popup element, please
			if (!tipElement.data('hasMouseMove')) {
				$document.on({
					mousemove: positionTipOnCursor,
					scroll: positionTipOnCursor
				});
			}
			tipElement.data('hasMouseMove', true);
		}

		// if we want to be able to mouse onto the popup then we need to attach
		// hover events to the popup that will cancel a close request on hover
		// and start a new close request on mouseleave
		if (options.followMouse || options.mouseOnToPopup) {
			tipElement.on({
				mouseenter: function() {
					if (tipElement.data('followMouse') || tipElement.data('mouseOnToPopup')) {
						// check activeHover in case the mouse cursor entered
						// the tooltip during the fadeOut and close cycle
						if (session.activeHover) {
							session.activeHover.data('displayController').cancel();
						}
					}
				},
				mouseleave: function() {
					if (tipElement.data('mouseOnToPopup')) {
						// check activeHover in case the mouse cursor entered
						// the tooltip during the fadeOut and close cycle
						if (session.activeHover) {
							session.activeHover.data('displayController').hide();
						}
					}
				}
			});
		}

		/**
		 * Gives the specified element the active-hover state and queues up
		 * the showTip function.
		 * @private
		 * @param {Object} element The element that the tooltip should target.
		 */
		function beginShowTip(element) {
			element.data('hasActiveHover', true);
			// show popup, asap
			tipElement.queue(function(next) {
				showTip(element);
				next();
			});
		}

		/**
		 * Shows the tooltip popup, as soon as possible.
		 * @private
		 * @param {Object} element The element that the popup should target.
		 */
		function showTip(element) {
			// it is possible, especially with keyboard navigation, to move on
			// to another element with a tooltip during the queue to get to
			// this point in the code. if that happens then we need to not
			// proceed or we may have the fadeout callback for the last tooltip
			// execute immediately after this code runs, causing bugs.
			if (!element.data('hasActiveHover')) {
				return;
			}

			// if the popup is open and we got asked to open another one then
			// the old one is still in its fadeOut cycle, so wait and try again
			if (session.isPopOpen) {
				if (!session.isClosing) {
					hideTip(session.activeHover);
				}
				tipElement.delay(100).queue(function(next) {
					showTip(element);
					next();
				});
				return;
			}

			// trigger powerTipPreRender event
			element.trigger('powerTipPreRender');

			var tipText = element.data('powertip'),
				tipTarget = element.data('powertiptarget'),
				tipElem = element.data('powertipjq'),
				tipContent = tipTarget ? $('#' + tipTarget) : [];

			// set popup content
			if (tipText) {
				tipElement.html(tipText);
			} else if (tipElem && tipElem.length > 0) {
				tipElement.empty();
				tipElem.clone(true, true).appendTo(tipElement);
			} else if (tipContent && tipContent.length > 0) {
				tipElement.html($('#' + tipTarget).html());
			} else {
				// we have no content to display, give up
				return;
			}

			// trigger powerTipRender event
			element.trigger('powerTipRender');

			// hook close event for triggering from the api
			$document.on('closePowerTip', function() {
				element.data('displayController').hide(true);
			});

			session.activeHover = element;
			session.isPopOpen = true;

			tipElement.data('followMouse', options.followMouse);
			tipElement.data('mouseOnToPopup', options.mouseOnToPopup);

			// set popup position
			if (!options.followMouse) {
				positionTipOnElement(element);
				session.isFixedPopOpen = true;
			} else {
				positionTipOnCursor();
			}

			// fadein
			tipElement.fadeIn(options.fadeInTime, function() {
				// start desync polling
				if (!session.desyncTimeout) {
					session.desyncTimeout = setInterval(closeDesyncedTip, 500);
				}

				// trigger powerTipOpen event
				element.trigger('powerTipOpen');
			});
		}

		/**
		 * Hides the tooltip popup, immediately.
		 * @private
		 * @param {Object} element The element that the popup should target.
		 */
		function hideTip(element) {
			session.isClosing = true;
			element.data('hasActiveHover', false);
			element.data('forcedOpen', false);
			// reset session
			session.activeHover = null;
			session.isPopOpen = false;
			// stop desync polling
			session.desyncTimeout = clearInterval(session.desyncTimeout);
			// unhook close event api listener
			$document.off('closePowerTip');
			// fade out
			tipElement.fadeOut(options.fadeOutTime, function() {
				session.isClosing = false;
				session.isFixedPopOpen = false;
				tipElement.removeClass();
				// support mouse-follow and fixed position pops at the same
				// time by moving the popup to the last known cursor location
				// after it is hidden
				setTipPosition(
					session.currentX + options.offset,
					session.currentY + options.offset
				);

				// trigger powerTipClose event
				element.trigger('powerTipClose');
			});
		}

		/**
		 * Checks for a tooltip desync and closes the tooltip if one occurs.
		 * @private
		 */
		function closeDesyncedTip() {
			// It is possible for the mouse cursor to leave an element without
			// firing the mouseleave event. This seems to happen (in FF) if the
			// element is disabled under mouse cursor, the element is moved out
			// from under the mouse cursor (such as a slideDown() occurring
			// above it), or if the browser is resized by code moving the
			// element from under the mouse cursor. If this happens it will
			// result in a desynced tooltip because we wait for any exiting
			// open tooltips to close before opening a new one. So we should
			// periodically check for a desync situation and close the tip if
			// such a situation arises.
			if (session.isPopOpen && !session.isClosing) {
				var isDesynced = false;

				// case 1: user already moused onto another tip - easy test
				if (session.activeHover.data('hasActiveHover') === false) {
					isDesynced = true;
				} else {
					// case 2: hanging tip - have to test if mouse position is
					// not over the active hover and not over a tooltip set to
					// let the user interact with it.
					// for keyboard navigation, this only counts if the element
					// does not have focus.
					// for tooltips opened via the api we need to check if it
					// has the forcedOpen flag.
					if (!isMouseOver(session.activeHover) && !session.activeHover.is(":focus") && !session.activeHover.data('forcedOpen')) {
						if (tipElement.data('mouseOnToPopup')) {
							if (!isMouseOver(tipElement)) {
								isDesynced = true;
							}
						} else {
							isDesynced = true;
						}
					}
				}

				if (isDesynced) {
					// close the desynced tip
					hideTip(session.activeHover);
				}
			}
		}

		/**
		 * Moves the tooltip popup to the users mouse cursor.
		 * @private
		 */
		function positionTipOnCursor() {
			// to support having fixed powertips on the same page as cursor
			// powertips, where both instances are referencing the same popup
			// element, we need to keep track of the mouse position constantly,
			// but we should only set the pop location if a fixed pop is not
			// currently open, a pop open is imminent or active, and the popup
			// element in question does have a mouse-follow using it.
			if ((session.isPopOpen && !session.isFixedPopOpen) || (session.popOpenImminent && !session.isFixedPopOpen && tipElement.data('hasMouseMove'))) {
				// grab measurements
				var scrollTop = $window.scrollTop(),
					windowWidth = $window.width(),
					windowHeight = $window.height(),
					popWidth = tipElement.outerWidth(),
					popHeight = tipElement.outerHeight(),
					x = 0,
					y = 0;

				// constrain pop to browser viewport
				if ((popWidth + session.currentX + options.offset) < windowWidth) {
					x = session.currentX + options.offset;
				} else {
					x = windowWidth - popWidth;
				}
				if ((popHeight + session.currentY + options.offset) < (scrollTop + windowHeight)) {
					y = session.currentY + options.offset;
				} else {
					y = scrollTop + windowHeight - popHeight;
				}

				// position the tooltip
				setTipPosition(x, y);
			}
		}

		/**
		 * Sets the tooltip popup too the correct position relative to the
		 * specified target element. Based on options settings.
		 * @private
		 * @param {Object} element The element that the popup should target.
		 */
		function positionTipOnElement(element) {
			var tipWidth = tipElement.outerWidth(),
				tipHeight = tipElement.outerHeight(),
				priorityList,
				placementCoords,
				finalPlacement,
				collisions;

			// with smart placement we will try a series of placement
			// options and use the first one that does not collide with the
			// browser view port boundaries.
			if (options.smartPlacement) {

				// grab the placement priority list
				priorityList = $.fn.powerTip.smartPlacementLists[options.placement];

				// iterate over the priority list and use the first placement
				// option that does not collide with the viewport. if they all
				// collide then the last placement in the list will be used.
				$.each(priorityList, function(idx, pos) {
					// get placement coordinates
					placementCoords = computePlacementCoords(
						element,
						pos,
						tipWidth,
						tipHeight
					);
					finalPlacement = pos;

					// find collisions
					collisions = getViewportCollisions(
						placementCoords,
						tipWidth,
						tipHeight
					);

					// break if there were no collisions
					if (collisions.length === 0) {
						return false;
					}
				});

			} else {

				// if we're not going to use the smart placement feature then
				// just compute the coordinates and do it
				placementCoords = computePlacementCoords(
					element,
					options.placement,
					tipWidth,
					tipHeight
				);
				finalPlacement = options.placement;

			}

			// add placement as class for CSS arrows
			tipElement.addClass(finalPlacement);

			// position the tooltip
			setTipPosition(placementCoords.x, placementCoords.y);
		}

		/**
		 * Compute the top/left coordinates to display the tooltip at the
		 * specified placement relative to the specified element.
		 * @private
		 * @param {Object} element The element that the tooltip should target.
		 * @param {String} placement The placement for the tooltip.
		 * @param {Number} popWidth Width of the tooltip element in pixels.
		 * @param {Number} popHeight Height of the tooltip element in pixels.
		 * @retun {Object} An object with the x and y coordinates.
		 */
		function computePlacementCoords(element, placement, popWidth, popHeight) {
			// grab measurements
			var objectOffset = element.offset(),
				objectWidth = element.outerWidth(),
				objectHeight = element.outerHeight(),
				x = 0,
				y = 0;

			// calculate the appropriate x and y position in the document
			switch (placement) {
			case 'n':
				x = (objectOffset.left + (objectWidth / 2)) - (popWidth / 2);
				y = objectOffset.top - popHeight - options.offset;
				break;
			case 'e':
				x = objectOffset.left + objectWidth + options.offset;
				y = (objectOffset.top + (objectHeight / 2)) - (popHeight / 2);
				break;
			case 's':
				x = (objectOffset.left + (objectWidth / 2)) - (popWidth / 2);
				y = objectOffset.top + objectHeight + options.offset;
				break;
			case 'w':
				x = objectOffset.left - popWidth - options.offset;
				y = (objectOffset.top + (objectHeight / 2)) - (popHeight / 2);
				break;
			case 'nw':
				x = (objectOffset.left - popWidth) + 20;
				y = objectOffset.top - popHeight - options.offset;
				break;
			case 'ne':
				x = (objectOffset.left + objectWidth) - 20;
				y = objectOffset.top - popHeight - options.offset;
				break;
			case 'sw':
				x = (objectOffset.left - popWidth) + 20;
				y = objectOffset.top + objectHeight + options.offset;
				break;
			case 'se':
				x = (objectOffset.left + objectWidth) - 20;
				y = objectOffset.top + objectHeight + options.offset;
				break;
			}

			return {
				x: Math.round(x),
				y: Math.round(y)
			};
		}

		/**
		 * Sets the tooltip CSS position on the document.
		 * @private
		 * @param {Number} x Left position in pixels.
		 * @param {Number} y Top position in pixels.
		 */
		function setTipPosition(x, y) {
			tipElement.css('left', x + 'px');
			tipElement.css('top', y + 'px');
		}

		// expose methods
		return {
			showTip: beginShowTip,
			hideTip: hideTip
		};
	}

	/**
	 * Hooks mouse position tracking to mousemove and scroll events.
	 * Prevents attaching the events more than once.
	 * @private
	 */
	function initMouseTracking() {
		var lastScrollX = 0,
			lastScrollY = 0;

		if (!session.mouseTrackingActive) {
			session.mouseTrackingActive = true;

			// grab the current scroll position on load
			$(function() {
				lastScrollX = $document.scrollLeft();
				lastScrollY = $document.scrollTop();
			});

			// hook mouse position tracking
			$document.on({
				mousemove: trackMouse,
				scroll: function() {
					var x = $document.scrollLeft(),
						y = $document.scrollTop();
					if (x !== lastScrollX) {
						session.currentX += x - lastScrollX;
						lastScrollX = x;
					}
					if (y !== lastScrollY) {
						session.currentY += y - lastScrollY;
						lastScrollY = y;
					}
				}
			});
		}
	}

	/**
	 * Saves the current mouse coordinates to the powerTip session object.
	 * @private
	 * @param {Object} event The mousemove event for the document.
	 */
	function trackMouse(event) {
		session.currentX = event.pageX;
		session.currentY = event.pageY;
	}

	/**
	 * Tests if the mouse is currently over the specified element.
	 * @private
	 * @param {Object} element The element to check for hover.
	 * @return {Boolean}
	 */
	function isMouseOver(element) {
		var elementPosition = element.offset();
		return session.currentX >= elementPosition.left &&
			session.currentX <= elementPosition.left + element.outerWidth() &&
			session.currentY >= elementPosition.top &&
			session.currentY <= elementPosition.top + element.outerHeight();
	}

	/**
	 * Finds any viewport collisions that an element (the tooltip) would have
	 * if it were absolutely positioned at the specified coordinates.
	 * @private
	 * @param {Object} coords Coordinates for the element. (e.g. {x: 123, y: 123})
	 * @param {Number} elementWidth Width of the element in pixels.
	 * @param {Number} elementHeight Height of the element in pixels.
	 * @return {Array} Array of words representing directional collisions.
	 */
	function getViewportCollisions(coords, elementWidth, elementHeight) {
		var scrollLeft = $window.scrollLeft(),
			scrollTop = $window.scrollTop(),
			windowWidth = $window.width(),
			windowHeight = $window.height(),
			collisions = [];

		if (coords.y < scrollTop) {
			collisions.push('top');
		}
		if (coords.y + elementHeight > scrollTop + windowHeight) {
			collisions.push('bottom');
		}
		if (coords.x < scrollLeft) {
			collisions.push('left');
		}
		if (coords.x + elementWidth > scrollLeft + windowWidth) {
			collisions.push('right');
		}

		return collisions;
	}

}(jQuery));
