import { Packet } from "./packet.js";
import { socket, game, random } from "./main.js";
import { RsaPubKey } from "../crypto/rsa.js";
import { PaillierPubKey, ReadOnlyCiphertext } from "../crypto/paillier.js";
import { Region } from "./map.js";
import { showDefenseDom } from "./dom.js";
import { proveRegions, verifyRegions } from "./proofs.js";

// Timeout to consider a player disconnected
const TIMEOUT = 30_000;

const PHASE_REINFORCE = 1;
const PHASE_ATTACK = 2;
const PHASE_FORTIFY = 3;

let totalDice = 0;

export class Player {
    constructor(id, local, rsaKey, paillierKey) {
        // Game state
        this.totalStrength = 0;
        this.ready = false;

        // Protocol state
        this.timeout = null;
        this.id = id;
        this.rsaPubKey = RsaPubKey.fromJSON(rsaKey);
        this.paillierPubKey = PaillierPubKey.fromJSON(paillierKey);
        this.lastPacket = 0;

        // Data which is reset every turn
        this.isPlaying = false;
        this.reinforcementsPlaced = 0;
        this.reinforcementsAvailable = 0;
        this.offenderRegion = null;
        this.turnPhase = PHASE_REINFORCE;

        // Data for defending
        this.defenderPromise = null;
        this.defenderAmount = null;

        this.resetColor();

        if (local) {
            // Emit keepalive messages to inform other players we are still here
            window.setInterval(() => {
                socket.emit("message", Packet.createKeepAlive());
            }, TIMEOUT / 2);
        }
    }

    resetTimeout(game) {
        if (this.timeout !== null) {
            window.clearTimeout(this.timeout);
        }

        this.timeout = window.setTimeout(() => {
            game.removePlayer(this);
        }, TIMEOUT);
    }

    /**
     * Get our color as used on the board.
     */
    getColor() {
        return this.color;
    }

    resetColor() {
        let randomColor = Math.random() * 360;
        this.color = `hsl(${randomColor} 57% 50%)`;
    }

    /**
     * Get all regions controlled by this player.
     */
    getRegions() {
        return Region.getAllRegions().filter((r) => r.owner === this);
    }

    /**
     * Claim a region of the map.
     *
     * @param data Data received via socket.
     */
    claim(data) {
        let region = Region.getRegion(data.region);

        if (region.owner === null) {
            region.claim(
                this,
                new ReadOnlyCiphertext(this.paillierPubKey, BigInt(data.cipherText))
            );

            this.totalStrength += 1;

            return true;
        } else {
            return false;
        }
    }

    /**
     * Reinforce a region of the map.
     *
     * Todo. Check object before committing changes.
     *
     * @param data Data received via socket.
     */
    reinforce(data) {
        if (!verifyRegions(data.verification, this.paillierPubKey)) {
            console.log("Failed to verify reinforcements!");
            return false;
        }

        for (let regionName of Object.keys(data.regions)) {
            let region = Region.getRegion(regionName);

            if (region.owner === this) {
                region.reinforce(
                    new ReadOnlyCiphertext(
                        this.paillierPubKey,
                        BigInt(data.regions[regionName])
                    )
                );
            }
        }

        this.totalStrength += 1;

        // request proofs
        for (let region of this.getRegions()) {
            if ([...region.neighbours.values()].find((r) => r.owner === game.us)) {
                region.requestProof();
            }
        }

        return true;
    }

    sendClaim(region) {
        let cipherText = this.paillierPubKey.encrypt(1n);
        Region.getRegion(region).claim(this, cipherText);

        socket.emit("message", Packet.createRegionClaim(region, cipherText.toString()));

        this.totalStrength += 1;

        this.endTurn();
    }

    sendReinforce(region) {
        let regions = {};
        for (let ourRegion of this.getRegions()) {
            let cipherText;
            if (ourRegion.name !== region) {
                cipherText = this.paillierPubKey.encrypt(0n);
            } else {
                cipherText = this.paillierPubKey.encrypt(1n);
            }

            regions[ourRegion.name] = cipherText;
            ourRegion.reinforce(cipherText);
        }

        let verification = proveRegions(regions);

        socket.emit("message", Packet.createReinforce(regions, verification));

        this.totalStrength += 1;

        if (game.isPregame()) {
            game.us.endTurn();

            if (game.allReinforcementsPlaced()) {
                game.incrementState();
            }
        }
    }

    /**
     * Process a generic action packet representing a player's move.
     *
     * @param data Data received via socket
     * @returns {boolean} Whether this player's turn should now end or not.
     */
    async act(data) {
        if (this.turnPhase === PHASE_REINFORCE) {
            if (data.regions !== undefined) {
                if (this === game.us) {
                    this.reinforcementsPlaced += 1;
                } else if (this.reinforce(data)) {
                    this.reinforcementsPlaced += 1;
                }

                if (this.reinforcementsPlaced === this.reinforcementsAvailable) {
                    this.turnPhase = PHASE_ATTACK;
                }
            }

            return false;
        } else {
            // End turn prematurely.
            if (data.action === "END") {
                return true;
            }

            if (this.turnPhase === PHASE_ATTACK && data.action === "ATTACK") {
                await this.attack(data);
                return false;
            }

            this.turnPhase = PHASE_FORTIFY;

            if (data.action === "FORTIFY") {
                return this.fortify(data);
            }
        }
        return false;
    }

