/* Copyright 2009 Simon Oxtoby
 * This program is free software. It comes without any warranty, to
 * the extent permitted by applicable law. You can redistribute it
 * and/or modify it under the terms of the WTFPL, Version 2, as
 * published by Sam Hocevar. See http://sam.zoy.org/wtfpl/COPYING
 * for more details.
*/

(function($) {
    $.fn.balloonTip = function(text, options) {
        var opts = $.extend({}, $.fn.balloonTip.defaults, options);   // Stack user-set options on top of defaults, on top of empty object, so as not to overwrite defaults

        // Make text a function if it isn't already
        var getText = $.isFunction(text) ? text : function() { return text; };

        // Create a balloonTip for each selected element
        this.each(function balloonTip_each() {
            var $this = $(this);

            // Add balloon to DOM
            var balloon;
            if (opts.insertAt) {
                balloon = $.fn.balloonTip.newBalloon().appendTo(opts.insertAt);
            } else {
                balloon = $.fn.balloonTip.newBalloon().insertAfter($this);
            }

            // Set balloon contents
            balloon.find('.balloonTip-body').html(getText());

            // Get relative position (relative to 'this' element)
            var relPosAxes = opts.relativePosition.split('-');
            var relPos = {};
            relPos.x = $.fn.balloonTip.getRelativePosition($this, relPosAxes[1], 'left', 'width') + opts.adjustX;
            relPos.y = $.fn.balloonTip.getRelativePosition($this, relPosAxes[0], 'top', 'height') + opts.adjustY;

            // Determine orientation
            var vOrientation = opts.orientation.split('-')[0];
            if (vOrientation === 'auto') vOrientation = relPos.y - balloon.height() - opts.arrowLength > 0 ? 'top' : 'bottom';
            var hOrientation = opts.orientation.split('-')[1];
            if (hOrientation === 'auto') hOrientation = relPos.x + balloon.width() < $(window).width() ? 'right' : 'left';

            // Add arrow
            if (opts.arrowLength) {
                // Add table row for arrow
                var row, insertPos;
                if (vOrientation === 'bottom') {
                    row = 'first'; insertPos = 'Before';
                } else {
                    row = 'last'; insertPos = 'After';
                }
                var arrowRow = $('<tr><td></td><td></td><td></td></tr>');
                var arrowCell = arrowRow['insert' + insertPos](balloon.find('tr:' + row)).find('td:eq(1)');

                // Insert arrow
                var arrow = $('<div class="balloonTip-arrow" style="width:0;height:0;' +
                              'border-' + vOrientation + '-width:' + opts.arrowLength + 'px;' + 'border-' + vOrientation + '-style:solid;' +
                              'border-' + hOrientation + ':' + opts.arrowLength + 'px solid transparent;' +
                              '"></div>');
                arrowCell.append(arrow);

                // Make arrow row transparent and remove padding, so arrow is flush with side of balloonTip
                arrowRow.find('td').css({
                    'background-color': 'transparent',
                    'padding': '0'
                });

                // Move arrow to the right edge if necessary
                if (hOrientation === 'left') arrow.css('float', 'right');
            }

            // Set balloon position
            var balloonLeft = relPos.x;
            if (hOrientation === 'left') balloonLeft -= balloon.width();
            if (opts.arrowLength) balloonLeft += (hOrientation === 'left' ? balloon.find('td.balloonTip-right').width() :
                                                                            -balloon.find('td.balloonTip-left').width());  // Adjust to match arrow point
            var balloonTop = relPos.y;
            if (vOrientation === 'top') balloonTop -= balloon.height();
            balloon.css({ left: balloonLeft, top: balloonTop });

            // Set up close conditions
            // Set click conditions after a delay, so tip isn't closed by current event bubbling up to document
            setTimeout(function balloonTip_setClickEvents() {
                // Hook up custom click handler
                balloon.bind('click', opts.clickData, opts.click);
                // Hook up click events for closing balloon
                balloon.data('balloonTip-close', function() { $.fn.balloonTip.disposeTip(balloon); });
                if (opts.closeOnAnyClick) {
                    $().click(balloon.data('balloonTip-close'));
                    balloon.data('balloonTip-clickTarget', $());
                } else if (opts.closeOnClick) {
                    balloon.click(balloon.data('balloonTip-close'));
                    balloon.data('balloonTip-clickTarget', balloon);
                }
            }, 1);
            // Timeout
            if (opts.timeout) {
                balloon.data('balloonTip-timeout',
                    setTimeout(function balloonTip_timeout() {
                        balloon.fadeOut(opts.fadeOutSpeed, function() { $.fn.balloonTip.disposeTip(balloon); });
                    }, opts.timeout)
                );
            }

            // Fade balloon in
            if (opts.opacity === 1) {
                balloon.hide().fadeIn(opts.fadeInSpeed);    // IE craps out on transparent PNGs if we fade in to 100% opacity
            } else {
                balloon.fadeTo(0, 0).fadeTo(opts.fadeInSpeed, opts.opacity);
            }
        });
    };

    $.fn.balloonTip.newBalloon = function balloonTip_newBalloon() {
        return $(
            '<table class="balloonTip" style="position:absolute;border-collapse:collapse;">' +
            '<tr><td class="balloonTip-topLeft"></td><td class="balloonTip-top"></td><td class="balloonTip-topRight"></td></tr>' +
            '<tr><td class="balloonTip-left"></td><td class="balloonTip-body"></td><td class="balloonTip-right"></td></tr>' +
            '<tr><td class="balloonTip-bottomLeft"></td><td class="balloonTip-bottom"></td><td class="balloonTip-bottomRight"></td></tr></table>'
        );
    };

    $.fn.balloonTip.getRelativePosition = function(elem, positioning, lesserPosition, length) {
        var relPos = elem.offset()[lesserPosition];
        if (positioning === 'center') {
            relPos += elem[length]() / 2;
        } else if (positioning !== lesserPosition) {
            relPos += elem[length]();
        }
        return relPos;
    };

    $.fn.balloonTip.disposeTip = function balloonTip_disposeTip(balloon) {
        // Abort timeout
        clearTimeout(balloon.data('balloonTip-timeout'));

        // Remove click event handler
        if (balloon.data('balloonTip-clickTarget')) {
            balloon.data('balloonTip-clickTarget').unbind('click', balloon.data('balloonTip-close'));
        }

        // Remove balloon
        balloon.remove();

        return false;   // Stop event bubbling
    };

    $.fn.balloonTip.defaults = {
        insertAt: false, // Insert balloonTip alongside selected element
        relativePosition: 'top-right', // top, bottom - left, right
        adjustX: 0,
        adjustY: 0,
        orientation: 'auto-auto', // top, bottom, auto - left, right, auto
        closeOnClick: true,     // Close tip if user clicks on the balloon
        closeOnAnyClick: true,  // Close tip if user clicks anywhere in the document
        fadeInSpeed: 'normal',  // jQuery speed - fast, normal, slow or number of ms
        fadeOutSpeed: 'normal', // jQuery speed - fast, normal, slow or number of ms
        opacity: 1, // 0-1
        timeout: 5000, // In ms
        arrowLength: 16, // In px
        click: function() { }, // Balloon tip custom click event handler
        clickData: null // Custom click event custom data
    };

} (jQuery));
