/**
 * Emulation of the Bombe machine.
 *
 * @author s2224834
 * @author The National Museum of Computing - Bombe Rebuild Project
 * @copyright Crown Copyright 2019
 * @license Apache-2.0
 */

import OperationError from "../errors/OperationError";
import Utils from "../Utils";
import {Rotor, Plugboard, a2i, i2a} from "./Enigma";

/**
 * Convenience/optimisation subclass of Rotor
 *
 * This allows creating multiple Rotors which share backing maps, to avoid repeatedly parsing the
 * rotor spec strings and duplicating the maps in memory.
 */
class CopyRotor extends Rotor {
    /**
     * Return a copy of this Rotor.
     * @returns {Object}
     */
    copy() {
        const clone = {
            map: this.map,
            revMap: this.revMap,
            pos: this.pos,
            step: this.step,
            transform: this.transform,
            revTransform: this.revTransform,
        };
        return clone;
    }
}

/**
 * Node in the menu graph
 *
 * A node represents a cipher/plaintext letter.
 */
class Node {
    /**
     * Node constructor.
     * @param {number} letter - The plain/ciphertext letter this node represents (as a number).
     */
    constructor(letter) {
        this.letter = letter;
        this.edges = new Set();
        this.visited = false;
    }
}

/**
 * Edge in the menu graph
 *
 * An edge represents an Enigma machine transformation between two letters.
 */
class Edge {
    /**
     * Edge constructor - an Enigma machine mapping between letters
     * @param {number} pos - The rotor position, relative to the beginning of the crib, at this edge
     * @param {number} node1 - Letter at one end (as a number)
     * @param {number} node2 - Letter at the other end
     */
    constructor(pos, node1, node2) {
        this.pos = pos;
        this.node1 = node1;
        this.node2 = node2;
        node1.edges.add(this);
        node2.edges.add(this);
        this.visited = false;
    }

    /**
     * Given the node at one end of this edge, return the other end.
     * @param node {number} - The node we have
     * @returns {number}
     */
    getOther(node) {
        return this.node1 === node ? this.node2 : this.node1;
    }
}

/**
 * As all the Bombe's rotors move in step, at any given point the vast majority of the scramblers
 * in the machine share the majority of their state, which is hosted in this class.
 */
class SharedScrambler {
    /**
     * SharedScrambler constructor.
     * @param {Object[]} rotors - List of rotors in the shared state _only_.
     * @param {Object} reflector - The reflector in use.
     */
    constructor(rotors, reflector) {
        this.lowerCache = new Array(26);
        this.higherCache = new Array(26);
        for (let i=0; i<26; i++) {
            this.higherCache[i] = new Array(26);
        }
        this.changeRotors(rotors, reflector);
    }

    /**
     * Replace the rotors and reflector in this SharedScrambler.
     * This takes care of flushing caches as well.
     * @param {Object[]} rotors - List of rotors in the shared state _only_.
     * @param {Object} reflector - The reflector in use.
     */
    changeRotors(rotors, reflector) {
        this.reflector = reflector;
        this.rotors = rotors;
        this.rotorsRev = [].concat(rotors).reverse();
        this.cacheGen();
    }

    /**
     * Step the rotors forward.
     * @param {number} n - How many rotors to step. This includes the rotors which are not part of
     * the shared state, so should be 2 or more.
     */
    step(n) {
        for (let i=0; i<n-1; i++) {
            this.rotors[i].step();
        }
        this.cacheGen();
    }

    /**
     * Optimisation: We pregenerate all routes through the machine with the top rotor removed,
     * as these rarely change. This saves a lot of lookups. This function generates this route
     * table.
     * We also just-in-time cache the full routes through the scramblers, because after stepping
     * the fast rotor some scramblers will be in states occupied by other scrambles on previous
     * iterations.
     */
    cacheGen() {
        for (let i=0; i<26; i++) {
            this.lowerCache[i] = undefined;
            for (let j=0; j<26; j++) {
                this.higherCache[i][j] = undefined;
            }
        }
        for (let i=0; i<26; i++) {
            if (this.lowerCache[i] !== undefined) {
                continue;
            }
            let letter = i;
            for (const rotor of this.rotors) {
                letter = rotor.transform(letter);
            }
            letter = this.reflector.transform(letter);
            for (const rotor of this.rotorsRev) {
                letter = rotor.revTransform(letter);
            }
            // By symmetry
            this.lowerCache[i] = letter;
            this.lowerCache[letter] = i;
        }
    }

