
Script.namespace("com.civicscience.ui");

(function() {

    // tooltip
    com.civicscience.ui.Tooltip = function(element) {
    
        var document = element.ownerDocument;
        
        element["com.civicscience.ui.Tooltip.value"] = this;
            
        // create tooltip structure
        var tipBody = null;
        var tipBox = (function() {
            var box = document.createElement("DIV");
            box.className = "tooltip";
            box.style.position = "absolute";
            var elt;
            elt = document.createElement("DIV");
            elt.className = "tooltip_top";
            box.appendChild(elt);
            elt = document.createElement("DIV");
            elt.className = "tooltip_mid";
            box.appendChild(elt);
            tipBody = elt;
            elt = document.createElement("DIV");
            elt.className = "tooltip_bot";
            box.appendChild(elt);
            return box;
        })();
        
        var layout = function() {
            
            var geomHTML = com.civicscience.geom.HTML;
            var Point = com.civicscience.geom.Point;
            
            var winRect = geomHTML.getViewportBounds();
            var eltRect = geomHTML.getElementBounds(element);
            
            var IPlacement = new Interface();
            IPlacement.method("getPointer");
            IPlacement.method("getTarget");
            IPlacement.method("place");
            IPlacement.method("unplace");
            
            var placements = [];
            
            // placement to the right of the element
            var right = {
                getPointer: function() {
                    var rect = geomHTML.getElementBounds(tipBox);
                    return new Point(rect.x, rect.y + Math.floor(rect.height / 2));
                },
                getTarget: function() {
                    return new Point(eltRect.x + eltRect.width, eltRect.y + Math.floor(eltRect.height / 2));
                },
                place: function() {
                    DOMUtil.addClass(tipBox, "tooltip_east");
                    var rect = geomHTML.getElementBounds(tipBox);
                    var tgt = this.getTarget();
                    rect.x = tgt.x;
                    rect.y = tgt.y - Math.floor(rect.height / 2);
                    if (rect.y < winRect.y == rect.bottom() > winRect.bottom()) {
                        // within view or too big to fit
                    } else if (rect.y < winRect.y) {
                        // slide down
                        rect.y = Math.min(winRect.y, eltRect.bottom() - Math.ceil(rect.height / 2));
                    } else {
                        // slide up
                        rect.y = Math.max(winRect.bottom() - rect.height, eltRect.y - Math.floor(rect.height / 2));
                    }
                    var org = geomHTML.getElementOrigin(tipBox);
                    tipBox.style.left = (rect.x - org.x) + "px";
                    tipBox.style.top = (rect.y - org.y) + "px";
                },
                unplace: function() {
                    DOMUtil.removeClass(tipBox, "tooltip_east");
                    tipBox.style.left = "";
                    tipBox.style.top = "";
                }
            };
            placements.push(right);
            
            // placement above the element
            var top = {
                getPointer: function() {
                    var rect = geomHTML.getElementBounds(tipBox);
                    return new Point(rect.x + Math.floor(rect.width / 2), rect.y + rect.height);
                },
                getTarget: function() {
                    return new Point(eltRect.x + Math.floor(eltRect.width / 2), eltRect.y);
                },
                place: function() {
                    DOMUtil.addClass(tipBox, "tooltip_north");
                    var rect = geomHTML.getElementBounds(tipBox);
                    var tgt = this.getTarget();
                    rect.x = tgt.x - Math.floor(rect.width / 2);
                    rect.y = tgt.y - rect.height;
                    if (rect.x < winRect.x == rect.right() > winRect.right()) {
                        // within view or too big to fit
                    } else if (rect.x < winRect.x) {
                        // slide right
                        rect.x = Math.min(winRect.x, eltRect.right() - Math.ceil(rect.width / 2));
                    } else {
                        // slide left
                        rect.x = Math.max(winRect.right() - rect.width, eltRect.x - Math.floor(rect.width / 2));
                    }
                    var org = geomHTML.getElementOrigin(tipBox);
                    tipBox.style.left = (rect.x - org.x) + "px";
                    tipBox.style.top = (rect.y - org.y) + "px";
                },
                unplace: function() {
                    DOMUtil.removeClass(tipBox, "tooltip_north");
                    tipBox.style.left = "";
                    tipBox.style.top = "";
                }
            };
            placements.push(top);
            
            // placement below the element
            var bottom = {
                getPointer: function() {
                    var rect = geomHTML.getElementBounds(tipBox);
                    return new Point(rect.x + Math.floor(rect.width / 2), rect.y);
                },
                getTarget: function() {
                    return new Point(eltRect.x + Math.floor(eltRect.width / 2), eltRect.y + eltRect.height);
                },
                place: function() {
                    DOMUtil.addClass(tipBox, "tooltip_south");
                    var rect = geomHTML.getElementBounds(tipBox);
                    var tgt = this.getTarget();
                    rect.x = tgt.x - Math.floor(rect.width / 2);
                    rect.y = tgt.y;
                    if (rect.x < winRect.x == rect.right() > winRect.right()) {
                        // within view or too big to fit
                    } else if (rect.x < winRect.x) {
                        // slide right
                        rect.x = Math.min(winRect.x, eltRect.right() - Math.ceil(rect.width / 2));
                    } else {
                        // slide left
                        rect.x = Math.max(winRect.right() - rect.width, eltRect.x - Math.floor(rect.width / 2));
                    }
                    var org = geomHTML.getElementOrigin(tipBox);
                    tipBox.style.left = (rect.x - org.x) + "px";
                    tipBox.style.top = (rect.y - org.y) + "px";
                },
                unplace: function() {
                    DOMUtil.removeClass(tipBox, "tooltip_south");
                    tipBox.style.left = "";
                    tipBox.style.top = "";
                }
            };
            placements.push(bottom);
            
            // placement to the left of the element
            var left = {
                getPointer: function() {
                    var rect = geomHTML.getElementBounds(tipBox);
                    return new Point(rect.x + rect.width, rect.y + Math.floor(rect.height / 2));
                },
                getTarget: function() {
                    return new Point(eltRect.x, eltRect.y + Math.floor(eltRect.height / 2));
                },
                place: function() {
                    DOMUtil.addClass(tipBox, "tooltip_west");
                    var rect = geomHTML.getElementBounds(tipBox);
                    var tgt = this.getTarget();
                    rect.x = tgt.x - rect.width;
                    rect.y = tgt.y - Math.floor(rect.height / 2);
                    if (rect.y < winRect.y == rect.bottom() > winRect.bottom()) {
                        // within view or too big to fit
                    } else if (rect.y < winRect.y) {
                        // slide down
                        rect.y = Math.min(winRect.y, eltRect.bottom() - Math.ceil(rect.height / 2));
                    } else {
                        // slide up
                        rect.y = Math.max(winRect.bottom() - rect.height, eltRect.y - Math.floor(rect.height / 2));
                    }
                    var org = geomHTML.getElementOrigin(tipBox);
                    tipBox.style.left = (rect.x - org.x) + "px";
                    tipBox.style.top = (rect.y - org.y) + "px";
                },
                unplace: function() {
                    DOMUtil.removeClass(tipBox, "tooltip_west");
                    tipBox.style.left = "";
                    tipBox.style.top = "";
                }
            };
            placements.push(left);
    
            // gather places tied for greatest visible area of the tip container
            var bestArea = 0;
            var bestPlaces = [];
            var i = 0;
            for (i = 0; i < placements.length; i++) {
                var place = placements[i];
                IPlacement.assert(place);
                place.place();
                var rect = geomHTML.getElementBounds(tipBox);
                var sect = rect.intersection(winRect);
                var area = rect.intersection(winRect).area() / rect.area();
                if (area == bestArea) {
                    bestPlaces.push(place);
                } else if (area > bestArea) {
                    bestArea = area;
                    bestPlaces.length = 1;
                    bestPlaces[0] = place;
                }
                place.unplace();
            }
            
            var winner = null;
            if (bestPlaces.length == 1) {
                // found a winner based on visible area
                winner = bestPlaces[0];
            } else {
                // use distance between pointer and target to break the tie
                var bestDist = null;
                var i;
                for (i = 0; i < bestPlaces.length; i++) {
                    var place = bestPlaces[i];
                    place.place();
                    var dist = place.getPointer().distance(place.getTarget());
                    if (bestDist === null || dist < bestDist) {
                        bestDist = dist;
                        winner = place;
                    }
                    place.unplace();
                }
            }
            
            winner.place();
            
        };
        
        // HTML content
        var setHTML = function(html) {
            tipBody.innerHTML = html;
            if (tipBox.parentNode != null && tipBox.parentNode.nodeType == 1) {
                tipBox.style.visibility = "hidden";
                layout();
                tipBox.style.visibility = "";
            }
        };
        this.setHTML = setHTML;
    
        var visible = false;
        var show = function() {
            if (!visible) {
                var body = document.getElementsByTagName("BODY")[0];
                var pn = element.parentNode;
                while (pn != null && pn.nodeType == 1 && pn != body) {
                    pn = pn.parentNode;
                }
                if (pn == body) {
                    tipBox.style.visibility = "hidden";
                    body.appendChild(tipBox);
                    layout();
                    tipBox.style.visibility = "";
                    visible = true;
                }
            }
        };
        var hide = function() {
            if (visible) {
                if (tipBox.parentNode != null) {
                    tipBox.parentNode.removeChild(tipBox);
                }
                visible = false;
            }
        };
        
        // enable/disable the tooltip
        var isEnabled = true;
        var setEnabled = function(enabled) {
            isEnabled = !!enabled;
            if (!isEnabled) {
                hide();
            }
        };
        this.setEnabled = setEnabled;
        this.isEnabled = function() {
            return isEnabled;
        };
    
        var freezable = window.location.search.match(/[?&]jsdebug\.tooltip\.freezable=on/) != null;
        var frozen = false;
        
        var delay = null;
        Events.addListener(element, "mouseover", function() {
            if (isEnabled) {
                delay = setTimeout(function() {
                    delay = null;
                    show();
                }, 500);
            }
        });
        Events.addListener(element, "mouseout", function() {
            if (!frozen) {
                if (delay != null) {
                    clearTimeout(delay);
                    delay = null;
                }
                hide();
            }
        });
        Events.addListener(element, "click", function(evt) {
            if (freezable && evt.ctrlKey) {
                frozen = !frozen;
            }
        });
        
    }; // com.civicscience.ui.Tooltip

    // static methods
    com.civicscience.ui.Tooltip.getTooltip = function(element) {
        return element["com.civicscience.ui.Tooltip.value"];
    };

})();

