/*
 * 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 static java.util.Objects.requireNonNull;

import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeoutException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.elgamal.bean.ElGamalPublicKey;
import com.scytl.cryptolib.elgamal.cryptoapi.Ciphertext;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.ov.mixing.commons.beans.proofs.BayerGrothProof;
import com.scytl.ov.mixing.commons.beans.proofs.Proof;
import com.scytl.ov.mixing.commons.homomorphic.Cryptosystem;
import com.scytl.ov.mixing.commons.homomorphic.impl.GjosteenElGamal;
import com.scytl.ov.mixing.commons.tools.CiphertextTools;
import com.scytl.ov.mixing.commons.tools.MultiExponentiation;
import com.scytl.ov.mixing.commons.tools.MultiExponentiationImpl;
import com.scytl.ov.mixing.commons.tools.MatrixArranger;
import com.scytl.ov.mixing.core.InvalidJobParametersException;
import com.scytl.ov.mixing.core.InvalidMixablesException;
import com.scytl.ov.mixing.core.JobEvent;
import com.scytl.ov.mixing.core.JobEventListener;
import com.scytl.ov.mixing.core.JobParameters;
import com.scytl.ov.mixing.core.JobState;
import com.scytl.ov.mixing.core.Mixable;
import com.scytl.ov.mixing.core.MixingException;
import com.scytl.ov.mixing.core.VerificationFailedException;
import com.scytl.ov.mixing.core.VerifyJob;
import com.scytl.ov.mixing.proofs.bg.shuffle.ShuffleProofVerifier;

/**
 * Verifies a set of proofs.
 *
 * @author afries
 * @date May 4, 2018
 */
public final class ElGamalVerifyJob implements VerifyJob {
    private static final Logger LOG = LoggerFactory.getLogger("std");

    private static final int ZERO_PERCENT = 0;

    private static final int PROGRESS_WHEN_CANCELED_OR_FAILED = ZERO_PERCENT;

    private static final int ONE_HUNDRED_PERCENT = 100;

    private List<JobEventListener> listeners;

    private JobState jobState = JobState.CREATED;

    private int progress;

    private ElGamalJobParameters elGamalJobParameters;

    private InvalidMixablesException invalidMixablesException;

    private MixingException mixingException;

    private VerificationFailedException verificationFailedException;

    public ElGamalVerifyJob() {

        listeners = Collections.synchronizedList(new ArrayList<>());

        updateStateAndProgress(JobState.CREATED, ZERO_PERCENT);

        invalidMixablesException = null;
        mixingException = null;
        verificationFailedException = null;
    }

    @Override
    public <T extends Mixable> void start(List<T> mixables, List<T> mix, Proof proof) {

        verifyStateCorrectToStartJob();

        performBasicValidationsOnStartInputs(mixables, mix, proof);

        try {

            BayerGrothProof bayerGrothProof;
            if (proof instanceof BayerGrothProof) {
                bayerGrothProof = (BayerGrothProof) proof;
            } else {
                throw new InvalidMixablesException("The received proof was not a BayerGrothProof");
            }

            updateStateAndProgress(JobState.STARTED, ZERO_PERCENT);

            updateProgress(10);

            ZpSubgroup group = elGamalJobParameters.getProofGenerationConfigParams().getZpGroup();
            ElGamalPublicKey elGamalPublicKey = elGamalJobParameters.publicKey();

            Cryptosystem cryptosystem = new GjosteenElGamal(group, elGamalPublicKey);

            int m = 1;
            int n = mixables.size();
            int mu = 2;
            int numIterations = 0;
            MultiExponentiation limMultiExpo = MultiExponentiationImpl.getInstance();
            CiphertextTools ciphertextTools = new CiphertextTools(limMultiExpo);

            final Ciphertext[][] encryptedCiphertexts = arrangeListTo2DArray(mixables, m, n);

            final Ciphertext[][] mixedEncryptedCiphertexts = arrangeListTo2DArray(mix, m, n);

            ShuffleProofVerifier verifier;

            verifier = new ShuffleProofVerifier(group, cryptosystem, bayerGrothProof.getCommitmentParams(),
                encryptedCiphertexts, mixedEncryptedCiphertexts, m, n, mu, numIterations, ciphertextTools, limMultiExpo);

            updateProgress(20);

            boolean result = verifier.verifyProof(bayerGrothProof.getInitialMessage(), bayerGrothProof.getFirstAnswer(),
                bayerGrothProof.getSecondAnswer());

            updateStateAndProgress(JobState.FINISHED, ONE_HUNDRED_PERCENT);

            LOG.info("Result: " + result);

            if (!result) {
                throw new VerificationFailedException("The proof did not verify");
            }

        } catch (GeneralCryptoLibException e) {

            setStateOfFailedJob();
            this.mixingException =
                new MixingException("Crypto exception while trying to verify proof: " + e.getMessage(), e);

        } catch (InvalidMixablesException e) {

            setStateOfFailedJob();
            this.invalidMixablesException = e;

        } catch (VerificationFailedException e) {

            setStateOfFailedJob();
            this.verificationFailedException = e;
        }
    }

