KEYCODES = {
    RETURN : 13,
    RIGHT : 39,
    LEFT : 37,
    UP:   38,
    DOWN: 40,
    BACKSPACE: 8,
    TAB: 9,
    SHIFT: 16,
    ESCAPE: 27,
    ALT: 18,
    DELETE: 46,
    HOME: 36,
    END: 35,
    PAGEUP: 33,
    PAGEDOWN: 34,
    INSERT: 45
};

Suggest = newClass(null, {

    constructor: function(options) {
        if (!(options &&
              options.container &&
              options.target &&
              options.searchType)) {
            //mandatory parameters: if at least one is empty, abort suggestion functionality
            return;
        }
        //text field to bind suggestions
        this.textField = $('#' + options.target);
        if (!(this.textField.attr('autocomplete') &&
              this.textField.attr('autocomplete') == 'off')) {
            //as suggest performs alternative autocompletion functionality,
            //standard browser autocompletion should be turned off
            this.textField.attr('autocomplete', 'off');
        }

        //external div container for suggestions, used to show hide the suggestion pane
        this.container = $('#' + options.container);
        //internal div container for suggestions, used to hold the content there
        this.containerInternal = $('#' + (options.containerInternal || function() {
            //if id for internal div was not specified we
            // create a new internal div and append it as a child of <code>container</code>.
            var id = $(document.createElement('div')).
                    appendTo('#' + options.container).
                    attr('id', options.container + '-internal').
                    addClass("suggest-container-internal").
                    attr('id');
            options.containerInternal = id;
            return id;
        }() ));
        //timeout for
        this.timeout = null;
        //last term entered to the search field
        this.lastSearch = null;
        //instance of AJAX request, used to abort AJAX if user disables suggest while request is being executed
        this.request = null;
        //currently selected element of the list
        this.current = null;

        //passing the environment through bind makes sure that context is preserved when function is being executed
        this.textField.bind('keyup', {env: this}, this.suggest);
        this.textField.bind("blur", {env: this}, this.blur);
        this.textField.bind("keydown", {env: this}, this.navigate);
        this.containerInternal.bind("mouseover", {env: this}, this.hover);
        this.containerInternal.bind("mousedown", {env: this}, this.click);

        //url to access query service
        options.url = options.url || '/suggest';
        //parameter to query
        options.primarySearchParameter = options.primarySearchParameter || options.target;
        //secondary search parameter that should be respected during the search
        options.secondarySearchParameter = options.secondarySearchParameter || null;
        //prefix for ids for generated elements
        options.elementIdPrefix = options.elementIdPrefix || options.target + "-suggest";
        //class to denote selected element
        options.activeElementClass = options.activeElementClass || "active";
        //class for every element
        options.elementClass = options.elementClass || "";
        //delay to perform search for suggestion
        options.suggestDelay = options.suggestDelay || 200;
        //minimum length to trigger suggestion
        options.suggestLength = options.suggestLength || 1;

        //function to ajust position and size of container div by javascript. Does nothing by default
        //if user provides custom function options.adjustContainerPositionStrategy then
        //override default behaviour
        this.positionSuggestDiv = options.adjustContainerPositionStrategy|| this.positionSuggestDiv;
        this.options = options;
    },
    /**
     * Triggered by mouseover on the suggest container
     * Selects the element below the mouse
     *
     * @param  {Event}  e  The triggered event object
     */
    hover: function(e) {
        var env = e.data.env;
        var element = e.target;
        if (element.tagName === "LI") {
            env.unselect(env);
            env.select(element, env);
        }
    },
    /**
     * Selects the active element and closes the suggest
     *
     * @param  {Event}  e  The triggered event object
     */
    click: function(e) {
        var env = e.data.env;
        var element = e.target;
        if (element.tagName == "LI") {
            var suggestionItems = $('#' + env.options.containerInternal + ' li');
            var elementIndex = env.getElementIndex(suggestionItems, element);
            env.textField.attr('value', env.longNames[elementIndex]);
            env.hideSuggest(env);
            env.textField.focus();
            //this.setCaretPosition();
        }
        e.preventDefault();
    },
    /**
     * When focus is lost we make sure to abort any active request
     *
     * @param  {Event}  e  The triggered event object
     */
    blur: function (e) {
        var env = e.data.env;
        clearTimeout(env.timeout);
        if (env.request) {
            env.request.abort();
        }
        env.hideSuggest(env);
    },
    navigate: function(e) {
        var env = e.data.env;
        var key = e.which || e.charCode || e.keyCode;

        if (key === KEYCODES.UP || key === KEYCODES.DOWN) {
            if (!env.isSuggestVisible(env)) {
                //if suggest div is not visible we try to provoke the dialog again
                env.suggest(e, true);
                e.preventDefault();
                return;
            }

            var suggestionItems = $('#' + env.options.containerInternal + ' li');
            var itemsSize = suggestionItems.size();

            if (itemsSize) {
                var index;
                if (!env.current) {
                    //if no element selected we should choose first or last, depending on the key pressed
                    if (key === KEYCODES.DOWN) {
                        index = 0;
                    } else if (key === KEYCODES.UP) {
                        index = itemsSize - 1;
                    }
                    env.select(suggestionItems[index], env);
                } else {//if (env.current) {
                    // one element selected, we should deselect it and select a new one
                    index = env.getElementIndex(suggestionItems, env.current);
                    env.unselect(env);
                    if (key === KEYCODES.DOWN && (index + 1) < itemsSize) {
                        index++;
                        env.select(suggestionItems[index], env);

                    } else if (key === KEYCODES.UP && (index - 1) >= 0) {
                        index--;
                        env.select(suggestionItems[index], env);
                    }
                }


                if (env.current) {
                    //if an element has been selected successfully take the long names
                    env.textField.attr('value', env.longNames[index] || env.current);
                } else {
                    //as the typed value hasn't been changed by up and down and
                    //suggestion is never called on empty input, we may do so
                    env.textField.attr('value', env.typedValue);
                }
            }
            e.preventDefault();
        } else if (key === KEYCODES.TAB || key === KEYCODES.RETURN) {
            // Select with Tab or Enter key: if the list is on should not cause traverse or form submit
            if (env.current && env.isSuggestVisible(env)) {
                e.preventDefault();
            }
            clearTimeout(env.timeout);
            env.hideSuggest(env);
        }

    },
    /*
     * Function suggest handles normal text input from user, validates it and schedules
     * the AJAX request to get suggestions.
     *
     * Update is scheduled after X seconds if more that Y symbols have been entered
     *
     * @param {Event}  The triggered event object
     * @param instant  if true, suggested dialog is called immediately for every key. Used to let the user to force suggest update
     */
    suggest : function(e, instant) {
        var env = e.data.env;
        var key = e.which || e.charCode || e.keyCode;
        //check that navigation keys and specific characters do not cause suggest to appear
        //however instant should still provoke it
        if (instant ||
            key !== KEYCODES.UP &&
            key !== KEYCODES.DOWN &&
            key !== KEYCODES.TAB &&
            key !== KEYCODES.RETURN &&
            key !== KEYCODES.SHIFT &&
            key !== KEYCODES.ESCAPE) {
            //trim value not to send empty parameters
            env.typedValue = jQuery.trim(env.textField.attr('value'));
            if (env.typedValue.length > env.options.suggestLength) {
                //not empty input
                if (env.typedValue != env.lastSearch) {
                    //input does not match (literally) to the last search, so we should perform a search
                    //this helps us to filter out various shift, alt and other supplementary keys
                    clearTimeout(env.timeout);
                    env.lastSearch = env.typedValue;
                    //by applying explicit timeout func we impel the remote suggest to be executed in the current function's
                    //context instead of window context, so all the variables are available
                    var timeoutFunc = function () {
                        env.remoteSuggest(env.typedValue, env);
                    };
                    env.timeout = setTimeout(timeoutFunc, env.options.suggestDelay);

                } else if (instant) {
                    //force to show the dialog immediately!
                    env.remoteSuggest(env.typedValue, env);
                }
            } else {
                clearTimeout(env.timeout);
                env.lastSearch = "";
                env.hideSuggest(env);
                if (env.request) {
                    //aborting already running request
                    env.request.abort();
                }
            }
            e.preventDefault();
        } else if (key === KEYCODES.ESCAPE) {
            //in case of escape remove suggestion: clear timeout and existing suggestion div
            env.hideSuggest(env);
            clearTimeout(env.timeout);
        }
    },

    /**
     * Configures request parameters and sends the remote request
     *
     * Note: standard security settings in most browsers do no allow to make
     * requests to the page on other domain, so we have to use rejta server as a proxy to pass the
     * request to eniro backend.
     */
    remoteSuggest: function(term, env) {
        if (term) {

            var parameters = {};
            //default eniro params
            parameters[env.options.primarySearchParameter] = term;
            parameters['tpl'] = 'xml';
            parameters['suggest_type'] = env.options.searchType[0];
            if (env.isSecondaryField(env)) {
                //if there is a secondary search parameter, we add it to the search and
                // use mutual suggest type
                var paramName = env.options.secondarySearchParameter.term ||
                                        env.options.secondarySearchParameter.field;
                parameters[paramName] = jQuery.trim($('#'+env.options.secondarySearchParameter.field).attr('value'));
                parameters['suggest_type'] = env.options.searchType[1];
            } else {
                //if there is no secondary search parameter we use single suggest type
                parameters['suggest_type'] = env.options.searchType[0];
            }

            env.request = $.ajax({
                type: "get",
                dataType: "xml",
                url: env.options.url,
                data: parameters,
                success: function(data) {
                    env.handleResponse(data, env);
                },
                error: function() {
                    env.containerInternal.html('<!-- an error has occurred while receiving suggestions -->');
                }
            });
        }
    },
    isSecondaryField: function(env) {
        if (env.options.secondarySearchParameter &&
            env.options.secondarySearchParameter.field) {
            var field = $('#' + env.options.secondarySearchParameter.field);
            if (field && field.attr('value') &&
                //trim
                //jQuery.trim(field.attr('value')).length > env.options.suggestLength
                jQuery.trim(field.attr('value')).length > 0) {
                return true;
            }
        }
    },
    /**
     * Parses the response and inserts it into the suggest container
     *
     * @param {Object} xml  object representing xml response with suggesting data retrieved from the server
     */
    handleResponse: function(xml, env) {
        //Get item nodes from xml result.
        var items = $(xml).find('item');

        if (items.length > 0) {
            env.clearSuggestions(env);
            env.longNames = new Array(items.length);
            var ul = $(document.createElement("ul"));
            jQuery.each(items, function(i, val) {
                var item_short = val.getElementsByTagName("item_short")[0];
                var item_full = val.getElementsByTagName("item_full")[0];
                var header_code = val.getElementsByTagName("header_code")[0];
                env.longNames[i] = unescape(item_full.firstChild.nodeValue);
                var innerHTML = unescape(item_short.firstChild.nodeValue);

                var li = $(document.createElement('li')).
                        attr({'id': env.options.elementIdPrefix + i}).
                        addClass(env.options.elementClass);
                if (header_code && header_code.firstChild) {
                    li.addClass("heading");
                }
                li.append(innerHTML);
                ul.append(li);
            });

            env.containerInternal.append(ul);
            env.showSuggest(env);
        }
        else {
            env.hideSuggest(env);
        }
    },
    /**
     * Remove all suggestions from the suggest container
     */
    clearSuggestions: function(env) {
        env.containerInternal.html('');
        delete env.current;
        delete env.longNames;
    },
	/**
	 * Hides the suggest
	 */
    hideSuggest: function(env) {
        env.container.slideUp(100);
        env.clearSuggestions(env);
    },
    /**
     * Displays the suggest
     */
    showSuggest: function(env) {
        env.container.slideDown(100);
        env.positionSuggestDiv(env);
    },
    /**
     * Positions the suggest just below the input field it is attached to
     */
    positionSuggestDiv: function(env) {
        //empty by default. You may set proper function to parameter options.adjustContainerPositionStrategy
        //to override this method for particular instance of suggest
    },
    
    isSuggestVisible: function(env) {
        return env.container.css('display') == "block";
    },

    /**
     * Selects an element in the list
     *
     * @param  {Element}  element  The element to select
     */
    select: function(element, env) {
        env.current = element;
        //wrap HTML dom element to access jQuery API
        $(element).addClass(env.options.activeElementClass);
    },
    /**
     * Clears the selection of the currently selected element
     */
    unselect: function(env) {
        if (env.current) {
            $(env.current).removeClass(env.options.activeElementClass);
            delete env.current;
        }
    },
    /**
     * Retieves the index of the element in the array
     *
     * @param array    array to search the element for
     * @param element  element to search the index
     */
    getElementIndex: function(array, element) {
        if (array && element) {
            for (var i = 0; i < array.size(); i++) {
                if (array[i] === element) return i;
            }
        }
        return -1;
    }
});