    /**
     * Process an action which is to attack another region.
     *
     * @param data Data received via socket
     */
    async attack(data) {
        let offender = Region.getRegion(data.startRegion);
        let defender = Region.getRegion(data.endRegion);
        let offenderStrength = parseInt(data.strength);

        // Basic validation on game state
        if (
            offender.owner !== this ||
            defender.owner === this ||
            offenderStrength > 3 ||
            offenderStrength <= 0 ||
            offenderStrength >= offender.strength ||
            (this.offenderRegion !== null && this.offenderRegion !== offender)
        ) {
            return false;
        }

        // If we're the defender, we need to send a packet to state our defense.
        if (defender.owner === game.us) {
            showDefenseDom(defender.name);
        }

        // Grab the defense amount from
        let defenderStrength = 0;
        while (
            defenderStrength <= 0 ||
            defenderStrength > Math.min(2, defender.strength)
        ) {
            console.log("waiting");
            defenderStrength = await defender.owner.getDefense();
        }

        console.log(defenderStrength);

        /* How do Risk attacks work?
        - Offender signs 1-3 armies, defender signs 1-2 armies
        - Both roll respective dice
        - Compare pairs of highest die in the rolls to remove armies.
         */

        if (this.offenderRegion === null) {
            this.offenderRegion = offender;
        }

        let offenderRolls = [];
        let defenderRolls = [];

        // Get random values
        for (let i = 0; i < offenderStrength; i++) {
            offenderRolls.push(await random.get(6, `dice-${totalDice}`));
            totalDice += 1;
        }

        for (let i = 0; i < defenderStrength; i++) {
            defenderRolls.push(await random.get(6, `dice-${totalDice}`));
            totalDice += 1;
        }

        console.log(`attacker rolls: ${offenderRolls}`);
        console.log(`defender rolls: ${defenderRolls}`);

        offenderRolls.sort();
        defenderRolls.sort();

        // Compare and settle.
        while (offenderRolls.length * defenderRolls.length > 0) {
            let offenderResult = offenderRolls.pop();
            let defenderResult = defenderRolls.pop();

            if (offenderResult > defenderResult) {
                defender.strength.transparentUpdate(-1n);
            } else {
                offender.strength.transparentUpdate(-1n);
            }

            // Handle region capture.
            if (defender.strength === 0) {
                defender.strength = offenderRolls.length + 1;
                offender.strength -= offenderRolls.length + 1;
                defender.owner = this;
                break;
            }
        }

        // Reset the promises in case they attack again.
        defender.owner.defenderPromise = null;
        defender.owner.defenderAmount = null;
    }

    async setDefense(amount) {
        await navigator.locks.request("defender", () => {
            this.defenderAmount = amount;
            if (this.defenderPromise !== null) {
                this.defenderPromise(amount);
            }
        });
    }

    async getDefense() {
        let promise, resolver;
        await navigator.locks.request("defender", () => {
            if (this.defenderAmount === null) {
                promise = new Promise((resolve) => {
                    resolver = resolve;
                });
                this.defenderPromise = resolver;
            } else {
                promise = new Promise((resolve) => {
                    resolve(this.defenderAmount);
                });
            }
        });
        return promise;
    }

    /**
     * Process an action which is to attack another region.
     *
     * @param data Data received via socket
     */
    fortify(data) {
        let sender = Region.getRegion(data.startRegion);
        let receiver = Region.getRegion(data.endRegion);
        let strength = parseInt(data.strength);

        if (
            sender.owner === this &&
            receiver.owner === this &&
            sender.strength > strength &&
            strength > 0
        ) {
            receiver.reinforce(strength);
            sender.strength -= strength;
            return true;
        } else {
            return false;
        }
    }

    /**
     * Calculate the number of additional reinforcements to gain on this turn.
     */
    additionalReinforcements() {
        return Math.min(
            3,
            Math.floor(
                Object.values(Region.getAllRegions()).filter(
                    (region) => region.owner === this
                ).length / 3
            )
        );
    }

    /**
     * Start a player's turn.
     */
    startTurn() {
        this.isPlaying = true;
        this.reinforcementsPlaced = 0;
        this.reinforcementsAvailable = this.additionalReinforcements();
        this.offenderRegion = null;
        this.turnPhase = PHASE_REINFORCE;
    }

    /**
     * End player's turn
     */
    endTurn() {
        this.isPlaying = false;
        this.nextPlayer().startTurn();

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

    nextPlayer() {
        // todo move this to :class:Game
        let sorted = Object.values(game.players).sort((a, b) => (a.id < b.id ? -1 : 1));
        let ourIndex = sorted.findIndex((player) => player.id === this.id);

        return sorted[(ourIndex + 1) % sorted.length];
    }
}
