/*
 * 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.products.ov.mixnet.proofs.bg.multiexp.basic;

import java.math.BigInteger;

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

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.bean.CiphertextImpl;
import com.scytl.cryptolib.elgamal.cryptoapi.Ciphertext;
import com.scytl.cryptolib.mathematical.groups.impl.Exponent;
import com.scytl.products.ov.mixnet.commons.beans.proofs.MultiExponentiationBasicProofInitialMessage;
import com.scytl.products.ov.mixnet.commons.homomorphic.Randomness;
import com.scytl.products.ov.mixnet.commons.proofs.bg.commitments.CommitmentParams;
import com.scytl.products.ov.mixnet.commons.proofs.bg.commitments.PrivateCommitment;
import com.scytl.products.ov.mixnet.commons.proofs.bg.commitments.PublicCommitment;
import com.scytl.products.ov.mixnet.commons.proofs.bg.config.ProofsGeneratorConfigurationParams;
import com.scytl.products.ov.mixnet.commons.proofs.bg.multiexp.basic.InitialMessageHelper;
import com.scytl.products.ov.mixnet.commons.tools.ExponentTools;
import com.scytl.products.ov.mixnet.commons.tools.MultiExponentiation;
import com.scytl.products.ov.mixnet.commons.tools.RandomOracleHash;
import com.scytl.products.ov.mixnet.proofs.bg.multiexp.ComputeAllE;
import com.scytl.products.ov.mixnet.proofs.bg.multiexp.MultiExponentiationProofGenerator;
import com.scytl.products.ov.mixnet.proofs.bg.shuffle.SecondCommitmentGenerator;
import com.scytl.products.ov.mixnet.proofs.bg.state.CommonStateLabels;
import com.scytl.products.ov.mixnet.proofs.bg.state.StateHarvester;

/**
 * Implements the parallel mode.
 */
public class ParallelBasicInitialMessageGenerator implements BasicInitialMessageGenerator {

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

    private final ProofsGeneratorConfigurationParams config;

    private final CommitmentParams comParams;

    private final MultiExponentiation multiExponentiation;

    private final ComputeAllE computeAllE;

    private final int concurrencyLevel;

    public ParallelBasicInitialMessageGenerator(final ProofsGeneratorConfigurationParams config,
            final CommitmentParams comParams, final MultiExponentiation multiExponentiation,
            final ComputeAllE computeAllE, final int concurrencyLevel) {

        this.config = config;
        this.comParams = comParams;

        this.multiExponentiation = multiExponentiation;
        this.computeAllE = computeAllE;
        this.concurrencyLevel = concurrencyLevel;
    }

    @Override
    public void generate(final StateHarvester harvester) throws GeneralCryptoLibException {

        LOG.debug("\t MEP B IM preparing...");
        prepare(harvester);

        LOG.debug("\t MEP B IM calculating Ca0...");
        calculateCA0(harvester);

        LOG.debug("\t MEP B IM calculating private commitments...");
        calculatePrivateCommitments(harvester);

        LOG.debug("\t MEP B IM calculate E...");
        calculateE(harvester);

        LOG.debug("\t MEP B IM collecting initial message...");
        collectInitialMessage(harvester);
    }

    private void prepare(final StateHarvester harvester) {

        final BigInteger order = config.getZpGroupParams().getQ();

        // b
        final Ciphertext[][] ciphertexts = harvester.offer(CommonStateLabels.CIPHERTEXTS, Ciphertext[][].class);
        final int m = ciphertexts.length;
        final int n = ciphertexts[0].length;
        Exponent[] b = InitialMessageHelper.calculateExponentsMultiarrayB(m, order);
        harvester.collect(Produced.EXPONENTS_MULTIARR_B, b);

        // a0
        Exponent[] a0 = ExponentTools.getVectorRandomExponent(n, order);
        harvester.collect(Produced.EXPONENTS_A0, a0);
    }

    private void calculateCA0(final StateHarvester harvester) {

        final BigInteger order = config.getZpGroupParams().getQ();
        final Exponent r0 = ExponentTools.getRandomExponent(order);
        harvester.collect(Produced.EXPONENTS_R0, r0);

        final Exponent[] a0 = harvester.offer(Produced.EXPONENTS_A0, Exponent[].class);

        PrivateCommitment cA0 = new PrivateCommitment(a0, r0, comParams, multiExponentiation);

        harvester.collect(Produced.PRIVATE_COMMIT_BASIC_MEXP, cA0);
    }

