Balloon = {
    beakImg: null,                 // Image of the beak.
    padding: '1em',                // Padding for the text inside balloon
    hideTime: 200,                 // 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, width) {
        e.balloonMouseOver = true;
        if (e.balloonTimeout) {
            clearTimeout(e.balloonTimeout);
            e.balloonTimeout = null;
        }
        this.show(e, text, width);
    },

    out: function(e) {
        var th = this;
        e.balloonMouseOver = false;
        e.balloonTimeout = setTimeout(function() { th.hide(e) }, this.hideTime);
    },
    
    getBeakImg: function(left) {
        var beak = this.beakImg.replace(/(_[lr])?(\.[^.]+$)/, (left? '_r' : '_l') + '$2');
        return beak;
    },
    
    show: function(e, text, width, left) {
        if (e.balloon) return;        
        var th = this;
        e.balloon = false;
        this.loadText(text, function(text) {
            if (e.balloon !== false) return;
            
            // Detect balloon position (to the left or to the right from source).
            var pos = th.getAbsPos(e);
            if (left == null) {
                left = (pos.x + e.offsetWidth/2) < document.body.offsetWidth/2? false : true;
            }
            
            var outer = document.createElement('DIV');
            var table = '<table'+(width ? ' width="'+width+'"' : '')+' cellpadding="0" cellspacing="0" border="0"><tr>';
            if (!left) {
                table += '<td valign=top><img style="margin-top:' + th.minVertMargins[0] + 'px; margin-bottom: ' + th.minVertMargins[1] + 'px" src="' + th.getBeakImg(left) + '"></td>';
            }
            table += '<td style="border:1px solid #ccc; padding:' + th.padding + '; background:#fff;" nowrap>' + text + '</td>';
	        if (left) {
	            table += '<td valign=top><img style="margin-top:' + th.minVertMargins[0] + 'px; margin-bottom: ' + th.minVertMargins[1] + 'px" src="' + th.getBeakImg(left) + '"></td>';
	        }
            table += '</tr></table>';
			outer.innerHTML = table;

            var b = outer.childNodes[0];
            outer.style.position = 'absolute';
            outer.style.visibility = 'hidden';
            document.body.insertBefore(outer, document.body.firstChild);

            var beakImg = b.getElementsByTagName('IMG')[0];
            var posOuter = th.getAbsPos(b);
            var posBeak = th.getAbsPos(beakImg);
            
            // beak should be shifted slightly AFTER its position was taken
            // above, otherwise this is working incorrectly in IE
            beakImg.style.position = 'relative';
            if (left) {
	            beakImg.style.right = '1px';
            } else {
	            beakImg.style.left = '1px';
	        }
            
            b.style.position = 'absolute';
            if (left) {
	          	b.style.left = (pos.x - b.offsetWidth)+'px';
	        } else {
		        b.style.left = (pos.x + e.offsetWidth)+'px';
	        }
	        setTimeout(function() {
	            // We need a timeout, because sometimes layer is positioned 
	            // wrongly - lower than needed. Don't know why. 
                b.style.top = (pos.y - ((posBeak.y + beakImg.height) - posOuter.y) + e.offsetHeight/2 + th.topAddOffset)+'px';
                outer.style.visibility = 'visible';
	        }, 100);

            b.style.zIndex = '999';
            
            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;
    }
}