// Copyright (c) 2008-2015 Kris Maglione <maglione.k at Gmail>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
"use strict";

/** @scope modules */

var ProcessorStack = Class("ProcessorStack", {
    init: function (mode, hives, builtin) {
        this.main = mode.main;
        this._actions = [];
        this.actions = [];
        this.buffer = "";
        this.events = [];

        if (events.debug)
            events.dbg("STACK " + mode);

        let main = { __proto__: mode.main, params: mode.params };
        this.modes = [...(mode.params.keyModes || []),
                      main,
                      ...mode.main.allBases.slice(1)];

        if (builtin)
            hives = hives.filter(h => h.name === "builtin");

        this.processors = this.modes.flatMap(mode => hives.map(hive => KeyProcessor(mode, hive)))
        this.ownsBuffer = !this.processors.some(p => p.main.ownsBuffer);

        for (let input of this.processors) {
            let params = input.main.params;

            if (params.preExecute)
                input.preExecute = params.preExecute;

            if (params.postExecute)
                input.postExecute = params.postExecute;

            if (params.onKeyPress && input.hive === mappings.builtin)
                input.fallthrough = function fallthrough(events) {
                    return params.onKeyPress(events) === false ? Events.KILL : Events.PASS;
                };
            }

        let hive = options.get("passkeys")[this.main.input ? "inputHive" : "commandHive"];
        if (!builtin && hive.active && (!dactyl.focusedElement || events.isContentNode(dactyl.focusedElement)))
            this.processors.unshift(KeyProcessor(modes.BASE, hive));
    },

    passUnknown: Class.Memoize(function () {
        return options.get("passunknown").getKey(this.modes);
    }),

    notify: function () {
        events.keyEvents = [];
        events.processor = null;
        if (!this.execute(undefined, true)) {
            events.processor = this;
            events.keyEvents = this.keyEvents;
        }
    },

    _result: function (result) {
        return (result === Events.KILL         ? "KILL"  :
                result === Events.PASS         ? "PASS"  :
                result === Events.PASS_THROUGH ? "PASS_THROUGH"  :
                result === Events.ABORT        ? "ABORT" :
                callable(result) ? result.toSource().substr(0, 50) : result);
    },

    execute: function execute(result, force) {
        if (events.debug)
            events.dbg("EXECUTE(" + this._result(result) + ", " + force + ") events:" + this.events.length
                       + " processors:" + this.processors.length + " actions:" + this.actions.length);

        let length = 1;

        if (force)
            this.processors = [];

        if (this.ownsBuffer)
            statusline.inputBuffer = this.processors.length ? this.buffer : "";

        if (this.processors.every(p => p.extended) && this.actions.length) {
            // We have matching actions and no processors other than
            // those waiting on further arguments. Execute actions as
            // long as they continue to return PASS.

            for (var action of this.actions) {
                while (callable(action)) {
                    length = action.eventLength;
                    action = dactyl.trapErrors(action);
                    if (events.debug)
                        events.dbg("ACTION RES: " + length + " " + this._result(action));
                }
                if (action !== Events.PASS)
                    break;
            }

            // Result is the result of the last action. Unless it's
            // PASS, kill any remaining argument processors.
            result = action !== undefined ? action : Events.KILL;
            if (action !== Events.PASS)
                this.processors.length = 0;
        }
        else if (this.processors.length) {
            // We're still waiting on the longest matching processor.
            // Kill the event, set a timeout to give up waiting if applicable.

            result = Events.KILL;
            if (options["timeout"] && (this.actions.length || events.hasNativeKey(this.events[0], this.main, this.passUnknown)))
                this.timer = services.Timer(this, options["timeoutlen"], services.Timer.TYPE_ONE_SHOT);
        }
        else if (result !== Events.KILL && !this.actions.length &&
                 !(this.events[0].isReplay || this.passUnknown ||
                   this.modes.some(mode => mode.passEvent(this.events[0])))) {

            // No patching processors, this isn't a fake, pass-through
            // event, we're not in pass-through mode, and we're not
            // choosing to pass unknown keys. Kill the event and beep.

            result = Events.ABORT;
            if (!Events.isEscape(this.events.slice(-1)[0]))
                dactyl.beep();
            events.feedingKeys = false;
        }
        else if (result === undefined)
            // No matching processors, we're willing to pass this event,
            // and we don't have a default action from a processor. Just
            // pass the event.
            result = Events.PASS;

        if (events.debug)
            events.dbg("RESULT: " + length + " " + this._result(result) + "\n\n");

        if (result !== Events.PASS || this.events.length > 1)
            if (result !== Events.ABORT || !this.events[0].isReplay)
                Events.kill(this.events[this.events.length - 1]);

        if (result === Events.PASS_THROUGH || result === Events.PASS && this.passUnknown)
            events.passing = true;

        if (events.debug && result === Events.PASS_THROUGH && this.keyEvents.length)
            events.dbg("PASS_THROUGH:\n\t" + this.keyEvents.map(e => [e.type, DOM.Event.stringify(e)]).join("\n\t"));

        if (result === Events.PASS_THROUGH)
            events.feedevents(null, this.keyEvents, { skipmap: true, isMacro: true, isReplay: true });
        else {
            let list = this.events.filter(e => e.defaultPrevented && !e.dactylDefaultPrevented);

            if (events.debug) {
                if (result === Events.PASS)
                    events.dbg("PASS THROUGH: " + list.slice(0, length).filter(e => e.type === "keypress").map(DOM.Event.bound.stringify));
                if (list.length > length)
                    events.dbg("REFEED: " + list.slice(length).filter(e => e.type === "keypress").map(DOM.Event.bound.stringify));
            }

            if (result === Events.PASS)
                events.feedevents(null, list.slice(0, length), { skipmap: true, isMacro: true, isReplay: true });
            if (list.length > length && this.processors.length === 0)
                events.feedevents(null, list.slice(length));
        }

        return this.processors.length === 0;
    },

    process: function process(event) {
        if (this.timer)
            this.timer.cancel();

        let key = event.dactylString || DOM.Event.stringify(event);
        this.events.push(event);
        if (this.keyEvents)
            this.keyEvents.push(event);

        this.buffer += key;

        let actions = [];
        let processors = [];

        if (events.debug)
            events.dbg("PROCESS(" + key + ") skipmap: " + event.skipmap + " macro: " + event.isMacro + " replay: " + event.isReplay);

        for (let input of this.processors) {
            let res = input.process(event);
            if (res !== Events.ABORT)
                var result = res;

            if (events.debug)
                events.dbg("RES: " + input + " " + this._result(res));

            if (res === Events.KILL)
                break;

            if (callable(res))
                actions.push(res);

            if (res === Events.WAIT || input.waiting)
                processors.push(input);
            if (isinstance(res, KeyProcessor))
                processors.push(res);
        }

        if (events.debug) {
            events.dbg("RESULT: " + event.defaultPrevented + " " + this._result(result));
            events.dbg("ACTIONS: " + actions.length + " " + this.actions.length);
            events.dbg("PROCESSORS:", processors, "\n");
        }

        this._actions = actions;
        this.actions = actions.concat(this.actions);

        for (let action of actions)
            if (!("eventLength" in action))
                action.eventLength = this.events.length;

        if (result === Events.KILL)
            this.actions = [];
        else if (!this.actions.length && !processors.length)
            for (let input of this.processors)
                if (input.fallthrough) {
                    if (result === Events.KILL)
                        break;
                    result = dactyl.trapErrors(input.fallthrough, input, this.events);
                }

        this.processors = processors;

        return this.execute(result, options["timeout"] && options["timeoutlen"] === 0);
    }
});

