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

import static com.scytl.products.ov.commons.safestream.StreamSerializableUtil.retrieveCertificateValueWithNullCheck;
import static com.scytl.products.ov.commons.safestream.StreamSerializableUtil.retrieveIntValueWithNullCheck;
import static com.scytl.products.ov.commons.safestream.StreamSerializableUtil.retrieveStringValueWithNullCheck;
import static com.scytl.products.ov.commons.safestream.StreamSerializableUtil.storeCertificateValueWithNullCheck;
import static com.scytl.products.ov.commons.safestream.StreamSerializableUtil.storeIntValueWithNullCheck;
import static com.scytl.products.ov.commons.safestream.StreamSerializableUtil.storeStringValueWithNullCheck;


import java.io.IOException;
import java.io.InputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.io.PrintWriter;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.products.ov.commons.beans.domain.model.messaging.CryptolibPayloadSignature;
import com.scytl.products.ov.commons.beans.domain.model.messaging.Payload;
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 org.msgpack.core.MessagePacker;
import org.msgpack.core.MessageUnpacker;

public class ChoiceCodeGenerationResPayload implements Payload, WithDTOResultsKeyFields {

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

    private String tenantId;

    private String electionEventId;

    private String verificationCardSetId;
    
    private int chunkId;
    
    private List<ChoiceCodeGenerationOutput> choiceCodeGenerationOutputList;
    
    @JsonDeserialize(as = CryptolibPayloadSignature.class)
    private PayloadSignature signature;
    
    public ChoiceCodeGenerationResPayload() {
    }

    public ChoiceCodeGenerationResPayload(String tenantId, String electionEventId, String verificationCardSetId, int chunkId) {
        this.tenantId = tenantId;
        this.electionEventId = electionEventId;
        this.verificationCardSetId = verificationCardSetId;
        this.chunkId = chunkId;
        this.choiceCodeGenerationOutputList = new ArrayList<>();
    }

    @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(() -> {
            try (PrintWriter writer = new PrintWriter(pos)) {
                writer.write(tenantId);
                writer.write(electionEventId);
                writer.write(verificationCardSetId);
                writer.write(Integer.toString(chunkId));
                for(ChoiceCodeGenerationOutput entry : choiceCodeGenerationOutputList) {
                    writer.write(entry.getComputedBallotCastingKey());
                    writer.write(entry.getComputedRepresentations());
                    writer.write(entry.getChoiceCodesKeyCommitmentJson());
                    writer.write(entry.getBallotCastingKeyCommitmentJson());
                    writer.write(entry.getEncryptedBallotCastingKey());
                    writer.write(entry.getVerificationCardId());
                }
            }
        });

