/*
 * 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.cryptolib.interoperation.js.commands.proofs;

import java.util.List;

import javax.script.ScriptException;

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

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.api.proofs.ProofsServiceAPI;
import com.scytl.cryptolib.elgamal.bean.ElGamalComputationsValues;
import com.scytl.cryptolib.elgamal.cryptoapi.Ciphertext;
import com.scytl.cryptolib.elgamal.cryptoapi.Witness;
import com.scytl.cryptolib.elgamal.utils.ElGamalTestDataGenerator;
import com.scytl.cryptolib.interoperation.js.commands.Command;
import com.scytl.cryptolib.interoperation.js.commands.CommandException;
import com.scytl.cryptolib.interoperation.js.constants.CommonConstants;
import com.scytl.cryptolib.interoperation.js.constants.ProofConstants;
import com.scytl.cryptolib.interoperation.js.utils.MathematicalCollectionSerializer;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.cryptolib.mathematical.groups.impl.ZpSubgroup;
import com.scytl.cryptolib.mathematical.groups.utils.MathematicalTestDataGenerator;
import com.scytl.cryptolib.proofs.bean.ProofPreComputedValues;
import com.scytl.cryptolib.proofs.cryptoapi.ProofPreComputerAPI;
import com.scytl.cryptolib.proofs.cryptoapi.ProofProverAPI;
import com.scytl.cryptolib.proofs.cryptoapi.ProofVerifierAPI;
import com.scytl.cryptolib.proofs.proof.Proof;
import com.scytl.cryptolib.proofs.service.ProofsService;
import com.scytl.cryptolib.proofs.utils.ProofsTestDataGenerator;

/**
 * Tests the Java-JavaScript interoperation for plaintext exponent equality zero
 * knowledge proof of knowledge applications.
 */
public class TestPlaintextExponentEqualityProof extends Command {

    private static final Logger LOGGER =
        LoggerFactory.getLogger(TestPlaintextExponentEqualityProof.class);

    private static final String JS_GENERATED_PLAINTEXT_EXPONENT_EQUALITY_PROOF_EXCEPTION_DATA_KEY =
        "Plaintext exponent equality zero knowledge proof (JSON format) [JS]";

    private static final String JAVA_PRE_COMPUTED_PLAINTEXT_EXPONENT_EQUALITY_PROOF_VALUES_EXCEPTION_DATA_KEY =
        "Pre-computed plaintext exponent equality proof values (JSON format) [Java]";

    private static final String JS_PRE_COMPUTED_PLAINTEXT_EXPONENT_EQUALITY_PROOF_VALUES_EXCEPTION_DATA_KEY =
        "Pre-computed plaintext exponent equality proof values (JSON format) [JS]";

    private final Object _namespace;

    private final ZpSubgroup _zpSubgroup;

    private final String _zpSubgroupJson;

    private final ProofProverAPI _prover;

    private final ProofVerifierAPI _verifier;

    private final ProofPreComputerAPI _preComputer;

    private List<ZpGroupElement> _baseElements;

    private String _baseElementsJson;

    private Witness _witness1;

    private String _witness1Json;

    private Witness _witness2;

    private String _witness2Json;

    private Ciphertext _ciphertext;

    private String _ciphertextJson;

    private ProofPreComputedValues _javaPreComputedValues;

    private String _javaPreComputedValuesJson;

    private String _jsPreComputedValuesJson;

    /**
     * Default constructor.
     *
     * @throws CommandException
     *             if the script engine cannot evaluate the plaintext exponent
     *             equality zero knowledge proof of knowledge interoperation
     *             tests implemented in JavaScript.
     */
    public TestPlaintextExponentEqualityProof() throws CommandException {
        super(
            ProofConstants.JS_PLAINTEXT_EXPONENT_EQUALITY_PROOF_TEST_PATH);

        _namespace = _engine.get(
            ProofConstants.JS_PLAINTEXT_EXPONENT_EQUALITY_PROOF_TEST_NAMESPACE);

        clearExceptionData();

        try {
            _zpSubgroup = MathematicalTestDataGenerator.getZpSubgroup();
            _zpSubgroupJson = _zpSubgroup.toJson();
            putExceptionData("Zp subgroup (JSON format) [Java]",
                _zpSubgroupJson);

            ProofsServiceAPI proofsService = new ProofsService();
            _prover = proofsService.createProofProverAPI(_zpSubgroup);
            _verifier = proofsService.createProofVerifierAPI(_zpSubgroup);
            _preComputer =
                proofsService.createProofPreComputerAPI(_zpSubgroup);

            generateInputData();
        } catch (GeneralCryptoLibException e) {
            throw new CommandException.Builder().addCause(e).build();
        }
    }