Script.update();

Script.include("/js/css/selectors.js");

new Script.Action(function() {
    
    // manage document tooltips
    com.civicscience.ui.TooltipManager = (function() {
        
        var Tooltip = com.civicscience.ui.Tooltip;
            
        var PROP_ENTRIES = "com.civicscience.ui.TooltipManager.entries";
    
        // selector/content pairing
        var Entry = function(ordinal, selector, html) {
            this.ordinal = ordinal;
            this.selector = selector;
            this.html = html;
        };
        
        // create selector/content entry lists
        var parser = new com.civicscience.css.selectors.Parser();
        var parseEntries = function(input) {
            var entries = [];
            var regexp = /^([^{]*)\{([^}]*)\}/;
            var matches = null;
            while (input.length != 0) {
                matches = input.match(regexp);
                if (matches == null) {
                    var brace = input.indexOf("}");
                    if (brace == -1) {
                        break;
                    } else {
                        input = input.substr(brace + 1);
                        continue;
                    }
                }
                var selc = matches[1].trim();
                var html = matches[2].trim();
                if (selc && html) {
                    var arr = parser.parse(selc);
                    var i;
                    for (i = 0; i < arr.length; i++) {
                        entries.push(new Entry(entries.length, arr[i], html));
                    }
                }
                input = input.substr(matches[0].length);
            }
            entries.sort(function(e1, e2) {
                var spec1 = e1.selector.getSpecificity();
                var spec2 = e2.selector.getSpecificity();
                if (spec1 == spec2) {
                    return e2.ordinal - e1.ordinal;
                } else {
                    return spec2 - spec1;
                }
            });
            return entries;
        };

        // match tooltip resource specification
        var REGEXP_TOOLTIP_URL = /(?:^|\s)tooltipURL\[([^\]]+)\](?:$|\s)/;
        
        var update = function(elt, disable) {
            var entry = null;
            if (!disable) {
                if (elt[PROP_ENTRIES] == null) {
                    var matches = elt.className.match(REGEXP_TOOLTIP_URL);
                    if (matches != null) {
                        // initialize entries for the element
                        var url = matches[1];
                        var req = AjaxUtil.newRequest();
                        if (req != null) {
                            req.open("GET", url);
                            req.onreadystatechange = function() {
                                if (req.readyState == 4 && req.status == 200) {
                                    var entries = parseEntries(req.responseText);
                                    elt[PROP_ENTRIES] = entries;
                                    update(elt, disable);
                                }
                            };
                            req.send(null);
                            return;
                        }
                    }
                }
                // look for a matching selector/content pairing
                var node = elt;
                while (node != null && node.nodeType == 1) {
                    var entries = node[PROP_ENTRIES];
                    if (entries) {
                        var i;
                        var n = entries.length;
                        for (i = 0; i < n && !entries[i].selector.matches(elt); i++);
                        if (i < n) {
                            entry = entries[i];
                            break;
                        }
                    }
                    node = node.parentNode;
                }
            }
            // adjust element's tooltip
            var tip = Tooltip.getTooltip(elt);
            if (tip == null && entry != null) {
                tip = new Tooltip(elt);
            }
            if (tip != null) {
                tip.setEnabled(!disable && entry != null);
                if (entry != null) {
                    tip.setHTML(entry.html);
                }
            }
            // recurse on children
            disable = disable || entry != null;
            var i = 0;
            var cn = elt.childNodes;
            var n = cn.length;
            for (i = 0; i < n; i++) {
                var c = cn[i];
                if (c.nodeType == 1) {
                    update(c, disable);
                }
            }
        };
        
        // TooltipManager object
        return {
            update: function(elt) {
                var disable = false;
                var p = elt.parentNode;
                while (p != null && p.nodeType == 1 && !disable) {
                    disable = Tooltip.getTooltip(p) != null;
                    p = p.parentNode;
                }
                update(elt, disable);
            }
        };
        
    })(); // com.civicscience.ui.TooltipManager
    
    Script.update();
    
    // initialize document tooltips
    com.civicscience.ui.TooltipManager.update(document.documentElement);
    
}).requires(
        "com.civicscience.css.selectors.Parser"
    ).run();


