/**
 * (C) Copyright 2007-2010,2012 John J. Foerch
 * (C) Copyright 2007-2008 Jeremy Maitin-Shepard
 *
 * Use, modification, and distribution are subject to the terms specified in the
 * COPYING file.
**/

define_variable("default_minibuffer_auto_complete_delay", 150,
    "Delay (in milliseconds) after the most recent key-stroke "+
    "before auto-completing.");

define_variable("minibuffer_auto_complete_preferences", {});

define_variable("minibuffer_auto_complete_default", false,
    "Boolean specifying whether to auto-complete by default. "+
    "The user variable `minibuffer_auto_complete_preferences' "+
    "overrides this.");

var minibuffer_history_data = {};

/* FIXME: These should possibly be saved to disk somewhere */
define_variable("minibuffer_history_max_items", 100,
    "Maximum number of minibuffer history entries stored. Older "+
    "history entries are truncated after this limit is reached.");

define_variable("minibuffer_completion_rows", 8,
    "Number of minibuffer completions to display at one time.");

var atom_service = Cc["@mozilla.org/atom-service;1"].getService(Ci.nsIAtomService);

function completions_tree_view (minibuffer_state) {
    this.minibuffer_state = minibuffer_state;
}
completions_tree_view.prototype = {
    constructor: completions_tree_view,
    QueryInterface: XPCOMUtils.generateQI([Ci.nsITreeView]),
    get rowCount () {
        var c = this.minibuffer_state.completions;
        if (!c)
            return 0;
        return c.count;
    },
    getCellText: function (row,column) {
        var c = this.minibuffer_state.completions;
        if (row >= c.count)
            return null;
        if (column.index == 0)
            return c.get_string(row);
        return c.get_description(row);
    },
    setTree: function (treebox) { this.treeBox = treebox; },
    isContainer: function (row) { return false; },
    isSeparator: function (row) { return false; },
    isSorted: function () { return false; },
    getLevel: function (row) { return 0; },
    getImageSrc: function (row, col) {
        var c = this.minibuffer_state.completions;
        if (this.minibuffer_state.enable_icons &&
            col.index == 0)
        {
            return c.get_icon(row);
        }
        return null;
    },
    getRowProperties: function (row, props) {},
    getCellProperties: function (row, col, props) {
        if (col.index == 0)
            var a = atom_service.getAtom("completion-string");
        else
            a = atom_service.getAtom("completion-description");
        if (props)
            props.AppendElement(a);
        return a;
    },
    getColumnProperties: function (colid, col, props) {}
};


/* The parameter `args' specifies the arguments.  In addition, the
 * arguments for basic_minibuffer_state are also allowed.
 *
 * history:           [optional] specifies a string to identify the history list to use
 *
 * completer
 *
 * require_match
 *
 * default_completion  only used if require_match is set to true
 *
 * $valiator          [optional]
 *          specifies a function
 */
define_keywords("$keymap", "$history", "$validator",
                "$completer", "$require_match", "$default_completion",
                "$auto_complete", "$auto_complete_initial", "$auto_complete_conservative",
                "$auto_complete_delay", "$enable_icons",
                "$space_completes");
