/*
  Copyright (C) 2009 Permeance Technologies Pty Ltd. All Rights Reserved.
  
  This library is free software; you can redistribute it and/or modify it under
  the terms of the GNU Lesser General Public License as published by the Free
  Software Foundation; either version 2.1 of the License, or (at your option)
  any later version.
  
  This library is distributed in the hope that it will be useful, but WITHOUT
  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
  details.
  
  You should have received a copy of the GNU Lesser General Public License
  along with this library; if not, write to the Free Software Foundation, Inc.,
  59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

var HighlightGlossary = {
    definitions: new Array(),
    
    template: new Template("<span><a href=\"#\" class=\"tip\">" 
                         + "#{anchor_text}</a>"
                         + "<div class=\"definitioncontainer\" style=\"display: none;\">"
                         + "<span class=\"definition\">"
                         + "#{definition_name}"
                         + "</span><span class=\"definitioncontent\">"
                         + "#{definition_description}"
                         + "</span></div></span>"),
                         
    skipTags: ["A", "H1", "H2", "SCRIPT"],
    
    /*
     * Add a new glossary item to the index
     */
    add: function(definitionName, definitionDescription) {
        var strippedName = definitionName.replace(/\'/, "\'");
        this.definitions.push({defnName: definitionName, 
                               desc: this.unescapeHTML(definitionDescription),
                               regexp: new RegExp("\\b" + strippedName + "\\b", "gi")});
        // Sort the definitions so that the definition with the longest name
        // is at the start of the array, see buildNewContent for the reasoning
        this.definitions.sort(this.definitionComparator);
        this.definitions.reverse();
    },
    
    definitionComparator: function(d1, d2) {
        if (d1.defnName.length > d2.defnName.length) {
            return 1;
        } else if (d1.defnName.length < d2.defnName.length) {
            return -1;
        }
        return 0;
    },
    
    /*
     * Scan an element to find occurances of the definitions.
     */
    scanElement: function(ele) {
        this.doScanElement(ele);
        
        $(ele).select("a.tip").each(function(a) {
            new HighlightGlossary.DefinitionLink(a);
        }.bind(this));
    },
    
    doScanElement: function(ele) {
        var element = $(ele);
        
        // Skip any nodes that we don't want to perform substitution inside.
        if (this.skipTags.indexOf(element.tagName.toUpperCase()) >= 0) {
            return;
        }
        
        if (element) {
            for (var i = 0; i < element.childNodes.length; i++) {
                var childNode = element.childNodes[i];
                // Is this a text node
                if (childNode.nodeType == 3) {
                    var nodeContent = { content: childNode.nodeValue,
                                        substitutions: new Array() };
                    
                    // Iterate over the definitions and perform the replacements
                    this.definitions.each(this.scanTextNode.bind(this, nodeContent));
                    
                    // If any definition was found, insert a SPAN tag
                    if (nodeContent.substitutions.length > 0) {
                        var span = document.createElement("SPAN");
                        span.innerHTML = this.buildNewContent(nodeContent);
                        element.insertBefore(span, childNode);
                        element.removeChild(childNode);
                    }
                } else {
                    this.doScanElement(childNode);
                }
            }
        }
    },

    scanTextNode: function(nodeContent, definition) {
        // Can not do a straight replace as we need to preserve the case of the 
        // source text. Annoying. Could be so neat... Also, need to prevent
        // against words in the template text being picked up in the scans.
        var result = null;
        while ((result = definition.regexp.exec(nodeContent.content)) != null) {
            var subsString = this.template.evaluate({anchor_text: result[0],
                                                     definition_name: definition.defnName,
                                                     definition_description: definition.desc});
            
            nodeContent.substitutions.push({index: result.index, 
                                            text: result[0],
                                            replaceWith: subsString});
        }
    },
    
    buildNewContent: function(nodeContent) {
        var index = 0;
        var newContent = "";
        nodeContent.substitutions.each(function(subs) {
            // This is to prevent double substitutions on the same text,
            // for example, if there are definitions "Hello" and "Hello World"
            // and the text says "And I said Hello World and ..." we only want
            // Hello World to get the treatment. The longer definition will be
            // first in the nodeContent.substitutions array because of the 
            // sorting performed in HighlightGlossary.add
            if (subs.index > index || index == 0) {
                newContent += this.convertLeadingAndTrailingSpace(nodeContent.content.substring(index, subs.index));
                newContent += subs.replaceWith;
                index = subs.index + subs.text.length;
            }
        }.bind(this));
        newContent += this.convertLeadingAndTrailingSpace(nodeContent.content.substring(index));
        return newContent;
    },
    
    // This is purely to make IE6 happy.
    // 
    // Leading and trailing spaces around the injected definition are converted
    // to a single non-breaking space. Otherwise, it all gets contracted to
    // nothing and there is no space between the words.
    // Also...
    // FCKEditor seems to have a habit of sticking "\n         " after some 
    // lines. Convert the whole lot to a single nbsp.
    convertLeadingAndTrailingSpace: function(text) {
        if (text == null || text.length == 0) {
            return "";
        }
        
        // Leading spaces, cut n (n > 0) spaces down to 1
        var lead = "";
        var l = 0;
        while (text.substring(l, l + 1) == " " 
               || text.substring(l, l + 1) == '\n') 
        {
            l++;
        }
        if (l > 0) {
            lead = "&nbsp;";
        }
        
        // Trailing spaces, cut n (n > 0) spaces down to 1
        var trail = "";
        var t = 0;
        while (text.substring(text.length - (t + 1), text.length - t) == " "
               || text.substring(text.length - (t + 1), text.length - t) == '\n')
        {
            t++;
        }
        if (t > 0) {
            trail = "&nbsp;";
        }
        
        return lead + text.substring(l, text.length - t) + trail;
    },
    
    unescapeHTML: function(html) {
        var htmlNode = document.createElement("DIV");
        htmlNode.innerHTML = html;
        if (htmlNode.innerText) {
            // IE
            return htmlNode.innerText;
        } else {
            // Proper browsers
            return htmlNode.textContent;
        }
    }
};

