/*
 * 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.product;

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.ProductProofMessage;
import com.scytl.ov.mixing.commons.beans.proofs.SingleValueProductProofAnswer;
import com.scytl.ov.mixing.commons.beans.proofs.SingleValueProductProofInitialMessage;
import com.scytl.ov.mixing.commons.configuration.MatrixDimensions;
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.ExponentTools;
import com.scytl.ov.mixing.commons.tools.MultiExponentiation;
import com.scytl.ov.mixing.commons.tools.RandomOracleHash;
import com.scytl.ov.mixing.proofs.bg.hadamard.HadamardProductProofGenerator;
import com.scytl.ov.mixing.proofs.bg.hadamard.ParallelHadamardProductProofGenerator;
import com.scytl.ov.mixing.proofs.bg.shuffle.FirstCommitmentGenerator;
import com.scytl.ov.mixing.proofs.bg.shuffle.SecondCommitmentGenerator;
import com.scytl.ov.mixing.proofs.bg.shuffle.ShuffleProofGenerator;
import com.scytl.ov.mixing.proofs.bg.singlevalprod.ParallelSingleValueProductProofGenerator;
import com.scytl.ov.mixing.proofs.bg.singlevalprod.SingleValueProductProofGenerator;
import com.scytl.ov.mixing.proofs.bg.state.StateHarvester;

/**
 * Implements the parallel mode.
 */
public class ParallelProductProofGenerator implements ProductProofGenerator {

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

    private final CommitmentParams params;

    private final int m;

    private final int n;

    private final BigInteger order;

    private final SingleValueProductProofGenerator singleValueProofGenerator;

    private final HadamardProductProofGenerator hadamardProductProofGenerator;

    private final MultiExponentiation multiExponentiation;

    public ParallelProductProofGenerator(final MatrixDimensions dimensions, final CommitmentParams params,
            final BigInteger order, final MultiExponentiation multiExponentiation) {

        this.m = dimensions.getNumberOfRows();
        this.n = dimensions.getNumberOfColumns();
        this.params = params;
        this.order = order;

        this.multiExponentiation = multiExponentiation;

        this.singleValueProofGenerator =
            new ParallelSingleValueProductProofGenerator(this.n, this.params, this.order, this.multiExponentiation);

        this.hadamardProductProofGenerator =
            new ParallelHadamardProductProofGenerator(this.m, this.n, order, this.params, this.multiExponentiation);

    }

    @Override
    public void generate(final StateHarvester harvester) {

        LOG.info("\t PP Preparing Ca...");
        prepareCa(harvester);
        LOG.info("\t PP Preparing Cb...");
        prepareCb(harvester);

        LOG.info("\t PP Generating the single value proof...");
        singleValueProofGenerator.generate(harvester);
        LOG.info("\t PP Single value proof has been generated successfully.");

        LOG.info("\t PP Generating the hadamard product proof...");
        hadamardProductProofGenerator.generate(harvester);
        LOG.info("\t PP Hadamard product proof has been generated successfully.");

        collectProductProofMessage(harvester);
    }

    private void prepareCa(final StateHarvester harvester) {

        try {

            PrivateCommitment[] privateCommitments = new PrivateCommitment[m];

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

            final PrivateCommitment[] privateCommitmentsA =
                harvester.offer(FirstCommitmentGenerator.Produced.PRIVATE_COMMITS, PrivateCommitment[].class);

            final PrivateCommitment[] privateCommitmentsB =
                harvester.offer(SecondCommitmentGenerator.Produced.PRIVATE_COMMITS, PrivateCommitment[].class);

            final Exponent challengeY = randomOracle.getHash();

            final PrivateCommitment[] privateCommitmentsCd = new PrivateCommitment[m];

            for (int i = 0; i < privateCommitmentsCd.length; i++) {
                privateCommitmentsCd[i] =
                    privateCommitmentsA[i].exponentiate(challengeY).multiply(privateCommitmentsB[i]);
            }

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

            randomOracle.reset();

            final Exponent[] minusZ = new Exponent[n];

            for (int i = 0; i < n; i++) {
                minusZ[i] = challengeZ.negate();
            }

            final PrivateCommitment[] cMinusZ = new PrivateCommitment[m];
            Exponent zero = new Exponent(order, BigInteger.ZERO);
            for (int i = 0; i < m; i++) {
                cMinusZ[i] = new PrivateCommitment(minusZ, zero, params, multiExponentiation);
            }

            for (int i = 0; i < m; i++) {
                privateCommitments[i] = privateCommitmentsCd[i].multiply(cMinusZ[i]);
            }

            // handle single dimensional array (m=1)
            if (m == 1) {

                PrivateCommitment existingPrivateCommitment = privateCommitments[0];

                Exponent[] exponents = ExponentTools.get1Vector(n, params.getGroup().getQ());

                PrivateCommitment privateCommitmentToBeAppended =
                    new PrivateCommitment(exponents, new Exponent(params.getGroup().getQ(), BigInteger.ZERO), params,
                        multiExponentiation);

                PrivateCommitment[] privateCommitmentsUpdated = new PrivateCommitment[2];
                privateCommitmentsUpdated[0] = existingPrivateCommitment;
                privateCommitmentsUpdated[1] = privateCommitmentToBeAppended;

                privateCommitments = privateCommitmentsUpdated;
            }

            harvester.collect(Produced.PRIVATE_COMMITS_Ca, privateCommitments);

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

    }

    private void prepareCb(final StateHarvester harvester) {

        try {

            PrivateCommitment cB;

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

            final Exponent[][] a = new Exponent[m][n];
            for (int i = 0; i < m; i++) {
                a[i] = firstPrivateCommitments[i].getM();
            }

            final Exponent[] intermediateA = new Exponent[n];
            // compute cb as the commitment to the product of a's
            Exponent one = new Exponent(order, BigInteger.ONE);
            for (int i = 0; i < n; i++) {
                intermediateA[i] = one;
                for (int j = 0; j < m; j++) {
                    intermediateA[i] = intermediateA[i].multiply(a[j][i]);
                }
            }

            final Exponent s = ExponentTools.getRandomExponent(order);

            cB = new PrivateCommitment(intermediateA, s, params, multiExponentiation);

            final PublicCommitment publicCb = cB.makePublicCommitment();
            harvester.collect(Produced.PUBLIC_COMMITMENT_Cb, publicCb);
            harvester.collect(Produced.PRIVATE_COMMITS_Cb, cB);

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

    private static void collectProductProofMessage(final StateHarvester harvester) {

        final SingleValueProductProofInitialMessage iniSVA = harvester
            .offer(SingleValueProductProofGenerator.Produced.INITIAL_MSG, SingleValueProductProofInitialMessage.class);

        final SingleValueProductProofAnswer ansSVA =
            harvester.offer(SingleValueProductProofGenerator.Produced.ANSWER, SingleValueProductProofAnswer.class);

        final HadamardProductProofInitialMessage iniHPA = harvester
            .offer(HadamardProductProofGenerator.Produced.INITIAL_MSG, HadamardProductProofInitialMessage.class);

        final HadamardProductProofAnswer ansHPA =
            harvester.offer(HadamardProductProofGenerator.Produced.ANSWER, HadamardProductProofAnswer.class);

        final PublicCommitment publicCb = harvester.offer(Produced.PUBLIC_COMMITMENT_Cb, PublicCommitment.class);

        harvester.collect(Produced.PRODUCT_PROOF_MSG,
            new ProductProofMessage(publicCb, iniSVA, ansSVA, iniHPA, ansHPA));
    }
}
