/*
 * bubbletip
 *
 * Copyright (c) 2009, UhLeeKa
 * Version: 
 *      1.0.5
 * Licensed under the GPL license:
 *     http://www.gnu.org/licenses/gpl.html
 * Author Website: 
 *     http://www.uhleeka.com
 * Description: 
 *     A bubble-styled tooltip extension
 *      - multiple tips on a page
 *      - multiple tips per jQuery element 
 *      - tips open outward in four directions:
 *         - up
 *         - down
 *         - left
 *         - right
 *      - tips can be: 
 *         - anchored to the triggering jQuery element
 *         - absolutely positioned
 *         - opened at the current mouse coordinates
 *         - anchored to a specified jQuery element
 *      - IE png transparency is handled via filters
 */
; (function($) {
    var bindIndex = 0;
    $.fn.extend({
        bubbletip: function(tip, options) {

            // check to see if the tip is a descendant of 
            // a table.bubbletip element and therefore
            // has already been instantiated as a bubbletip
            if ($('table.bubbletip #' + $(tip).get(0).id).length > 0) {
                return this;
            }

            var _this, _tip, _options, _calc, _timeoutAnimate, _timeoutRefresh, _isActive, _isHiding, _wrapper, _bindIndex;
            // hack for IE6,IE7
            var _windowWidth, _windowHeight;

            _this = $(this);
            _tip = $(tip);
            _bindIndex = bindIndex++;  // for window.resize namespace binding
            _options = {
                positionAt: 'element', // element | body | mouse
                positionAtElement: _this,
                offsetTop: 0,
                offsetLeft: 0,
                deltaPosition: 30,
                deltaDirection: 'up', // direction: up | down | left | right
                animationDuration: 250,
                animationEasing: 'swing', // linear | swing
                bindShow: 'mouseover', // mouseover | focus | click | etc.
                bindHide: 'mouseout', // mouseout | blur | etc.
                delayShow: 0,
                delayHide: 500,
                calculateOnShow: false
            };
            if (options) {
                _options = $.extend(_options, options);
            }
            // calculated values
            _calc = {
                top: 0,
                left: 0,
                delta: 0,
                mouseTop: 0,
                mouseLeft: 0,
                tipHeight: 0,
                bindShow: (_options.bindShow + ' ').replace(/ +/g, '.bubbletip' + _bindIndex),
                bindHide: (_options.bindHide + ' ').replace(/ +/g, '.bubbletip' + _bindIndex)
            };
            _timeoutAnimate = null;
            _timeoutRefresh = null;
            _isActive = false;
            _isHiding = false;

            // store the tip id for removeBubbletip
            if (!_this.data('bubbletip_tips')) {
                _this.data('bubbletip_tips', [[_tip.get(0).id, _bindIndex]]);
            } else {
                _this.data('bubbletip_tips', $.merge(_this.data('bubbletip_tips'), [[_tip.get(0).id, _bindIndex]]));
            }

            // validate _options
            if (!_options.positionAt.match(/^element|body|mouse$/i)) {
                _options.positionAt = 'element';
            }
            if (!_options.deltaDirection.match(/^up|down|left|right$/i)) {
                _options.deltaDirection = 'up';
            }
            _options.defaultDirection = _options.deltaDirection;

            // unified HTML blob
            _wrapper = $('<table id="' + $(tip).get(0).id + '_bt" class="bubbletip btDir-' + _options.deltaDirection + '" cellspacing="0" cellpadding="0"><tbody><tr><td class="bt-topleft"></td><td class="bt-top"></td><td class="dirDown"><table class="bt-top" cellspacing="0" cellpadding="0"><tr><th class="left"></th><td><div></div></td><th class="right"></th></tr></table></td><td class="bt-topright"></td></tr><tr><td class="bt-left"></td><td class="bt-left-tail dirRight"><div class="bt-left"></div><div class="bt-left-tail"></div><div class="bt-left"></div></td><td class="bt-content"></td><td class="bt-right"></td><td class="bt-right-tail dirLeft"><div class="bt-right"></div><div class="bt-right-tail"></div><div class="bt-right"></div></td></tr><tr><td class="bt-bottomleft"></td><td class="bt-bottom"></td><td class="dirUp"><table class="bt-bottom" cellspacing="0" cellpadding="0"><tr><th class="left"></th><td><div></div></td><th class="right"></th></tr></table></td><td class="bt-bottomright"></td></tr></tbody></table>');
            //}

            // append the wrapper to the document body
            _wrapper.appendTo('body');

            // apply IE filters to _wrapper elements
            if ((/msie/.test(navigator.userAgent.toLowerCase())) && (!/opera/.test(navigator.userAgent.toLowerCase()))) {
                $('*', _wrapper).each(function() {
                    var image = $(this).css('background-image');
                    if (image.match(/^url\(["']?(.*\.png)["']?\)$/i)) {
                        image = RegExp.$1;
                        $(this).css({
                            'backgroundImage': 'none',
                            'filter': 'progid:DXImageTransform.Microsoft.AlphaImageLoader(enabled=true, sizingMethod=' + ($(this).css('backgroundRepeat') == 'no-repeat' ? 'crop' : 'scale') + ', src=\'' + image + '\')'
                        }).each(function() {
                            var position = $(this).css('position');
                            if (position != 'absolute' && position != 'relative')
                                $(this).css('position', 'relative');
                        });
                    }
                });
            }

            // move the tip element into the content section of the wrapper
            $('.bt-content', _wrapper).append(_tip);
            // show the tip (in case it is hidden) so that we can calculate its dimensions
            _tip.show();
            // handle left|right delta
            if (_options.deltaDirection.match(/^left|right$/i)) {
                // tail is 40px, so divide height by two and subtract 20px;
                _calc.tipHeight = parseInt(_tip.height() / 2);
                // handle odd integer height
                if ((_tip.height() % 2) == 1) {
                    _calc.tipHeight++;
                }
                _calc.tipHeight = (_calc.tipHeight < 20) ? 1 : _calc.tipHeight - 20;
                if (_options.deltaDirection.match(/^left$/i)) {
                    $('div.bt-right', _wrapper).css('height', _calc.tipHeight + 'px');
                } else {
                    $('div.bt-left', _wrapper).css('height', _calc.tipHeight + 'px');
                }
            }
            // set the opacity of the wrapper to 0
            _wrapper.css('opacity', 0);
            // execute initial calculations
            _Calculate();
            _wrapper.hide();

            // handle window.resize
            $(window).bind('resize.bubbletip' + _bindIndex, function() {
                var w = $(window).width();
                var h = $(window).height();

                if ((w === _windowWidth) && (h === _windowHeight)) {
                    return;
                }
                _windowWidth = w;
                _windowHeight = h;

                if (_timeoutRefresh) {
                    clearTimeout(_timeoutRefresh);
                }

                _timeoutRefresh = setTimeout(function() {
                    _Calculate();
                }, 250);
            });

            // handle mouseover and mouseout events
            $([_wrapper.get(0), this.get(0)]).bind(_calc.bindShow, function(e) {
                if (_timeoutAnimate) {
                    clearTimeout(_timeoutAnimate);
                }
                _timeoutAnimate = setTimeout(function() {
                    if (_isActive) {
                        return;
                    }
                    _isActive = true;
                    if (_isHiding) {
                        _wrapper.stop(true, false);
                    }

                    var animation;

                    if (_options.calculateOnShow) {
                        // CAM - reset to the default before each calculate on the show in case the page has moved/scrolled since the last show                        
                        _options.deltaDirection = _options.defaultDirection;
                        _Calculate();
                    }

                    if (_options.positionAt.match(/^element|body$/i)) {
                        if (_options.deltaDirection.match(/^up|down$/i)) {
                            if (!_isHiding) {
                                _wrapper.css('top', parseInt(_calc.top + _calc.delta) + 'px');
                            }
                            animation = { 'top': _calc.top + 'px' };
                        } else {
                            if (!_isHiding) {
                                _wrapper.css('left', parseInt(_calc.left + _calc.delta) + 'px');
                            }
                            animation = { 'left': _calc.left + 'px' };
                        }
                    } else {
                        if (_options.deltaDirection.match(/^up|down$/i)) {
                            if (!_isHiding) {
                                _calc.mouseTop = e.pageY + _calc.top;
                                _wrapper.css({ 'top': parseInt(_calc.mouseTop + _calc.delta) + 'px', 'left': parseInt(e.pageX - (_wrapper.width() / 2)) + 'px' });
                            }
                            animation = { 'top': _calc.mouseTop + 'px' };
                        } else {
                            if (!_isHiding) {
                                _calc.mouseLeft = e.pageX + _calc.left;
                                _wrapper.css({ 'left': parseInt(_calc.mouseLeft + _calc.delta) + 'px', 'top': parseInt(e.pageY - (_wrapper.height() / 2)) + 'px' });
                            }
                            animation = { 'left': _calc.left + 'px' };
                        }
                    }
                    _isHiding = false;
                    _wrapper.show();
                    animation = $.extend(animation, { 'opacity': 1 });
                    _wrapper.animate(animation, _options.animationDuration, _options.animationEasing, function() {
                        _wrapper.css('opacity', '');
                        _isActive = true;
                    });
                }, _options.delayShow);

                return false;
            }).bind(_calc.bindHide, function(e) {
                if (_timeoutAnimate) {
                    clearTimeout(_timeoutAnimate);
                }
                _timeoutAnimate = setTimeout(function() {
                    var animation;

                    _isActive = false;
                    _isHiding = true;
                    if (_options.positionAt.match(/^element|body$/i)) {
                        if (_options.deltaDirection.match(/^up|down$/i)) {
                            animation = { 'top': parseInt(_calc.top - _calc.delta) + 'px' };
                        } else {
                            animation = { 'left': parseInt(_calc.left - _calc.delta) + 'px' };
                        }
                    } else {
                        if (_options.deltaDirection.match(/^up|down$/i)) {
                            animation = { 'top': parseInt(_calc.mouseTop - _calc.delta) + 'px' };
                        } else {
                            animation = { 'left': parseInt(_calc.mouseLeft - _calc.delta) + 'px' };
                        }
                    }
                    animation = $.extend(animation, { 'opacity': 0 });
                    _wrapper.animate(animation, _options.animationDuration, _options.animationEasing, function() {
                        _wrapper.hide();
                        _isHiding = false;
                    });

                }, _options.delayHide);

                return false;
            });

            // make sure our left is on the page
            function _MakeLeftSane(offset) {

                if (_calc.left < 20 + $(window).scrollLeft()) {
                    _calc.left = offset.left + ((_options.positionAtElement.width() - _wrapper.width()) / 2);
                    if (_calc.left < 20 + $(window).scrollLeft()) {
                        _calc.left = 20 + $(window).scrollLeft();
                    }
                }

                if ((_wrapper.width() + _calc.left - $(window).scrollLeft()) > $(window).width()) {
                    _calc.left = $(window).scrollLeft() + 20;
                }

            }

            function _Calculate() {

                // remove all of the possible directional classes
                _wrapper.removeClass("btDir-up btDir-down btDir-left btDir-right");
                // Reset the directional class to the current direction
                _wrapper.addClass("btDir-" + _options.deltaDirection);

                // calculate values
                if (_options.positionAt.match(/^element$/i)) {
                    var offset = _options.positionAtElement.offset();
                    if (_options.deltaDirection.match(/^up$/i)) {
                        _calc.top = offset.top + _options.offsetTop - _wrapper.height();
                        _calc.left = offset.left + _options.offsetLeft + ((_options.positionAtElement.width() - _wrapper.width()) / 2);
                        _calc.delta = _options.deltaPosition;

                        _MakeLeftSane(offset);

                    } else if (_options.deltaDirection.match(/^down$/i)) {
                        _calc.top = offset.top + _options.positionAtElement.height() + _options.offsetTop;
                        _calc.left = offset.left + _options.offsetLeft + ((_options.positionAtElement.width() - _wrapper.width()) / 2);
                        _calc.delta = -_options.deltaPosition;

                        var bNeedToReCalc = false;

                        if (((_calc.top + _wrapper.height() - $(window).scrollTop())) > $(window).height()) {
                            _options.deltaDirection = "up";
                            bNeedToReCalc = true;
                        }

                        _MakeLeftSane(offset);

                        if (bNeedToReCalc) {
                            _Calculate();
                        }

                    } else if (_options.deltaDirection.match(/^left$/i)) {
                        _calc.top = offset.top + _options.offsetTop + ((_options.positionAtElement.height() - _wrapper.height()) / 2);
                        _calc.left = offset.left + _options.offsetLeft - _wrapper.width();
                        _calc.delta = _options.deltaPosition;
                    } else if (_options.deltaDirection.match(/^right$/i)) {
                        _calc.top = offset.top + _options.offsetTop + ((_options.positionAtElement.height() - _wrapper.height()) / 2);
                        _calc.left = offset.left + _options.positionAtElement.width() + _options.offsetLeft;
                        _calc.delta = -_options.deltaPosition;
                    }
                } else if (_options.positionAt.match(/^body$/i)) {
                    if (_options.deltaDirection.match(/^up|left$/i)) {
                        _calc.top = _options.offsetTop;
                        _calc.left = _options.offsetLeft;
                        // up or left
                        _calc.delta = _options.deltaPosition;
                    } else {
                        if (_options.deltaDirection.match(/^down$/i)) {
                            _calc.top = parseInt(_options.offsetTop + _wrapper.height());
                            _calc.left = _options.offsetLeft;
                        } else {
                            _calc.top = _options.offsetTop;
                            _calc.left = parseInt(_options.offsetLeft + _wrapper.width());
                        }
                        // down or right
                        _calc.delta = -_options.deltaPosition;
                    }
                } else if (_options.positionAt.match(/^mouse$/i)) {
                    if (_options.deltaDirection.match(/^up|left$/i)) {
                        if (_options.deltaDirection.match(/^up$/i)) {
                            _calc.top = -(_options.offsetTop + _wrapper.height());
                            _calc.left = _options.offsetLeft;
                        } else if (_options.deltaDirection.match(/^left$/i)) {
                            _calc.top = _options.offsetTop;
                            _calc.left = -(_options.offsetLeft + _wrapper.width());
                        }
                        // up or left
                        _calc.delta = _options.deltaPosition;
                    } else {
                        _calc.top = _options.offsetTop;
                        _calc.left = _options.offsetLeft;
                        // down or right
                        _calc.delta = -_options.deltaPosition;
                    }
                }
                // handle the wrapper (element|body) positioning
                if (_options.positionAt.match(/^element|body$/i)) {
                    _wrapper.css({
                        'position': 'absolute',
                        'top': _calc.top + 'px',
                        'left': _calc.left + 'px'
                    });
                }
            };
            return this;
        },
        removeBubbletip: function(tips) {
            var tipsActive;
            var tipsToRemove = new Array();
            var tipsActiveAdjusted = new Array();
            var arr, i, ix;
            var elem;

            tipsActive = $.makeArray($(this).data('bubbletip_tips'));

            // convert the parameter array of tip id's or elements to id's
            arr = $.makeArray(tips);
            for (i = 0; i < arr.length; i++) {
                tipsToRemove.push($(arr[i]).get(0).id);
            }

            for (i = 0; i < tipsActive.length; i++) {
                ix = null;
                if ((tipsToRemove.length == 0) || ((ix = $.inArray(tipsActive[i][0], tipsToRemove)) >= 0)) {
                    // remove all tips if there are none specified
                    // otherwise, remove only specified tips

                    // find the surrounding table.bubbletip
                    elem = $('#' + tipsActive[i][0]).get(0).parentNode;
                    while (elem.tagName.toLowerCase() != 'table') {
                        elem = elem.parentNode;
                    }
                    // attach the tip element to body and hide
                    $('#' + tipsActive[i][0]).appendTo('body').hide();
                    // remove the surrounding table.bubbletip
                    $(elem).remove();

                    // unbind show/hide events
                    $(this).unbind('.bubbletip' + tipsActive[i][1]);

                    // unbind window.resize event
                    $(window).unbind('.bubbletip' + tipsActive[i][1]);
                } else {
                    // tip is not being removed, so add it to the adjusted array
                    tipsActiveAdjusted.push(tipsActive[i]);
                }
            }
            $(this).data('bubbletip_tips', tipsActiveAdjusted);

            return this;
        }
    });
})(jQuery);