HighlightGlossary.DefinitionLink = Class.create();
HighlightGlossary.DefinitionLink.prototype = {
    anchor: null,
    span: null,
    timeout: null,
    
    initialize: function(a) {
        this.anchor = a;
        this.div = a.next("div.definitioncontainer");
        
        Event.observe(this.anchor, "mouseover", this.positionAndShowPopup.bindAsEventListener(this));
        Event.observe(this.anchor, "mouseout", this.setPopupTimeout.bindAsEventListener(this));
        Event.observe(this.div, "mouseout", this.hideDefinition.bindAsEventListener(this));
        Event.observe(this.div, "mouseover", this.showPopup.bindAsEventListener(this));
    },
    
    positionAndShowPopup: function(e) {
        var spanHeight = this.div.getHeight();
        var spanWidth = this.div.getWidth();
        var anchorWidth = this.anchor.getWidth();
        var offset = this.anchor.cumulativeOffset();
        this.div.style.top = (offset.top - 2 - spanHeight) + "px";
        this.div.style.left = (offset.left + (anchorWidth / 2) - (spanWidth / 2)) + "px";
        this.showPopup();
    },
    
    showPopup: function() {
        this.div.show();
        if (this.timeout) {
            window.clearTimeout(this.timeout);
            this.timeout = null;
        }
    },
    
    hideDefinition: function(e) {
        this.div.hide();
    },
    
    setPopupTimeout: function() {
        this.timeout = window.setTimeout(this.hideDefinition.bind(this), 250);
    }
}

Event.observe(window, "load", HighlightGlossary.scanElement.bind(HighlightGlossary, "cm"));