    /**
     * Generates an plaintext exponent equality zero knowledge proof, verifies
     * the result using JavaScript, generates an plaintext exponent equality
     * zero knowledge proof with the same data using JavaScript and verifies the
     * result using Java. Repeats the same steps described above, but using
     * pre-computed values. If no exceptions are thrown and all verifications
     * are true, then the interoperation test for plaintext exponent equality
     * zero knowledge proof of knowledge applications was successful.
     *
     * @throws CommandException
     *             if a JavaScript method cannot be found, a cryptographic
     *             operation fails or a proof verification fails.
     */
    @Override
    public void execute() throws CommandException {

        String proofJson = generateProofUsingJava();

        verifyProofUsingJavaScript(proofJson);

        proofJson = generateProofUsingJavaScript();

        verifyProofUsingJava(proofJson);

        proofJson = generatePreComputedProofUsingJava();

        verifyPreComputedProofUsingJavaScript(proofJson);

        proofJson = generatePreComputedProofUsingJavaScript();

        verifyPreComputedProofUsingJava(proofJson);

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "INTEROPERATION TEST FOR PLAINTEXT EXPONENT EQUALITY ZERO KNOWLEDGE PROOF WAS SUCCESSFUL.");
    }

    private void generateInputData()
            throws GeneralCryptoLibException, CommandException {

        _baseElements = MathematicalTestDataGenerator.getZpGroupElements(
            _zpSubgroup,
            ProofConstants.PLAINTEXT_EXPONENT_EQUALITY_PROOF_PLAINTEXT_LENGTH);
        _baseElementsJson = MathematicalCollectionSerializer
            .zpGroupElementsToJson(_baseElements);
        putExceptionData("Base elements (JSON format) [Java]",
            _baseElementsJson);

        _witness1 = ElGamalTestDataGenerator.getWitness(_zpSubgroup);
        _witness1Json = _witness1.getExponent().toJson();
        putExceptionData("Witness 1 (JSON format) [Java]", _witness1Json);

        _witness2 = ElGamalTestDataGenerator.getWitness(_zpSubgroup);
        _witness2Json = _witness2.getExponent().toJson();
        putExceptionData("Witness 2 (JSON format) [Java]", _witness2Json);

        _ciphertext = ProofsTestDataGenerator
            .getPlaintextExponentEqualityProofCiphertext(_zpSubgroup,
                _baseElements, _witness1, _witness2);
        ElGamalComputationsValues elGamalComputationsElements =
            new ElGamalComputationsValues(_ciphertext.getElements());
        _ciphertextJson = elGamalComputationsElements.toJson();
        putExceptionData("Ciphertext (JSON format) [Java]",
            _ciphertextJson);

        _javaPreComputedValues = _preComputer
            .preComputePlaintextExponentEqualityProof(_baseElements);
        _javaPreComputedValuesJson = MathematicalCollectionSerializer
            .proofPreComputedValuesToJson(_javaPreComputedValues);

        try {
            _jsPreComputedValuesJson =
                (String) getInvocable().invokeMethod(_namespace,
                    ProofConstants.JS_PROOF_PRE_COMPUTED_VALUES_GENERATION_FUNCTION,
                    _zpSubgroupJson, _baseElementsJson);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder()
                .addMessage(
                    "Attempt by script engine to call non-existent function "
                        + ProofConstants.JS_PLAINTEXT_EXPONENT_EQUALITY_PROOF_TEST_NAMESPACE
                        + "."
                        + ProofConstants.JS_PROOF_PRE_COMPUTED_VALUES_GENERATION_FUNCTION
                        + " in file "
                        + ProofConstants.JS_PLAINTEXT_EXPONENT_EQUALITY_PROOF_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not generate pre-computed plaintext equality proof values, using JavaScript.",
                e);
        }
    }

    private String generateProofUsingJava() throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Generating plaintext exponent equality proof, using Java...");

        try {
            return _prover.createPlaintextExponentEqualityProof(
                _ciphertext, _baseElements, _witness1, _witness2).toJson();
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not generate plaintext exponent equality proof, using Java.",
                e);
        }
    }

    private void verifyProofUsingJavaScript(final String proofJson)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Verifying plaintext exponent equality proof, using JavaScript...");

        try {
            getInvocable().invokeMethod(_namespace,
                ProofConstants.JS_PROOF_VERIFICATION_FUNCTION,
                _zpSubgroupJson, _baseElementsJson, _ciphertextJson,
                proofJson);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder().addMessage(
                "Attempt by script engine to call non-existent function "
                    + ProofConstants.JS_PLAINTEXT_EXPONENT_EQUALITY_PROOF_TEST_NAMESPACE
                    + "." + ProofConstants.JS_PROOF_VERIFICATION_FUNCTION
                    + " in resources file "
                    + ProofConstants.JS_PLAINTEXT_EXPONENT_EQUALITY_PROOF_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not verify Java generated plaintext exponent equality proof, using JavaScript.",
                e);
        }
    }

    private String generateProofUsingJavaScript() throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Generating plaintext exponent equality proof, using JavaScript...");

        try {
            return (String) getInvocable().invokeMethod(_namespace,
                ProofConstants.JS_PROOF_GENERATION_FUNCTION,
                _zpSubgroupJson, _baseElementsJson, _ciphertextJson,
                _witness1Json, _witness2Json);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder()
                .addMessage(
                    "Attempt by script engine to call non-existent function "
                        + ProofConstants.JS_PLAINTEXT_EXPONENT_EQUALITY_PROOF_TEST_NAMESPACE
                        + "." + ProofConstants.JS_PROOF_GENERATION_FUNCTION
                        + " in file "
                        + ProofConstants.JS_PLAINTEXT_EXPONENT_EQUALITY_PROOF_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not generate plaintext exponent equality proof, using JavaScript.",
                e);
        }
    }

    private void verifyProofUsingJava(final String proofJson)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Verifying plaintext exponent equality proof, using Java...");

        putExceptionData(
            JS_GENERATED_PLAINTEXT_EXPONENT_EQUALITY_PROOF_EXCEPTION_DATA_KEY,
            proofJson);

        try {
            boolean verified =
                _verifier.verifyPlaintextExponentEqualityProof(_ciphertext,
                    _baseElements, Proof.fromJson(proofJson));

            if (!verified) {
                throw new GeneralCryptoLibException(
                    "Verification of plaintext exponent equality proof (JSON format) "
                        + proofJson + " was false.");
            }
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not verify JavaScript generated plaintext exponent equality proof, using Java.",
                e);
        }
    }

    private String generatePreComputedProofUsingJava()
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Generating plaintext exponent equality proof, via pre-computed values, using Java...");

        removeExceptionData(
            JS_PRE_COMPUTED_PLAINTEXT_EXPONENT_EQUALITY_PROOF_VALUES_EXCEPTION_DATA_KEY);
        putExceptionData(
            JAVA_PRE_COMPUTED_PLAINTEXT_EXPONENT_EQUALITY_PROOF_VALUES_EXCEPTION_DATA_KEY,
            _javaPreComputedValuesJson);

        try {
            return _prover.createPlaintextExponentEqualityProof(
                _ciphertext, _baseElements, _witness1, _witness2,
                _javaPreComputedValues).toJson();
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not generate plaintext exponent equality proof, via pre-computed values, using Java.",
                e);
        }
    }

    private void verifyPreComputedProofUsingJavaScript(
            final String proofJson) throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Verifying pre-computed plaintext exponent equality proof, using JavaScript...");

        try {
            getInvocable().invokeMethod(_namespace,
                ProofConstants.JS_PROOF_VERIFICATION_FUNCTION,
                _zpSubgroupJson, _baseElementsJson, _ciphertextJson,
                proofJson);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder().addMessage(
                "Attempt by script engine to call non-existent function "
                    + ProofConstants.JS_PLAINTEXT_EXPONENT_EQUALITY_PROOF_TEST_NAMESPACE
                    + "." + ProofConstants.JS_PROOF_VERIFICATION_FUNCTION
                    + " in resources file "
                    + ProofConstants.JS_PLAINTEXT_EXPONENT_EQUALITY_PROOF_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not verify Java generated plaintext exponent equality proof, using JavaScript.",
                e);
        }
    }

    private String generatePreComputedProofUsingJavaScript()
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Generating plaintext exponent equality proof, via pre-computed values, using JavaScript...");

        try {
            return (String) getInvocable().invokeMethod(_namespace,
                ProofConstants.JS_PROOF_GENERATION_FUNCTION,
                _zpSubgroupJson, _baseElementsJson, _ciphertextJson,
                _witness1Json, _witness2Json, _jsPreComputedValuesJson);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder()
                .addMessage(
                    "Attempt by script engine to call non-existent function "
                        + ProofConstants.JS_PLAINTEXT_EXPONENT_EQUALITY_PROOF_TEST_NAMESPACE
                        + "." + ProofConstants.JS_PROOF_GENERATION_FUNCTION
                        + " in file "
                        + ProofConstants.JS_PLAINTEXT_EXPONENT_EQUALITY_PROOF_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not generate plaintext exponent equality proof, via pre-computed values, using JavaScript.",
                e);
        }
    }

    private void verifyPreComputedProofUsingJava(final String proofJson)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Verifying plaintext exponent equality proof, using Java...");

        putExceptionData(
            JS_GENERATED_PLAINTEXT_EXPONENT_EQUALITY_PROOF_EXCEPTION_DATA_KEY,
            proofJson);
        removeExceptionData(
            JAVA_PRE_COMPUTED_PLAINTEXT_EXPONENT_EQUALITY_PROOF_VALUES_EXCEPTION_DATA_KEY);
        putExceptionData(
            JS_PRE_COMPUTED_PLAINTEXT_EXPONENT_EQUALITY_PROOF_VALUES_EXCEPTION_DATA_KEY,
            _jsPreComputedValuesJson);

        try {
            boolean verified =
                _verifier.verifyPlaintextExponentEqualityProof(_ciphertext,
                    _baseElements, Proof.fromJson(proofJson));

            if (!verified) {
                throw new GeneralCryptoLibException(
                    "Verification of plaintext exponent equality proof (JSON format) "
                        + proofJson + " was false.");
            }
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not verify JavaScript generated pre-computed plaintext exponent equality proof, using Java.",
                e);
        }
    }
}
