/*
 * 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.commons.dto.mixing;

import static com.scytl.products.ov.commons.safestream.StreamSerializableUtil.*;

import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.PrintWriter;
import java.math.BigInteger;
import java.time.ZonedDateTime;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import com.scytl.products.ov.commons.beans.exceptions.LambdaException;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.products.ov.commons.beans.domain.model.ballotbox.BallotBoxIdImpl;
import com.scytl.products.ov.commons.beans.domain.model.messaging.CryptolibPayloadSignature;
import com.scytl.products.ov.commons.beans.domain.model.messaging.PayloadSignature;
import com.scytl.products.ov.commons.beans.domain.model.messaging.SafeStreamDeserializationException;
import com.scytl.products.ov.commons.beans.domain.model.messaging.StreamSerializableClassType;
import com.scytl.products.ov.commons.beans.domain.model.vote.CiphertextEncryptedVote;
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.beans.domain.model.vote.VoteSetId;
import com.scytl.products.ov.commons.beans.domain.model.vote.VoteSetIdImpl;

import org.msgpack.core.MessagePacker;
import org.msgpack.core.MessageUnpacker;

@JsonInclude(value = Include.NON_NULL)
public class MixingPayloadImpl implements MixingPayload {

    private static final long serialVersionUID = 6001917254573285624L;

    private static final Executor executor = Executors.newSingleThreadExecutor();

    private ElGamalPublicKey voteEncryptionKey;

    @JsonDeserialize(contentAs = StringEncryptedVote.class)
    private List<EncryptedVote> votes;

    private String electoralAuthorityId;

    private ZpSubgroup encryptionParameters;

    @JsonDeserialize(as = VoteSetIdImpl.class)
    private VoteSetId voteSetId;

    @JsonDeserialize(as = CryptolibPayloadSignature.class)
    private PayloadSignature signature;

    private List<String> decryptionProofs;

    @JsonDeserialize(contentAs = StringEncryptedVote.class)
    private List<EncryptedVote> shuffledVotes;

    private String shuffleProof;

    @JsonDeserialize(contentAs = StringEncryptedVote.class)
    private List<EncryptedVote> previousVotes;

    private ElGamalPublicKey previousVoteEncryptionKey;

    private List<String> commitmentParameters;

    private ZonedDateTime timestamp;

    public MixingPayloadImpl() {
        
    }
    /**
     * Creates a message for starting a mixing process.
     *
     * @param voteEncryptionKey
     *            the key used to re-encrypt the votes in shuffling.
     * @param voteSetId
     *            the identifier of the vote set within the ballot box
     * @param votes
     *            the votes to mix
     * @param electoralAuthorityId
     *            the electoral authority identifier
     * @param encryptionParameters
     *            the Zp group for the votes
     */
    public MixingPayloadImpl(ElGamalPublicKey voteEncryptionKey, VoteSetId voteSetId, List<EncryptedVote> votes,
            String electoralAuthorityId, ZpSubgroup encryptionParameters) {
        this.voteEncryptionKey = Objects.requireNonNull(voteEncryptionKey);
        this.voteSetId = Objects.requireNonNull(voteSetId);
        this.votes = Objects.requireNonNull(votes);
        this.electoralAuthorityId = Objects.requireNonNull(electoralAuthorityId);
        this.encryptionParameters = encryptionParameters;
        timestamp = ZonedDateTime.now();
    }

    /**
     * Create the payload for a recently finished mixing process.
     *
     * @param remainingVoteEncryptionKey
     *            the key that resulted from removing the part corresponding to
     *            the recently visited node.
     * @param decryptedVotes
     *            the votes that resulted from partially decrypting the shuffled
     *            votes
     * @param decryptionProofs
     *            the proofs of the vote decryption
     * @param shuffledVotes
     *            the votes that resulted from shuffling the input set
     * @param shuffleProof
     *            the proofs of the shuffling
     * @param previousPayload
     *            the payload that was used to generate the results on this one
     * @param commitmentParameters
     *            the commitment parameters generated for this mixing
     */
    public MixingPayloadImpl(MixingPayload previousPayload, ElGamalPublicKey remainingVoteEncryptionKey,
            List<EncryptedVote> decryptedVotes, List<String> decryptionProofs, List<EncryptedVote> shuffledVotes,
            String shuffleProof, List<String> commitmentParameters) {
        this(remainingVoteEncryptionKey, previousPayload.getVoteSetId(), decryptedVotes,
            previousPayload.getElectoralAuthorityId(), previousPayload.getEncryptionParameters());

        Objects.requireNonNull(previousPayload, "The previous payload is required");
        previousVotes = previousPayload.getVotes();
        previousVoteEncryptionKey = previousPayload.getVoteEncryptionKey();

        this.decryptionProofs = Objects.requireNonNull(decryptionProofs);
        this.shuffledVotes = Objects.requireNonNull(shuffledVotes);
        this.shuffleProof = Objects.requireNonNull(shuffleProof);
        this.commitmentParameters = Objects.requireNonNull(commitmentParameters);
    }

    /**
     * JSON serialisation constructor.
     */
    @JsonCreator
    private MixingPayloadImpl(@JsonProperty("voteEncryptionKey") ElGamalPublicKey voteEncryptionKey,
            @JsonProperty("voteSetId") VoteSetId voteSetId, @JsonProperty("votes") List<EncryptedVote> votes,
            @JsonProperty("electoralAuthorityId") String electoralAuthorityId,
            @JsonProperty("encryptionParameters") ZpSubgroup encryptionParameters,
            @JsonProperty("decryptionProofs") List<String> decryptionProofs,
            @JsonProperty("shuffledVotes") List<EncryptedVote> shuffledVotes,
            @JsonProperty("shuffleProof") String shuffleProof,
            @JsonProperty("commitmentParameters") List<String> commitmentParameters,
            @JsonProperty("timestamp") ZonedDateTime timestamp) {
        this.voteEncryptionKey = voteEncryptionKey;
        this.voteSetId = voteSetId;
        this.votes = votes;
        this.electoralAuthorityId = electoralAuthorityId;
        this.encryptionParameters = encryptionParameters;
        this.decryptionProofs = decryptionProofs;
        this.shuffledVotes = shuffledVotes;
        this.shuffleProof = shuffleProof;
        this.commitmentParameters = commitmentParameters;
        this.timestamp = timestamp;
    }

    @Override
    public ElGamalPublicKey getVoteEncryptionKey() {
        return voteEncryptionKey;
    }

    @Override
    public VoteSetId getVoteSetId() {
        return voteSetId;
    }

    @Override
    public List<EncryptedVote> getVotes() {
        return votes;
    }

    @Override
    public List<String> getDecryptionProofs() {
        return decryptionProofs;
    }

    @Override
    public List<EncryptedVote> getShuffledVotes() {
        return shuffledVotes;
    }

    @Override
    public String getShuffleProof() {
        return shuffleProof;
    }

    @Override
    public List<EncryptedVote> getPreviousVotes() {
        return previousVotes;
    }

    @Override
    public ElGamalPublicKey getPreviousVoteEncryptionKey() {
        return previousVoteEncryptionKey;
    }

    @Override
    public List<String> getCommitmentParameters() {
        return commitmentParameters;
    }

    @Override
    public ZonedDateTime getTimestamp() {
        return timestamp;
    }

    @Override
    public String getElectoralAuthorityId() {
        return electoralAuthorityId;
    }

    @Override
    public ZpSubgroup getEncryptionParameters() {
        return encryptionParameters;
    }

    @Override
    public InputStream getSignableContent() throws IOException {
        // A pipe is created to be able to push the relevant objects in the
        // payload to an output
        // stream. Another pipe is connected to the former, and provides an
        // input stream to read
        // the aforementioned objects, which will be the source of the
        // signature.

        // Output stream upon which the relevant DTO properties are dumped.
        PipedInputStream pis = new PipedInputStream();
        PipedOutputStream pos = new PipedOutputStream(pis);

        // Feed the piped output stream from another thread to prevent
        // deadlocks.
        executor.execute(() -> streamSignableContent(pos));

        return pis;
    }

    /**
     * Streams the data to be signed into the provided output stream
     * 
     * @param os
     *            the output stream to push the data into
     */
    private void streamSignableContent(OutputStream os) {
        try (PrintWriter pr = new PrintWriter(os)) {
            // These objects are always present.
            pr.print(voteSetId);
            votes.stream().forEach(vote -> pr.print(vote.toString()));
            pr.print(voteEncryptionKey.toJson());

            // These objects are null for the initial mixing payload and
            // should not be committed to the signature in that case.
            if (null != commitmentParameters) {
                commitmentParameters.forEach(pr::print);
            }
            if (null != decryptionProofs) {
                decryptionProofs.forEach(pr::print);
            }
            if (null != shuffledVotes) {
                shuffledVotes.forEach(pr::print);
            }
            if (null != shuffleProof) {
                pr.print(shuffleProof);
            }
            pr.print((null == timestamp) ? "" : timestamp.toEpochSecond() + "." + timestamp.getNano());
            if (null != previousVotes) {
                previousVotes.forEach(pr::print);
            }
            pr.print((null == previousVoteEncryptionKey) ? "" : previousVoteEncryptionKey.toJson());
        } catch (GeneralCryptoLibException e) {
            throw new LambdaException(e);
        }
    }

    @Override
    public PayloadSignature getSignature() {
        return signature;
    }

    @Override
    public void setSignature(PayloadSignature signature) {
        this.signature = signature;
    }
    
    /**
     * https://docs.oracle.com/javase/6/docs/platform/serialization/spec/output.
     * html#861
     */
    private void writeObject(ObjectOutputStream stream) throws IOException {
        try {
            // PLEASE NOTE THAT the order of the writeObject() calls is
            // significant. At all times
            // it must be aligned with the order of the readObject() calls in
            // this class's
            // readObject() method.
            stream.writeObject(voteEncryptionKey.toJson());
            stream.writeObject(votes);
            stream.writeObject(electoralAuthorityId);
            stream.writeObject(encryptionParameters.toJson());
            stream.writeObject(voteSetId);
            stream.writeObject(signature);
            stream.writeObject(decryptionProofs);
            stream.writeObject(shuffledVotes);
            stream.writeObject(shuffleProof);
            stream.writeObject(previousVotes);
            stream.writeObject((null == previousVoteEncryptionKey) ? null : previousVoteEncryptionKey.toJson());
            stream.writeObject(commitmentParameters);
            stream.writeObject(timestamp);
        } catch (GeneralCryptoLibException e) {
            throw new IOException(e);
        }
    }

    /**
     * https://docs.oracle.com/javase/6/docs/platform/serialization/spec/input.
     * html#2971
     */
    @SuppressWarnings("unchecked")
    private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
        try {
            // PLEASE NOTE THAT the order of the readObject() calls is
            // significant. At all times
            // it must be aligned with the order of the writeObject() calls in
            // this class's
            // writeObject() method.
            voteEncryptionKey = ElGamalPublicKey.fromJson((String) stream.readObject());
            votes = (List<EncryptedVote>) stream.readObject();
            electoralAuthorityId = (String) stream.readObject();
            encryptionParameters = ZpSubgroup.fromJson((String) stream.readObject());
            voteSetId = (VoteSetId) stream.readObject();
            signature = (PayloadSignature) stream.readObject();
            decryptionProofs = (List<String>) stream.readObject();
            shuffledVotes = (List<EncryptedVote>) stream.readObject();
            shuffleProof = (String) stream.readObject();
            previousVotes = (List<EncryptedVote>) stream.readObject();
            String previousVoteEncryptionKeyJson = (String) stream.readObject();
            if (previousVoteEncryptionKeyJson != null) {
                previousVoteEncryptionKey = ElGamalPublicKey.fromJson(previousVoteEncryptionKeyJson);
            }
            commitmentParameters = (List<String>) stream.readObject();
            timestamp = (ZonedDateTime) stream.readObject();
        } catch (GeneralCryptoLibException e) {
            throw new IOException(e);
        }
    }
    
    @Override
    public void serialize(MessagePacker packer) throws IOException {
        try {
            storeElGamalPublicKeyValueWithNullCheck(packer, voteEncryptionKey);
       
            writeVotesToPacker(votes, packer);
            
            storeStringValueWithNullCheck(packer, electoralAuthorityId);
            
            storeBigIntegerValueWithNullCheck(packer, encryptionParameters.getP());
            storeBigIntegerValueWithNullCheck(packer, encryptionParameters.getQ());
            storeBigIntegerValueWithNullCheck(packer, encryptionParameters.getG());
    
            storeStringValueWithNullCheck(packer, voteSetId.getBallotBoxId().getElectionEventId());
            storeStringValueWithNullCheck(packer, voteSetId.getBallotBoxId().getId());
            storeStringValueWithNullCheck(packer, voteSetId.getBallotBoxId().getTenantId());
            
            packer.packInt(voteSetId.getIndex());
    
            if (signature == null) {
                packer.packNil();
            } else {
                packer.packArrayHeader(signature.getCertificateChain().length);
                for (X509Certificate cert : signature.getCertificateChain()) {
                    storeCertificateValueWithNullCheck(packer, cert);
                }
                packer.packBinaryHeader(signature.getSignatureContents().length);
                packer.addPayload(signature.getSignatureContents());
                
            }
            if (decryptionProofs == null) {
                packer.packNil();
            } else {
                packer.packArrayHeader(decryptionProofs.size());
                for (String decryptionProof : decryptionProofs) {
                    storeStringValueWithNullCheck(packer, decryptionProof);
                }
            }

            writeVotesToPacker(shuffledVotes, packer);
            
            storeStringValueWithNullCheck(packer, shuffleProof);
            
            writeVotesToPacker(previousVotes, packer);
            
            storeElGamalPublicKeyValueWithNullCheck(packer, previousVoteEncryptionKey);
    
            if (commitmentParameters == null) {
                packer.packNil();
            } else {
                packer.packArrayHeader(commitmentParameters.size());
                for (String commitment : commitmentParameters) {
                    storeStringValueWithNullCheck(packer, commitment);
                }
            }
            storeDateValueWithNullCheck(packer, timestamp);
        } catch (GeneralCryptoLibException e) {
           throw new IOException(e);
        }
    }
    /**
     * Function to write ciphertexts (votes) to the message packer.
     * 
     * @param encryptedVotes The ciphertexts to be serialized.
     * 
     * @param packer The packer which the ciphertexts should be serialized to.
     * 
     * @throws IOException In case the serialization cannot happen.
     */
    private void writeVotesToPacker(List<EncryptedVote> encryptedVotes, MessagePacker packer) throws IOException {
        if(encryptedVotes == null) {
            packer.packNil();
            return;
        }
        if(encryptedVotes.isEmpty()) {
            packer.packArrayHeader(0);
            return;
        }
        packer.packArrayHeader(encryptedVotes.size());
        for (EncryptedVote encVote : encryptedVotes) {
            storeBigIntegerValueWithNullCheck(packer, encVote.getGamma());
            packer.packArrayHeader(encVote.getPhis().size());
            for (BigInteger phi : encVote.getPhis()) {
                storeBigIntegerValueWithNullCheck(packer, phi);
            }
        }
    }

    @Override
    public void deserialize(MessageUnpacker unpacker) throws SafeStreamDeserializationException {
        try {
            this.voteEncryptionKey = retrieveElGamalPublicKeyValueWithNullCheck(unpacker);

            this.votes = readVotesFromUnpacker(unpacker);
            this.electoralAuthorityId = retrieveStringValueWithNullCheck(unpacker);

            BigInteger p = retrieveBigIntegerValueWithNullCheck(unpacker);
            BigInteger q = retrieveBigIntegerValueWithNullCheck(unpacker);
            BigInteger g = retrieveBigIntegerValueWithNullCheck(unpacker);
            this.encryptionParameters = new ZpSubgroup(g, p, q);

            String electioneventId = retrieveStringValueWithNullCheck(unpacker);
            String id = retrieveStringValueWithNullCheck(unpacker);
            String tenantId = retrieveStringValueWithNullCheck(unpacker);
            BallotBoxIdImpl bbid = new BallotBoxIdImpl(tenantId, electioneventId, id);
            int index = unpacker.unpackInt();
            this.voteSetId = new VoteSetIdImpl(bbid, index);

            if (!unpacker.tryUnpackNil()) {
                int signatureCertChainSize = unpacker.unpackArrayHeader();
                X509Certificate[] signatureCertChain = new X509Certificate[signatureCertChainSize];
                for (int i = 0; i < signatureCertChainSize; i++) {
                    signatureCertChain[i] = retrieveCertificateValueWithNullCheck(unpacker);
                }
                int signatureLength = unpacker.unpackBinaryHeader();
                byte[] keySignature = unpacker.readPayload(signatureLength);
                this.signature = new CryptolibPayloadSignature(keySignature, signatureCertChain);
            } else {
                this.signature = null;
            }

            if (!unpacker.tryUnpackNil()) {
                int decryptionProofsSize = unpacker.unpackArrayHeader();
                this.decryptionProofs = new ArrayList<>(decryptionProofsSize);
                for (int i = 0; i < decryptionProofsSize; i++) {
                    decryptionProofs.add(retrieveStringValueWithNullCheck(unpacker));
                }
            } else {
                this.decryptionProofs = null;
            }

            this.shuffledVotes = readVotesFromUnpacker(unpacker);

            this.shuffleProof = retrieveStringValueWithNullCheck(unpacker);

            this.previousVotes = readVotesFromUnpacker(unpacker);

            this.previousVoteEncryptionKey = retrieveElGamalPublicKeyValueWithNullCheck(unpacker);

            if (!unpacker.tryUnpackNil()) {
                int commitmentParamsSize = unpacker.unpackArrayHeader();
                this.commitmentParameters = new ArrayList<>(commitmentParamsSize);
                for (int i = 0; i < commitmentParamsSize; i++) {
                    commitmentParameters.add(retrieveStringValueWithNullCheck(unpacker));
                }
            } else {
                this.commitmentParameters = null;
            }

            this.timestamp = retrieveDateValueWithNullCheck(unpacker);

        } catch (GeneralCryptoLibException | IOException e) {
            throw new SafeStreamDeserializationException(e);
        }
    }
    
    /**
     * Function to read ciphertexts (votes) from the message unpacker.
     * 
     * @param unpacker The unpacker which the ciphertexts should be deserialized from.
     * 
     * @return The deserialized ciphertexts.
     * 
     * @throws IOException In case the deserialization cannot happen.
     */
    private List<EncryptedVote> readVotesFromUnpacker(MessageUnpacker unpacker) throws IOException {
        if(unpacker.tryUnpackNil()) {
            return null;
        }
        int size = unpacker.unpackArrayHeader();
        List<EncryptedVote> votes = new ArrayList<>(size);
        for (int i = 0; i < size; i++) {
            BigInteger gamma = retrieveBigIntegerValueWithNullCheck(unpacker);
            int phiSize = unpacker.unpackArrayHeader();
            List<BigInteger> phis = new ArrayList<>(phiSize);
            for (int j = 0; j < phiSize; j++) {
                phis.add(retrieveBigIntegerValueWithNullCheck(unpacker));
            }
            votes.add(new CiphertextEncryptedVote(gamma, phis));
        }
        return votes;
    }

    @Override
    public StreamSerializableClassType type() {
        return StreamSerializableClassType.MIXING_PAYLOAD_IMPL;
    }
}
