/*
    Copyright 2018 0kims association.

    This file is part of zksnark JavaScript library.

    zksnark JavaScript library is a free software: you can redistribute it and/or
    modify it under the terms of the GNU General Public License as published by the
    Free Software Foundation, either version 3 of the License, or (at your option)
    any later version.

    zksnark JavaScript library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
    or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
    more details.

    You should have received a copy of the GNU General Public License along with
    zksnark JavaScript library. If not, see <https://www.gnu.org/licenses/>.
*/

const bigInt = require("./bigint.js");

const BN128 = require("./bn128.js");
const PolField = require("./polfield.js");
const ZqField = require("./zqfield.js");
const RatField = require("./ratfield.js");

const bn128 = new BN128();
const G1 = bn128.G1;
const G2 = bn128.G2;
const PolF = new PolField(new ZqField(bn128.r));
const RatPolF = new PolField(new RatField(new ZqField(bn128.r)));
const F = new ZqField(bn128.r);

module.exports = function setup(circuit) {
    const setup = {
        vk_proof : {
            nVars: circuit.nVars,
            nPublic: circuit.nPubInputs + circuit.nOutputs
        },
        vk_verifier: {
            nPublic: circuit.nPubInputs + circuit.nOutputs
        },
        toxic: {}
    };

    calculatePolynomials2(setup, circuit);
    setup.toxic.t = F.random();
    calculateEncriptedValuesAtT(setup, circuit);
    calculateHexps(setup, circuit);

    return setup;
};

function calculatePolynomials(setup, circuit) {
    // Calculate the points that must cross each polynomial

/*
    setup.toxic.aExtra = [];
    setup.toxic.bExtra = [];
    setup.toxic.cExtra = [];
    const aPoints = [];
    const bPoints = [];
    const cPoints = [];
    for (let s = 0; s<circuit.nVars; s++) {
        aPoints[s] = [];
        bPoints[s] = [];
        cPoints[s] = [];
        for (let c=0; c<circuit.nConstraints; c++) {
            aPoints[s].push([[bigInt(c), F.one], [circuit.a(c, s), F.one]]);
            bPoints[s].push([[bigInt(c), F.one], [circuit.b(c, s), F.one]]);
            cPoints[s].push([[bigInt(c), F.one], [circuit.c(c, s), F.one]]);
        }
        // Add an extra point to avoid constant polinolials.
        setup.toxic.aExtra[s] = F.random();
        setup.toxic.bExtra[s] = F.random();
        setup.toxic.cExtra[s] = F.random();
        aPoints[s].push([[bigInt(circuit.nConstraints), F.one], [setup.toxic.aExtra[s], F.one]]);
        bPoints[s].push([[bigInt(circuit.nConstraints), F.one], [setup.toxic.bExtra[s], F.one]]);
        cPoints[s].push([[bigInt(circuit.nConstraints), F.one], [setup.toxic.cExtra[s], F.one]]);
    }

    // Calculate the polynomials using Lagrange
    setup.vk_proof.polsA = [];
    setup.vk_proof.polsB = [];
    setup.vk_proof.polsC = [];
    for (let s=0; s<circuit.nVars; s++) {
//        console.log(`Caclcualte Pol ${s}/${circuit.nVars}`);
        const pA = RatPolF.lagrange( aPoints[s] );
        const pB = RatPolF.lagrange( bPoints[s] );
        const pC = RatPolF.lagrange( cPoints[s] );

        setup.vk_proof.polsA.push( unrat(pA) );
        setup.vk_proof.polsB.push( unrat(pB) );
        setup.vk_proof.polsC.push( unrat(pC) );

    }
*/

    setup.toxic.aExtra = [];
    setup.toxic.bExtra = [];
    setup.toxic.cExtra = [];

    let allZerosPol = [bigInt(1)];

    for (let c=0; c<=circuit.nConstraints; c++) {
        allZerosPol = PolF.mul(allZerosPol, [F.neg(bigInt(c)), F.one]);
    }

    setup.vk_proof.polsA = [];
    setup.vk_proof.polsB = [];
    setup.vk_proof.polsC = [];
    for (let s = 0; s<circuit.nVars; s++) {
        setup.vk_proof.polsA.push([]);
        setup.vk_proof.polsB.push([]);
        setup.vk_proof.polsC.push([]);
    }

    for (let c=0; c<circuit.nConstraints; c++) {
        console.log("Pol: "+ c);
        const mpol = PolF.ruffini(allZerosPol, bigInt(c));
        const normalizer = PolF.F.inverse(PolF.eval(mpol, bigInt(c)));
        for (let s = 0; s<circuit.nVars; s++) {
            if (!circuit.a(c, s).isZero()) {
                const factorA = PolF.F.mul(normalizer, circuit.a(c, s));
                const spolA = PolF.mulScalar(mpol, factorA);
                setup.vk_proof.polsA[s] = PolF.add(setup.vk_proof.polsA[s], spolA);
            }

            if (!circuit.b(c, s).isZero()) {
                const factorB = PolF.F.mul(normalizer, circuit.b(c, s));
                const spolB = PolF.mulScalar(mpol, factorB);
                setup.vk_proof.polsB[s] = PolF.add(setup.vk_proof.polsB[s], spolB);
            }

            if (!circuit.c(c, s).isZero()) {
                const factorC = PolF.F.mul(normalizer, circuit.c(c, s));
                const spolC = PolF.mulScalar(mpol, factorC);
                setup.vk_proof.polsC[s] = PolF.add(setup.vk_proof.polsC[s], spolC);
            }
        }
        if (global.gc) {
            if (c%100 == 0) global.gc();
        }
    }
    const mpol = PolF.ruffini(allZerosPol, bigInt(circuit.nConstraints));
    const normalizer = PolF.F.inverse(PolF.eval(mpol, bigInt(circuit.nConstraints)));
    for (let s = 0; s<circuit.nVars; s++) {
        setup.toxic.aExtra[s] = F.random();
        const factorA = PolF.F.mul(normalizer, setup.toxic.aExtra[s]);
        const spolA = PolF.mulScalar(mpol, factorA);
        setup.vk_proof.polsA[s] = PolF.add(setup.vk_proof.polsA[s], spolA);

        setup.toxic.bExtra[s] = F.random();
        const factorB = PolF.F.mul(normalizer, setup.toxic.bExtra[s]);
        const spolB = PolF.mulScalar(mpol, factorB);
        setup.vk_proof.polsB[s] = PolF.add(setup.vk_proof.polsB[s], spolB);

        setup.toxic.cExtra[s] = F.random();
        const factorC = PolF.F.mul(normalizer, setup.toxic.cExtra[s]);
        const spolC = PolF.mulScalar(mpol, factorC);
        setup.vk_proof.polsC[s] = PolF.add(setup.vk_proof.polsC[s], spolC);
    }



    // Calculate Z polynomial
    // Z = 1
    setup.vk_proof.polZ = [bigInt(1)];
    for (let c=0; c<circuit.nConstraints; c++) {
        // Z = Z * (x - p_c)
        setup.vk_proof.polZ = PolF.mul(
            setup.vk_proof.polZ,
            [F.neg(bigInt(c)), bigInt(1)] );
    }
}

