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

import java.math.BigInteger;

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

import com.scytl.cryptolib.elgamal.cryptoapi.Ciphertext;
import com.scytl.products.oscore.logging.api.domain.Level;
import com.scytl.products.oscore.logging.api.domain.LogContent;
import com.scytl.products.oscore.logging.api.writer.SecureLoggingWriter;
import com.scytl.products.oscore.logging.core.factory.SecureLoggingFactoryLog4j;
import com.scytl.products.oscore.logging.core.formatter.SplunkFormatter;
import com.scytl.products.oscore.logging.core.provider.TransactionInfoProvider;
import com.scytl.products.ov.mixnet.commons.beans.Permutation;
import com.scytl.products.ov.mixnet.commons.beans.proofs.ShuffleProof;
import com.scytl.products.ov.mixnet.commons.beans.proofs.ShuffleProof.Builder;
import com.scytl.products.ov.mixnet.commons.beans.proofs.ShuffleProofSecondAnswer;
import com.scytl.products.ov.mixnet.commons.constants.Constants;
import com.scytl.products.ov.mixnet.commons.exceptions.ProofGenerationException;
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.PublicCommitment;
import com.scytl.products.ov.mixnet.commons.proofs.bg.config.ProofsGeneratorConfigurationParams;
import com.scytl.products.ov.mixnet.commons.tools.MultiExponentiation;
import com.scytl.products.ov.mixnet.commons.tools.RandomOracleHash;
import com.scytl.products.ov.mixnet.infrastructure.log.MixingAndVerifyingLogEvents;
import com.scytl.products.ov.mixnet.proofs.bg.multiexp.ComputeAllE;
import com.scytl.products.ov.mixnet.proofs.bg.state.CommonStateLabels;
import com.scytl.products.ov.mixnet.proofs.bg.state.MapBasedHarvester;
import com.scytl.products.ov.mixnet.proofs.bg.state.StateHarvester;
import com.scytl.products.ov.mixnet.proofs.bg.state.StateLabel;

/**
 * Generator of the proof of knowledge of a permutation and a randomness that have been applied to a set of ciphertexts.
 * This proof generator combines a multi-exponentiation proof and a product proof.
 */
public class ShuffleProofGenerator {

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

    private final Ciphertext[][] originalCiphertexts;

    private final Ciphertext[][] reEncryptedCiphertexts;

    private final FirstCommitmentGenerator firstCommitmentGenerator;

    private final SecondCommitmentGenerator secondCommitmentGenerator;

    private final SecondAnswerGenerator secondAnswerGenerator;

    private final BigInteger groupOrder;

    private final SecureLoggingWriter loggingWriter;

    private RandomOracleHash randomOracle;

    public ShuffleProofGenerator(final ProofsGeneratorConfigurationParams config, final CommitmentParams comParams,
            final Ciphertext[][] originalCiphertexts, final Permutation permutation,
            final PrivateAndPublicCommitmentsGenerator privateAndPublicCommitmentsGenerator,
            final MultiExponentiation limMultiExpo, final int concurrencyLevel,
            final Ciphertext[][] reEncryptedCiphertexts, final Randomness[] rho, final ComputeAllE computeAllE) {

        this.loggingWriter = initializeLoggingWriter();

        validateInputs(config, originalCiphertexts, permutation, reEncryptedCiphertexts, rho);

        this.originalCiphertexts = originalCiphertexts;

        this.reEncryptedCiphertexts = reEncryptedCiphertexts;

        this.firstCommitmentGenerator = new FirstCommitmentGenerator(config, permutation, comParams,
            privateAndPublicCommitmentsGenerator, limMultiExpo, concurrencyLevel);

        this.secondCommitmentGenerator = new SecondCommitmentGenerator(config, comParams,
            privateAndPublicCommitmentsGenerator, limMultiExpo, concurrencyLevel);

        this.secondAnswerGenerator = new ParallelSecondAnswerGenerator(config, rho, reEncryptedCiphertexts, comParams,
            computeAllE, limMultiExpo, concurrencyLevel);

        this.groupOrder = config.getZpGroupParams().getQ();
    }

    private static SecureLoggingWriter initializeLoggingWriter() {

        TransactionInfoProvider transactionInfoProvider = new TransactionInfoProvider();
        transactionInfoProvider.generate(Constants.LOGGING_TENANT, Constants.LOGGING_LOCALHOST,
            Constants.LOGGING_LOCALHOST);
        SplunkFormatter splunkFormatter =
            new SplunkFormatter(Constants.LOGGING_APPLICATION, Constants.LOGGING_APPLICATION, transactionInfoProvider);
        final SecureLoggingFactoryLog4j loggerFactory = new SecureLoggingFactoryLog4j(splunkFormatter);
        return loggerFactory.getLogger(Constants.SECURE_LOGGER_NAME);
    }

