/*
 * 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.channel.cc.mixing.service.conversion;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.KeyManagementException;
import java.util.Arrays;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Collectors;

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

import com.fasterxml.jackson.databind.ObjectMapper;
import com.googlecode.jcsv.reader.CSVReader;
import com.googlecode.jcsv.reader.internal.CSVReaderBuilder;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.bean.CiphertextImpl;
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.products.ov.channel.cc.commons.keymanagement.CompiledBatchResultsException;
import com.scytl.products.ov.commons.beans.domain.model.vote.EncryptedVote;
import com.scytl.products.ov.commons.beans.domain.model.vote.StringEncryptedVote;
import com.scytl.products.ov.commons.dto.mixing.MixingPayload;
import com.scytl.products.ov.commons.dto.mixing.MixingPayloadImpl;
import com.scytl.products.ov.mixnet.commons.beans.VoteWithProof;
import com.scytl.products.ov.mixnet.commons.configuration.locations.LocationsProvider;
import com.scytl.products.ov.mixnet.commons.configuration.locations.MixerLocationsProvider;

/**
 * Converts the results of mixing, in this case a set of
 * {@link java.io.InputStream}s contained in a {@link LocationsProvider} object,
 * into a {@link com.scytl.products.ov.commons.dto.MixingDTO}.
 */
public class LocationsProviderMixingOutputConverter implements MixingOutputConverter {

    private static final Logger LOG = LoggerFactory.getLogger(LocationsProviderMixingOutputConverter.class);

    /**
     * Compiles the results of all batches resulting of a mixing and decryption
     * process.
     * <P>
     * *********************************************************************************************
     * <P>
     * NOTE: Please keep in mind that this method works on the assumption that
     * the batch size has been set so that only one batch is ever produced.
     * <P>
     * *********************************************************************************************
     * <P>
     *
     * @param inputPayload
     *            a {@link MixingPayload} with the input that was fed to the
     *            mixing process.
     * @param mixingOutput
     *            a {@link LocationsProvider} object with the locations of the
     *            mixing results.
     * @param remainingVoteEncryptionKeySupplier
     *            a supplier for the vote encryption key that results from
     *            removing the used part. A supplier is used so that the
     *            relatively expensive operation of removing the used part from
     *            the key is not executed until it is necessary.
     */
    @Override
    public MixingPayload convert(MixingPayload inputPayload, Object mixingOutput,
            Supplier<ElGamalPublicKey> remainingVoteEncryptionKeySupplier)
            throws ConverterException {
        if (!(mixingOutput instanceof LocationsProvider)) {
            throw new IllegalArgumentException("The mixing output must be a LocationsProvider instance");
        }

        LocationsProvider locationsProvider = (LocationsProvider) mixingOutput;

        // Not zero-based, for some unfathomable reason.
        int batchNum = 1;

        // Gather the results from each of the batches.
        MixerLocationsProvider mixerLocationsProvider = locationsProvider.getMixerLocationsProvider();
        try {
            List<EncryptedVote> shuffledVoteSet =
                getList(mixerLocationsProvider.getReEncryptedBallotsOutputOfBatch(batchNum)).stream()
                    .filter(voteString -> !voteString.isEmpty()).map(StringEncryptedVote::new)
                    .collect(Collectors.toList());

            String shuffleProof = getFromStream(mixerLocationsProvider.getProofsOutputOfBatch(batchNum));

            List<EncryptedVote> decryptedVoteSet =
                getList(mixerLocationsProvider.getDecryptedVotesOutputOfBatch(batchNum)).stream()
                    .filter(voteString -> !voteString.isEmpty()).map(StringEncryptedVote::new)
                    .collect(Collectors.toList());

            List<String> commitmentParameters =
                getList(mixerLocationsProvider.getCommitmentParametersOutputOfBatch(batchNum));

            // Extract the decryption proofs from the VoteWithProof structures.
            // Get the votes with proofs CSV string.
            String votesWithProofString =
                getFromStream(mixerLocationsProvider.getVotesWithProofOutputOfBatch(batchNum));
            // Convert the CSV string into a list of votes with proofs.
            List<VoteWithProof> votesWithProof = getVoteWithProofReader(votesWithProofString).readAll();
            // Collect the decryption proofs.
            List<String> decryptionProofs =
                votesWithProof.stream().map(VoteWithProof::getProof).collect(Collectors.toList());

            return buildOutputPayload(inputPayload, decryptedVoteSet, decryptionProofs, shuffledVoteSet, shuffleProof,
                commitmentParameters, remainingVoteEncryptionKeySupplier.get());

        } catch (IOException | KeyManagementException e) {
            throw new ConverterException(e);
        }
    }