function calculatePolynomials2(setup, circuit) {
    setup.toxic.aExtra = [];
    setup.toxic.bExtra = [];
    setup.toxic.cExtra = [];
    setup.vk_proof.polsA = [];
    setup.vk_proof.polsB = [];
    setup.vk_proof.polsC = [];
    const aPoints = new Array(circuit.nConstraints+1);
    const bPoints = new Array(circuit.nConstraints+1);
    const cPoints = new Array(circuit.nConstraints+1);

    for (let s = 0; s<circuit.nVars; s++) {
        for (let c=0; c<circuit.nConstraints; c++) {
            aPoints[c] = circuit.a(c, s);
            bPoints[c] = circuit.b(c, s);
            cPoints[c] = circuit.c(c, s);
        }
        // Add an extra point to avoid constant polinolials.
        setup.toxic.aExtra[s] = F.random();
        setup.toxic.bExtra[s] = F.random();
        setup.toxic.cExtra[s] = F.random();
        aPoints[circuit.nConstraints] = setup.toxic.aExtra[s];
        bPoints[circuit.nConstraints] = setup.toxic.bExtra[s];
        cPoints[circuit.nConstraints] = setup.toxic.cExtra[s];

        const pA = PolF.ifft( aPoints );
        const pB = PolF.ifft( bPoints );
        const pC = PolF.ifft( cPoints );

        setup.vk_proof.polsA.push( PolF.affine(pA) );
        setup.vk_proof.polsB.push( PolF.affine(pB) );
        setup.vk_proof.polsC.push( PolF.affine(pC) );

        console.log(s);

        if (global.gc) {
            if (s%100 == 0) global.gc();
        }
    }

    setup.polsLen=PolF._next2Power(circuit.nConstraints+1);

    // Calculate Z polynomial
    // Z = 1


    const bits= log2(circuit.nConstraints)+1;
    const rt1 = PolF.w[bits];

    let rt = bigInt(1);
    setup.vk_proof.polZ = [F.one];
    for (let c=0; c<circuit.nConstraints; c++) {
        // Z = Z * (x - p_c)
        setup.vk_proof.polZ = PolF.mul(
            setup.vk_proof.polZ,
            [F.neg(rt), F.one] );

        rt = F.mul(rt, rt1);
    }

    setup.vk_proof.polZ = PolF.affine(setup.vk_proof.polZ);

    function log2( V )
    {
        return( ( ( V & 0xFFFF0000 ) !== 0 ? ( V &= 0xFFFF0000, 16 ) : 0 ) | ( ( V & 0xFF00FF00 ) !== 0 ? ( V &= 0xFF00FF00, 8 ) : 0 ) | ( ( V & 0xF0F0F0F0 ) !== 0 ? ( V &= 0xF0F0F0F0, 4 ) : 0 ) | ( ( V & 0xCCCCCCCC ) !== 0 ? ( V &= 0xCCCCCCCC, 2 ) : 0 ) | ( ( V & 0xAAAAAAAA ) !== 0 ) );
    }
}

