/**
 * tools.tooltip 1.1.1 - Tooltips done right.
 *
 * Copyright (c) 2009 Tero Piirainen
 * http://flowplayer.org/tools/tooltip.html
 *
 * Dual licensed under MIT and GPL 2+ licenses
 * http://www.opensource.org/licenses
 *
 * Launch  : November 2008
 * Date: ${date}
 * Revision: ${revision}
 */
(function($) {

	// static constructs
	$.tools = $.tools || {};

	$.tools.tooltip = {
		version: '1.1.1',

		conf: {

			// default effect variables
			effect: 'fade',
			fadeOutSpeed: 1,
                        fadeInSpeed: 1,
			tip: null,

			predelay: 0,
			delay: 30,
			opacity: 1,
			lazy: undefined,

			// 'top', 'bottom', 'right', 'left', 'center'
			position: ['top', 'center'],
			offset: [0, 0],
			cancelDefault: true,
			relative: false,

			// type to event mapping
			events: {
				def: 			"mouseover,mouseout",
				input: 		"focus,blur",
				widget:		"focus mouseover,blur mouseout"
			},

			api: false
		},

		addEffect: function(name, loadFn, hideFn) {
			effects[name] = [loadFn, hideFn];
		}
	};


	var effects = {
		toggle: [
			function(done) {
				var conf = this.getConf();
				this.getTip().css({opacity: conf.opacity}).show();
				done.call();
			},

			function(done) {
				this.getTip().hide();
				done.call();
			}
		],

		fade: [
			function(done) { this.getTip().fadeIn(this.getConf().fadeInSpeed, done); },
			function(done) { this.getTip().fadeOut(this.getConf().fadeOutSpeed, done); }
		]
	};

	function Tooltip(trigger, conf) {

		var self = this, $self = $(this);

		trigger.data("tooltip", self);

		// find the tip
		var tip = trigger.next();

		if (conf.tip) {

			tip = $(conf.tip);

			// multiple tip elements
			if (tip.length > 1) {

				// find sibling
				tip = trigger.nextAll(conf.tip).eq(0);

				// find sibling from the parent element
				if (!tip.length) {
					tip = trigger.parent().nextAll(conf.tip).eq(0);
				}
			}
		}

		/* calculate tip position relative to the trigger */
		function getPosition(e) {

			// get origin top/left position
			var top = conf.relative ? trigger.position().top : trigger.offset().top,
				 left = conf.relative ? trigger.position().left : trigger.offset().left,
				 pos = conf.position[0];

			top  -= tip.outerHeight() - conf.offset[0];
			left += trigger.outerWidth() + conf.offset[1];

			// adjust Y
			var height = tip.outerHeight() + trigger.outerHeight();
			if (pos == 'center') 	{ top += height / 2; }
			if (pos == 'bottom') 	{ top += height; }

			// adjust X
			pos = conf.position[1];
			var width = tip.outerWidth() + trigger.outerWidth();
			if (pos == 'center') 	{ left -= width / 2; }
			if (pos == 'left')   	{ left -= width; }

			return {top: top, left: left};
		}


		// event management
		var isInput = trigger.is(":input"),
			 isWidget = isInput && trigger.is(":checkbox, :radio, select, :button"),
			 type = trigger.attr("type"),
			 evt = conf.events[type] || conf.events[isInput ? (isWidget ? 'widget' : 'input') : 'def'];

		evt = evt.split(/,\s*/);

		trigger.bind(evt[0], function(e) {

			// see if the tip was launched by this trigger
			var t = tip.data("trigger");
			if (t && t[0] != this) { tip.hide(); }

			e.target = this;
			self.show(e);
			tip.hover(self.show, function() { self.hide(e); });

		});

		trigger.bind(evt[1], function(e) {
			self.hide(e);
		});

		// ensure that the tip really shows up. IE cannot catch up with this.
		if (!$.browser.msie && !isInput) {
			trigger.mousemove(function()  {
				if (!self.isShown()) {
					trigger.triggerHandler("mouseover");
				}
			});
		}

		// for PNG backgrounds opacity changes will generate a black border for IE
		if (conf.opacity < 1) {
			tip.css("opacity", conf.opacity);
		}

		var pretimer = 0, title = trigger.attr("title");

		if (title && conf.cancelDefault) {
			trigger.removeAttr("title");
			trigger.data("title", title);
		}

		$.extend(self, {

			show: function(e) {

				if (e) { trigger = $(e.target); }

				clearTimeout(tip.data("timer"));

				if (tip.is(":animated") || tip.is(":visible")) { return self; }

				function show() {

					// remember the trigger element for this tip
					tip.data("trigger", trigger);

					// get position
					var pos = getPosition(e);

					// title attribute
					if (conf.tip && title) {
						tip.html(trigger.data("title"));
					}

					// onBeforeShow
					var evt = $.Event("onBeforeShow");
					$self.trigger(evt, [pos]);
					if (evt.isDefaultPrevented()) { return self; }


					// onBeforeShow may have altered the configuration
					pos = getPosition(e);

					// set position
					tip.css({position:'absolute', top: pos.top, left: pos.left});

					// invoke effect
					effects[conf.effect][0].call(self, function()  {
						$self.trigger("onShow");
					});

				}

				if (conf.predelay) {
					clearTimeout(pretimer);
					pretimer = setTimeout(show, conf.predelay);

				} else {
					show();
				}

				return self;
			},

			hide: function(ev) {

				clearTimeout(tip.data("timer"));
				clearTimeout(pretimer);

				if (!tip.is(":visible")) { return; }

				function hide() {

					// onBeforeHide
					var e = $.Event("onBeforeHide");
					$self.trigger(e);
					if (e.isDefaultPrevented()) { return; }

					effects[conf.effect][1].call(self, function() {
						$self.trigger("onHide");
					});
				}

				if (conf.delay && ev) {
					tip.data("timer", setTimeout(hide, conf.delay));

				} else {
					hide();
				}

				return self;
			},

			isShown: function() {
				return tip.is(":visible, :animated");
			},

			getConf: function() {
				return conf;
			},

			getTip: function() {
				return tip;
			},

			getTrigger: function() {
				return trigger;
			},

			// callback functions
			bind: function(name, fn) {
				$self.bind(name, fn);
				return self;
			},

			onHide: function(fn) {
				return this.bind("onHide", fn);
			},

			onBeforeShow: function(fn) {
				return this.bind("onBeforeShow", fn);
			},

			onShow: function(fn) {
				return this.bind("onShow", fn);
			},

			onBeforeHide: function(fn) {
				return this.bind("onBeforeHide", fn);
			},

			unbind: function(name) {
				$self.unbind(name);
				return self;
			}

		});

		// bind all callbacks from configuration
		$.each(conf, function(name, fn) {
			if ($.isFunction(fn)) { self.bind(name, fn); }
		});

	}


	// jQuery plugin implementation
	$.prototype.tooltip = function(conf) {

		// return existing instance
		var api = this.eq(typeof conf == 'number' ? conf : 0).data("tooltip");
		if (api) { return api; }

		// setup options
		var globals = $.extend(true, {}, $.tools.tooltip.conf);

		if ($.isFunction(conf)) {
			conf = {onBeforeShow: conf};

		} else if (typeof conf == 'string') {
			conf = {tip: conf};
		}

		conf = $.extend(true, globals, conf);

		// can also be given as string
		if (typeof conf.position == 'string') {
			conf.position = conf.position.split(/,?\s/);
		}

		// assign tip's only when apiement is being mouseovered
		if (conf.lazy !== false && (conf.lazy === true || this.length > 20)) {

			this.one("mouseover", function(e) {
				api = new Tooltip($(this), conf);
				api.show(e);
			});

		} else {

			// install tooltip for each entry in jQuery object
			this.each(function() {
				api = new Tooltip($(this), conf);
			});
		}

		return conf.api ? api: this;

	};

}) (jQuery);