function Suggest(input, get_options) {

    // The input box we're bound to
    this.input = $(input);

    // The <div> to display the suggestions in
    this.suggestion_box = DIV();

    // Function to provide options. Must have the signature:
    //
    // function get_options(value) {
    //      return [ 'one', 'two', 'three' ];
    // }
    //
    this.get_options = get_options;

    var self = this;

	this.input.setAttribute("autocomplete","OFF");

    this.selected_index = -1;
    this._lastvalue = "";

	// find the actual position of the input element 
    var position   = getElementPosition(this.input);
    var dimensions = getElementDimensions(this.input);

	this.suggestion_box.style.position = "absolute";
	this.suggestion_box.className = "suggestion-box";
	this.suggestion_box.style.top = position.y + dimensions.h + 'px';
	this.suggestion_box.style.left = position.x + 'px';
	this.suggestion_box.style.width = dimensions.w;
	this.suggestion_box.style.display = "none";

	document.body.insertBefore(this.suggestion_box, document.body.firstChild);

    connect(document, 'onclick', function () { self.hide_select(); });
    if (!isUndefinedOrNull(this.input.form)) {
        connect(document, 'onkeydown',
            function (e) {
                if (e.key().string == 'KEY_ENTER' && self.is_showing()) {
                    self.hide_select(); 
                    self.input.value = self.selected_value();
                    e.stop();
                }
            }
        );
    }
    connect(document, 'onkeydown', function (e) { if (e.key().string == 'KEY_ESCAPE') { self.hide_select(); }});
    connect(this.input, 'onkeyup', function(e) {
		self.show_suggestions(e.src().value, e);
	});
}

Suggest.prototype = {

    /**
     * Hide the selection box.
     */
    hide_select: function() {
        this.suggestion_box.style.display = "none";
    },

    /**
     * Return true if the suggestion box is currently being displayed.
     */
    is_showing: function() {
        return this.suggestion_box.childNodes.length > 0 && this.suggestion_box.style.display != "none";
    },


    /**
     * Remove all selections from the suggestion_box.
     */
    _clear_suggestions: function() {
        this.selected_index = -1;
        this.suggestion_box.scrollTop = 0;
        while (this.suggestion_box.childNodes.length > 0) {
            this.suggestion_box.removeChild(this.suggestion_box.childNodes[0]);
        }
    },

    /**
     * Set a new list of suggestions
     */
    set_suggestions: function(options) {
        var self = this;
        this._clear_suggestions();
        for (var ix = 0; ix < options.length; ix++) {
            var div = DIV({}, options[ix]);
            div.className = 'suggestion-item';
            connect(div, 'onmouseover', function (e) { self.select_by_node(e.src(), null) });
            this.suggestion_box.appendChild(div);
        }
    },
 
    selected_value: function () {
        return map(
            function (node) {
                return (typeof node.data == 'undefined') ? "" : node.data;
            },
            this.suggestion_box.childNodes[this.selected_index].childNodes
        ).join('');
    },
 
    show_suggestions: function(value, ev) {

        if (this.suggestion_box.childNodes.length > 0) {
            if (ev.key().string == 'KEY_ARROW_DOWN') {
                // reset to last typed value
                this.input.value = this._lastvalue;
                if (this.selected_index < 0) {
                    this.select_by_index(0);
                } else if (this.selected_index < this.suggestion_box.childNodes.length - 1) {
                    this.select_by_index(this.selected_index + 1);
                }
                ev.stop();
                return;
            }
            else if (ev.key().string == 'KEY_ARROW_UP') {
                // reset to last typed value
                this.input.value = this._lastvalue;
                if (this.selected_index < 0) {
                    this.select_by_index(this.suggestion_box.childNodes.length - 1);
                } else if (this.selected_index > 0) {
                    this.select_by_index(this.selected_index - 1);
                }
                ev.stop();
                return;
            }
            else if (ev.key().string == 'KEY_ESCAPE') {
                this.hide_select();
                ev.stop();
                return;
            }
            else if (ev.key().string == 'KEY_ENTER') {
                this.hide_select();
                this.input.value = this.selected_value();
                ev.stop();
                return;
            }
        }

        if (value == "" || value == this._lastvalue) {
            return;
        }
        this._lastvalue = value;
        this.set_suggestions(this.get_options(value));
        this.suggestion_box.style.display = (this.suggestion_box.childNodes.length > 0) ? "" : "none";
        this.input.focus();
    },

    /* Select a single item.
     *
     * index
     *      index of item to select (will become the new value of this.selected_index)
     * node
     *      DIV element of item to select
     */
    _select: function(index, node){ 

        // Which way are we scrolling, up (-1) or down (1)?
        var direction = (index < this.selected_index) ? -1 : 1;

        // null node value means we deselect everything
        if (node == null) {
            if (this.selected_index >= 0) {
                try {
                    this.suggestion_box.childNodes[this.selected_index].className = 'suggestion-item';
                } catch (e) {}
            }
            this.selected_index = -1
            return;
        }

        // IE misreports if we use mochikit's getElementPosition
        node.className = 'suggestion-item-selected';
        if (this.selected_index >= 0) {
            this.suggestion_box.childNodes[this.selected_index].className = 'suggestion-item';
        }
        this.selected_index = index;

        // Set the input box value to the currently selected item.
        // What the user typed himself should not be deselected; the
        // autocompleted remainder should be selected

        // Record what the user typed
        var typed_value = this.input.value;

        // Overwrite with the currently selected item
        this.input.value = this.selected_value();

        // Select from the end of the users input to the end of the input box value
        if (this.input.createTextRange) {
            var range = this.input.createTextRange();
            range.moveStart("character", typed_value.length);
            range.moveEnd("character", this.input.value.length);
            range.select();
        } else if (this.input.setSelectionRange) {
            this.input.setSelectionRange(typed_value.length, this.input.value.length);
        }

        // Scroll the suggestion_box DIV so we can see the selection

        // Position of the top of the selected item, relative to the viewport
        var ypos_top = node.offsetTop - this.suggestion_box.scrollTop;

        // Position of the bottom of the selected item, relative to the viewport.
        // XXX: added 1.5 fudge factor
        var ypos_bottom = ypos_top + (1.5 * getElementDimensions(node).h);

        // Size of the viewport
        var sbox_dimensions = getElementDimensions(this.suggestion_box);

        // Item out of view to the top of the suggestion_box, scroll selection
        // to the top of the container
        if (ypos_top < 0) {
            this.suggestion_box.scrollTop += ypos_top;
        }

        // Item out of view to the bottom of the suggestion_box, scroll
        // selection to bottom of container
        else if (ypos_bottom >= sbox_dimensions.h) {
            this.suggestion_box.scrollTop += ypos_bottom - sbox_dimensions.h;
        }
    },

    select_by_index: function(index) {
        if (index < 0 || index >= this.suggestion_box.childNodes.length) {
            return this._select(index, null);
        } else {
            return this._select(index, this.suggestion_box.childNodes[index]);
        }
    },

    select_by_node: function(node) {
        if (node == null) {
            return this._select(-1, null);
        } else {
            for (var ix = 0; ix < this.suggestion_box.childNodes.length; ix++) {
                if (this.suggestion_box.childNodes[ix] == node) {
                    return this._select(ix, node);
                }
            }
            return this._select(-1, null);
        }
    }
}
 

