Balloon = {
    beakImg: null,                 // Image of the beak.
    beakHeight: null,              // Height of beak image.
    padding: '1em',                // Padding for the text inside balloon
    hideTime: 400,                 // Time to wait before balloon hiding.
    minVertMargins: [ 30, 10 ],    // Minimal vertical margins of beak image (top, bottom)
    topAddOffset: 2,               // Additional number of pixels to lower the balloon by.
    
    over: function(e, text) {
        e.balloonMouseOver = true;
        if (e.balloonTimeout) {
            clearTimeout(e.balloonTimeout);
            e.balloonTimeout = null;
        }
        this.show(e, text);
    },
    
    out: function(e) {
        var th = this;
        e.balloonMouseOver = false;
        e.balloonTimeout = setTimeout(function() { th.hide(e) }, this.hideTime);
    },
    
    show: function(e, text) {
        if (e.balloon) return;        
        var th = this;
        e.balloon = false;
        this.loadText(text, function(text) {
            if (e.balloon !== false) return;
            var outer = document.createElement('DIV');
            outer.innerHTML =
                '<table cellpadding="0" cellspacing="0" border="0"><tr>' +
                '<td><img style="position:relative; left:1px; margin-top:' + th.minVertMargins[0] + 'px; margin-bottom: ' + th.minVertMargins[1] + 'px" src="' + th.beakImg + '"></td>' +
                '<td style="border:1px solid #ccc; padding:' + th.padding + '; background:#fff;">' + text + '</td>' +
                '</tr></table>';
            var b = outer.childNodes[0];
            document.body.appendChild(b);

            var beakImg = b.getElementsByTagName('IMG')[0];
            var pos = th.getAbsPos(e);
            var posOuter = th.getAbsPos(b);
            var posBeak = th.getAbsPos(beakImg);
            b.style.position = 'absolute';
            b.style.left = pos.x + e.offsetWidth ;
            b.style.top = pos.y - ((posBeak.y + th.beakHeight) - posOuter.y) + e.offsetHeight/2 + th.topAddOffset;
            b.onmouseover = function() { th.over(e) }
            b.onmouseout = function() { th.out(e) }
            
            e.balloon = b;
        });
    },
    
    hide: function(e) {
        var balloon = e.balloon;
        e.balloon = null;
        if (!balloon) return;
        balloon.parentNode.removeChild(balloon);
    },
    
    loadText: function(text, callbackToShow) {
        if (text instanceof Function) {
            // Run specified text getter. After loading is finished this
            // loader must call callback.
            text = text(callbackToShow);
        } else {
            // Plain text specified. 
            callbackToShow(text);
        }
    },
    
    getAbsPos: function (p) {
        var s = { x:0, y:0 };
        while (p.offsetParent) {
            s.x += p.offsetLeft;
            s.y += p.offsetTop;
            p = p.offsetParent;
        }
        return s;
    }
}
