/*
 * 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.retrieveBigIntegerValueWithNullCheck;
import static com.scytl.products.ov.commons.safestream.StreamSerializableUtil.retrieveCertificateValueWithNullCheck;
import static com.scytl.products.ov.commons.safestream.StreamSerializableUtil.retrieveStringValueWithNullCheck;
import static com.scytl.products.ov.commons.safestream.StreamSerializableUtil.storeBigIntegerValueWithNullCheck;
import static com.scytl.products.ov.commons.safestream.StreamSerializableUtil.storeCertificateValueWithNullCheck;
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.io.Serializable;
import java.math.BigInteger;
import java.security.cert.X509Certificate;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;

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

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;

public class ChoiceCodesVerificationResPayload implements Payload, Serializable {

    private static final long serialVersionUID = 7270602003518788884L;

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

    private Map<BigInteger, BigInteger> primeToComputedPrime;

    private String exponentiationProofJson;

    private String choiceCodesDerivedKeyJson;

    private String castCodeDerivedKeyJson;

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

    public ChoiceCodesVerificationResPayload() {
    }

    public Map<BigInteger, BigInteger> getPrimeToComputedPrime() {
        return primeToComputedPrime;
    }

    public void setPrimeToComputedPrime(Map<BigInteger, BigInteger> primeToComputedPrime) {
        this.primeToComputedPrime = primeToComputedPrime;
    }

    public String getExponentiationProofJson() {
        return exponentiationProofJson;
    }

    public void setExponentiationProofJson(String exponentiationProofJson) {
        this.exponentiationProofJson = exponentiationProofJson;
    }

    public String getChoiceCodesDerivedKeyJson() {
        return choiceCodesDerivedKeyJson;
    }

    public void setChoiceCodesDerivedKeyJson(String choiceCodesDerivedKeyJson) {
        this.choiceCodesDerivedKeyJson = choiceCodesDerivedKeyJson;
    }

    @Override
    public void serialize(MessagePacker packer) throws IOException {
        if (primeToComputedPrime == null) {
            packer.packNil();
        } else {
            packer.packMapHeader(primeToComputedPrime.size());
            Set<Entry<BigInteger, BigInteger>> entrySet = primeToComputedPrime.entrySet();
            for (Entry<BigInteger, BigInteger> entry : entrySet) {
                storeBigIntegerValueWithNullCheck(packer, entry.getKey());
                storeBigIntegerValueWithNullCheck(packer, entry.getValue());
            }
        }
        storeStringValueWithNullCheck(packer, exponentiationProofJson);
        storeStringValueWithNullCheck(packer, choiceCodesDerivedKeyJson);
        storeStringValueWithNullCheck(packer, castCodeDerivedKeyJson);
        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 {
            if (unpacker.tryUnpackNil()) {
                primeToComputedPrime = null;
            } else {
                int mapSize = unpacker.unpackMapHeader();
                primeToComputedPrime = new LinkedHashMap<>(mapSize);
                for (int i = 0; i < mapSize; i++) {
                    BigInteger key = retrieveBigIntegerValueWithNullCheck(unpacker);
                    BigInteger value = retrieveBigIntegerValueWithNullCheck(unpacker);
                    primeToComputedPrime.put(key, value);
                }
            }
            exponentiationProofJson = retrieveStringValueWithNullCheck(unpacker);
            choiceCodesDerivedKeyJson = retrieveStringValueWithNullCheck(unpacker);
            castCodeDerivedKeyJson = retrieveStringValueWithNullCheck(unpacker);
            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 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)) {
                for (Map.Entry<BigInteger, BigInteger> entry : primeToComputedPrime.entrySet()) {
                    writer.write(entry.getKey() + ":" + entry.getValue());
                }
                writer.write(exponentiationProofJson);
                if (choiceCodesDerivedKeyJson != null)
                    writer.write(choiceCodesDerivedKeyJson);
                if (castCodeDerivedKeyJson != null)
                    writer.write(castCodeDerivedKeyJson);
            }
        });

        return pis;
    }

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

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

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

    public String getCastCodeDerivedKeyJson() {
        return castCodeDerivedKeyJson;
    }

    public void setCastCodeDerivedKeyJson(String castCodeDerivedKeyJson) {
        this.castCodeDerivedKeyJson = castCodeDerivedKeyJson;
    }
}
