/*
 * 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.ov.mixing.core.elgamal;

import java.util.ArrayList;
import java.util.List;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.bean.ElGamalEncryptionParameters;
import com.scytl.cryptolib.elgamal.cryptoapi.Ciphertext;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.ov.mixing.commons.beans.proofs.Proof;
import com.scytl.ov.mixing.commons.homomorphic.Randomness;
import com.scytl.ov.mixing.commons.homomorphic.impl.GjosteenElGamal;
import com.scytl.ov.mixing.commons.homomorphic.impl.GjosteenElGamalRandomness;
import com.scytl.ov.mixing.commons.proofs.bg.config.ProofsGeneratorConfigurationParams;
import com.scytl.ov.mixing.commons.tools.CiphertextTools;
import com.scytl.ov.mixing.commons.tools.MatrixArranger;
import com.scytl.ov.mixing.commons.tools.MultiExponentiation;
import com.scytl.ov.mixing.commons.tools.MultiExponentiationImpl;
import com.scytl.ov.mixing.core.InvalidMixablesException;
import com.scytl.ov.mixing.core.JobState;
import com.scytl.ov.mixing.core.MixAndProveJob;
import com.scytl.ov.mixing.core.Mixable;
import com.scytl.ov.mixing.core.MixingException;
import com.scytl.ov.mixing.core.MixingProcess;
import com.scytl.ov.mixing.core.ProofGenerationConfigParams;
import com.scytl.ov.mixing.proofs.bg.multiexp.ComputeAllE;
import com.scytl.ov.mixing.proofs.bg.multiexp.ParallelComputeAllE;
import com.scytl.ov.mixing.proofs.bg.shuffle.ParallelPrivateAndPublicCommitmentsGenerator;
import com.scytl.ov.mixing.proofs.bg.shuffle.PrivateAndPublicCommitmentsGenerator;
import com.scytl.ov.mixing.proofs.bg.shuffle.ShuffleProofGenerator;

/**
 * A job that allows lists of mixables to be mixed and a proof to be obtained.
 */
public final class ElGamalMixAndProveJob extends ElGamalMixJob implements MixAndProveJob {

    private Proof proof;

    @Override
    public void start(List<Mixable> mixables) {

        super.start(mixables);

        if (this.jobState == JobState.FINISHED) {
            return;
        }

        // temporary state, just simulating the increases in progress
        updateProgress(MixingProcess.PROOF_GENERATION, 10);
        updateProgress(MixingProcess.PROOF_GENERATION, 40);

        try {

            ProofsGeneratorConfigurationParams config = getProofsGeneratorConfigurationParams();
            int m = config.getMatrixDimensions().getNumberOfRows();
            int n = config.getMatrixDimensions().getNumberOfColumns();

            final Ciphertext[][] encryptedCiphertexts =
                MatrixArranger.arrangeInCiphertextMatrix(encryptedMixablesCiphertexts, m, n);

            final Ciphertext[][] reEncryptedCiphertexts =
                MatrixArranger.arrangeInCiphertextMatrix(getCiphertextsFromMixables(), m, n);

            final Randomness[] rho = new Randomness[exponentsUsedDuringEncryption.size()];
            for (int i = 0; i < exponentsUsedDuringEncryption.size(); i++) {
                rho[i] = new GjosteenElGamalRandomness(exponentsUsedDuringEncryption.get(i));
            }

            final PrivateAndPublicCommitmentsGenerator privateAndPublicCommitmentsGenerator =
                new ParallelPrivateAndPublicCommitmentsGenerator();
            final MultiExponentiation limMultiExpo = MultiExponentiationImpl.getInstance();
            CiphertextTools ciphertextTools = new CiphertextTools(limMultiExpo);
            ComputeAllE computeAllE = new ParallelComputeAllE(ciphertextTools);

            ShuffleProofGenerator shuffleProofGenerator =
                new ShuffleProofGenerator(config, encryptedCiphertexts, this.permutation,
                    privateAndPublicCommitmentsGenerator, limMultiExpo, reEncryptedCiphertexts, rho, computeAllE);

            proof = shuffleProofGenerator.generate();

            updateProgress(MixingProcess.PROOF_GENERATION, ONE_HUNDRED_PERCENT);

        } catch (GeneralCryptoLibException exception) {

            setStateOfFailedJob();
            this.mixingException =
                new MixingException("Exception during proof generation: " + exception.getMessage(), exception);
        }
    }

    private List<Ciphertext> getCiphertextsFromMixables() {

        List<Ciphertext> mixedCiphertexts = new ArrayList<>();
        for (ElGamalMixable mixable : this.mixedMixables) {
            mixedCiphertexts.add(mixable.getCiphertext());
        }

        return mixedCiphertexts;
    }

    private ProofsGeneratorConfigurationParams getProofsGeneratorConfigurationParams()
            throws GeneralCryptoLibException {

        int concurrencyLevel = 1;
        ElGamalEncryptionParameters encryptionParameters = this.elGamalJobParameters.encryptionParameters();
        ZpSubgroup zp =
            new ZpSubgroup(encryptionParameters.getG(), encryptionParameters.getP(), encryptionParameters.getQ());
        GjosteenElGamal elgamal = new GjosteenElGamal(zp, this.elGamalJobParameters.publicKey());

        ProofGenerationConfigParams proofGenerationConfigParams =
            this.elGamalJobParameters.getProofGenerationConfigParams();

        ProofsGeneratorConfigurationParams config = new ProofsGeneratorConfigurationParams(
            proofGenerationConfigParams.getMatrixDimensions().getNumberOfRows(),
            proofGenerationConfigParams.getMatrixDimensions().getNumberOfColumns(), elgamal,
            proofGenerationConfigParams.getNumIterations(), proofGenerationConfigParams.getMu(), zp, concurrencyLevel);

        return config;
    }

    @Override
    public Proof getProof() throws InvalidMixablesException, MixingException {

        validateStateCorrectWhenRetrievingProof();

        return proof;
    }

    protected void validateStateCorrectWhenRetrievingProof() throws MixingException {

        if (this.invalidMixablesException != null) {
            throw this.invalidMixablesException;
        } else if (this.mixingException != null) {
            throw this.mixingException;
        }

        if (this.jobState == JobState.CREATED) {
            throw new IllegalStateException("Tried to retrieve the proof before the job was started");
        } else if (this.jobState == JobState.CANCELED) {
            throw new IllegalStateException("Tried to retrieve the proof from a job that was canceled");
        } else if (this.jobState != JobState.FINISHED) {
            throw new IllegalStateException("Tried to retrieve the proof from a job that had not finished");
        }
    }

    // Temporary crude approach that assumes that each of the 3 steps (shuffle, reencryption, proof generation) takes
    // the same amount of time
    @Override
    protected void updateProgress(MixingProcess mixingProcess, int progress) {

        final int factor = 3;

        if (mixingProcess == MixingProcess.SHUFFLE) {
            updateProgress(progress / factor);
        } else if (mixingProcess == MixingProcess.REENCRYPTION) {
            updateProgress((100 / factor) + (progress / factor));
        } else {

            if (progress == ONE_HUNDRED_PERCENT) {
                updateStateAndProgress(JobState.FINISHED, ONE_HUNDRED_PERCENT);
            } else {
                updateProgress(((ONE_HUNDRED_PERCENT / factor) * 2) + (progress / factor));
            }
        }

        updateListeners();
    }
}
