import {
    cryptoRandom,
    generate_prime,
    generate_safe_prime,
    KEY_SIZE,
} from "./random_primes.js";
import { gcd, mod_exp } from "./math.js";

const PAILLIER = 0;
const JURIK = 1;

function RSTransform(g, a, p) {
    let plainText = p.toString(16);
    if (plainText.length % 2 !== 0) {
        plainText = "0" + plainText;
    }

    let aStr = a.toString(16);
    if (aStr.length % 2 !== 0) {
        aStr = "0" + aStr;
    }

    let hasher = new jsSHA("SHAKE256", "HEX");
    hasher.update(g.toString(16));
    hasher.update(plainText);
    hasher.update(aStr);

    return BigInt("0x" + hasher.getHash("HEX", { outputLen: 2048 }));
}

class Ciphertext {
    constructor(key, plainText, r, set) {
        if (set !== undefined) {
            this.pubKey = key;
            this.plainText = plainText;

            this.readOnly = false;

            return;
        }

        if (r === undefined) {
            // Use the optimised form using Jacobi classes
            r = cryptoRandom();

            // Compute g^m by binomial theorem.
            let gm = (1n + key.n * plainText) % key.n2;

            // Compute g^m h^r.
            this.cipherText = (gm * key.hn_exp(r)) % key.n2;

            // Force into range.
            while (this.cipherText < 0n) {
                this.cipherText += key.n2;
            }

            this.mode = JURIK;
            this.r = key.h_exp(r);
        } else {
            // Use the standard form
            // Compute g^m by binomial theorem.
            let gm = (1n + key.n * plainText) % key.n2;

            // Compute g^m r^n.
            this.cipherText = (gm * mod_exp(r, key.n, key.n2)) % key.n2;

            // Force into range.
            while (this.cipherText < 0n) {
                this.cipherText += key.n2;
            }

            this.mode = PAILLIER;
            this.r = r;
        }

        this.pubKey = key;
        this.plainText = plainText;

        while (this.plainText < 0n) {
            this.plainText += key.n2;
        }

        this.readOnly = false;
    }

    update(c) {
        this.cipherText = (this.cipherText * c.cipherText) % this.pubKey.n2;
        this.r = (this.r * c.r) % this.pubKey.n2;
        this.plainText = (this.plainText + c.plainText) % this.pubKey.n2;

        // Force into range
        while (this.cipherText < 0n) {
            this.cipherText += this.pubKey.n2;
        }
        while (this.plainText < 0n) {
            this.plainText += this.pubKey.n2;
        }
    }

    toString() {
        return "0x" + this.cipherText.toString(16);
    }

    toJSON() {
        return "0x" + this.cipherText.toString(16);
    }

    prove() {
        return new ValueProofSessionProver(this);
    }

    // Construct a non-interactive proof
    proveNI() {
        let rp = cryptoRandom(KEY_SIZE * 2);
        while (rp >= this.pubKey.n) {
            rp = cryptoRandom(KEY_SIZE * 2);
        }

        let a = mod_exp(rp, this.pubKey.n, this.pubKey.n2);

        let challenge = RSTransform(this.pubKey.g, a, this.plainText);

        return {
            plainText: "0x" + this.plainText.toString(16),
            a: "0x" + a.toString(16),
            proof:
                "0x" +
                (
                    ((rp % this.pubKey.n) * mod_exp(this.r, challenge, this.pubKey.n)) %
                    this.pubKey.n
                ).toString(16),
        };
    }

    asReadOnlyCiphertext() {
        return new ReadOnlyCiphertext(this.pubKey, this.cipherText);
    }

    clone() {
        let c = new Ciphertext(this.pubKey, this.plainText, 0, true);
        c.cipherText = this.cipherText;
        c.r = this.r;
        c.mode = this.mode;

        return c;
    }
}

class ValueProofSessionProver {
    constructor(cipherText) {
        this.cipherText = cipherText;

        this.rp = cryptoRandom(KEY_SIZE * 2);
        while (this.rp >= this.cipherText.pubKey.n) {
            this.rp = cryptoRandom(KEY_SIZE * 2);
        }
    }

    get a() {
        return mod_exp(this.rp, this.cipherText.pubKey.n, this.cipherText.pubKey.n2);
    }

    noise() {
        return mod_exp(this.rp, this.cipherText.pubKey.n, this.cipherText.pubKey.n2);
    }

    prove(challenge) {
        return (
            ((this.rp % this.cipherText.pubKey.n) *
                mod_exp(this.cipherText.r, challenge, this.cipherText.pubKey.n)) %
            this.cipherText.pubKey.n
        );
    }

    asVerifier() {
        return this.cipherText
            .asReadOnlyCiphertext()
            .prove(this.cipherText.plainText, this.noise());
    }
}

window.Ciphertext = Ciphertext;

export class ReadOnlyCiphertext {
    constructor(key, cipherText) {
        this.cipherText = cipherText;
        this.pubKey = key;

        this.readOnly = true;
    }

    update(c) {
        this.cipherText = (this.cipherText * c.cipherText) % this.pubKey.n2;

        // Force into range
        while (this.cipherText < 0n) {
            this.cipherText += this.pubKey.n2;
        }
    }