/* FIXME: support completing in another thread */
function text_entry_minibuffer_state (minibuffer) {
    keywords(arguments, $keymap = minibuffer_keymap,
             $enable_icons = false);

    basic_minibuffer_state.call(this, minibuffer, forward_keywords(arguments));

    let deferred = Promise.defer();
    this.deferred = deferred;
    this.promise = make_simple_cancelable(deferred);

    if (arguments.$history) {
        this.history = minibuffer_history_data[arguments.$history] =
            minibuffer_history_data[arguments.$history] || [];
        this.history_index = -1;
        this.saved_last_history_entry = null;
    }

    this.validator = arguments.$validator;

    if (arguments.$completer != null) {
        this.completer = arguments.$completer;
        let auto = arguments.$auto_complete;
        while (typeof auto == "string")
            auto = minibuffer_auto_complete_preferences[auto];
        if (auto == null)
            auto = minibuffer_auto_complete_default;
        this.auto_complete = auto;
        this.auto_complete_initial = !!arguments.$auto_complete_initial;
        this.auto_complete_conservative = !!arguments.$auto_complete_conservative;
        let delay = arguments.$auto_complete_delay;
        if (delay == null)
            delay = default_minibuffer_auto_complete_delay;
        this.auto_complete_delay = delay;
        this.completions = null;
        this.completions_valid = false;
        this.space_completes = !!arguments.$space_completes;
        if (this.space_completes)
            this.keymaps.push(minibuffer_space_completion_keymap);
        this.completions_timer_ID = null;
        this.completions_display_element = null;
        this.selected_completion_index = -1;
        this.require_match = !!arguments.$require_match;
        this.require_match_default = this.require_match;
        if (this.require_match)
            this.default_completion = arguments.$default_completion;
        this.enable_icons = arguments.$enable_icons;
    }
}
text_entry_minibuffer_state.prototype = {
    constructor: text_entry_minibuffer_state,
    __proto__: basic_minibuffer_state.prototype,
    load: function () {
        basic_minibuffer_state.prototype.load.call(this);
        var window = this.minibuffer.window;
        if (this.completer) {
            // Create completion display element if needed
            if (! this.completion_element) {
                /* FIXME: maybe use the dom_generator */
                var tree = create_XUL(window, "tree");
                var s = this;
                tree.addEventListener("select", function () {
                        s.selected_completion_index = s.completions_display_element.currentIndex;
                        s.handle_completion_selected();
                    }, true);
                tree.setAttribute("class", "completions");

                tree.setAttribute("rows", minibuffer_completion_rows);

                tree.setAttribute("collapsed", "true");

                tree.setAttribute("hidecolumnpicker", "true");
                tree.setAttribute("hideheader", "true");
                if (this.enable_icons)
                    tree.setAttribute("hasicons", "true");

                var treecols = create_XUL(window, "treecols");
                tree.appendChild(treecols);
                var treecol = create_XUL(window, "treecol");
                treecol.setAttribute("flex", "1");
                treecols.appendChild(treecol);
                treecol = create_XUL(window, "treecol");
                treecol.setAttribute("flex", "1");
                treecols.appendChild(treecol);
                tree.appendChild(create_XUL(window, "treechildren"));

                this.minibuffer.insert_before(tree);
                tree.view = new completions_tree_view(this);
                this.completions_display_element = tree;

                // This is the initial loading of this minibuffer state.
                // If this.auto_complete_initial is true, generate
                // completions.
                if (this.auto_complete_initial)
                    this.handle_input();
            }
            this.update_completions_display();
        }
    },

    unload: function () {
        if (this.completions_display_element)
            this.completions_display_element.setAttribute("collapsed", "true");
        basic_minibuffer_state.prototype.unload.call(this);
    },

    destroy: function () {
        if (this.completions)
            this.completions.destroy();
        delete this.completions;
        if (this.completions_cont)
            this.completions_cont.cancel();
        delete this.completions_cont;

        var el = this.completions_display_element;
        if (el) {
            el.parentNode.removeChild(el);
            this.completions_display_element = null;
        }
        if (this.promise)
            this.promise.cancel();
        basic_minibuffer_state.prototype.destroy.call(this);
    },

    handle_input: function () {
        if (! this.completer)
            return;
        this.completions_valid = false;
        if (! this.auto_complete)
            return;
        var s = this;
        var window = this.minibuffer.window;
        if (this.auto_complete_delay > 0) {
            if (this.completions_timer_ID != null)
                window.clearTimeout(this.completions_timer_ID);
            this.completions_timer_ID = window.setTimeout(
                function () {
                    s.completions_timer_ID = null;
                    s.update_completions(true /* auto */, true /* update completions display */);
                }, this.auto_complete_delay);
        } else {
            s.update_completions(true /* auto */, true /* update completions display */);
        }
    },

    update_completions_display: function () {
        var m = this.minibuffer;
        if (m.current_state == this) {
            if (this.completions && this.completions.count > 0) {
                this.completions_display_element.view = this.completions_display_element.view;
                this.completions_display_element.setAttribute("collapsed", "false");
                this.completions_display_element.currentIndex = this.selected_completion_index;
                var max_display = this.completions_display_element.treeBoxObject.getPageLength();
                var mid_point = Math.floor(max_display / 2);
                if (this.completions.count - this.selected_completion_index <= mid_point)
                    var pos = this.completions.count - max_display;
                else
                    pos = Math.max(0, this.selected_completion_index - mid_point);
                this.completions_display_element.treeBoxObject.scrollToRow(pos);
            } else {
                this.completions_display_element.setAttribute("collapsed", "true");
            }
        }
    },

    /* If auto is true, this update is due to auto completion, rather
     * than specifically requested. */
    update_completions: function (auto, update_display) {
        var window = this.minibuffer.window;
        if (this.completions_timer_ID != null) {
            window.clearTimeout(this.completions_timer_ID);
            this.completions_timer_ID = null;
        }
        var m = this.minibuffer;
        if (this.completions_cont) {
            this.completions_cont.cancel();
            this.completions_cont = null;
        }
        if (m._selection_start > 0 || ! auto || ! this.auto_complete_conservative)
            var c = this.completer.complete(m._input_text, m._selection_start);
        if (is_coroutine(c)) {
            var s = this;
            var already_done = false;
            this.completions_cont = spawn(function () {
                try {
                    var x = yield c;
                } catch (e) {
                    handle_interactive_error(window, e);
                } finally {
                    s.completions_cont = null;
                    already_done = true;
                }
                s.update_completions_done(x, update_display);
            }());
            // In case the completer actually already finished
            if (already_done)
                this.completions_cont = null;
        } else
            this.update_completions_done(c, update_display);
    },

    update_completions_done: function (c, update_display) {
        /* The completer should return undefined if completion was not
         * attempted due to auto being true.  Otherwise, it can return
         * null to indicate no completions. */
        if (this.completions)
            this.completions.destroy();

        this.completions = c;
        this.completions_valid = true;
        this.applied_common_prefix = false;

        this.require_match = this.completer.require_match;
        if (this.require_match == null)
            this.require_match = this.require_match_default;

        if (c && c.count > 0) {
            var i = -1;
            if (this.require_match) {
                if (c.count == 1)
                    i = 0;
                else if (c.default_completion != null)
                    i = c.default_completion;
                else if (this.default_completion)
                    i = this.completions.index_of(this.default_completion);
            }
            this.selected_completion_index = i;
        }

        if (update_display)
            this.update_completions_display();
    },

    select_completion: function (i) {
        this.selected_completion_index = i;
        this.completions_display_element.currentIndex = i;
        if (i >= 0)
            this.completions_display_element.treeBoxObject.ensureRowIsVisible(i);
        this.handle_completion_selected();
    },

    handle_completion_selected: function () {
        /**
         * When a completion is selected, apply it to the input text
         * if a match is not "required"; otherwise, the completion is
         * only displayed.
         */
        var i = this.selected_completion_index;
        var m = this.minibuffer;
        var c = this.completions;

        if (this.completions_valid && c && !this.require_match && i >= 0 && i < c.count)
            m.set_input_state(c.get_input_state(i));
    }
};

