/**
 * Emulation of the Bombe machine.
 * This version carries out multiple Bombe runs to handle unknown rotor configurations.
 *
 * @author s2224834
 * @copyright Crown Copyright 2019
 * @license Apache-2.0
 */

import Operation from "../Operation.mjs";
import OperationError from "../errors/OperationError.mjs";
import { BombeMachine } from "../lib/Bombe.mjs";
import { ROTORS, ROTORS_FOURTH, REFLECTORS, Reflector } from "../lib/Enigma.mjs";
import { isWorkerEnvironment } from "../Utils.mjs";


/**
 * Convenience method for flattening the preset ROTORS object into a newline-separated string.
 * @param {Object[]} - Preset rotors object
 * @param {number} s - Start index
 * @param {number} n - End index
 * @returns {string}
 */
function rotorsFormat(rotors, s, n) {
    const res = [];
    for (const i of rotors.slice(s, n)) {
        res.push(i.value);
    }
    return res.join("\n");
}

/**
 * Combinatorics choose function
 * @param {number} n
 * @param {number} k
 * @returns number
 */
function choose(n, k) {
    let res = 1;
    for (let i=1; i<=k; i++) {
        res *= (n + 1 - i) / i;
    }
    return res;
}

/**
 * Bombe operation
 */
class MultipleBombe extends Operation {
    /**
     * Bombe constructor
     */
    constructor() {
        super();

        this.name = "Multiple Bombe";
        this.module = "Bletchley";
        this.description = "Emulation of the Bombe machine used to attack Enigma. This version carries out multiple Bombe runs to handle unknown rotor configurations.<br><br>You should test your menu on the single Bombe operation before running it here. See the description of the Bombe operation for instructions on choosing a crib.<br><br>More detailed descriptions of the Enigma, Typex and Bombe operations <a href='https://github.com/gchq/CyberChef/wiki/Enigma,-the-Bombe,-and-Typex'>can be found here</a>.";
        this.infoURL = "https://wikipedia.org/wiki/Bombe";
        this.inputType = "string";
        this.outputType = "JSON";
        this.presentType = "html";
        this.args = [
            {
                "name": "Standard Enigmas",
                "type": "populateMultiOption",
                "value": [
                    {
                        name: "German Service Enigma (First - 3 rotor)",
                        value: [
                            rotorsFormat(ROTORS, 0, 5),
                            "",
                            rotorsFormat(REFLECTORS, 0, 1)
                        ]
                    },
                    {
                        name: "German Service Enigma (Second - 3 rotor)",
                        value: [
                            rotorsFormat(ROTORS, 0, 8),
                            "",
                            rotorsFormat(REFLECTORS, 0, 2)
                        ]
                    },
                    {
                        name: "German Service Enigma (Third - 4 rotor)",
                        value: [
                            rotorsFormat(ROTORS, 0, 8),
                            rotorsFormat(ROTORS_FOURTH, 1, 2),
                            rotorsFormat(REFLECTORS, 2, 3)
                        ]
                    },
                    {
                        name: "German Service Enigma (Fourth - 4 rotor)",
                        value: [
                            rotorsFormat(ROTORS, 0, 8),
                            rotorsFormat(ROTORS_FOURTH, 1, 3),
                            rotorsFormat(REFLECTORS, 2, 4)
                        ]
                    },
                    {
                        name: "User defined",
                        value: ["", "", ""]
                    },
                ],
                "target": [1, 2, 3]
            },
            {
                name: "Main rotors",
                type: "text",
                value: ""
            },
            {
                name: "4th rotor",
                type: "text",
                value: ""
            },
            {
                name: "Reflectors",
                type: "text",
                value: ""
            },
            {
                name: "Crib",
                type: "string",
                value: ""
            },
            {
                name: "Crib offset",
                type: "number",
                value: 0
            },
            {
                name: "Use checking machine",
                type: "boolean",
                value: true
            }
        ];
    }

    /**
     * Format and send a status update message.
     * @param {number} nLoops - Number of loops in the menu
     * @param {number} nStops - How many stops so far
     * @param {number} progress - Progress (as a float in the range 0..1)
     */
    updateStatus(nLoops, nStops, progress, start) {
        const elapsed = Date.now() - start;
        const remaining = (elapsed / progress) * (1 - progress) / 1000;
        const hours = Math.floor(remaining / 3600);
        const minutes = `0${Math.floor((remaining % 3600) / 60)}`.slice(-2);
        const seconds = `0${Math.floor(remaining % 60)}`.slice(-2);
        const msg = `Bombe run with ${nLoops} loop${nLoops === 1 ? "" : "s"} in menu (2+ desirable): ${nStops} stops, ${Math.floor(100 * progress)}% done, ${hours}:${minutes}:${seconds} remaining`;
        self.sendStatusMessage(msg);
    }