    prove(plainText, a) {
        return new ValueProofSessionVerifier(this, plainText, a);
    }

    verifyNI(statement) {
        let challenge = RSTransform(
            this.pubKey.g,
            BigInt(statement.a),
            BigInt(statement.plainText)
        );

        let verifier = new ValueProofSessionVerifier(
            this,
            BigInt(statement.plainText),
            BigInt(statement.a),
            challenge
        );

        if (verifier.verify(BigInt(statement.proof))) {
            return BigInt(statement.plainText);
        } else {
            return null;
        }
    }

    clone() {
        return new ReadOnlyCiphertext(this.pubKey, this.cipherText);
    }
}

class ValueProofSessionVerifier {
    constructor(cipherText, plainText, a, challenge) {
        // Clone, otherwise the update below will mutate the original value
        this.cipherText = cipherText.clone();
        this.cipherText.update(this.cipherText.pubKey.encrypt(-1n * plainText, 1n));

        if (challenge === undefined) {
            // Shift the challenge down by 1 to ensure it is smaller than either prime factor.
            this.challenge = cryptoRandom(KEY_SIZE) << 1n;
        } else {
            this.challenge = challenge;
        }

        this.a = a;

        this.plainText = plainText;
    }

    verify(proof) {
        // check coprimality
        if (gcd(proof, this.cipherText.pubKey.n) !== 1n) return -1;
        if (gcd(this.cipherText.cipherText, this.cipherText.pubKey.n) !== 1n) return -2;
        if (gcd(this.a, this.cipherText.pubKey.n) !== 1n) return -3;

        // check exp
        return mod_exp(proof, this.cipherText.pubKey.n, this.cipherText.pubKey.n2) ===
            (this.a *
                mod_exp(
                    this.cipherText.cipherText,
                    this.challenge,
                    this.cipherText.pubKey.n2
                )) %
                this.cipherText.pubKey.n2
            ? 1
            : -4;
    }
}

window.ReadOnlyCiphertext = ReadOnlyCiphertext;

export class PaillierPubKey {
    constructor(n, h) {
        this.n = n;

        if (h === undefined) {
            let x = cryptoRandom(KEY_SIZE * 2);
            while (x >= this.n) {
                x = cryptoRandom(KEY_SIZE * 2);
            }

            this.h = ((-1n * x ** 2n) % this.n) + this.n;
        } else {
            this.h = h;
        }

        this.g = this.n + 1n;

        this.n2 = this.n ** 2n;
        this.hn = mod_exp(this.h, this.n, this.n2);

        this._h_cache = [];
        this._hn_cache = [];

        for (let i = 0n; i < BigInt(KEY_SIZE); i++) {
            this._h_cache.push(mod_exp(this.h, 2n ** i, this.n));
            this._hn_cache.push(mod_exp(this.hn, 2n ** i, this.n2));
        }
    }

    h_exp(b) {
        let ctr = 1n;
        let i = 0;
        while (b !== 0n) {
            if (b % 2n === 1n) {
                ctr *= this._h_cache[i];
                ctr %= this.n;
            }
            i++;
            b >>= 1n;
        }

        return ctr;
    }

    hn_exp(b) {
        let ctr = 1n;
        let i = 0;
        while (b !== 0n) {
            if (b % 2n === 1n) {
                ctr *= this._hn_cache[i];
                ctr %= this.n2;
            }
            i++;
            b >>= 1n;
        }

        return ctr;
    }

    encrypt(m, r) {
        return new Ciphertext(this, m, r);
    }

    toJSON() {
        return {
            n: "0x" + this.n.toString(16),
            h: "0x" + this.h.toString(16),
        };
    }

    static fromJSON(data) {
        return new PaillierPubKey(BigInt(data.n), BigInt(data.h));
    }
}

class PaillierPrivKey {
    constructor(p, q) {
        this.n = p * q;
        // precompute square of n
        this.n2 = this.n ** 2n;
        this.lambda = (p - 1n) * (q - 1n);
        this.mu = mod_exp(this.lambda, this.lambda - 1n, this.n);
    }

    decrypt(c) {
        return (((mod_exp(c, this.lambda, this.n2) - 1n) / this.n) * this.mu) % this.n;
    }
}

function check_gcd(primes, new_prime) {
    for (let prime of primes) {
        if (gcd(prime - 1n, new_prime - 1n) === 2n) {
            return prime;
        }
    }
    return null;
}

export function generate_keypair() {
    let p, q, pubKey, privKey;

    if (
        window.sessionStorage.getItem("p") !== null &&
        window.sessionStorage.getItem("q") !== null
    ) {
        p = BigInt(window.sessionStorage.getItem("p"));
        q = BigInt(window.sessionStorage.getItem("q"));
    } else {
        p = generate_safe_prime();
        q = generate_safe_prime();
    }

    window.sessionStorage.setItem("p", p);
    window.sessionStorage.setItem("q", q);

    pubKey = new PaillierPubKey(p * q);
    privKey = new PaillierPrivKey(p, q);

    return { pubKey, privKey };
}

// p = a.prove(); v = p.asVerifier(); v.verify(p.prove(v.challenge));