	private static <T extends Mixable> void performBasicValidationsOnStartInputs(List<T> mixables, List<T> mix,
			Proof proof) {
	    requireNonNull(mixables, "Mixables list is null");
        if (mixables.isEmpty()) {
            throw new IllegalArgumentException("The received list of mixables was not initialized and non-empty");
        }
        requireNonNull(mix, "Mix is null");
        if (mix.isEmpty()) {
            throw new IllegalArgumentException("The received list of mixed mixables was not initialized and non-empty");
        }
        if (mixables.size() != mix.size()) {
            throw new IllegalArgumentException("The original list and the mixed list have different sizes");
        }
        requireNonNull(proof, "Proof is null");
    }

    @Override
    public void addEventListener(JobEventListener listener) {
        listeners.add(listener);
        updateListeners();
    }

    @Override
    public void await() throws InterruptedException {

    }

    @Override
    public void await(Duration timeout) throws InterruptedException, TimeoutException {

    }

    @Override
    public void cancel() {
        updateStateAndProgress(JobState.CANCELED, PROGRESS_WHEN_CANCELED_OR_FAILED);
    }

    @Override
    public int progress() {
        return progress;
    }

    @Override
    public void removeEventListener(JobEventListener listener) {
        listeners.add(listener);
    }

    @Override
    public void setParameters(JobParameters parameters) throws InvalidJobParametersException {

        if (parameters instanceof ElGamalJobParameters) {
            elGamalJobParameters = (ElGamalJobParameters) parameters;
        } else {
            throw new IllegalArgumentException("Recieved job parameters which are not for ElGamal");
        }
    }

    @Override
    public JobState state() {
        return jobState;
    }

    @Override
    public void check() throws MixingException {

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

    private void verifyStateCorrectToStartJob() {

        if (elGamalJobParameters == null) {
            throw new IllegalStateException("Started the job without first setting the job parameters");
        }

        if (this.jobState == JobState.STARTED) {
            throw new IllegalStateException("Tried to start a job that was already started");
        } else if (this.jobState == JobState.CANCELED) {
            throw new IllegalStateException("Tried to start a job that was canceled");
        } else if (this.jobState == JobState.FINISHED) {
            throw new IllegalStateException("Tried to start a job that was finished");
        }
    }

    private void updateProgress(int progress) {

        this.progress = progress;
        updateListeners();
    }

    private void setStateOfFailedJob() {
        updateStateAndProgress(JobState.FINISHED, PROGRESS_WHEN_CANCELED_OR_FAILED);
    }

    private static <T> Ciphertext[][] arrangeListTo2DArray(List<T> mixables, int m, int n) {

        List<Ciphertext> ciphertexts = new ArrayList<>();
        for (T mixable : mixables) {
            ciphertexts.add(((ElGamalMixable) mixable).getCiphertext());
        }

        return MatrixArranger.arrangeInCiphertextMatrix(ciphertexts, m, n);
    }

    protected void updateStateAndProgress(JobState jobState, int progress) {

        this.jobState = jobState;
        this.progress = progress;
        updateListeners();
    }

    protected void updateListeners() {

        synchronized (listeners) {
            for (JobEventListener jobEventListener : listeners) {
                JobEvent jobEvent = new JobEvent(this);
                jobEventListener.stateChanged(jobEvent);
            }
            for (JobEventListener jobEventListener : listeners) {
                JobEvent jobEvent = new JobEvent(this);
                jobEventListener.progressChanged(jobEvent);
            }
        }
    }
}
