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

import java.util.ArrayList;
import java.util.List;

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.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.elgamal.cryptoapi.Ciphertext;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
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.ballots.ElGamalEncryptedBallots;
import com.scytl.products.ov.mixnet.commons.beans.Permutation;
import com.scytl.products.ov.mixnet.commons.beans.ShuffleOutput;
import com.scytl.products.ov.mixnet.commons.constants.Constants;
import com.scytl.products.ov.mixnet.commons.exceptions.ShufflerException;
import com.scytl.products.ov.mixnet.commons.homomorphic.Randomness;
import com.scytl.products.ov.mixnet.commons.homomorphic.impl.GjosteenElGamalRandomness;
import com.scytl.products.ov.mixnet.commons.homomorphic.impl.RandomnessGenerator;
import com.scytl.products.ov.mixnet.infrastructure.log.MixingAndVerifyingLogEvents;
import com.scytl.products.ov.mixnet.shuffler.permutation.PermutationGenerator;
import com.scytl.products.ov.mixnet.shuffler.permutation.Permutator;
import com.scytl.products.ov.mixnet.shuffler.reencryption.ElGamalReEncrypter;
import com.scytl.products.ov.mixnet.shuffler.reencryption.ReEncrypter;

public class ElGamalShuffler implements Shuffler<ElGamalEncryptedBallots> {

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

    private final PermutationGenerator permutationGenerator;

    private final Permutator permutator;

    private final TransactionInfoProvider transactionInfoProvider = new TransactionInfoProvider();

    private final SecureLoggingWriter loggingWriter;

    public ElGamalShuffler(final Permutator permutator, final PermutationGenerator permutationGenerator) {

        // create in configurable way
        this.permutator = permutator;

        this.permutationGenerator = permutationGenerator;

        this.transactionInfoProvider.generate(Constants.LOGGING_TENANT, Constants.LOGGING_LOCALHOST,
            Constants.LOGGING_LOCALHOST);

        SplunkFormatter splunkFormatter =
            new SplunkFormatter(Constants.LOGGING_APPLICATION, Constants.LOGGING_APPLICATION, this.transactionInfoProvider);
        final SecureLoggingFactoryLog4j loggerFactory = new SecureLoggingFactoryLog4j(splunkFormatter);
        this.loggingWriter = loggerFactory.getLogger(Constants.SECURE_LOGGER_NAME);
    }

    @Override
    public ShuffleOutput<ElGamalEncryptedBallots> sequentialShuffle(final ZpSubgroup group,
            final ElGamalPublicKey publicKey, final int concurrencyLevel,
            final ElGamalEncryptedBallots encryptedBallots, final Randomness[] batchRandomExponents,
            final List<Ciphertext> batchPreComputations) throws ShufflerException {

        int numRequiredSubkeys = encryptedBallots.getBallots().get(0).getPhis().size();
        ElGamalPublicKey keyWithRequiredNumElements = obtainKeyWithRequiredNumSubkeys(publicKey, numRequiredSubkeys);

        ReEncrypter reEncrypter = new ElGamalReEncrypter(group, keyWithRequiredNumElements, concurrencyLevel);

        final Permutation permutation;
        final ElGamalEncryptedBallots reEncryptedBallots;

        final int numBallots = encryptedBallots.getBallots().size();
        ElGamalEncryptedBallots shuffledEncryptedBallots;

        try {

            transactionInfoProvider.generate(Constants.LOGGING_TENANT, Constants.LOGGING_LOCALHOST,
                Constants.LOGGING_LOCALHOST);

            LOG.info("Generating permutation...");
            permutation = permutationGenerator.generate(numBallots);

            LOG.info("Permuting ballots...");
            final List<Ciphertext> permutatedListEncryptedBallots =
                permutator.createPermutatedList(encryptedBallots.getBallots(), permutation);

            shuffledEncryptedBallots = new ElGamalEncryptedBallots(permutatedListEncryptedBallots);

        } catch (Exception e) {

            loggingWriter.log(Level.ERROR,
                new LogContent.LogContentBuilder()
                    .logEvent(MixingAndVerifyingLogEvents.SHUFREENCR_ERROR_SHUFFLING_THE_ENCRYPTED_BALLOTS)
                    .user("adminID").electionEvent("-").additionalInfo("mbb_id", "-")
                    .additionalInfo("err_desc", e.getMessage()).createLogInfo());
            throw new ShufflerException("An error occurred while trying to shuffle the encrypted ballots", e);
        }

        final Randomness[] completedBatchRandomExponents =
            generateLackingRandomExponents(group, batchRandomExponents, numBallots);

        try {

            LOG.info("Re-encrypting ballots...");
            reEncryptedBallots =
                reEncrypter.reEncrypt(shuffledEncryptedBallots, completedBatchRandomExponents, batchPreComputations);
        } catch (Exception e) {

            loggingWriter.log(Level.ERROR,
                new LogContent.LogContentBuilder()
                    .logEvent(MixingAndVerifyingLogEvents.SHUFREENCR_ERROR_REENCRYPTING_THE_ENCRYPTED_BALLOTS)
                    .user("adminID").electionEvent("-").additionalInfo("mbb_id", "-")
                    .additionalInfo("err_desc", e.getMessage()).createLogInfo());
            throw new ShufflerException("An error occurred while trying to reencrypt the encrypted ballots", e);
        }
        return new ShuffleOutput<ElGamalEncryptedBallots>(permutation, completedBatchRandomExponents,
            reEncryptedBallots);
    }

    private static ElGamalPublicKey obtainKeyWithRequiredNumSubkeys(ElGamalPublicKey publicKey, int numRequiredSubkeys) {

        List<ZpGroupElement> keyElements = publicKey.getKeys();

        if (keyElements.size() > numRequiredSubkeys) {

            List<ZpGroupElement> reducedListKeyElements = new ArrayList<ZpGroupElement>();
            for (int i = 0; i < numRequiredSubkeys; i++) {
                reducedListKeyElements.add(keyElements.get(i));
            }

            try {
                return new ElGamalPublicKey(reducedListKeyElements, publicKey.getGroup());
            } catch (GeneralCryptoLibException e) {
                throw new CryptoLibException(e);
            }

        } else {
            return publicKey;
        }
    }

    private static Randomness[] generateLackingRandomExponents(final ZpSubgroup group, final Randomness[] randomExponents,
            final int sizeEncryptedBallots) {

        Randomness[] returnValue = new GjosteenElGamalRandomness[sizeEncryptedBallots];

        if (randomExponents == null) {
            LOG.info("Generating " + sizeEncryptedBallots + " random exponents...");
            final RandomnessGenerator randomnessGenerator = new RandomnessGenerator(group.getQ());
            returnValue = randomnessGenerator.generate(sizeEncryptedBallots);
        } else if (randomExponents.length == sizeEncryptedBallots) {
            LOG.info("Getting the preGenerated exponents...");
            // the maximum number of exponents is the batch size
            System.arraycopy(randomExponents, 0, returnValue, 0, sizeEncryptedBallots);
        } else { // there are less random exponents than required
            final int numExponentsToGenerate = sizeEncryptedBallots - randomExponents.length;
            LOG.info("Generating " + numExponentsToGenerate + " random exponents...");

            final RandomnessGenerator randomnessGenerator = new RandomnessGenerator(group.getQ());
            final Randomness[] lackingExponents = randomnessGenerator.generate(numExponentsToGenerate);

            System.arraycopy(randomExponents, 0, returnValue, 0, randomExponents.length);
            System.arraycopy(lackingExponents, 0, returnValue, randomExponents.length, numExponentsToGenerate);
        }

        return returnValue;
    }
}
