/** @module Logic */
/**
 * definitions of logic states and basic logic functions used in the simulation
 */
export default class Logic {
    /**
     * Enum for logic states.
     *
     * States:
     * - `off`
     * - `on`
     * - `unknown`
     * - `oscillating`
     * @type {Number}
     */
    static get state() {
        return {
            off: 0,
            on: 1,
            unknown: 2,
            oscillating: 3
        };
    }

    /**
     * list of all states that can be used in the simulation
     *
     * This getter iterates over Logic.state and returns an array containing all values of Logic.state's members
     * @type {Array}
     */
    static get stateList() {
        let states = [];

        // iterate over all defined states and add their values to the states array
        Object.keys(Logic.state).forEach(key => {
            states.push(Logic.state[key]);
        });

        return states;
    }

    /**
     * Logic AND
     * @param  {Logic.state} a first input state
     * @param  {Logic.state} b second input state
     * @return {Logic.state}   output state
     */
    static and(a, b) {
        return Logic.runSymmetricRules(a, b, [
            [Logic.state.on, Logic.state.on, Logic.state.on],
            [Logic.state.on, Logic.state.off, Logic.state.off],
            [Logic.state.on, Logic.state.unknown, Logic.state.unknown],
            [Logic.state.on, Logic.state.oscillating, Logic.state.oscillating],

            [Logic.state.off, Logic.state.off, Logic.state.off],
            [Logic.state.off, Logic.state.unknown, Logic.state.off],
            [Logic.state.off, Logic.state.oscillating, Logic.state.off],

            [Logic.state.unknown, Logic.state.unknown, Logic.state.unknown],
            [Logic.state.unknown, Logic.state.oscillating, Logic.state.unknown],

            [Logic.state.oscillating, Logic.state.oscillating, Logic.state.oscillating]
        ]);
    }
    /**
     * Logic NAND
     * @param  {Logic.state} a first input state
     * @param  {Logic.state} b second input state
     * @return {Logic.state}   output state
     */
    static nand(a, b) {
        return Logic.not(Logic.and(a, b));
    }

    /**
     * Logic NOR
     * @param  {Logic.state} a first input state
     * @param  {Logic.state} b second input state
     * @return {Logic.state}   output state
     */
    static nor(a, b) {
        return Logic.not(Logic.or(a, b));
    }

    /**
     * Logic NOT
     * @param  {Logic.state} a first input state
     * @return {Logic.state}   output state
     */
    static not(a) {
        if (a === Logic.state.on) {
            return Logic.state.off;
        } else if (a === Logic.state.off) {
            return Logic.state.on;
        } else {
            return a;
        }
    }

    /**
     * Logic OR
     * @param  {Logic.state} a first input state
     * @param  {Logic.state} b second input state
     * @return {Logic.state}   output state
     */
    static or(a, b) {
        return Logic.runSymmetricRules(a, b, [
            [Logic.state.on, Logic.state.on, Logic.state.on],
            [Logic.state.on, Logic.state.off, Logic.state.on],
            [Logic.state.on, Logic.state.unknown, Logic.state.on],
            [Logic.state.on, Logic.state.oscillating, Logic.state.on],

            [Logic.state.off, Logic.state.off, Logic.state.off],
            [Logic.state.off, Logic.state.unknown, Logic.state.unknown],
            [Logic.state.off, Logic.state.oscillating, Logic.state.oscillating],

            [Logic.state.unknown, Logic.state.unknown, Logic.state.unknown],
            [Logic.state.unknown, Logic.state.oscillating, Logic.state.unknown],

            [Logic.state.oscillating, Logic.state.oscillating, Logic.state.oscillating]
        ]);
    }

    /**
     * Logic XNOR
     * @param  {Logic.state} a first input state
     * @param  {Logic.state} b second input state
     * @return {Logic.state}   output state
     */
    static xnor(a, b) {
        return Logic.not(Logic.xor(a, b));
    }

    /**
     * Logic XOR
     * @param  {Logic.state} a first input state
     * @param  {Logic.state} b second input state
     * @return {Logic.state}   output state
     */
    static xor(a, b) {
        return Logic.runSymmetricRules(a, b, [
            [Logic.state.on, Logic.state.on, Logic.state.off],
            [Logic.state.on, Logic.state.off, Logic.state.on],
            [Logic.state.on, Logic.state.unknown, Logic.state.unknown],
            [Logic.state.on, Logic.state.oscillating, Logic.state.oscillating],

            [Logic.state.off, Logic.state.off, Logic.state.off],
            [Logic.state.off, Logic.state.unknown, Logic.state.unknown],
            [Logic.state.off, Logic.state.oscillating, Logic.state.oscillating],

            [Logic.state.unknown, Logic.state.unknown, Logic.state.unknown],
            [Logic.state.unknown, Logic.state.oscillating, Logic.state.unknown],

            [Logic.state.oscillating, Logic.state.oscillating, Logic.state.oscillating]
        ]);
    }

    /**
     * Finds the correct rule in the array of rules and returns the corresponding return value.
     * This function expects rules to be symmetric (so `a RULE b` should returns the same value as `b RULE a`),
     * which allows to cut down on the `rules` array quite a bit
     * @param  {Logic.state} a     first input state
     * @param  {Logic.state} b     second input state
     * @param  {Array} rules       Array of arrays. Each inner array represents a rule in the format [input1, input2, output].
     *                             The function finds an array, where `a === input1` and `b === input1` (or `a === input2` and `b === input1`)
     *                             and returns `output` from this array.
     * @return {Logic.state}       output state
     */
    static runSymmetricRules(a, b, rules) {
        // iterate through all the rules
        for (const rule of rules) {
            if ((rule[0] === a && rule[1] === b) || (rule[0] === b && rule[1] === a)) {
                return rule[2];
            }
        }

        // if no rule matches, the output state is unknown
        return Logic.state.unknown;
    }
}