    private static void validateInputs(final ProofsGeneratorConfigurationParams config,
            final Ciphertext[][] encryptedCiphertexts, final Permutation permutation,
            final Ciphertext[][] reEncryptedCiphertexts, final Randomness[] rho) {

        final int n = config.getMatrixDimensions().getNumberOfColumns();
        final int m = config.getMatrixDimensions().getNumberOfRows();

        if (encryptedCiphertexts == null) {
            throw new IllegalArgumentException("The encrypted ciphertext matrix is null");
        }

        if (permutation == null) {
            throw new IllegalArgumentException("The given permutation is null");
        }

        if (permutation.getLength() != (n * m)) {
            throw new IllegalArgumentException("The given permutation length does not match with the specified 'm*n'");
        }

        if (reEncryptedCiphertexts == null) {
            throw new IllegalArgumentException("The re-encrypted ciphertext matrix is null");
        }

        if (reEncryptedCiphertexts.length != m) {
            throw new IllegalArgumentException("Re-encrypted ciphertext matrix does not have 'm' rows");
        }

        if (reEncryptedCiphertexts[0] == null) {
            throw new IllegalArgumentException("Re-encrypted ciphertext matrix contains null elements");
        }

        if (reEncryptedCiphertexts[0].length != n) {
            throw new IllegalArgumentException("Re-encrypted ciphertext matrix does not have 'n' columns");
        }

        if (rho == null) {
            throw new IllegalArgumentException("The given array of random exponents is null");
        }

        if (rho.length != (n * m)) {
            throw new IllegalArgumentException(
                "The given random exponents length does not match with the specified 'm*n'");
        }
    }

    /**
     * Generates:
     * <ul>
     * <li>the initial message.
     * <li>the first answer.
     * <li>the second answer.
     * </ul>
     *
     * @return a {@link ShuffleProof}.
     * @throws ProofGenerationException
     */
    public ShuffleProof generate() throws ProofGenerationException {

        final StateHarvester harvester = new MapBasedHarvester();
        final ShuffleProof proof;

        try {

            harvester.collect(CommonStateLabels.ORIGINAL_CIPHERTEXTS, originalCiphertexts);
            randomOracle = new RandomOracleHash(groupOrder);
            harvester.collect(Produced.RANDOM_ORACLE, randomOracle);

            randomOracle.addDataToRO(originalCiphertexts);
            randomOracle.addDataToRO(reEncryptedCiphertexts);

            LOG.info("Generating first commitment...");
            firstCommitmentGenerator.generate(harvester);

            LOG.info("Generating second commitment...");
            secondCommitmentGenerator.generate(harvester);

            LOG.info("Generating second answer...");
            secondAnswerGenerator.generate(harvester);
            LOG.info("Shuffle proof has been generated successfully");

            proof = retrieveShuffleProofFrom(harvester);

        } catch (Exception e) {

            loggingWriter.log(Level.ERROR,
                new LogContent.LogContentBuilder()
                    .logEvent(MixingAndVerifyingLogEvents.SHUFREENCR_ERROR_GENERATING_CRYPTOGRAPHIC_PROOF_OF_MIXING)
                    .user("adminID").electionEvent("-").additionalInfo("mbb_id", "-")
                    .additionalInfo("err_desc", e.getMessage()).createLogInfo());

            throw new ProofGenerationException("Error while trying to generate the shuffle proof", e);
        }

        return proof;
    }

    private static ShuffleProof retrieveShuffleProofFrom(final StateHarvester harvester) {

        final Builder builder = new ShuffleProof.Builder();

        builder.withInitialMessage( //
            harvester.offer(FirstCommitmentGenerator.Produced.PUBLIC_COMMITS, PublicCommitment[].class));

        builder.withFirstAnswer( //
            harvester.offer(SecondCommitmentGenerator.Produced.PUBLIC_COMMITS, PublicCommitment[].class));

        builder.withSecondAnswer( //
            harvester.offer(SecondAnswerGenerator.Produced.SHUFFLE_SECOND_ANSWER, ShuffleProofSecondAnswer.class));

        return builder.build();
    }

    public enum Produced implements StateLabel {
        RANDOM_ORACLE
    }

}