new Script.Action(function() {
    var Tooltip = com.civicscience.ui.Tooltip;
    var TooltipManager = com.civicscience.ui.TooltipManager;
    var update = function(loader) {
        // update decendant tooltips
        var cn = loader.element.childNodes;
        var n = cn.length;
        var i;
        for (i = 0; i < n; i++) {
            var c = cn[i];
            if (c.nodeType == 1) {
                TooltipManager.update(c);
            }
        }
    };
    var disable = function(elt) {
        // disable tips for element and descendents
        var tip = Tooltip.getTooltip(elt);
        if (tip != null) {
            tip.setEnabled(false);
        }
        var cn = elt.childNodes;
        var n = cn.length;
        var i;
        for (i = 0; i < n; i++) {
            var c = cn[i];
            if (c.nodeType == 1) {
                disable(c);
            }
        }
    };
    // reinitialize root loader content
    var roots = ContentLoader.getRootLoaders();
    var i;
    for (i = 0; i < roots.length; i++) {
        update(roots[i]);
    }
    // respond to future content load events
    ContentLoader.addListener({
        loadedContent: function(loader) {
            update(loader);
        },
        unloadedContent: function(loader, fragment) {
            var cn = fragment.childNodes;
            var n = cn.length;
            var i;
            for (i = 0; i < n; i++) {
                var c = cn[i];
                if (c.nodeType == 1) {
                    disable(c);
                }
            }
        }
    });
}).requires(
        "ContentLoader",
        "com.civicscience.ui.TooltipManager"
    ).run();
