//
// TODO:
//
// Support for the <enter> key. (It currently submits the form, but it should
// act exactly like the <tab> key when the user is highlighting a completion.)
// There appear to be major portability issues between MSIE and Mozilla here.
//
// Support an additive-style completion field, where choosing a suggestion
// appends it to the input field with a comma separator, and subsequent
// completion searches ignore everything prior to the comma. This style would
// be preferable in Notes.
//
// There is never any need to run another autocomplete search after the user
// tab- or enter-completes a highlighted selection.
//

/**
 * install autocomplete for an input element.
 *
 * @param url Base URL that provides suggested completions. must accept seq & s
 *          parameters. The page should use subclass of ui_Autocomplete.
 * @param url_params Hash or function that returns a hash of param name, value
 *          pairs for constructing the URL that will return the suggested
 *          completions.
 * @param input_field_id id of html element that has the text our backend will
 *          provide suggestions for.
 * @param max_results_per_request maximum number of results that can be
 *          returned.
 * @param arguments[4] function that will be called when selection is made.
 */
var __autocomplete_divs = new Array();

function install_autocomplete(url,
                              input_field_id,
                              options)
{
    var url_params              = options['url_params'];
    var max_results_per_request = options['max_results_per_request'];
    var enable_request_cache    = options['enable_request_cache'];
    var select_handler          = options['select_handler'];
    // +-------------------+
    // | utility functions |
    // +-------------------+

    function get_text(o) {
        return (typeof(o.textContent) != 'undefined')
               ? o.textContent
               : (typeof(o.text) != 'undefined') ? o.text : o.innerHTML;
    }

    function get_width(e) {
        if (navigator &&
            navigator.userAgent.toLowerCase().indexOf('msie') == -1)
        {
            return e.offsetWidth - 2;
        }
        return e.offsetWidth;
    }
    function get_event(event) {
        if (!event && window.event)
            return window.event;
        return event;
    }
    function get_key_code_from_event(event) {
        event = get_event(event);
        return event ? event.keyCode : null;
    }

    var prev_input_field_value = '';    // input on previous keypress
    var curr_input_field_value = '';    // input on last keypress
    var autocomplete_div;               // where we display the results
    var browser = navigator.userAgent.toLowerCase();

    var input_field = document.getElementById(input_field_id);
    input_field.autocomplete = 'off';

    if(browser.indexOf("msie") != -1) {
        var ieFrame = document.createElement('iframe');
        ieFrame.id = 'ieFrame' + input_field_id;
        ieFrame.style.zIndex = "10";
        ieFrame.style.position = 'absolute';
        ieFrame.style.backgroundColor = "white";
        ieFrame.style.border = "0px";
        ieFrame.show = function() { this.style.visibility = 'visible' };
        ieFrame.hide = function() { this.style.visibility = 'hidden' };
        ieFrame.adjust_size = function() {
            this.style.left = (YAHOO.util.Dom.getX(input_field) - 1) + 'px';
            this.style.top =
                (YAHOO.util.Dom.getY(input_field) + input_field.offsetHeight - 1) + 'px';
            this.style.width = get_width(input_field) + 'px';

            var auto_div = document.getElementById('autocomplete__' + input_field_id);
            if (auto_div != null) {
                this.style.height = auto_div.offsetHeight + 'px';
            }
        };

        ieFrame.adjust_size();
        ieFrame.hide();
        document.body.appendChild(ieFrame);
    }

    autocomplete_div = document.createElement('div');
    autocomplete_div.id = 'autocomplete__' + input_field_id;
    autocomplete_div.className = 'autocomplete_results';
    autocomplete_div.style.zIndex = '20';
    autocomplete_div.style.position = 'absolute';
    autocomplete_div.style.border = "1px solid #333333";
    autocomplete_div.style.padding = "2px";
    autocomplete_div.style.backgroundColor = "white";
    autocomplete_div.show = function() {
        this.style.visibility = 'visible';
        if(browser.indexOf("msie") != -1) {
            ieFrame.show();
        }
    };
    autocomplete_div.hide = function() {
        this.style.visibility = 'hidden';
        if(browser.indexOf("msie") != -1) {
            ieFrame.hide();
        }
    };
    autocomplete_div.adjust_size = function() {
        this.style.left = (YAHOO.util.Dom.getX(input_field) - 1) + 'px';
        this.style.top =
            (YAHOO.util.Dom.getY(input_field) + input_field.offsetHeight - 1)
            + 'px';
        this.style.width = get_width(input_field) + 'px';

        if(browser.indexOf("msie") != -1) {
            ieFrame.adjust_size();
        }
    };

    autocomplete_div.adjust_size();
    autocomplete_div.hide();
    document.body.appendChild(autocomplete_div);

    window.onresize = append_function(
        function() { autocomplete_div.adjust_size() },
        window.onresize);

    // +-----------------------------------+
    // | highlighting autocomplete results |
    // +-----------------------------------+

    var results = new Array();          // current autocomplete results
    var hl_result = null;               // the currently highlighted result
    var hl_result_index = -1;           // offset of hl_result in results

    function set_results(r) {
        clear_highlight();
        results = r;
    }
    function highlight_result(r) {
        if (!r)
            return;
        for (var i in results)
            if (results[i] == r)
                return highlight_result_by_index(i);
    }
    function highlight_result_by_index(i) {
        if (i < 0 || i >= results.length)
            return;
        clear_highlight();
        if (results[i]) {
            hl_result_index = i;
            hl_result = results[i];
            hl_result.activate();
        }
    }
    function clear_highlight() {
        if (hl_result)
            hl_result.deactivate();
        hl_result = null;
        hl_result_index = -1;
    }
    function choose_highlighted_result() {
        if (hl_result) {
            var spans = hl_result.getElementsByTagName('span');
            var display = get_text(spans[0]);
            var data = get_text(spans[1]);

            input_field.value = display;

            curr_input_field_value = input_field.value;
            prev_input_field_value = curr_input_field_value
            clear_highlight();
            autocomplete_div.hide();

            if (select_handler)
                select_handler(data);

            return true;
        }
        return false;
    }

    // +-------------------------+
    // | keypress event handlers |
    // +-------------------------+

    function handle_key_down(event) {
        var key_code = get_key_code_from_event(event);
        if ((key_code==13 || key_code==9) &&
            choose_highlighted_result()) {
            return false;
        }
        return true;
    };

    autocomplete_div.onkeydown = function(event) {
        handle_key_down(event);
    };

    document.onkeydown = function(event) {
        for (var i = 0; i < __autocomplete_divs.length; i++) {
            __autocomplete_divs[i].onkeydown(event);
        }
    };

    input_field.onkeyup = function(event) {
        curr_input_field_value = input_field.value;

        var key_code = get_key_code_from_event(event);
        if (key_code==38 || key_code==40) { // 38=up; 40=down
            var direction = key_code==38 ? -1 : +1;
            highlight_result_by_index(hl_result_index + direction);
            return false;
        }
        return true;
    };

    input_field.onblur = append_function(
        function() { autocomplete_div.hide() },
        input_field.onblur);

    // +--------------------------------+
    // | rendering autocomplete results |
    // +--------------------------------+

    function make_suggestion_element(text) {
        var new_elem = document.createElement('div');

        new_elem.activate = function() {
            this.style.backgroundColor = '#3366cc';
            this.style.color = 'white';
        };
        new_elem.deactivate = function() {
            this.style.backgroundColor = 'white';
            this.style.color = 'black';
        };
        new_elem.onmousedown = function() {
            choose_highlighted_result();
        };
        new_elem.onmouseover = function() {
            highlight_result(this);
        };
        new_elem.onmouseout = function() {
            clear_highlight();
        };

        new_elem.deactivate();

        var data = text;
        if (arguments.length > 1 && arguments[1])
            data = arguments[1];

        new_elem.innerHTML =
            '<span>' + text + '</span>' +
            '<span style="display: none;">' + data + '</span>';

        return new_elem;
    }

    //
    // Given a list of suggestion strings, puts them in the autocomplete_div.
    // If suggestions is empty, hides the autocomplete_div completely.
    //
    function display_suggestions(suggestions) {
        // Remove any suggestions previously put in the autocomplete_div.
        while (autocomplete_div.childNodes.length > 0)
            autocomplete_div.removeChild(autocomplete_div.childNodes[0]);

        var results = new Array();
        set_results(results);

        // If there were no suggestions, just hide the div.
        if (suggestions.length == 0) {
            autocomplete_div.hide();
            return;
        }

        // Add each string in suggestions, using make_suggestion_element.
        var num = 0;
        for (var i in suggestions) {
            var e;
            if (typeof(suggestions[i]) == 'object')
                e = make_suggestion_element(suggestions[i][0],
                                            suggestions[i][1]);
            else
                e = make_suggestion_element(suggestions[i]);
            results[results.length] = e;
            autocomplete_div.appendChild(e);
            num++;
            if (num >= max_results_per_request)
                break;
        }
        set_results(results);
        autocomplete_div.adjust_size();
        autocomplete_div.show();
    }

    // +-----------+
    // | main loop |
    // +-----------+

    var seq = 0;                        // for sequencing server requests
    var num_outstanding_reqs = 0;       // adjusts timeout on main loop
    var cache = {};                     // caches server results

    function main_loop() {
        var last_seq = -1;

        function save_to_cache(key, results) {
            if (!enable_request_cache)
                return;

            cache[key] = results;
        }

        function find_in_cache(key) {
            if (!enable_request_cache)
                return;

            if (cache[key])
                return cache[key];

            // Optimization: even though we didn't find an exact match in the
            // cache, we may be able to determine the completions for the key
            // without sending a request to the server. If a previous request
            // was sent using a front-anchored substring of the key, and its
            // response contained fewer than the maximum number of results, we
            // can use that response to determine the completions of our key.

            for (var i = key.length - 1; i > 0; i--) {
                var results = cache[key.substr(0, i)];
                if (!results || results.length >= max_results_per_request)
                    continue;
                var answer = new Array();
                for (var j in results) {
                    var result_substring = results[j].substr(0, key.length);
                    if (result_substring.toLowerCase() == key.toLowerCase())
                        answer[answer.length] = results[j];
                }
                return answer;
            }

            return null;
        }

        function handle_response(req) {
            num_outstanding_reqs = Math.max(0, num_outstanding_reqs - 1);

            var response;
            eval(req.responseText);  // assigns to response variable
            if (!response) return;

            var seq = response[0];
            var search_key = response[1];
            var suggestions = response[2];

            if (last_seq < seq && input_field.value == search_key) {
                last_seq = seq;
                display_suggestions(suggestions);
            }

            save_to_cache(search_key, suggestions);
        }

        // Exponentially increases the delay between requests based on the
        // number of requests that are still waiting for a response.
        function calculate_timeout() {
            return 2000*Math.pow(2, num_outstanding_reqs) + 50;
        }

        function hash_to_qs(hash){
            var pairs = [];
            for (var key in hash)
            {
                pairs.push( key + '=' + escape(hash[key]) );
            }
            return pairs.join('&');
        }


        function main() {
            // If the state of the input field has not changed, do nothing.
            if (prev_input_field_value == curr_input_field_value)
                return;

            prev_input_field_value = curr_input_field_value;

            // Do not auto-complete strings with fewer than 2 characters.
            if (curr_input_field_value.length < 2) {
                autocomplete_div.hide();
                return;
            }

            // Try to retrieve the autocomplete results from cache first.
            var cached = find_in_cache(curr_input_field_value);
            if (cached) {
                display_suggestions(cached);
                return;
            }

            // url_params might be a function.
            params = url_params;
            if (typeof(params) == 'function') {
                params = params();
            }

            // Looks like we have to talk to the server...
            var request_url = url
                + '?seq='
                + (++seq)
                + '&s='
                + escape(curr_input_field_value)
                + '&' + hash_to_qs(params);

            get_url_async(request_url, handle_response);
            ++num_outstanding_reqs;
        }

        main();
        setTimeout(function() { main_loop() }, calculate_timeout());
        return true;
    }

    __autocomplete_divs[__autocomplete_divs.length] = autocomplete_div;
    setTimeout(function() { main_loop() }, 10);
}