function minibuffer_complete (window, count) {
    var m = window.minibuffer;
    var s = m.current_state;
    if (! (s instanceof text_entry_minibuffer_state))
        throw new Error("Invalid minibuffer state");
    if (! s.completer)
        return;
    var just_completed_manually = false;
    if (! s.completions_valid || s.completions === undefined) {
        if (s.completions_timer_ID == null)
            just_completed_manually = true;
        //XXX: may need to use ignore_input_events here
        s.update_completions(false /* not auto */, true /* update completions display */);

        // If the completer is a coroutine, nothing we can do here
        if (! s.completions_valid)
            return;
    }

    var c = s.completions;

    if (! c || c.count == 0)
        return;

    var e = s.completions_display_element;
    var new_index = -1;
    var common_prefix;

    if (count == 1 && ! s.applied_common_prefix &&
        (common_prefix = c.common_prefix_input_state))
    {
        //XXX: may need to use ignore_input_events here
        m.set_input_state(common_prefix);
        s.applied_common_prefix = true;
    } else if (!just_completed_manually) {
        if (e.currentIndex != -1) {
            new_index = (e.currentIndex + count) % c.count;
            if (new_index < 0)
                new_index += c.count;
        } else {
            new_index = (count - 1) % c.count;
            if (new_index < 0)
                new_index += c.count;
        }
    }

    if (new_index != -1) {
       try {
            m.ignore_input_events = true;
            s.select_completion(new_index);
        } finally {
            m.ignore_input_events = false;
        }
    }
}
interactive("minibuffer-complete", null,
    function (I) { minibuffer_complete(I.window, I.p); });
interactive("minibuffer-complete-previous", null,
    function (I) { minibuffer_complete(I.window, -I.p); });

function exit_minibuffer (window) {
    var m = window.minibuffer;
    var s = m.current_state;
    if (! (s instanceof text_entry_minibuffer_state))
        throw new Error("Invalid minibuffer state");

    var val = m._input_text;

    if (s.validator != null && ! s.validator(val, m))
        return;

    var match = null;

    if (s.completer && s.require_match) {
        if (! s.completions_valid || s.completions === undefined)
            s.update_completions(false /* not conservative */, false /* don't update */);

        let c = s.completions;
        let i = s.selected_completion_index;
        if (c != null && i >= 0 && i < c.count) {
            match = c.get_value(i);
        } else {
            m.message("No match");
            return;
        }
    }

    if (s.history) {
        s.history.push(val);
        if (s.history.length > minibuffer_history_max_items)
            s.history.splice(0, s.history.length - minibuffer_history_max_items);
    }
    var deferred = s.deferred;
    if (deferred) {
        if (s.require_match) {
            deferred.resolve(match);
        }
        else {
            deferred.resolve(val);
        }
    }
    m.pop_state();
}
interactive("exit-minibuffer", null,
    function (I) { exit_minibuffer(I.window); });