        return pis;
    }

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

    @Override
    public void setSignature(PayloadSignature signature) {
        this.signature = signature;
    }

    public List<ChoiceCodeGenerationOutput> getChoiceCodeGenerationOutputList() {
        return choiceCodeGenerationOutputList;
    }

    public void setChoiceCodeGenerationOutputList(List<ChoiceCodeGenerationOutput> choiceCodeGenerationOutputList) {
        this.choiceCodeGenerationOutputList = choiceCodeGenerationOutputList;
    }
    
    public String getTenantId() {
        return tenantId;
    }

    public void setTenantId(String tenantId) {
        this.tenantId = tenantId;
    }

    public String getElectionEventId() {
        return electionEventId;
    }

    public void setElectionEventId(String electionEventId) {
        this.electionEventId = electionEventId;
    }

    public String getVerificationCardSetId() {
        return verificationCardSetId;
    }

    public void setVerificationCardSetId(String verificationCardSetId) {
        this.verificationCardSetId = verificationCardSetId;
    }
    
    public int getChunkId() {
        return chunkId;
    }

    public void setChunkId(int chunkId) {
        this.chunkId = chunkId;
    }

    @Override
    @JsonIgnore
    public String[] getResultsKeyFields() {
        return new String[] { tenantId, electionEventId, verificationCardSetId, Integer.toString(chunkId) };
    }

    @Override
    public void serialize(MessagePacker packer) throws IOException {
        storeStringValueWithNullCheck(packer, tenantId);
        storeStringValueWithNullCheck(packer, electionEventId);
        storeStringValueWithNullCheck(packer, verificationCardSetId);
        storeIntValueWithNullCheck(packer, chunkId);
        if (choiceCodeGenerationOutputList == null) {
            packer.packNil();
        } else {
            packer.packArrayHeader(choiceCodeGenerationOutputList.size());
            for (ChoiceCodeGenerationOutput output : choiceCodeGenerationOutputList) {
                storeStringValueWithNullCheck(packer, output.getComputedBallotCastingKey());
                storeStringValueWithNullCheck(packer, output.getComputedRepresentations());
                storeStringValueWithNullCheck(packer, output.getChoiceCodesKeyCommitmentJson());
                storeStringValueWithNullCheck(packer, output.getBallotCastingKeyCommitmentJson());
                storeStringValueWithNullCheck(packer, output.getEncryptedBallotCastingKey());
                storeStringValueWithNullCheck(packer, output.getVerificationCardId());
            }
        }
        if (signature == null) {
            packer.packNil();
        } else {
            packer.packArrayHeader(signature.getCertificateChain().length);
            for (X509Certificate cert : signature.getCertificateChain()) {
                try {
                    storeCertificateValueWithNullCheck(packer, cert);
                } catch (GeneralCryptoLibException e) {
                    throw new IOException(e);
                }
            }
            packer.packBinaryHeader(signature.getSignatureContents().length);
            packer.addPayload(signature.getSignatureContents());
        }
    }

    @Override
    public void deserialize(MessageUnpacker unpacker) throws SafeStreamDeserializationException {
        try {
            this.tenantId = retrieveStringValueWithNullCheck(unpacker);
            this.electionEventId = retrieveStringValueWithNullCheck(unpacker);
            this.verificationCardSetId = retrieveStringValueWithNullCheck(unpacker);
            this.chunkId = retrieveIntValueWithNullCheck(unpacker);
            if (unpacker.tryUnpackNil()) {
                this.choiceCodeGenerationOutputList = new ArrayList<>();
            } else {
                int listSize = unpacker.unpackArrayHeader();
                choiceCodeGenerationOutputList = new ArrayList<>(listSize);
                for (int i = 0; i < listSize; i++) {
                    String computedBallotCastingKey = retrieveStringValueWithNullCheck(unpacker);
                    String computedRespresentations = retrieveStringValueWithNullCheck(unpacker);
                    String choiceCodesKeyCommitmentJson = retrieveStringValueWithNullCheck(unpacker);
                    String ballotCastingKeyExponentCommitmentJson = retrieveStringValueWithNullCheck(unpacker);
                    String encryptedBallotCastingKey = retrieveStringValueWithNullCheck(unpacker);
                    String verificationCardId = retrieveStringValueWithNullCheck(unpacker);
                    ChoiceCodeGenerationOutput output = new ChoiceCodeGenerationOutput(verificationCardId, encryptedBallotCastingKey, computedBallotCastingKey, computedRespresentations, choiceCodesKeyCommitmentJson, ballotCastingKeyExponentCommitmentJson);
                    choiceCodeGenerationOutputList.add(output);
                }
            }
            if (unpacker.tryUnpackNil()) {
                this.signature = null;
            } else {
                int arraySize = unpacker.unpackArrayHeader();
                X509Certificate[] certs = new X509Certificate[arraySize];
                for (int i = 0; i < arraySize; i++) {
                    try {
                        certs[i] = retrieveCertificateValueWithNullCheck(unpacker);
                    } catch (GeneralCryptoLibException e) {
                        throw new SafeStreamDeserializationException(e);
                    }
                }

                int signatureLength = unpacker.unpackBinaryHeader();
                byte[] signatureContents = unpacker.readPayload(signatureLength);

                this.signature = new CryptolibPayloadSignature(signatureContents, certs);
            }
        } catch (IOException e) {
            throw new SafeStreamDeserializationException(e);
        }
    }

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