const {unstringifyBigInts} = require("./stringifybigint.js");
const fs = require("fs");
const bigInt = require("big-integer");
const assert = require("assert");

const version = require("../package").version;

const argv = require("yargs")
    .version(version)
    .usage(`node buildpkey.js -i "proving_key.json" -o "proving_key.bin"
  Default: circuit.json
        `)
    .alias("i", "input")
    .alias("o", "output")
    .help("h")
    .alias("h", "help")
    .epilogue(`Copyright (C) 2018  0kims association
    This program comes with ABSOLUTELY NO WARRANTY;
    This is free software, and you are welcome to redistribute it
    under certain conditions; see the COPYING file in the official
    repo directory at  https://github.com/iden3/circom `)
    .argv;

const inputName = (argv.input) ? argv.input : "proving_key.json";
const outputName = (argv.output) ? argv.output : "proving_key.bin";


const provingKey = unstringifyBigInts(JSON.parse(fs.readFileSync(inputName, "utf8")));





function writeUint32(h, val) {
    h.dataView.setUint32(h.offset, val, true);
    h.offset += 4;
}

function writeUint32ToPointer(h, p, val) {
    h.dataView.setUint32(p, val, true);
}


function alloc(h, n) {
    const o = h.offset;
    h.offset += n;
    return o;
}

function writeBigInt(h, bi) {
    for (let i=0; i<8; i++) {
        const v = bi.shiftRight(i*32).and(0xFFFFFFFF).toJSNumber();
        writeUint32(h, v);
    }
}

function toMontgomeryQ(p) {
    const q = bigInt("21888242871839275222246405745257275088696311157297823662689037894645226208583");
    return p.times(bigInt.one.shiftLeft(256)).mod(q);
}

function toMontgomeryR(p) {
    const r = bigInt("21888242871839275222246405745257275088548364400416034343698204186575808495617");
    return p.times(bigInt.one.shiftLeft(256)).mod(r);
}

function writePoint(h, p) {
    writeBigInt(h, toMontgomeryQ(p[0]));
    writeBigInt(h, toMontgomeryQ(p[1]));
}

function writePoint2(h, p) {
    writeBigInt(h, toMontgomeryQ(p[0][0]));
    writeBigInt(h, toMontgomeryQ(p[0][1]));
    writeBigInt(h, toMontgomeryQ(p[1][0]));
    writeBigInt(h, toMontgomeryQ(p[1][1]));
}

function writeTransformedPolynomial(h, p) {

    const keys = Object.keys(p);

    writeUint32(h, keys.length);

    for (let i=0; i<keys.length; i++) {
        writeUint32(h, keys[i]);
        writeBigInt(h, toMontgomeryR(p[keys[i]]));
    }
}

function calculateBuffLen(provingKey) {
    function polSize(pol) {
        const l= Object.keys(pol).length;
        return 36*l + 4;
    }

    let size = 40;

    // alfa1, beta1, delta1
    size += 3 * (32*2);

    // beta2, delta2
    size += 2 * (32*4);

    for (let i=0; i<provingKey.nVars; i++) {
        size += polSize(provingKey.polsA[i]);
        size += polSize(provingKey.polsB[i]);
    }

    size += provingKey.nVars* (32*2);
    size += provingKey.nVars* (32*2);
    size += provingKey.nVars* (32*4);
    size += (provingKey.nVars - provingKey.nPublic  - 1)* (32*2);
    size += provingKey.domainSize * (32*2);

    return size;
}


const buffLen = calculateBuffLen(provingKey);

const buff = new ArrayBuffer(buffLen);

const h = {
    dataView: new DataView(buff),
    offset: 0
};


writeUint32(h, provingKey.nVars);
writeUint32(h, provingKey.nPublic);
writeUint32(h, provingKey.domainSize);
const pPolsA = alloc(h, 4);
const pPolsB = alloc(h, 4);
const pPointsA = alloc(h, 4);
const pPointsB1 = alloc(h, 4);
const pPointsB2 = alloc(h, 4);
const pPointsC = alloc(h, 4);
const pPointsHExps = alloc(h, 4);

writePoint(h, provingKey.vk_alfa_1);
writePoint(h, provingKey.vk_beta_1);
writePoint(h, provingKey.vk_delta_1);
writePoint2(h, provingKey.vk_beta_2);
writePoint2(h, provingKey.vk_delta_2);

writeUint32ToPointer(h, pPolsA, h.offset);
for (let i=0; i<provingKey.nVars; i++) {
    writeTransformedPolynomial(h, provingKey.polsA[i]);
}

writeUint32ToPointer(h, pPolsB, h.offset);
for (let i=0; i<provingKey.nVars; i++) {
    writeTransformedPolynomial(h, provingKey.polsB[i]);
}

writeUint32ToPointer(h, pPointsA, h.offset);
for (let i=0; i<provingKey.nVars; i++) {
    writePoint(h, provingKey.A[i]);
}

writeUint32ToPointer(h, pPointsB1, h.offset);
for (let i=0; i<provingKey.nVars; i++) {
    writePoint(h, provingKey.B1[i]);
}

writeUint32ToPointer(h, pPointsB2, h.offset);
for (let i=0; i<provingKey.nVars; i++) {
    writePoint2(h, provingKey.B2[i]);
}

writeUint32ToPointer(h, pPointsC, h.offset);
for (let i=provingKey.nPublic+1; i<provingKey.nVars; i++) {
    writePoint(h, provingKey.C[i]);
}

writeUint32ToPointer(h, pPointsHExps, h.offset);
for (let i=0; i<provingKey.domainSize; i++) {
    writePoint(h, provingKey.hExps[i]);
}

assert.equal(h.offset, buffLen);

var wstream = fs.createWriteStream(outputName);
wstream.write(Buffer.from(buff));
wstream.end();

/*
NSignals
NPublic
DomainSize (2 multiple)
pPolsA
pPolsB
pPointsA
pPointsB1
pPointsB2
pPointsC
pPointsHExps
Alfa1
Beta1
Delta1
Beta2
Delta2
    PolinomialA_0
    PolinomialA_1
    PolinomialA_2
    ...
    PolinomialA_NVars-1

    PolinomialB_0
    PolinomialB_1
    PolinomialB_2
    ...
    PolinomialB_NVars-1

    PointA_0
    PointA_1
    ...
    PointA_NVars-1

    PointB1_0
    PointB1_1
    ...
    PointB1_NVars-1

    PointB2_0
    PointB2_1
    ...
    PointB2_NVars-1

    PointC_nPublics+1
    PointC_nPublics+2
    ...
    PointC_nPublics+NVars

    PointHExp_0
    PointHExp_1
    ...
    PointHExp_DomainSize-1
*/