    /**
     * Map a letter through this (partial) scrambler.
     * @param {number} i - The letter
     * @returns {number}
     */
    transform(i) {
        return this.lowerCache[i];
    }
}

/**
 * Scrambler.
 *
 * This is effectively just an Enigma machine, but it only operates on one character at a time and
 * the stepping mechanism is different.
 */
class Scrambler {
    /** Scrambler constructor.
     * @param {Object} base - The SharedScrambler whose state this scrambler uses
     * @param {Object} rotor - The non-shared fast rotor in this scrambler
     * @param {number} pos - Position offset from start of crib
     * @param {number} end1 - Letter in menu this scrambler is attached to
     * @param {number} end2 - Other letter in menu this scrambler is attached to
     */
    constructor(base, rotor, pos, end1, end2) {
        this.baseScrambler = base;
        this.initialPos = pos;
        this.changeRotor(rotor);
        this.end1 = end1;
        this.end2 = end2;
        // For efficiency reasons, we pull the relevant shared cache from the baseScrambler into
        // this object - this saves us a few pointer dereferences
        this.cache = this.baseScrambler.higherCache[pos];
    }

    /**
     * Replace the rotor in this scrambler.
     * The position is reset automatically.
     * @param {Object} rotor - New rotor
     */
    changeRotor(rotor) {
        this.rotor = rotor;
        this.rotor.pos += this.initialPos;
    }

    /**
     * Step the rotor forward.
     *
     * The base SharedScrambler needs to be instructed to step separately.
     */
    step() {
        // The Bombe steps the slowest rotor on an actual Enigma fastest, for reasons.
        // ...but for optimisation reasons I'm going to cheat and not do that, as this vastly
        // simplifies caching the state of the majority of the scramblers. The results are the
        // same, just in a slightly different order.
        this.rotor.step();
        this.cache = this.baseScrambler.higherCache[this.rotor.pos];
    }


    /**
     * Run a letter through the scrambler.
     * @param {number} i - The letter to transform (as a number)
     * @returns {number}
     */
    transform(i) {
        let letter = i;
        const cached = this.cache[i];
        if (cached !== undefined) {
            return cached;
        }
        letter = this.rotor.transform(letter);
        letter = this.baseScrambler.transform(letter);
        letter = this.rotor.revTransform(letter);
        this.cache[i] = letter;
        this.cache[letter] = i;
        return letter;
    }

    /**
     * Given one letter in the menu this scrambler maps to, return the other.
     * @param end {number} - The node we have
     * @returns {number}
     */
    getOtherEnd(end) {
        return this.end1 === end ? this.end2 : this.end1;
    }

    /**
     * Read the position this scrambler is set to.
     * Note that because of Enigma's stepping, you need to set an actual Enigma to the previous
     * position in order to get it to make a certain set of electrical connections when a button
     * is pressed - this function *does* take this into account.
     * However, as with the rest of the Bombe, it does not take stepping into account - the middle
     * and slow rotors are treated as static.
     * @return {string}
     */
    getPos() {
        let result = "";
        // Roll back the fast rotor by one step
        let pos = Utils.mod(this.rotor.pos - 1, 26);
        result += i2a(pos);
        for (let i=0; i<this.baseScrambler.rotors.length; i++) {
            pos = this.baseScrambler.rotors[i].pos;
            result += i2a(pos);
        }
        return result.split("").reverse().join("");
    }
}

/**
 * Bombe simulator class.
 */
