"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const light_1 = require("protobufjs/light");
const kyber_1 = require("@dedis/kyber");
const byzcoin_1 = require("../byzcoin");
const coin_instance_1 = tslib_1.__importDefault(require("../byzcoin/contracts/coin-instance"));
const instance_1 = tslib_1.__importDefault(require("../byzcoin/instance"));
const protobuf_1 = require("../protobuf");
const curve25519 = kyber_1.curve.newCurve("edwards25519");
class RoPaSciInstance extends instance_1.default {
    constructor(rpc, inst) {
        super(inst);
        this.rpc = rpc;
        if (inst.contractID.toString() !== RoPaSciInstance.contractID) {
            throw new Error(`mismatch contract name: ${inst.contractID} vs ${RoPaSciInstance.contractID}`);
        }
        this.struct = RoPaSciStruct.decode(this.data);
    }
    get stake() {
        return this.struct.stake;
    }
    get playerChoice() {
        return this.struct.firstPlayer;
    }
    /**
     * Getter for the second player ID
     * @returns id as a buffer
     */
    get adversaryID() {
        return this.struct.secondPlayerAccount;
    }
    /**
     * Getter for the second player choice
     * @returns the choice as a number
     */
    get adversaryChoice() {
        return this.struct.secondPlayer;
    }
    static fromObject(rpc, obj) {
        const inst = instance_1.default.fromBytes(obj.instance);
        const rps = new RoPaSciInstance(rpc, inst);
        rps.fillUp = obj.fillUp;
        rps.firstMove = obj.firstMove;
        return rps;
    }
    /**
     * Fetch the proof for the given instance and create a
     * RoPaSciInstance from it
     *
     * @param bc    The ByzCoinRPC to use
     * @param iid   The instance ID
     * @param waitMatch how many times to wait for a match - useful if its called just after an addTransactionAndWait.
     * @param interval how long to wait between two attempts in waitMatch.
     * @returns the new instance
     */
    static fromByzcoin(bc, iid, waitMatch = 0, interval = 1000) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            return new RoPaSciInstance(bc, yield instance_1.default.fromByzcoin(bc, iid, waitMatch, interval));
        });
    }
    /**
     * Update the instance data
     *
     * @param choice The choice of the first player
     * @param fillup The fillup of the first player
     */
    setChoice(choice, fillup) {
        this.firstMove = choice;
        this.fillUp = fillup;
    }
    /**
     * Returns the firstMove and the fillUp values.
     */
    getChoice() {
        return [this.firstMove, this.fillUp ? Buffer.from(this.fillUp) : undefined];
    }
    /**
     * Check if both players have played their moves
     *
     * @returns true when both have played, false otherwise
     */
    isDone() {
        return this.struct.secondPlayer >= 0;
    }
    /**
     * returns true if there is a CalypsoWrite instance stored.
     */
    isCalypso() {
        return !this.struct.calypsoWrite.equals(Buffer.alloc(32));
    }
    /**
     * ourGame returns if this game belongs to the given coin.
     * @param coinID
     */
    ourGame(coinID) {
        const player1 = this.struct.firstPlayerAccount;
        if (player1 !== undefined && !player1.equals(Buffer.alloc(32))) {
            return player1.equals(coinID);
        }
        return this.getChoice()[1] !== undefined;
    }
    /**
     * Play the adversary move
     *
     * @param coin      The CoinInstance of the second player
     * @param signer    Signer for the transaction
     * @param choice    The choice of the second player
     * @returns a promise that resolves on success, or rejects with the error
     */
    second(coin, signer, choice, lts) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (!coin.name.equals(this.struct.stake.name)) {
                throw new Error("not correct coin-type for player 2");
            }
            if (coin.value.lessThan(this.struct.stake.value)) {
                throw new Error("don't have enough coins to match stake");
            }
            const args = [
                new byzcoin_1.Argument({ name: "account", value: coin.id }),
                new byzcoin_1.Argument({ name: "choice", value: Buffer.from([choice % 3]) }),
            ];
            const priv = curve25519.scalar().pick();
            const pub = curve25519.point().mul(priv);
            if (this.isCalypso()) {
                if (lts === undefined) {
                    throw new Error("need LTS for calypso-ropascis");
                }
                args.push(new byzcoin_1.Argument({ name: "public", value: pub.marshalBinary() }));
            }
            const ctx = byzcoin_1.ClientTransaction.make(this.rpc.getProtocolVersion(), byzcoin_1.Instruction.createInvoke(coin.id, coin_instance_1.default.contractID, coin_instance_1.default.commandFetch, [
                new byzcoin_1.Argument({
                    name: coin_instance_1.default.argumentCoins,
                    value: Buffer.from(this.struct.stake.value.toBytesLE()),
                }),
            ]), byzcoin_1.Instruction.createInvoke(this.id, RoPaSciInstance.contractID, "second", args));
            yield ctx.updateCountersAndSign(this.rpc, [[signer], []]);
            yield this.rpc.sendTransactionAndWait(ctx);
            yield this.update();
            if (this.isCalypso()) {
                const dreply = yield lts.reencryptKey(yield this.rpc.getProof(this.struct.calypsoWrite), yield this.rpc.getProof(this.struct.calypsoRead));
                const preHash = yield dreply.decrypt(priv);
                this.firstMove = preHash[0];
                this.fillUp = Buffer.allocUnsafe(31);
                preHash.slice(1).copy(this.fillUp);
                yield this.confirm(coin);
            }
        });
    }
    /**
     * Reveal the move of the first player
     *
     * @param coin The CoinInstance of the first player
     * @returns a promise that resolves on success, or rejects
     * with the error
     */
    confirm(coin) {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            if (!coin.name.equals(this.struct.stake.name)) {
                throw new Error("not correct coin-type for player 1");
            }
            const preHash = Buffer.alloc(this.fillUp.length + 1, 0);
            preHash[0] = this.firstMove % 3;
            this.fillUp.copy(preHash, 1);
            const ctx = byzcoin_1.ClientTransaction.make(this.rpc.getProtocolVersion(), byzcoin_1.Instruction.createInvoke(this.id, RoPaSciInstance.contractID, "confirm", [
                new byzcoin_1.Argument({ name: "prehash", value: preHash }),
                new byzcoin_1.Argument({ name: "account", value: coin.id }),
            ]));
            yield this.rpc.sendTransactionAndWait(ctx);
            yield this.update();
        });
    }
    /**
     * Update the state of the instance
     *
     * @returns a promise that resolves with the updated instance,
     * or rejects with the error
     */
    update() {
        return tslib_1.__awaiter(this, void 0, void 0, function* () {
            const proof = yield this.rpc.getProofFromLatest(this.id);
            if (!proof.exists(this.id)) {
                throw new Error("fail to get a matching proof");
            }
            const inst = instance_1.default.fromProof(this.id, proof);
            this.data = inst.data;
            this.struct = RoPaSciStruct.decode(this.data);
            return this;
        });
    }
    toObject() {
        return {
            fillUp: this.fillUp,
            firstMove: this.firstMove,
            instance: this.toBytes(),
        };
    }
}
exports.default = RoPaSciInstance;
RoPaSciInstance.contractID = "ropasci";
/**
 * Data hold by a rock-paper-scissors instance
 */
