/*
 * 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 com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
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.PayloadSignature;
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 java.math.BigInteger;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;

/**
 * Helper to build mocks of mixing payload objects. Manual creation of such objects is cumbersome
 * and repetitive.
 */
public class MixingPayloadMockBuilder {

    private static final int PUBLIC_KEY_BITS = 8;
    
    private static final BigInteger P = BigInteger.valueOf(PUBLIC_KEY_BITS * 32);

    public static final String TENANT_ID = "TENANT";

    public static final String ELECTION_EVENT_ID = "EE";

    public static final String BALLOT_BOX_ID = "BB";

    public static final int DEFAULT_VOTE_SET_INDEX = 0;

    // If the randomiser gives out repeated values that fail tests, try another seed.
    private static final Random random = new Random(1L);

    private ElGamalPublicKey voteEncryptionKey;

    private VoteSetId voteSetId;

    private List<EncryptedVote> votes;

    private String electoralAuthorityId;

    private ZpSubgroup encryptionParameters;

    private PayloadSignature signature;

    private MixingPayload previousPayload;

    private List<String> decryptionProofs;

    private List<EncryptedVote> shuffledVotes;

    private String shuffleProof;

    private List<String> commitmentParameters;
    
    /**
     * @return a mock payload signature.
     */
    public static PayloadSignature getMockSignature() {
        return new MockPayloadSignature();
    }

    public MixingPayload build() throws GeneralCryptoLibException {
        MixingPayload payload;
        if (null == previousPayload) {
            payload = new MixingPayloadImpl(getVoteEncryptionKey(), getVoteSetId(), getVotes(),
                    getElectoralAuthorityId(), getEncryptionParameters());
        } else {
            payload = new MixingPayloadImpl(previousPayload, getVoteEncryptionKey(), getVotes(),
                    getDecryptionProofs(), getShuffledVotes(), getShuffleProof(),
                    getCommitmentParameters());
        }

        if (null != signature) {
            payload.setSignature(signature);
        }

        return payload;
    }

    private List<String> getCommitmentParameters() {
        if (null == commitmentParameters) {
            commitmentParameters = Arrays.asList("A", "B", "C");
        }

        return commitmentParameters;
    }

    private String getShuffleProof() {
        if (null == shuffleProof) {
            shuffleProof = "";
        }

        return shuffleProof;
    }

    private List<EncryptedVote> getShuffledVotes() {
        return (null == shuffledVotes) ? votes : shuffledVotes;
    }

    private List<String> getDecryptionProofs() {
        if (null == decryptionProofs) {
            decryptionProofs = Arrays.asList("1", "2", "3");
        }

        return decryptionProofs;
    }

    public MixingPayloadMockBuilder withSignature(PayloadSignature signature) {
        this.signature = signature;

        return this;
    }

    public MixingPayloadMockBuilder withVoteEncryptionKey(ElGamalPublicKey voteEncryptionKey) {
        this.voteEncryptionKey = voteEncryptionKey;

        return this;
    }

    public MixingPayloadMockBuilder withVoteSetId(VoteSetId voteSetId) {
        this.voteSetId = voteSetId;

        return this;
    }

    public MixingPayloadMockBuilder withVotes(List<EncryptedVote> votes) {
        this.votes = votes;

        return this;
    }

    public MixingPayloadMockBuilder withElectoralAuthorityId(String electoralAuthorityId) {
        this.electoralAuthorityId = electoralAuthorityId;

        return this;
    }

    public MixingPayloadMockBuilder withEncryptionParameters(ZpSubgroup encryptionParameters) {
        this.encryptionParameters = encryptionParameters;

        return this;
    }

    public MixingPayloadMockBuilder withPayload(MixingPayload previousPayload) {
        this.previousPayload = previousPayload;

        return this;
    }

    private List<EncryptedVote> getVotes() {
        if (null == votes) {
            votes = new ArrayList<>(3);
            // Unsorted on purpose.
            votes.add(new StringEncryptedVote("3;4"));
            votes.add(new StringEncryptedVote("1;2"));
            votes.add(new StringEncryptedVote("5;6"));
        }

        return votes;
    }

    private VoteSetId getVoteSetId() {
        if (null == voteSetId) {
            voteSetId = new VoteSetIdImpl(
                    new BallotBoxIdImpl(TENANT_ID, ELECTION_EVENT_ID, BALLOT_BOX_ID),
                    DEFAULT_VOTE_SET_INDEX);
        }

        return voteSetId;
    }

    private ZpSubgroup getEncryptionParameters() throws GeneralCryptoLibException {
        if (null == encryptionParameters) {
            // Default encryption parameters
            encryptionParameters =
                    new ZpSubgroup(new BigInteger("2"), P,
                            new BigInteger("1"));
        }

        return encryptionParameters;
    }

    private String getElectoralAuthorityId() {
        if (null == electoralAuthorityId) {
            electoralAuthorityId = "electoralAuthorityId";
        }

        return electoralAuthorityId;
    }

    private ElGamalPublicKey getVoteEncryptionKey() throws GeneralCryptoLibException {
        if (null == voteEncryptionKey) {
            // Create default vote encryption key.
            voteEncryptionKey = new ElGamalPublicKey(Arrays.asList(
                    new ZpGroupElement(
                            // Random between 1 and 255
                            new BigInteger(PUBLIC_KEY_BITS, random)
                                    .mod(P.subtract(BigInteger.ONE))
                                    .add(BigInteger.ONE),
                            getEncryptionParameters())), getEncryptionParameters());
        }

        return voteEncryptionKey;
    }

    /**
     * An actual class (rather than an anonymous one) is required by Jackson for serialisation.
     */
    private static class MockPayloadSignature implements PayloadSignature {

        private static final long serialVersionUID = 4017025164960812099L;

        private final X509Certificate[] certificateChain;

        private final byte[] signatureContents;

        private MockPayloadSignature() {
            certificateChain = new X509Certificate[0];
            signatureContents = new byte[0];
        }

        private MockPayloadSignature(X509Certificate[] certificateChain, byte[] signatureContents) {
            this.certificateChain = certificateChain;
            this.signatureContents = signatureContents;
        }

        @Override
        public X509Certificate[] getCertificateChain() {
            return certificateChain;
        }

        @Override
        public byte[] getSignatureContents() {
            return signatureContents;
        }
    }    
    
}
