import { generate_keypair, generate_rsa_keypair } from "../crypto/main.js";
import { Random } from "./random.js";
import { Barrier } from "./barrier.js";
import { Packet } from "./packet.js";
import { Game } from "./game.js";
import { Region } from "./map.js";
import "./dom.js";
import "./proofs.js";

export const ID = window.crypto.randomUUID();
export const game = new Game();
window.game = game;
export let socket;
export let random;
let barrier;
window.paillier = generate_keypair();
window.rsa = generate_rsa_keypair();

// Not totally reliable but better than nothing.
window.addEventListener("beforeunload", () => {
    socket.emit("message", Packet.createDisconnect());
});

document.addEventListener("DOMContentLoaded", () => {
    socket = io();

    random = new Random();
    barrier = new Barrier();

    socket.on("connect", () => {
        window.console.log("Connected!");
        window.console.log(`We are: ${ID}`);
        socket.emit("message", Packet.createAnnounce(window.rsa, window.paillier));
        game.addPlayer(ID, true, window.rsa.pubKey, window.paillier.pubKey);
    });

    socket.on("message", async (packet) => {
        // window.console.log(`Received size: ${JSON.stringify(packet).length}`);

        let data = packet.payload;
        if (data.type !== "KEEPALIVE") window.console.log(data);

        const sender = game.players[data.author];
        if (sender === undefined) {
            window.console.warn(
                `Not able to verify signature on packet ${data.id} as the sender is unknown.`
            );
        } else {
            let sig = BigInt(packet.sig);
            // decrypt and compare signature
            let dehash = sender.rsaPubKey.encrypt(sig);
            let hash = BigInt("0x" + CryptoJS.SHA3(JSON.stringify(data)).toString());
            if (dehash !== hash) {
                window.console.error(`Signature invalid! Ignoring packet ${data.id}.`);
                return;
            }

            // check if the packet is replayed.
            if (data.timestamp <= sender.lastPacket) {
                window.console.error(`Replay detected! Ignoring packet ${data.id}.`);
                return;
            }

            sender.lastPacket = data.timestamp;
        }

        switch (data.type) {
            case "RANDOM":
                if (data.author === ID) {
                    return;
                }
                await random.processCooperativeRandom(data);
                break;

            case "BARRIER":
                if (data.author === ID) {
                    return;
                }
                barrier.resolve(data);
                break;

            default:
                const event = new CustomEvent(data.type, { detail: data });
                document.dispatchEvent(event);
                break;
        }
    });
});

/**
 * Process player connect packets: these inform that a new player has joined.
 *
 * @param data Packet received
 */
document.addEventListener("ANNOUNCE", (ev) => {
    const data = ev.detail;
    if (data.author === ID) return;

    let is_new = game.addPlayer(data.author, false, data.rsaPubKey, data.paillierPubKey);

    // When a new player is seen, all announce to ensure they know all players.
    if (is_new) {
        socket.emit("message", Packet.createAnnounce(window.rsa, window.paillier));
    }
});

document.addEventListener("DISCONNECT", (ev) => {
    const data = ev.detail;
    game.removePlayer(data.author);
});

/**
 * Process keep-alive packets: these are packets that check players are still online.
 *
 * @param data Packet received
 */
document.addEventListener("KEEPALIVE", (ev) => {
    const data = ev.detail;
    game.keepAlive(data.author);
});

document.addEventListener("ACT", async (ev) => {
    const data = ev.detail;

    if (game.isWaiting()) {
        game.setReady(data.author, data.ready);
    } else {
        // Throw out our own packets
        if (data.author === game.us.id) {
            return;
        }

        if (data.author !== game.currentPlayer().id) {
            if (data.action === "DEFENSE") {
                await game.players[data.author].setDefense(data.amount);
            }

            return;
        }

        if (game.isPregame()) {
            if (!Region.allRegionsClaimed()) {
                // Claim a region in the pregame.
                if (game.currentPlayer().claim(data)) {
                    // Increment to next player.
                    game.currentPlayer().endTurn();
                }
            } else if (!game.allReinforcementsPlaced()) {
                if (game.currentPlayer().reinforce(data)) {
                    game.currentPlayer().endTurn();
                }
            }

            if (game.allReinforcementsPlaced()) {
                game.incrementState();
            }
        } else {
            if (await game.currentPlayer().act(data)) {
                game.currentPlayer().endTurn();
            } else {
                const event = new CustomEvent("turnProgress");
                document.dispatchEvent(event);
            }
        }
    }
});

// todo has to filter by player
document.addEventListener("PROOF", async (ev) => {
    const data = ev.detail;
    if (data.stage === "REQUEST") {
        let region = Region.getRegion(data.region);

        // todo check if this is a valid request e.g actually has neighbour
        if (region.owner === game.us) {
            region.prove();
        }
    }

    if (data.stage === "CONJECTURE") {
        let region = Region.getRegion(data.region);

        region.verify(BigInt(data.plainText), BigInt(data.a));
    }
});

document.addEventListener("endTurn", () => {
    if (game.isPregame() && game.allReinforcementsPlaced()) {
        game.incrementState();
    }
});

document.addEventListener("gameStateUpdate", async () => {
    if (game.isPregame()) {
        let firstPlayerIndex = await random.get(
            Object.keys(game.players).length,
            "first-player"
        );

        let firstPlayer = Object.values(game.players).sort((a, b) =>
            a.id < b.id ? -1 : 1
        )[firstPlayerIndex];

        firstPlayer.isPlaying = true;
        await barrier.wait();

        const event = new CustomEvent("playerChange");
        document.dispatchEvent(event);
    }
});
