RegExp.escape = function(text){
    if(!arguments.callee.sRE){
        var specials = [
        '/', '.', '*', '+', '?', '|',
        '(', ')', '[', ']', '{', '}', '\\'
        ];

        arguments.callee.sRE = new RegExp('(\\' + specials.join('|\\') + ')', 'g');
    }

    return text.replace(arguments.callee.sRE, '\\$1');
}

function AutoSuggest(elem, suggestions){
    //The 'me' variable allow you to access the AutoSuggest object
    //from the elem's event handlers defined below.
    var me = this;

    //A reference to the element we're binding the list to.
    this.elem = elem;

    //Limit the height of the list.  If this is exceeded, the list will scroll.
    //Hight is measured in list items.
    this.limit = 12;
    this.topScroll = 0;
 
    //Make the matching parts of suggestions bold or not
    me.boldMatching = false;
 
    //If a triggerChar is used, it's assumed that AJAX is used to
    //update the list of suggestions.  Thus, the name of the function
    //that returns the list is passed instead of the list itself.
    if(typeof suggestions == 'function'){
        this.suggestionsFunc = suggestions;
        this.suggestionsFunc(elem.value, this);
    }else{
        this.suggestions = suggestions;
    }

    //Arrow to store a subset of eligible suggestions that match the user's input
    this.eligible = new Array();

    //The text input by the user.
    this.inputText = '';

    //A pointer to the index of the highlighted eligible item. -1 means nothing highlighted.
    this.highlighted = -1;

    //A div to use to create the dropdown.
    this.div = document.getElementById("autosuggest_" + this.elem.id);

    //Do you want to remember what keycode means what? Me neither.
    var TAB = 9;
    var ENTER = 13;
    var ESC = 27;
    var KEYUP = 38;
    var KEYDN = 40;
    var SHIFT = 16;

    //The browsers' own autocomplete feature can be problematic, since it will 
    //be making suggestions from the users' past input.
    //Setting this attribute should turn it off.
    elem.setAttribute("autocomplete","off");

    //Keep track of whether or not the mouse is over the list
    this.mouseover = false;

    //We need to be able to reference the elem by id. If it doesn't have an id, set one.
    if(!elem.id){
        var id = "autosuggest" + idCounter;
        idCounter++;

        elem.id = id;
    }

    /********************************************************
    Scroll the list to the specified list item.
    ********************************************************/    
    this.scrollList = function(){
        this.div.scrollLeft = this.div.scrollWidth;

        if(this.highlighted > -1){
            this.topScroll = this.div.scrollTop;
            var highlighted = this.div.getElementsByTagName('LI')[this.highlighted];
            if(!highlighted){
                this.hideDiv();
                return false;
            }

            if(highlighted.offsetTop + highlighted.clientHeight > this.div.clientHeight + this.div.scrollTop){
                this.topScroll = highlighted.offsetTop + highlighted.clientHeight - this.div.clientHeight;
            }else if(highlighted.offsetTop < this.div.scrollTop){
                this.topScroll = highlighted.offsetTop;
            }

            this.div.scrollTop = this.topScroll;
        }
    }

    /********************************************************
    onkeydown event handler for the input elem.
    Tab key = use the highlighted suggestion, if there is one.
    Esc key = get rid of the autosuggest dropdown
    Up/down arrows = Move the highlight up and down in the suggestions.
    ********************************************************/
    elem.onkeydown = function(ev){
        var key = getKeyCode(ev);

        switch(key){
            case ENTER:
            if(me.highlighted > -1){
                me.useSuggestion();
                return false;
            }
            break;

            case TAB:
            me.useSuggestion();
            break;

            case ESC:
            me.hideDiv();
            break;

            case KEYUP:
            if(me.highlighted > 0){
                me.highlighted--;
            }else{
                me.highlighted = me.eligible.length - 1;
            }
            me.changeHighlight(key);
            if(me.eligible.length > me.limit){
                me.scrollList();
            }
            return false;
            break;

            case KEYDN:
            if(me.highlighted == -1){
                if(me.inputText != this.value){
                    if(me.suggestionsFunc){
                        me.suggestionsFunc(this.value, me);
                    }
    
                    me.inputText = this.value;
                    me.getEligible();
        
                    if(me.eligible.length > 0){
                        me.createDiv();
                        me.showDiv();
                        me.positionDiv();
                    }else{
                        me.hideDiv();
                    }
                }else if(me.eligible.length > 0){
                    me.createDiv();
                    me.showDiv();
                    me.positionDiv();
                }
            }
            if(me.highlighted < (me.eligible.length - 1)){
                me.highlighted++;
            }else{
                me.highlighted = 0;
            }
            me.changeHighlight(key);
            if(me.eligible.length > me.limit){
                me.scrollList();
            }
            return false;
            break;
        }
    };

    /********************************************************
    onkeyup handler for the elem
    If the text is of sufficient length, and has been changed, 
    then display a list of eligible suggestions.
    ********************************************************/
    elem.onkeyup = function(ev) {
        var key = getKeyCode(ev);

        switch(key){
            //The control keys were already handled by onkeydown, so do nothing.
            //case TAB:
            case ENTER:
            case ESC:
            case KEYUP:
            case KEYDN:
            case SHIFT:
                return;
            default:

            if(me.inputText != this.value){
                if(me.suggestionsFunc){
                    me.suggestionsFunc(this.value, me);
                }

                me.inputText = this.value;
                me.getEligible();
    
                if(me.eligible.length > 0){
                    me.createDiv();
                    me.showDiv();
                    me.positionDiv();
                }else{
                    me.hideDiv();
                }
            }
        }
    };

    elem.onfocus = function(){
        pathFocused = true;
    };

    elem.onblur = function(){
        if(!me.mouseover){
            me.hideDiv();
        }
        pathFocused = false;
    };

    /********************************************************
    Insert the highlighted suggestion into the input box, and 
    remove the suggestion dropdown.
    ********************************************************/
    this.useSuggestion = function(){
        if(this.highlighted > -1 && this.eligible[this.highlighted]){
            this.elem.value = this.eligible[this.highlighted];
            this.hideDiv();
            //It's impossible to cancel the Tab key's default behavior. 
            //So this undoes it by moving the focus back to our field right after
            //the event completes.
            setTimeout("document.getElementById('" + this.elem.id + "').focus()", 0);
        }
    };

    /********************************************************
    Display the dropdown. Pretty straightforward.
    ********************************************************/
    this.showDiv = function(){
        this.div.style.display = 'block';
        this.div.style.zIndex = '30';
    };

    /********************************************************
    Hide the dropdown and clear any highlight.
    ********************************************************/
    this.hideDiv = function(){
        this.div.style.display = 'none';
        this.highlighted = -1;
    };

    /********************************************************
    Modify the HTML in the dropdown to move the highlight.
    ********************************************************/
    this.changeHighlight = function(){
        var lis = this.div.getElementsByTagName('LI');
        for(var i = 0; i < lis.length; i++){
            var li = lis[i];

            if(li){
                li.className = (this.highlighted == i) ? "selected" : "";
            }
        }

        this.div.scrollLeft = this.div.scrollWidth;
    };

    /********************************************************
    Position the dropdown div below the input text field.
    ********************************************************/
    this.positionDiv = function(){
        var el = this.elem;
        var x = 0;
        var y = el.offsetHeight;
    
        //Walk up the DOM and add up all of the offset positions.
        while(el.offsetParent && el.tagName.toUpperCase() != 'BODY'){
            x += el.offsetLeft;
            y += el.offsetTop;
            el = el.offsetParent;
        }

        x += el.offsetLeft;
        y += el.offsetTop;

        this.div.style.left = x + 'px';
        //this.div.style.top = y + 'px';
    };

    /********************************************************
    Build the HTML for the dropdown div
    ********************************************************/
    this.createDiv = function(){
        var ul = document.createElement('ul');
    
        //Create an array of LI's for the words.
        for(var i = 0; i < this.eligible.length; i++){
            var word = this.eligible[i];
            var regex = new RegExp('('+RegExp.escape(this.inputText)+')', 'gi') 
    
            var li = document.createElement('li');
            var a = document.createElement('a');
            a.href="javascript:false";

            if(this.eligible.length == 1){
                this.highlighted = 0;
                a.innerHTML = word;
            }else{
                this.highlighted = -1;
                if(me.boldMatching){
                    a.innerHTML = word.replace(regex, "<b>" + "$1" + "</b>");
                }else{
                    a.innerHTML = word;
                }
            }

            li.appendChild(a);
    
            if(this.highlighted == i){
                li.className = "selected";
            }

            ul.appendChild(li);
        }

        this.div.replaceChild(ul, this.div.childNodes[0]);
        this.div.style.width = elem.clientWidth + 'px';
        this.div.style.overflowY = 'auto';
        this.div.style.overflowX = 'hidden';

        if(this.eligible.length > this.limit){
            this.div.style.height = this.limit * li.clientHeight + 'px';
        }else{
            this.div.style.height = '';
            this.topScroll = 0;
        }

        this.scrollList();

        /********************************************************
        mouseover handler for the dropdown ul
        move the highlighted suggestion with the mouse
        ********************************************************/
        ul.onmouseover = function(ev){
            me.mouseover = true;
 
           //Walk up from target until you find the LI.
            var target = getEventSource(ev);
            while(target.parentNode && target.tagName.toUpperCase() != 'LI'){
                target = target.parentNode;
            }
        
            var lis = me.div.getElementsByTagName('LI');
    
            for(var i in lis){
                var li = lis[i];
                if(li == target){
                    me.highlighted = i;
                    break;
                }
            }
            me.changeHighlight();
        };

        ul.onmouseout = function(){
            me.mouseover = false;
            me.highlighted = -1;
            me.changeHighlight();
        };

        /********************************************************
        click handler for the dropdown ul
        insert the clicked suggestion into the input
        ********************************************************/
        ul.onclick = function(ev){
            me.useSuggestion();
            me.hideDiv();
            cancelEvent(ev);
            return false;
        };
    
        this.div.className = "suggestion_list";
    };

    /********************************************************
    determine which of the suggestions matches the input
    ********************************************************/
    this.getEligible = function(){
        this.eligible = new Array();
        for(var i in this.suggestions){
            var suggestion = this.suggestions[i];
            
            if(typeof(suggestion) == "string" && suggestion.toLowerCase().indexOf(this.inputText.toLowerCase()) != -1){
                this.eligible[this.eligible.length] = suggestion;
            }
        }
    };
}

/********************************************************
Helper function to determine the keycode pressed in a 
browser-independent manner.
********************************************************/
getKeyCode = function(ev){
    if(ev)              //Moz
        return ev.keyCode;
    if(window.event)    //IE
        return window.event.keyCode;
};

/********************************************************
Helper function to determine the event source element in a 
browser-independent manner.
********************************************************/
getEventSource = function(ev){
    if(ev)              //Moz
        return ev.target;
    if(window.event)    //IE
        return window.event.srcElement;
};

/********************************************************
Helper function to cancel an event in a 
browser-independent manner.
(Returning false helps too).
********************************************************/
cancelEvent = function(ev){
    if(ev){             //Moz
        ev.preventDefault();
        ev.stopPropagation();
    }
    if(window.event)    //IE
        window.event.returnValue = false;
}

//counter to help create unique ID's
var idCounter = 0;