export class BombeMachine {
    /**
     * Construct a Bombe.
     *
     * Note that there is no handling of offsets here: the crib specified must exactly match the
     * ciphertext. It will check that the crib is sane (length is vaguely sensible and there's no
     * matching characters between crib and ciphertext) but cannot check further - if it's wrong
     * your results will be wrong!
     *
     * There is also no handling of rotor stepping - if the target Enigma stepped in the middle of
     * your crib, you're out of luck. TODO: Allow specifying a step point - this is fairly easy to
     * configure on a real Bombe, but we're not clear on whether it was ever actually done for
     * real (there would almost certainly have been better ways of attacking in most situations
     * than attempting to exhaust options for the stepping point, but in some circumstances, e.g.
     * via Banburismus, the stepping point might have been known).
     *
     * @param {string[]} rotors - list of rotor spec strings (without step points!)
     * @param {Object} reflector - Reflector object
     * @param {string} ciphertext - The ciphertext to attack
     * @param {string} crib - Known plaintext for this ciphertext
     * @param {boolean} check - Whether to use the checking machine
     * @param {function} update - Function to call to send status updates (optional)
     */
    constructor(rotors, reflector, ciphertext, crib, check, update=undefined) {
        if (ciphertext.length < crib.length) {
            throw new OperationError("Crib overruns supplied ciphertext");
        }
        if (crib.length < 2) {
            // This is the absolute bare minimum to be sane, and even then it's likely too short to
            // be useful
            throw new OperationError("Crib is too short");
        }
        if (crib.length > 25) {
            // A crib longer than this will definitely cause the middle rotor to step somewhere
            // A shorter crib is preferable to reduce this chance, of course
            throw new OperationError("Crib is too long");
        }
        for (let i=0; i<crib.length; i++) {
            if (ciphertext[i] === crib[i]) {
                throw new OperationError(`Invalid crib: character ${ciphertext[i]} at pos ${i} in both ciphertext and crib`);
            }
        }
        this.ciphertext = ciphertext;
        this.crib = crib;
        this.initRotors(rotors);
        this.check = check;
        this.updateFn = update;

        const [mostConnected, edges] = this.makeMenu();

        // This is the bundle of wires corresponding to the 26 letters within each of the 26
        // possible nodes in the menu
        this.wires = new Array(26*26);

        // These are the pseudo-Engima devices corresponding to each edge in the menu, and the
        // nodes in the menu they each connect to
        this.scramblers = new Array();
        for (let i=0; i<26; i++) {
            this.scramblers.push(new Array());
        }
        this.sharedScrambler = new SharedScrambler(this.baseRotors.slice(1), reflector);
        this.allScramblers = new Array();
        this.indicator = undefined;
        for (const edge of edges) {
            const cRotor = this.baseRotors[0].copy();
            const end1 = a2i(edge.node1.letter);
            const end2 = a2i(edge.node2.letter);
            const scrambler = new Scrambler(this.sharedScrambler, cRotor, edge.pos, end1, end2);
            if (edge.pos === 0) {
                this.indicator = scrambler;
            }
            this.scramblers[end1].push(scrambler);
            this.scramblers[end2].push(scrambler);
            this.allScramblers.push(scrambler);
        }
        // The Bombe uses a set of rotors to keep track of what settings it's testing. We cheat and
        // use one of the actual scramblers if there's one in the right position, but if not we'll
        // just create one.
        if (this.indicator === undefined) {
            this.indicator = new Scrambler(this.sharedScrambler, this.baseRotors[0].copy(), 0, undefined, undefined);
            this.allScramblers.push(this.indicator);
        }

        this.testRegister = a2i(mostConnected.letter);
        // This is an arbitrary letter other than the most connected letter
        for (const edge of mostConnected.edges) {
            this.testInput = [this.testRegister, a2i(edge.getOther(mostConnected).letter)];
            break;
        }
    }

    /**
     * Build Rotor objects from list of rotor wiring strings.
     * @param {string[]} rotors - List of rotor wiring strings
     */
    initRotors(rotors) {
        // This is ordered from the Enigma fast rotor to the slow, so bottom to top for the Bombe
        this.baseRotors = [];
        for (const rstr of rotors) {
            const rotor = new CopyRotor(rstr, "", "A", "A");
            this.baseRotors.push(rotor);
        }
    }

    /**
     * Replace the rotors and reflector in all components of this Bombe.
     * @param {string[]} rotors - List of rotor wiring strings
     * @param {Object} reflector - Reflector object
     */
    changeRotors(rotors, reflector) {
        // At the end of the run, the rotors are all back in the same position they started
        this.initRotors(rotors);
        this.sharedScrambler.changeRotors(this.baseRotors.slice(1), reflector);
        for (const scrambler of this.allScramblers) {
            scrambler.changeRotor(this.baseRotors[0].copy());
        }
    }