function calculateEncriptedValuesAtT(setup, circuit) {
    setup.vk_proof.A = [];
    setup.vk_proof.B = [];
    setup.vk_proof.C = [];
    setup.vk_proof.Ap = [];
    setup.vk_proof.Bp = [];
    setup.vk_proof.Cp = [];
    setup.vk_proof.Kp = [];
    setup.vk_verifier.A = [];

    setup.toxic.ka = F.random();
    setup.toxic.kb = F.random();
    setup.toxic.kc = F.random();
    setup.toxic.kbeta = F.random();
    setup.toxic.kgamma = F.random();

    const gb = F.mul(setup.toxic.kbeta, setup.toxic.kgamma);

    setup.vk_verifier.vk_a = G2.affine(G2.mulScalar( G2.g, setup.toxic.ka));
    setup.vk_verifier.vk_b = G1.affine(G1.mulScalar( G1.g, setup.toxic.kb));
    setup.vk_verifier.vk_c = G2.affine(G2.mulScalar( G2.g, setup.toxic.kc));
    setup.vk_verifier.vk_gb_1 = G1.affine(G1.mulScalar( G1.g, gb));
    setup.vk_verifier.vk_gb_2 = G2.affine(G2.mulScalar( G2.g, gb));
    setup.vk_verifier.vk_g = G2.affine(G2.mulScalar( G2.g, setup.toxic.kgamma));

    for (let s=0; s<circuit.nVars; s++) {

        // A[i] = G1 * polA(t)
        const at = F.affine(PolF.eval(setup.vk_proof.polsA[s], setup.toxic.t));
        const A = G1.affine(G1.mulScalar(G1.g, at));

        setup.vk_proof.A.push(A);

        if (s <= setup.vk_proof.nPublic) {
            setup.vk_verifier.A.push(A);
        }


        // B1[i] = G1 * polB(t)
        const bt = F.affine(PolF.eval(setup.vk_proof.polsB[s], setup.toxic.t));
        const B1 = G1.affine(G1.mulScalar(G1.g, bt));

        // B2[i] = G2 * polB(t)
        const B2 = G2.affine(G2.mulScalar(G2.g, bt));

        setup.vk_proof.B.push(B2);

        // C[i] = G1 * polC(t)
        const ct = F.affine(PolF.eval(setup.vk_proof.polsC[s], setup.toxic.t));
        const C = G1.affine(G1.mulScalar( G1.g, ct));
        setup.vk_proof.C.push (C);

        // K = G1 * (A+B+C)

        const kt = F.affine(F.add(F.add(at, bt), ct));
        const K = G1.affine(G1.mulScalar( G1.g, kt));



        const Ktest = G1.affine(G1.add(G1.add(A, B1), C));

        if (!G1.equals(K, Ktest)) {
            console.log ("=====FAIL======");
        }



        setup.vk_proof.Ap.push(G1.affine(G1.mulScalar(A, setup.toxic.ka)));
        setup.vk_proof.Bp.push(G1.affine(G1.mulScalar(B1, setup.toxic.kb)));
        setup.vk_proof.Cp.push(G1.affine(G1.mulScalar(C, setup.toxic.kc)));
        setup.vk_proof.Kp.push(G1.affine(G1.mulScalar(K, setup.toxic.kbeta)));
    }

    setup.vk_verifier.vk_z = G2.affine(G2.mulScalar(
        G2.g,
        PolF.eval(setup.vk_proof.polZ, setup.toxic.t)));
}

function calculateHexps(setup, circuit) {
    let maxA = 0;
    let maxB = 0;
    let maxC = 0;
    for (let s=0; s<circuit.nVars; s++) {
        maxA = Math.max(maxA, setup.vk_proof.polsA[s].length);
        maxB = Math.max(maxB, setup.vk_proof.polsB[s].length);
        maxC = Math.max(maxC, setup.vk_proof.polsC[s].length);
    }

    let maxFull = Math.max(maxA + maxB - 1, maxC);

    const maxH = maxFull - setup.vk_proof.polZ.length + 1;

    setup.vk_proof.hExps = new Array(maxH);
    setup.vk_proof.hExps[0] = G1.g;
    let eT = setup.toxic.t;
    for (let i=1; i<maxH; i++) {
        setup.vk_proof.hExps[i] = G1.affine(G1.mulScalar(G1.g, eT));
        eT = F.mul(eT, setup.toxic.t);
    }
}
/*
function unrat(p) {
    const res = new Array(p.length);
    for (let i=0; i<p.length; i++) {
        res[i] = RatPolF.F.toF(p[i]);
    }
    return res;
}
*/