    private void calculatePrivateCommitments(final StateHarvester harvester) {

        final Ciphertext[][] ciphertexts = harvester.offer(CommonStateLabels.CIPHERTEXTS, Ciphertext[][].class);

        final int m = ciphertexts.length;
        final BigInteger order = config.getZpGroupParams().getQ();

        // s
        final Exponent[] s = InitialMessageHelper.calculateExponentsArrayS(m, order);
        harvester.collect(Produced.EXPONENTS_ARR_S, s);

        final Exponent[] b = harvester.offer(Produced.EXPONENTS_MULTIARR_B, Exponent[].class);
        // cB
        PrivateCommitment[] cB = InitialMessageHelper.calculatePrivateCommitments(m, b, s, comParams,
            multiExponentiation);

        harvester.collect(Produced.PRIVATE_COMMITS_BASIC_MEXP, cB);
    }

    private void calculateE(final StateHarvester harvester) {

        final Ciphertext[][] ciphertexts = harvester.offer(CommonStateLabels.CIPHERTEXTS, Ciphertext[][].class);

        final int m = ciphertexts.length;

        // tau
        final Randomness rho =
            harvester.offer(MultiExponentiationProofGenerator.Produced.CALCULATED_RANDOM_EXPONENT, Randomness.class);
        final Randomness[] tau = config.getCryptosystem().getVectorFreshRandomness(2 * m);
        tau[m] = rho;
        harvester.collect(Produced.RANDOMNESS_TAU_ARR, tau);

        // E - initial

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

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

        final Exponent[] a0 = harvester.offer(Produced.EXPONENTS_A0, Exponent[].class);

        Ciphertext[] initialE =
            computeAllE.computeAllENoRandomizing(a, ciphertexts, config.getZpGroupParams().getQ());

        harvester.collect(Produced.EXPONENTS_ARR, a);

        initialE = computeAllE.addA0FactorsIncludingE0(initialE, ciphertexts, a0);

        // E
        final Exponent[] b = harvester.offer(Produced.EXPONENTS_MULTIARR_B, Exponent[].class);

        Ciphertext[] cE = computeAllE.addRandomizingEncryption(initialE, //
            b, tau, config.getCryptosystem(), concurrencyLevel);

        harvester.collect(Produced.E, cE);
    }

    private static void collectInitialMessage(final StateHarvester harvester) throws GeneralCryptoLibException {

        LOG.debug("\t MEP B IM C harvester cA0");
        final PrivateCommitment cA0 = harvester.offer(Produced.PRIVATE_COMMIT_BASIC_MEXP, PrivateCommitment.class);
        LOG.debug("\t MEP B IM C harvester cB");

        final PrivateCommitment[] cB = harvester.offer(Produced.PRIVATE_COMMITS_BASIC_MEXP, PrivateCommitment[].class);
        LOG.debug("\t MEP B IM C harvester E");
        final CiphertextImpl[] cE = harvester.offer(Produced.E, CiphertextImpl[].class);

        LOG.debug("\t MEP B IM C Generating MEBPIM.");
        final MultiExponentiationBasicProofInitialMessage ini =
            new MultiExponentiationBasicProofInitialMessage(cA0, cB, cE);

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

        final PublicCommitment[] statementCa =
            harvester.offer(SecondCommitmentGenerator.Produced.PUBLIC_COMMITS, PublicCommitment[].class);

        final Ciphertext[][] ciphertexts = harvester.offer(CommonStateLabels.CIPHERTEXTS, Ciphertext[][].class);

        CiphertextImpl statementC = cE[ciphertexts.length];

        randomOracle.addDataToRO(ciphertexts);
        randomOracle.addDataToRO(statementC);
        randomOracle.addDataToRO(statementCa);
        randomOracle.addDataToRO(ini);

        LOG.debug("\t MEP B IM C pushing initial message...");
        harvester.collect(Produced.BASIC_MULTIEXP_INIT_MSG, ini);
    }

}