function minibuffer_history_next (window, count) {
    var m = window.minibuffer;
    var s = m.current_state;
    if (! (s instanceof text_entry_minibuffer_state))
        throw new Error("Invalid minibuffer state");
    if (! s.history || s.history.length == 0)
        throw interactive_error("No history available.");
    if (count == 0)
        return;
    var index = s.history_index;
    if (count > 0 && index == -1)
        throw interactive_error("End of history; no next item");
    else if (count < 0 && index == 0)
        throw interactive_error("Beginning of history; no preceding item");
    if (index == -1) {
        s.saved_last_history_entry = m._input_text;
        index = s.history.length + count;
    } else
        index = index + count;

    if (index < 0)
        index = 0;

    m._restore_normal_state();
    if (index >= s.history.length) {
        index = -1;
        m._input_text = s.saved_last_history_entry;
    } else {
        m._input_text = s.history[index];
    }
    s.history_index = index;
    m._set_selection();
    s.handle_input();
}
interactive("minibuffer-history-next", null,
    function (I) { minibuffer_history_next(I.window, I.p); });
interactive("minibuffer-history-previous", null,
    function (I) { minibuffer_history_next(I.window, -I.p); });

// Define the asynchronous minibuffer.read function
minibuffer.prototype.read = function () {
    var s = new text_entry_minibuffer_state(this, forward_keywords(arguments));
    this.push_state(s);
    yield co_return(yield s.promise);
};

minibuffer.prototype.read_command = function () {
    keywords(
        arguments,
        $prompt = "Command", $history = "command",
        $completer = new prefix_completer(
            $completions = function (visitor) {
                for (let [k,v] in Iterator(interactive_commands)) {
                    visitor(v);
                }
            },
            $get_string = function (x) x.name,
            $get_description = function (x) x.shortdoc || "",
            $get_value = function (x) x.name),
        $require_match,
        $space_completes);
    var result = yield this.read(forward_keywords(arguments));
    yield co_return(result);
};

minibuffer.prototype.read_user_variable = function () {
    keywords(
        arguments,
        $prompt = "User variable", $history = "user_variable",
        $completer = new prefix_completer(
            $completions = function (visitor) {
                for (var i in user_variables) visitor(i);
            },
            $get_string = function (x) x,
            $get_description = function (x) user_variables[x].shortdoc || "",
            $get_value = function (x) x),
        $require_match,
        $space_completes);
    var result = yield this.read(forward_keywords(arguments));
    yield co_return(result);
};

minibuffer.prototype.read_preference = function () {
    keywords(arguments,
             $prompt = "Preference:", $history = "preference",
             $completer = new prefix_completer(
                 $completions = preferences.getBranch(null).getChildList("", {}),
                 $get_description = function (pref) {
                     let default_value = get_default_pref(pref);
                     let value = get_pref(pref);
                     if (value == default_value)
                         value = null;
                     let type;
                     switch (preferences.getBranch(null).getPrefType(pref)) {
                     case Ci.nsIPrefBranch.PREF_STRING:
                         type = "string";
                         break;
                     case Ci.nsIPrefBranch.PREF_INT:
                         type = "int";
                         break;
                     case Ci.nsIPrefBranch.PREF_BOOL:
                         type = "boolean";
                         break;
                     }
                     let out = type + ":";
                     if (value != null)
                         out += " " + pretty_print_value(value);
                     if (default_value != null)
                         out += " (" + pretty_print_value(default_value) + ")";
                     return out;
                 }),
             $require_match,
             $space_completes);
    var result = yield this.read(forward_keywords(arguments));
    yield co_return(result);
};


define_keywords("$object");
minibuffer.prototype.read_object_property = function () {
    keywords(arguments,
             $prompt = "Property:");
    var o = arguments.$object || {};
    var result = yield this.read(
        $prompt = arguments.$prompt,
        $completer = new prefix_completer(
            $completions = function (push) {
                for (var i in o)
                    push(i);
            }),
        $require_match,
        $space_completes);
    yield co_return(result);
};


provide("minibuffer-read");
