/*
 * Copyright 2018 Scytl Secure Electronic Voting SA
 *
 * All rights reserved
 *
 * See our extended copyright notice in *file 'Copyright.txt' which is part of this source code package
 */
package com.scytl.ov.mixing.proofs.bg.hadamard;

import java.math.BigInteger;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.mathematical.groups.impl.Exponent;
import com.scytl.ov.mixing.commons.beans.proofs.HadamardProductProofAnswer;
import com.scytl.ov.mixing.commons.beans.proofs.HadamardProductProofInitialMessage;
import com.scytl.ov.mixing.commons.beans.proofs.ZeroProofAnswer;
import com.scytl.ov.mixing.commons.beans.proofs.ZeroProofInitialMessage;
import com.scytl.ov.mixing.commons.proofs.bg.commitments.CommitmentParams;
import com.scytl.ov.mixing.commons.proofs.bg.commitments.PrivateCommitment;
import com.scytl.ov.mixing.commons.proofs.bg.commitments.PublicCommitment;
import com.scytl.ov.mixing.commons.tools.CommitmentTools;
import com.scytl.ov.mixing.commons.tools.ExponentTools;
import com.scytl.ov.mixing.commons.tools.MultiExponentiation;
import com.scytl.ov.mixing.commons.tools.RandomOracleHash;
import com.scytl.ov.mixing.proofs.bg.product.ProductProofGenerator;
import com.scytl.ov.mixing.proofs.bg.state.StateHarvester;
import com.scytl.ov.mixing.proofs.bg.zero.ParallelZeroProofGenerator;
import com.scytl.ov.mixing.proofs.bg.zero.ZeroProofGenerator;

/**
 * Defines the parallel model.
 */
public class ParallelHadamardProductProofGenerator implements HadamardProductProofGenerator {

    private static final Logger LOG = LoggerFactory.getLogger("std");

    private final int n;

    private final int m;

    private final ZeroProofGenerator zeroProofGenerator;

    private final BigInteger order;

    private final CommitmentParams comParams;

    private final MultiExponentiation multiExponentiation;

    public ParallelHadamardProductProofGenerator(final int m, final int n, final BigInteger order,
            final CommitmentParams comParams, final MultiExponentiation multiExponentiation) {

        this.n = n;
        this.m = m;
        this.comParams = comParams;
        this.order = order;

        this.multiExponentiation = multiExponentiation;
		this.zeroProofGenerator = new ParallelZeroProofGenerator(this.m, this.n, this.order, this.comParams,
				this.multiExponentiation);

    }

    @Override
    public void generate(final StateHarvester harvester) {

        harvester.collect(Produced.RANDOM_ORACLE, new RandomOracleHash(order));

        LOG.info("\t PP HPP Generating initial message...");
        initialMessage(harvester);

        preparePrivateCommitmentsD(harvester);

        LOG.info("\t PP HPP Generating answer...");
        computeAnswer(harvester);
    }