    /**
     * If we have a way of sending status messages, do so.
     * @param {...*} msg - Message to send.
     */
    update(...msg) {
        if (this.updateFn !== undefined) {
            this.updateFn(...msg);
        }
    }

    /**
     * Recursive depth-first search on the menu graph.
     * This is used to a) isolate unconnected sub-graphs, and b) count the number of loops in each
     * of those graphs.
     * @param {Object} node - Node object to start the search from
     * @returns {[number, number, Object, number, Object[]} - loop count, node count, most connected
     *      node, order of most connected node, list of edges in this sub-graph
     */
    dfs(node) {
        let loops = 0;
        let nNodes = 1;
        let mostConnected = node;
        let nConnections = mostConnected.edges.size;
        let edges = new Set();
        node.visited = true;
        for (const edge of node.edges) {
            if (edge.visited) {
                // Already been here from the other end.
                continue;
            }
            edge.visited = true;
            edges.add(edge);
            const other = edge.getOther(node);
            if (other.visited) {
                // We have a loop, record that and continue
                loops += 1;
                continue;
            }
            // This is a newly visited node
            const [oLoops, oNNodes, oMostConnected, oNConnections, oEdges] = this.dfs(other);
            loops += oLoops;
            nNodes += oNNodes;
            edges = new Set([...edges, ...oEdges]);
            if (oNConnections > nConnections) {
                mostConnected = oMostConnected;
                nConnections = oNConnections;
            }
        }
        return [loops, nNodes, mostConnected, nConnections, edges];
    }

    /**
     * Build a menu from the ciphertext and crib.
     * A menu is just a graph where letters in either the ciphertext or crib (Enigma is symmetric,
     * so there's no difference mathematically) are nodes and states of the Enigma machine itself
     * are the edges.
     * Additionally, we want a single connected graph, and of the subgraphs available, we want the
     * one with the most loops (since these generate feedback cycles which efficiently close off
     * disallowed states).
     * Finally, we want to identify the most connected node in that graph (as it's the best choice
     * of measurement point).
     * @returns [Object, Object[]] - the most connected node, and the list of edges in the subgraph
     */
    makeMenu() {
        // First, we make a graph of all of the mappings given by the crib
        // Make all nodes first
        const nodes = new Map();
        for (const c of this.ciphertext + this.crib) {
            if (!nodes.has(c)) {
                const node = new Node(c);
                nodes.set(c, node);
            }
        }
        // Then all edges
        for (let i=0; i<this.crib.length; i++) {
            const a = this.crib[i];
            const b = this.ciphertext[i];
            new Edge(i, nodes.get(a), nodes.get(b));
        }
        // list of [loop_count, node_count, most_connected_node, connections_on_most_connected, edges]
        const graphs = [];
        // Then, for each unconnected subgraph, we count the number of loops and nodes
        for (const start of nodes.keys()) {
            if (nodes.get(start).visited) {
                continue;
            }
            const subgraph = this.dfs(nodes.get(start));
            graphs.push(subgraph);
        }
        // Return the subgraph with the most loops (ties broken by node count)
        graphs.sort((a, b) => {
            let result = b[0] - a[0];
            if (result === 0) {
                result = b[1] - a[1];
            }
            return result;
        });
        this.nLoops = graphs[0][0];
        return [graphs[0][2], graphs[0][4]];
    }