    /**
     * Early rotor description string validation.
     * Drops stepping information.
     * @param {string} rstr - The rotor description string
     * @returns {string} - Rotor description with stepping stripped, if any
     */
    validateRotor(rstr) {
        // The Bombe doesn't take stepping into account so we'll just ignore it here
        if (rstr.includes("<")) {
            rstr = rstr.split("<", 2)[0];
        }
        // Duplicate the validation of the rotor strings here, otherwise you might get an error
        // thrown halfway into a big Bombe run
        if (!/^[A-Z]{26}$/.test(rstr)) {
            throw new OperationError("Rotor wiring must be 26 unique uppercase letters");
        }
        if (new Set(rstr).size !== 26) {
            throw new OperationError("Rotor wiring must be 26 unique uppercase letters");
        }
        return rstr;
    }

    /**
     * @param {string} input
     * @param {Object[]} args
     * @returns {string}
     */
    run(input, args) {
        const mainRotorsStr = args[1];
        const fourthRotorsStr = args[2];
        const reflectorsStr = args[3];
        let crib = args[4];
        const offset = args[5];
        const check = args[6];
        const rotors = [];
        const fourthRotors = [];
        const reflectors = [];
        for (let rstr of mainRotorsStr.split("\n")) {
            rstr = this.validateRotor(rstr);
            rotors.push(rstr);
        }
        if (rotors.length < 3) {
            throw new OperationError("A minimum of three rotors must be supplied");
        }
        if (fourthRotorsStr !== "") {
            for (let rstr of fourthRotorsStr.split("\n")) {
                rstr = this.validateRotor(rstr);
                fourthRotors.push(rstr);
            }
        }
        if (fourthRotors.length === 0) {
            fourthRotors.push("");
        }
        for (const rstr of reflectorsStr.split("\n")) {
            const reflector = new Reflector(rstr);
            reflectors.push(reflector);
        }
        if (reflectors.length === 0) {
            throw new OperationError("A minimum of one reflector must be supplied");
        }
        if (crib.length === 0) {
            throw new OperationError("Crib cannot be empty");
        }
        if (offset < 0) {
            throw new OperationError("Offset cannot be negative");
        }
        // For symmetry with the Enigma op, for the input we'll just remove all invalid characters
        input = input.replace(/[^A-Za-z]/g, "").toUpperCase();
        crib = crib.replace(/[^A-Za-z]/g, "").toUpperCase();
        const ciphertext = input.slice(offset);
        let update;
        if (isWorkerEnvironment()) {
            update = this.updateStatus;
        } else {
            update = undefined;
        }
        let bombe = undefined;
        const output = {bombeRuns: []};
        // I could use a proper combinatorics algorithm here... but it would be more code to
        // write one, and we don't seem to have one in our existing libraries, so massively nested
        // for loop it is
        const totalRuns = choose(rotors.length, 3) * 6 * fourthRotors.length * reflectors.length;
        let nRuns = 0;
        let nStops = 0;
        const start = Date.now();
        for (const rotor1 of rotors) {
            for (const rotor2 of rotors) {
                if (rotor2 === rotor1) {
                    continue;
                }
                for (const rotor3 of rotors) {
                    if (rotor3 === rotor2 || rotor3 === rotor1) {
                        continue;
                    }
                    for (const rotor4 of fourthRotors) {
                        for (const reflector of reflectors) {
                            nRuns++;
                            const runRotors = [rotor1, rotor2, rotor3];
                            if (rotor4 !== "") {
                                runRotors.push(rotor4);
                            }
                            if (bombe === undefined) {
                                bombe = new BombeMachine(runRotors, reflector, ciphertext, crib, check);
                                output.nLoops = bombe.nLoops;
                            } else {
                                bombe.changeRotors(runRotors, reflector);
                            }
                            const result = bombe.run();
                            nStops += result.length;
                            if (update !== undefined) {
                                update(bombe.nLoops, nStops, nRuns / totalRuns, start);
                            }
                            if (result.length > 0) {
                                output.bombeRuns.push({
                                    rotors: runRotors,
                                    reflector: reflector.pairs,
                                    result: result
                                });
                            }
                        }
                    }
                }
            }
        }
        return output;
    }


    /**
     * Displays the MultiBombe results in an HTML table
     *
     * @param {Object} output
     * @param {number} output.nLoops
     * @param {Array[]} output.result
     * @returns {html}
     */
    present(output) {
        let html = `Bombe run on menu with ${output.nLoops} loop${output.nLoops === 1 ? "" : "s"} (2+ desirable). Note: Rotors and rotor positions are listed left to right, ignore stepping and the ring setting, and positions start at the beginning of the crib. Some plugboard settings are determined. A decryption preview starting at the beginning of the crib and ignoring stepping is also provided.\n`;

        for (const run of output.bombeRuns) {
            html += `\nRotors: ${run.rotors.slice().reverse().join(", ")}\nReflector: ${run.reflector}\n`;
            html += "<table class='table table-hover table-sm table-bordered table-nonfluid'><tr><th>Rotor stops</th>  <th>Partial plugboard</th>  <th>Decryption preview</th></tr>\n";
            for (const [setting, stecker, decrypt] of run.result) {
                html += `<tr><td>${setting}</td>  <td>${stecker}</td>  <td>${decrypt}</td></tr>\n`;
            }
            html += "</table>\n";
        }
        return html;
    }
}

export default MultipleBombe;