    private void initialMessage(final StateHarvester harvester) {

        final PrivateCommitment[] ca =
            harvester.offer(ProductProofGenerator.Produced.PRIVATE_COMMITS_Ca, PrivateCommitment[].class);

        PublicCommitment statementCb =
            harvester.offer(ProductProofGenerator.Produced.PUBLIC_COMMITMENT_Cb, PublicCommitment.class);

        final PublicCommitment[] statementCa = CommitmentTools.makePublic(ca);
        harvester.collect(Produced.PUBLIC_COMMITMENTS_CA, statementCa);

        final PrivateCommitment cb =
            harvester.offer(ProductProofGenerator.Produced.PRIVATE_COMMITS_Cb, PrivateCommitment.class);

        final HadamardProductProofInitialMessage ini;
        final Exponent[][] exponentsMatrixB;
        final Exponent[] exponentsS;

        if (m == 1) {
            final PrivateCommitment[] cIntermediateB = new PrivateCommitment[2];
            cIntermediateB[0] = ca[0];
            cIntermediateB[1] = cb;
            ini = new HadamardProductProofInitialMessage(cIntermediateB);

            exponentsS = new Exponent[2];
            exponentsMatrixB = new Exponent[2][n];

            exponentsMatrixB[0] = ca[0].getM();
            exponentsS[0] = ca[0].getR();
            exponentsMatrixB[1] = cb.getM();
            exponentsS[1] = cb.getR();

        } else {

            // Define b_1,...,b_m as described in the paper
            exponentsMatrixB = new Exponent[m][n];
            exponentsMatrixB[0] = ca[0].getM();
            for (int i = 1; i < m; i++) {
                exponentsMatrixB[i] = ExponentTools.hadamardProduct(exponentsMatrixB[i - 1], ca[i].getM());
            }

            // get s1,..,sm-2 at random, fix s0, sm-1 as in the
            // paper
            exponentsS = ExponentTools.getVectorRandomExponent(m, order);
            exponentsS[0] = ca[0].getR();
            exponentsS[m - 1] = cb.getR();

            final PrivateCommitment[] cIntermediateB = new PrivateCommitment[m];
            cIntermediateB[0] = ca[0];
            for (int i = 1; i < cIntermediateB.length - 1; i++) {
                cIntermediateB[i] = new PrivateCommitment(exponentsMatrixB[i], exponentsS[i], comParams,
                    multiExponentiation);

            }
            cIntermediateB[m - 1] = cb;

            ini = new HadamardProductProofInitialMessage(cIntermediateB);
        }

        harvester.collect(Produced.INITIAL_MSG, ini);
        harvester.collect(Produced.EXPONENTS_MATRIX_B, exponentsMatrixB);
        harvester.collect(Produced.EXPONENTS_S, exponentsS);

        final RandomOracleHash randomOracle = harvester.offer(Produced.RANDOM_ORACLE, RandomOracleHash.class);

        randomOracle.addDataToRO(statementCa);
        randomOracle.addDataToRO(statementCb);
        randomOracle.addDataToRO(ini);

    }

    private void preparePrivateCommitmentsD(final StateHarvester harvester) {

        try {

            final Exponent[][] exponentsMatrixB = harvester.offer(Produced.EXPONENTS_MATRIX_B, Exponent[][].class);
            final Exponent[] exponentsS = harvester.offer(Produced.EXPONENTS_S, Exponent[].class);

            final RandomOracleHash randomOracle = harvester.offer(Produced.RANDOM_ORACLE, RandomOracleHash.class);

            // computes the values d0 ... dm-2, d (stored in position dm-1)
            final Exponent challengeX = randomOracle.getHash();

            randomOracle.addDataToRO("1");
            final Exponent challengeInnerProduct = randomOracle.getHash();

            int mSetToTwo = m;

            if (m == 1) {
                mSetToTwo = 2;
            }

            final Exponent[][] d = new Exponent[mSetToTwo][n];
            final Exponent[] t = new Exponent[mSetToTwo];

            Exponent acumulator = challengeX;
            Exponent[] auxD = ExponentTools.get0Vector(n, order);
            Exponent auxT = new Exponent(order, BigInteger.ZERO);

            for (int i = 0; i < mSetToTwo - 1; i++) {
                // compute d and t
                d[i] = ExponentTools.multiplyByScalar(exponentsMatrixB[i], acumulator);
                t[i] = exponentsS[i].multiply(acumulator);

                auxD = ExponentTools.addTwoVectors(auxD,
                    ExponentTools.multiplyByScalar(exponentsMatrixB[i + 1], acumulator));
                auxT = auxT.add(exponentsS[i + 1].multiply(acumulator));
                acumulator = acumulator.multiply(challengeX);
            }
            d[mSetToTwo - 1] = auxD;
            t[mSetToTwo - 1] = auxT;

            final PrivateCommitment[] cD = new PrivateCommitment[mSetToTwo];
            for (int i = 0; i < cD.length; i++) {
                cD[i] = new PrivateCommitment(d[i], t[i], comParams, multiExponentiation);
            }

            harvester.collect(Produced.CHALLENGE_INNER_PRODUCT, challengeInnerProduct);
            harvester.collect(Produced.PRIVATE_COMMITMENTS_D, cD);

        } catch (GeneralCryptoLibException e) {
            throw new CryptoLibException(e);
        }
    }

    private void computeAnswer(final StateHarvester harvester) {

        zeroProofGenerator.generate(harvester);
        LOG.info("\t PP HPP Zero proof has been generated successfully");

        final ZeroProofInitialMessage init =
            harvester.offer(ZeroProofGenerator.Produced.INITIAL_MSG, ZeroProofInitialMessage.class);

        final ZeroProofAnswer answer = harvester.offer(ZeroProofGenerator.Produced.ANSWER, ZeroProofAnswer.class);

        harvester.collect(Produced.ANSWER, //
            new HadamardProductProofAnswer(init, answer));

    }

}