class RoPaSciStruct extends light_1.Message {
    constructor(props) {
        super(props);
        this.firstPlayerHash = Buffer.from(this.firstPlayerHash || protobuf_1.EMPTY_BUFFER);
        this.secondPlayerAccount = Buffer.from(this.secondPlayerAccount || protobuf_1.EMPTY_BUFFER);
        Object.defineProperty(this, "firstplayer", {
            get() {
                return this.firstPlayer;
            },
            set(value) {
                this.firstPlayer = value;
            },
        });
        Object.defineProperty(this, "firstplayerhash", {
            get() {
                return this.firstPlayerHash;
            },
            set(value) {
                this.firstPlayerHash = value;
            },
        });
        Object.defineProperty(this, "secondplayer", {
            get() {
                return this.secondPlayer;
            },
            set(value) {
                this.secondPlayer = value;
            },
        });
        Object.defineProperty(this, "secondplayeraccount", {
            get() {
                return this.secondPlayerAccount;
            },
            set(value) {
                this.secondPlayerAccount = value;
            },
        });
        Object.defineProperty(this, "firstplayeraccount", {
            get() {
                return this.firstPlayerAccount;
            },
            set(value) {
                this.firstPlayerAccount = value;
            },
        });
        Object.defineProperty(this, "calypsowrite", {
            get() {
                return this.calypsoWrite;
            },
            set(value) {
                this.calypsoWrite = value;
            },
        });
        Object.defineProperty(this, "calypsoread", {
            get() {
                return this.calypsoRead;
            },
            set(value) {
                this.calypsoRead = value;
            },
        });
    }
    /**
     * @see README#Message classes
     */
    static register() {
        protobuf_1.registerMessage("personhood.RoPaSciStruct", RoPaSciStruct);
    }
    /**
     * Helper to encode the struct using protobuf
     *
     * @returns the data as a buffer
     */
    toBytes() {
        return Buffer.from(RoPaSciStruct.encode(this).finish());
    }
}
exports.RoPaSciStruct = RoPaSciStruct;
RoPaSciStruct.register();
