/**
 * tools.tooltip 1.1.3 - 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($) { 

    var instances = [];
    
    // static constructs
    $.tools = $.tools || {};
    
    $.tools.tooltip = {
        version: '1.1.3',
        
        conf: { 
            
            // default effect variables
            effect: 'toggle',            
            fadeOutSpeed: "fast",
            tip: null,
            
            predelay: 0,
            delay: 30,
            opacity: 1,            
            lazy: false,
            
            // 'top', 'bottom', 'right', 'left', 'center'
            position: ['bottom', 'center'], 
            offset: [0, 0],            
            cancelDefault: true,
            relative: true,
            oneInstance: true,
            
            
            // type to event mapping 
            events: {
                def:             "mouseover,ready",
                input:         "focus,blur",
                widget:        "focus mouseover,blur mouseout",
                tooltip:        "ready,mouseout"
            },            
            
            api: false
        },
        
        addEffect: function(name, loadFn, hideFn) {
            effects[name] = [loadFn, hideFn];    
        } 
    };
    
    
    var effects = { 
        toggle: [ 
            function(done) { 
                var conf = this.getConf(), tip = this.getTip(), o = conf.opacity;
                if (o < 1) { tip.css({opacity: o}); }
                tip.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*/); 
        if (evt.length != 2) { throw "Tooltip: bad events configuration for " + type; }
                
        trigger.bind(evt[0], function(e) {
            
            // close all instances
            if (conf.oneInstance) {
                $.each(instances, function()  {
                    this.hide();        
                });
            }
                
            // see if the tip was launched by this trigger
            var t = tip.data("trigger");            
            if (t && t[0] != this) { tip.hide().stop(true, true); }            
            
            e.target = this;
            self.show(e); 
            
            // tooltip close events
            evt = conf.events.tooltip.split(/,\s*/);
            tip.bind(evt[0], function() { self.show(e); });
            if (evt[1]) { tip.bind(evt[1], 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 && !conf.predelay) {
            trigger.mousemove(function()  {                    
                if (!self.isShown()) {
                    trigger.triggerHandler("mouseover");    
                }
            });
        }

        // avoid "black box" bug in IE with PNG background images
        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
                    e = e || $.Event();
                    e.type = "onBeforeShow";
                    $self.trigger(e, [pos]);                
                    if (e.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
                    var eff = effects[conf.effect];
                    if (!eff) { throw "Nonexistent effect \"" + conf.effect + "\""; }
                    
                    eff[0].call(self, function() {
                        e.type = "onShow";
                        $self.trigger(e);            
                    });                    
                    
                }
                
                if (conf.predelay) {
                    clearTimeout(pretimer);
                    pretimer = setTimeout(show, conf.predelay);    
                    
                } else {
                    show();    
                }
                
                return self;
            },
            
            hide: function(e) {

                clearTimeout(tip.data("timer"));
                clearTimeout(pretimer);
                
                if (!tip.is(":visible")) { return; }
                
                function hide() {
                    
                    // onBeforeHide
                    e = e || $.Event();
                    e.type = "onBeforeHide";
                    $self.trigger(e);                
                    if (e.isDefaultPrevented()) { return; }
                    
                    effects[conf.effect][1].call(self, function() {
                        e.type = "onHide";
                        $self.trigger(e);        
                    });
                }
                 
                if (conf.delay && e) {
                    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("ready", function(e) {    
                api = new Tooltip($(this), conf);
                api.show(e);
                instances.push(api);
            }); 
            
        } else {
            
            // install tooltip for each entry in jQuery object
            this.each(function() {
                api = new Tooltip($(this), conf); 
                instances.push(api);
            });
        } 

        return conf.api ? api: this;        
        
    };
        
}) (jQuery);