    /**
     * Bombe electrical simulation. Energise a wire. For all connected wires (both via the diagonal
     * board and via the scramblers), energise them too, recursively.
     * @param {number} i - Bombe wire bundle
     * @param {number} j - Bombe stecker hypothesis wire within bundle
     */
    energise(i, j) {
        const idx = 26*i + j;
        if (this.wires[idx]) {
            return;
        }
        this.wires[idx] = true;
        // Welchman's diagonal board: if A steckers to B, that implies B steckers to A. Handle
        // both.
        const idxPair = 26*j + i;
        this.wires[idxPair] = true;
        if (i === this.testRegister || j === this.testRegister) {
            this.energiseCount++;
            if (this.energiseCount === 26) {
                // no point continuing, bail out
                return;
            }
        }

        for (let k=0; k<this.scramblers[i].length; k++) {
            const scrambler = this.scramblers[i][k];
            const out = scrambler.transform(j);
            const other = scrambler.getOtherEnd(i);
            // Lift the pre-check before the call, to save some function call overhead
            const otherIdx = 26*other + out;
            if (!this.wires[otherIdx]) {
                this.energise(other, out);
                if (this.energiseCount === 26) {
                    return;
                }
            }
        }
        if (i === j) {
            return;
        }
        for (let k=0; k<this.scramblers[j].length; k++) {
            const scrambler = this.scramblers[j][k];
            const out = scrambler.transform(i);
            const other = scrambler.getOtherEnd(j);
            const otherIdx = 26*other + out;
            if (!this.wires[otherIdx]) {
                this.energise(other, out);
                if (this.energiseCount === 26) {
                    return;
                }
            }
        }
    }

    /**
     * Trial decryption at the current setting.
     * Used after we get a stop.
     * This applies the detected stecker pair if we have one. It does not handle the other
     * steckering or stepping (which is why we limit it to 26 characters, since it's guaranteed to
     * be wrong after that anyway).
     * @param {string} stecker - Known stecker spec string.
     * @returns {string}
     */
    tryDecrypt(stecker) {
        const fastRotor = this.indicator.rotor;
        const initialPos = fastRotor.pos;
        const res = [];
        const plugboard = new Plugboard(stecker);
        // The indicator scrambler starts in the right place for the beginning of the ciphertext.
        for (let i=0; i<Math.min(26, this.ciphertext.length); i++) {
            const t = this.indicator.transform(plugboard.transform(a2i(this.ciphertext[i])));
            res.push(i2a(plugboard.transform(t)));
            this.indicator.step(1);
        }
        fastRotor.pos = initialPos;
        return res.join("");
    }

    /**
     * Format a steckered pair, in sorted order to allow uniquing.
     * @param {number} a - A letter
     * @param {number} b - Its stecker pair
     * @returns {string}
     */
    formatPair(a, b) {
        if (a < b) {
            return `${i2a(a)}${i2a(b)}`;
        }
        return `${i2a(b)}${i2a(a)}`;
    }

    /**
     * The checking machine was used to manually verify Bombe stops. Using a device which was
     * effectively a non-stepping Enigma, the user would walk through each of the links in the
     * menu at the rotor positions determined by the Bombe. By starting with the stecker pair the
     * Bombe gives us, we find the stecker pair of each connected letter in the graph, and so on.
     * If a contradiction is reached, the stop is invalid. If not, we have most (but not
     * necessarily all) of the plugboard connections.
     * You will notice that this procedure is exactly the same as what the Bombe itself does, only
     * we start with an assumed good hypothesis and read out the stecker pair for every letter.
     * On the real hardware that wasn't practical, but fortunately we're not the real hardware, so
     * we don't need to implement the manual checking machine procedure.
     * @param {number} pair - The stecker pair of the test register.
     * @returns {string} - The empty string for invalid stops, or a plugboard configuration string
     *      containing all known pairs.
     */
    checkingMachine(pair) {
        if (pair !== this.testInput[1]) {
            // We have a new hypothesis for this stop - apply the new one.
            // De-energise the board
            for (let i=0; i<this.wires.length; i++) {
                this.wires[i] = false;
            }
            this.energiseCount = 0;
            // Re-energise with the corrected hypothesis
            this.energise(this.testRegister, pair);
        }

        const results = new Set();
        results.add(this.formatPair(this.testRegister, pair));
        for (let i=0; i<26; i++) {
            let count = 0;
            let other;
            for (let j=0; j<26; j++) {
                if (this.wires[i*26 + j]) {
                    count++;
                    other = j;
                }
            }
            if (count > 1) {
                // This is an invalid stop.
                return "";
            } else if (count === 0) {
                // No information about steckering from this wire
                continue;
            }
            results.add(this.formatPair(i, other));
        }
        return [...results].join(" ");
    }

    /**
     * Check to see if the Bombe has stopped. If so, process the stop.
     * @returns {(undefined|string[3])} - Undefined for no stop, or [rotor settings, plugboard settings, decryption preview]
     */
    checkStop() {
        // Count the energised outputs
        const count = this.energiseCount;
        if (count === 26) {
            return undefined;
        }
        // If it's not all of them, we have a stop
        let steckerPair;
        // The Bombe tells us one stecker pair as well. The input wire and test register we
        // started with are hypothesised to be a stecker pair.
        if (count === 25) {
            // Our steckering hypothesis is wrong. Correct value is the un-energised wire.
            for (let j=0; j<26; j++) {
                if (!this.wires[26*this.testRegister + j]) {
                    steckerPair = j;
                    break;
                }
            }
        } else if (count === 1) {
            // This means our hypothesis for the steckering is correct.
            steckerPair = this.testInput[1];
        } else {
            // This was known as a "boxing stop" - we have a stop but not a single hypothesis.
            // If this happens a lot it implies the menu isn't good enough.
            // If we have the checking machine enabled, we're going to just check each wire in
            // turn. If we get 0 or 1 hit, great.
            // If we get multiple hits, or the checking machine is off, the user will just have to
            // deal with it.
            if (!this.check) {
                // We can't draw any conclusions about the steckering (one could maybe suggest
                // options in some cases, but too hard to present clearly).
                return [this.indicator.getPos(), "??", this.tryDecrypt("")];
            }
            let stecker = undefined;
            for (let i = 0; i < 26; i++) {
                const newStecker = this.checkingMachine(i);
                if (newStecker !== "") {
                    if (stecker !== undefined) {
                        // Multiple hypotheses can't be ruled out.
                        return [this.indicator.getPos(), "??", this.tryDecrypt("")];
                    }
                    stecker = newStecker;
                }
            }
            if (stecker === undefined) {
                // Checking machine ruled all possibilities out.
                return undefined;
            }
            // If we got here, there was just one possibility allowed by the checking machine. Success.
            return [this.indicator.getPos(), stecker, this.tryDecrypt(stecker)];
        }
        let stecker;
        if (this.check) {
            stecker = this.checkingMachine(steckerPair);
            if (stecker === "") {
                // Invalid stop - don't count it, don't return it
                return undefined;
            }
        } else {
            stecker = `${i2a(this.testRegister)}${i2a(steckerPair)}`;
        }
        const testDecrypt = this.tryDecrypt(stecker);
        return [this.indicator.getPos(), stecker, testDecrypt];
    }

    /**
     * Having set up the Bombe, do the actual attack run. This tries every possible rotor setting
     * and attempts to logically invalidate them. If it can't, it's added to the list of candidate
     * solutions.
     * @returns {string[][3]} - list of 3-tuples of candidate rotor setting, plugboard settings, and decryption preview
     */
    run() {
        let stops = 0;
        const result = [];
        // For each possible rotor setting
        const nChecks = Math.pow(26, this.baseRotors.length);
        for (let i=1; i<=nChecks; i++) {
            // Benchmarking suggests this is faster than using .fill()
            for (let i=0; i<this.wires.length; i++) {
                this.wires[i] = false;
            }
            this.energiseCount = 0;
            // Energise the test input, follow the current through each scrambler
            // (and the diagonal board)
            this.energise(...this.testInput);

            const stop = this.checkStop();
            if (stop !== undefined) {
                stops++;
                result.push(stop);
            }
            // Step all the scramblers
            // This loop counts how many rotors have reached their starting position (meaning the
            // next one needs to step as well)
            let n = 1;
            for (let j=1; j<this.baseRotors.length; j++) {
                if ((i % Math.pow(26, j)) === 0) {
                    n++;
                } else {
                    break;
                }
            }
            if (n > 1) {
                this.sharedScrambler.step(n);
            }
            for (const scrambler of this.allScramblers) {
                scrambler.step();
            }
            // Send status messages at what seems to be a reasonably sensible frequency
            // (note this won't be triggered on 3-rotor runs - they run fast enough it doesn't seem necessary)
            if (n > 3) {
                this.update(this.nLoops, stops, i/nChecks);
            }
        }
        return result;
    }
}
