import { socket, game } from "./main.js";
import { Packet } from "./packet.js";

class RandomSession {
    constructor(range) {
        this.range = range;
        this.cipherTexts = {};
        this.cipherKeys = {};
        this.ourKey = CryptoJS.lib.WordArray.random(32).toString();
        // 32-bit as JavaScript does funny stuff at 53-bit levels.
        this.ourNoise = CryptoJS.lib.WordArray.random(4);
        this.finalValue = null;
        this.resolvers = [];
    }

    cipherText() {
        return CryptoJS.AES.encrypt(this.ourNoise, this.ourKey).toString();
    }
}

export class Random {
    constructor() {
        this.sessions = {};
    }

    async get(n, sessionId) {
        if (this.sessions[sessionId] === undefined) {
            this.initialiseSession(n, sessionId);
        }

        let promise;
        await navigator.locks.request(`random-${sessionId}`, () => {
            if (this.sessions[sessionId].finalValue === null) {
                let session = this.sessions[sessionId];
                let resolver;
                promise = new Promise((resolve) => {
                    resolver = resolve;
                });
                session.resolvers.push(resolver);
            } else {
                promise = new Promise((resolve) => {
                    resolve(this.sessions[sessionId].finalValue);
                });
            }
        });
        return promise;
    }

    /**
     * Start a cooperative random session.
     */
    initialiseSession(n, sessionId) {
        let session = new RandomSession(n);
        if (sessionId === undefined) {
            sessionId = window.crypto.randomUUID();
        }

        this.sessions[sessionId] = session;

        socket.emit(
            "message",
            Packet.createRandomCyphertext(sessionId, n, session.cipherText())
        );

        return session;
    }

    /**
     * Process cooperative random protocol.
     *
     * @param data Packet received
     */
    async processCooperativeRandom(data) {
        // Step 0: extract relevant information from data
        let session = this.sessions[data.session];
        const stage = data.stage;

        if (session === undefined) {
            session = this.initialiseSession(data.range, data.session);
        }

        if (stage === "CIPHERTEXT") {
            session.cipherTexts[data.author] = data.cipherText;

            if (
                Object.keys(session.cipherTexts).length ===
                Object.keys(game.players).length - 1
            ) {
                // Step 3: release our key once all players have sent a ciphertext
                socket.emit(
                    "message",
                    Packet.createRandomKey(data.session, session.ourKey)
                );
            }
        } else if (stage === "DECRYPT") {
            session.cipherKeys[data.author] = data.cipherKey;

            // Step 4: get final random value
            if (
                Object.keys(session.cipherKeys).length ===
                Object.keys(game.players).length - 1
            ) {
                // Lock out wait calls as they may resolve to never-ending promises.
                await navigator.locks.request(`random-${data.session}`, () => {
                    let total = parseInt(session.ourNoise, 16);

                    for (let participant of Object.keys(session.cipherKeys)) {
                        let decrypted = CryptoJS.AES.decrypt(
                            session.cipherTexts[participant],
                            session.cipherKeys[participant]
                        ).toString();

                        total += parseInt(decrypted, 16);
                    }

                    session.finalValue = total % session.range;

                    this.resolve(data.session);
                });
            }
        }
    }

    /**
     * Resolve a session by calling any callbacks associated with the session and then deleting it.
     *
     * @param sessionId
     */
    resolve(sessionId) {
        const session = this.sessions[sessionId];
        for (let resolve of session.resolvers) {
            resolve(session.finalValue);
        }
    }
}