    /**
     * Converts the output of the mixing/decryption to an output mixing payload.
     *
     * @param inputPayload
     *            the DTO that was used as the input of the mixing/decryption
     * @return an output DTO
     */
    private MixingPayload buildOutputPayload(MixingPayload inputPayload, List<EncryptedVote> decryptedVoteSet,
            List<String> decryptionProofs, List<EncryptedVote> shuffledVoteSet, String shuffleProof,
            List<String> commitmentParameters, ElGamalPublicKey remainingVoteEncryptionKey)
            throws KeyManagementException {

        // Validate the results of the mixing in relation with its input.
        validateVotes(inputPayload.getVotes(), decryptedVoteSet, decryptionProofs);

        // Get the mixing and decryption output.
        LOG.debug("Building an output DTO with the results of the mixing...");

        // Return a new DTO created from the input and the results of the mixing
        // and decryption.
        return new MixingPayloadImpl(inputPayload, remainingVoteEncryptionKey, decryptedVoteSet, decryptionProofs,
            shuffledVoteSet, shuffleProof, commitmentParameters);
    }

    /**
     * Get the contents of a stream into a string.
     *
     * @param outputOfBatch
     *            the output stream the string was written to
     * @return a string with the contents of the stream
     */
    private String getFromStream(OutputStream outputOfBatch) throws UnsupportedEncodingException {
        return ((ByteArrayOutputStream) outputOfBatch).toString(StandardCharsets.UTF_8.name());
    }

    /**
     * Get an output stream resulting from a batch operation as a list of
     * strings.
     *
     * @param outputOfBatch
     *            the output of the batch
     * @return a list of strings with the contents of the output stream
     */
    private List<String> getList(OutputStream outputOfBatch) throws UnsupportedEncodingException {
        return Arrays.asList(getFromStream(outputOfBatch).split("\r\n|\r|\n"));
    }

    /**
     * Provides a translator that can deserialise votes with proofs in CSV
     * format.
     *
     * @param votesWithProofString
     *            the CSV string
     * @return a list of votes with proof
     */
    private CSVReader<VoteWithProof> getVoteWithProofReader(String votesWithProofString) {
        final ObjectMapper objectMapper = new ObjectMapper();

        return new CSVReaderBuilder<VoteWithProof>(new StringReader(votesWithProofString)).entryParser(fields -> {
            try {
                // Get the Zp group elements in the first field.
                ZpGroupElement[] zpges = objectMapper.readValue(fields[0], ZpGroupElement[].class);
                // Gamma is the first element in the array.
                ZpGroupElement gamma = zpges[0];
                // The rest of elements are phis.
                ZpGroupElement[] phis = new ZpGroupElement[zpges.length - 1];
                System.arraycopy(zpges, 1, phis, 0, zpges.length - 1);
                // Create the ciphertext from the gamma and the phis.
                Ciphertext ciphertext = new CiphertextImpl(gamma, Arrays.asList(phis));

                // Get the array of numbers in the second field.
                BigInteger[] decryptedVotes = objectMapper.readValue(fields[1], BigInteger[].class);

                // Get the proof JSON in the third field.
                String proofJson = fields[2];
                return new VoteWithProof(ciphertext, Arrays.asList(decryptedVotes), proofJson);
            } catch (GeneralCryptoLibException | IOException e) {
                throw new CompiledBatchResultsException("Error trying to get vote with proof reader.", e);
            }
        }).build();
    }

    /**
     * Validates the results of the mixing in relation to its input.
     *
     * @param inputVotes
     *            the votes fed as input to the mixing process
     * @param decryptedVotes
     *            the votes that resulted from the mixing
     * @param decryptedProofs
     *            the proofs that resulted from the mixing
     */
    private void validateVotes(List<EncryptedVote> inputVotes, List<EncryptedVote> decryptedVotes,
            List<String> decryptedProofs) {
        // Ensure the same number of votes that went in comes out.
        int inputVotesSize = inputVotes.size();
        if (!(inputVotesSize == decryptedVotes.size()) && (inputVotesSize == decryptedProofs.size())) {
            throw new IllegalStateException(
                "The number of vote-related elements between the input and the output is not consistent");
        }
    }
}