var KeyProcessor = Class("KeyProcessor", {
    init: function init(main, hive) {
        this.main = main;
        this.events = [];
        this.hive = hive;
        this.wantCount = this.main.count;
    },

    get toStringParams() { return [this.main.name, this.hive.name]; },

    countStr: "",
    command: "",
    get count() {
        return this.countStr ? Number(this.countStr)
                             : this.main.params.count || null;
    },

    append: function append(event) {
        this.events.push(event);
        let key = event.dactylString || DOM.Event.stringify(event);

        if (this.wantCount && !this.command &&
                (this.countStr ? /^[0-9]$/ : /^[1-9]$/).test(key))
            this.countStr += key;
        else
            this.command += key;
        return this.events;
    },

    process: function process(event) {
        this.append(event);
        this.waiting = false;
        return this.onKeyPress(event);
    },

    execute: function execute(map, args) {
        return () => {
            if (this.preExecute)
                apply(this, "preExecute", args);

            args.self = this.main.params.mappingSelf || this.main.mappingSelf || map;
            let res = map.execute.call(map, args);

            if (this.postExecute)
                apply(this, "postExecute", args);
            return res;
        };
    },

    onKeyPress: function onKeyPress(event) {
        if (event.skipmap)
            return Events.ABORT;

        if (!this.command)
            return Events.WAIT;

        var map = this.hive.get(this.main, this.command);
        this.waiting = this.hive.getCandidates(this.main, this.command);
        if (map) {
            if (map.arg)
                return KeyArgProcessor(this, map, false, "arg");
            else if (map.motion)
                return KeyArgProcessor(this, map, true, "motion");

            return this.execute(map, {
                command: this.command,
                count: this.count,
                keyEvents: events.keyEvents,
                keypressEvents: this.events
            });
        }

        if (!this.waiting)
            return this.main.insert ? Events.PASS : Events.ABORT;

        return Events.WAIT;
    }
});

var KeyArgProcessor = Class("KeyArgProcessor", KeyProcessor, {
    init: function init(input, map, wantCount, argName) {
        init.supercall(this, input.main, input.hive);
        this.map = map;
        this.parent = input;
        this.argName = argName;
        this.wantCount = wantCount;
    },

    extended: true,

    onKeyPress: function onKeyPress(event) {
        if (Events.isEscape(event))
            return Events.KILL;
        if (!this.command)
            return Events.WAIT;

        let args = {
            command: this.parent.command,
            count:   this.count || this.parent.count,
            keyEvents: events.keyEvents,
            keypressEvents: this.parent.events.concat(this.events)
        };
        args[this.argName] = this.command;

        return this.execute(this.map, args);
    }
});

// vim: set fdm=marker sw=4 sts=4 ts=8 et:
