import { assert } from "../../assert.js";
import { dispatchViaProxy } from "../../events/signal/dispatchViaProxy.js";
import { findSignalHandlerIndexByHandle } from "../../events/signal/findSignalHandlerIndexByHandle.js";
import { SignalHandler } from "../../events/signal/SignalHandler.js";

import { noop } from "../../function/noop.js";
import { SimpleStateMachineDescription } from "./SimpleStateMachineDescription.js";

export class SimpleStateMachine {

    /**
     * Current state, -1 indicated "unset"
     * @type {number}
     * @private
     */
    __state = -1;

    /**
     *
     * @type {SignalHandler[]}
     * @private
     */
    __eventHandlersStateEntry = [];

    /**
     *
     * @type {SignalHandler[]}
     * @private
     */
    __eventHandlersStateExit = [];

    /**
     *
     * @param {SimpleStateMachineDescription} description
     */
    constructor(description) {
        assert.defined(description, 'description');
        assert.notNull(description, 'description');
        assert.ok(description.isSimpleStateMachineDescription, 'SimpleStateMachineDescription.isSimpleStateMachineDescription !== true');

        //assert.ok(description instanceof SimpleStateMachineDescription, 'description is not an instance of SimpleStateMachineDescription');

        /**
         *
         * @type {SimpleStateMachineDescription}
         */
        this.description = description;
    }

    /**
     *
     * @param {number} state
     * @param {function} handler
     * @param {*} [thisArg]
     */
    addEventHandlerStateEntry(state, handler, thisArg) {
        //assert.ok(this.description.stateExists(state), `state ${state} doesn't exist`);

        const handlers = this.__eventHandlersStateEntry[state];

        const signalHandler = new SignalHandler(handler, thisArg);

        if (handlers === undefined) {
            this.__eventHandlersStateEntry[state] = [signalHandler];
        } else {
            handlers.push(signalHandler);
        }
    }

    /**
     *
     * @param {number} state
     * @param {function} handler
     * @param {*} [thisArg]
     */
    removeEventHandlerStateEntry(state, handler, thisArg) {
        //assert.ok(this.description.stateExists(state), `state ${state} doesn't exist`);

        const handlers = this.__eventHandlersStateEntry[state];

        if (handlers !== undefined) {
            const i = findSignalHandlerIndexByHandle(handlers, handler, thisArg);

            if (i !== -1) {
                handlers.splice(i, 1);
                return true;
            }
        }

        return false;
    }

    /**
     *
     * @param {number} state
     * @param {function} handler
     * @param {*} [thisArg]
     */
    addEventHandlerStateExit(state, handler, thisArg) {
        //assert.ok(this.description.stateExists(state), `state ${state} doesn't exist`);

        const handlers = this.__eventHandlersStateExit[state];

        const signalHandler = new SignalHandler(handler, thisArg);

        if (handlers === undefined) {
            this.__eventHandlersStateExit[state] = [signalHandler];
        } else {
            handlers.push(signalHandler);
        }
    }

    /**
     *
     * @param {number} state
     * @param {function} handler
     * @param {*} [thisArg]
     */
    removeEventHandlerStateExit(state, handler, thisArg) {
        //assert.ok(this.description.stateExists(state), `state ${state} doesn't exist`);

        const handlers = this.__eventHandlersStateExit[state];

        if (handlers !== undefined) {

            const i = findSignalHandlerIndexByHandle(handlers, handler, thisArg);

            if (i !== -1) {
                handlers.splice(i, 1);

                return true;
            }
        }

        return false;
    }

    /**
     *
     * @param {number} s
     */
    setState(s) {
        //assert.ok(this.description.stateExists(s), `state ${s} doesn't exist`);

        const oldState = this.__state;


        const exitHandlers = this.__eventHandlersStateExit[oldState];

        if (exitHandlers !== undefined) {
            dispatchViaProxy(exitHandlers, [oldState, s]);
        }

        this.__state = s;

        //process event handlers
        const handlers = this.__eventHandlersStateEntry[s];

        if (handlers !== undefined) {
            dispatchViaProxy(handlers, [s, oldState]);
        }

    }

    /**
     *
     * @returns {number}
     */
    getState() {
        return this.__state;
    }

    /**
     * Performs navigation to target state via the shortest path
     * @throws {Error} if no path could be found to target state
     * @param {number} target
     */
    navigateTo(target) {
        assert.isNonNegativeInteger(target, 'target');

        const currentState = this.getState();

        if (currentState === target) {
            // already in the target state
            return;
        }

        const description = this.description;

        assert.ok(description.stateExists(target), `target state ${target} doesn't exist`);

        const path = description.findPath(currentState, target);

        if (path === null) {
            throw new Error(`No path exists from current state '${currentState}' to target state '${target}'`);
        }

        const n = path.length;
        for (let i = 1; i < n; i++) {
            const next = path[i];

            this.setState(next);
        }
    }

    /**
     * @template X
     * @param {X} [input] value will be fed into selector
     * @param {function} [preStateChangeHook]
     * @return {boolean}
     */
    advance(input, preStateChangeHook = noop) {
        const description = this.description;

        //get active selector
        const selector = description.__actions[this.__state];

        let targetState;

        if (selector === undefined) {

            //no selector
            const targetNodes = description.getOutgoingStates(this.__state);

            if (targetNodes.length !== 1) {
                //no selector and number of targets is ambiguous
                return false;
            }

            //
            targetState = targetNodes[0];

        } else {
            targetState = selector(input);
            //assert.ok(description.stateExists(targetState), `targetState ${targetState} does not exist`);
        }

        //assert.ok(description.edgeExists(this.__state, targetState), `no edge exists from ${this.__state} to ${targetState}`);

        preStateChangeHook(targetState, input);

        this.setState(targetState);

        return true;
    }
}
