/*
 * 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.bean.ElGamalPublicKey;
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.bean.SimplePlaintextCiphertextPair;
import com.scytl.cryptolib.proofs.bean.SimplePlaintextPublicKeyPair;
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 simple plaintext equality zero
 * knowledge proof of knowledge applications.
 */
public class TestSimplePlaintextEqualityProof extends Command {

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

    private static final String JS_GENERATED_SIMPLE_PLAINTEXT_EQUALITY_PROOF_EXCEPTION_DATA_KEY =
        "Simple plaintext equality zero knowledge proof (JSON format) [JS]";

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

    private static final String JS_PRE_COMPUTED_SIMPLE_PLAINTEXT_EQUALITY_PROOF_VALUES_EXCEPTION_DATA_KEY =
        "Pre-computed simple plaintext 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 String _simplePlaintextJson;

    private ElGamalPublicKey _primaryPublicKey;

    private String _primaryPublicKeyJson;

    private Witness _witness;

    private String _witnessJson;

    private Ciphertext _primaryCiphertext;

    private String _primaryCiphertextJson;

    private ElGamalPublicKey _secondaryPublicKey;

    private String _secondaryPublicKeyJson;

    private Ciphertext _secondaryCiphertext;

    private String _secondaryCiphertextJson;

    private ProofPreComputedValues _javaPreComputedValues;

    private String _javaPreComputedValuesJson;

    private String _jsPreComputedValuesJson;

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

        _namespace = _engine.get(
            ProofConstants.JS_SIMPLE_PLAINTEXT_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 a simple plaintext equality zero knowledge proof, verifies the
     * result using JavaScript, generates a simple plaintext 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 simple plaintext 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 SIMPLE PLAINTEXT EQUALITY ZERO KNOWLEDGE PROOF WAS SUCCESSFUL.");
    }

    private void generateInputData()
            throws GeneralCryptoLibException, CommandException {

        ElGamalPublicKey parentPublicKey = ElGamalTestDataGenerator
            .getKeyPair(_zpSubgroup,
                ProofConstants.SIMPLE_PLAINTEXT_EQUALITY_PROOF_PLAINTEXT_LENGTH)
            .getPublicKeys();
        SimplePlaintextPublicKeyPair publicKeyPair =
            ProofsTestDataGenerator.getSimplePlaintextPublicKeyPair(
                _zpSubgroup, parentPublicKey);

        List<ZpGroupElement> simplePlaintext =
            ProofsTestDataGenerator.getSimplePlaintext(_zpSubgroup,
                ProofConstants.SIMPLE_PLAINTEXT_EQUALITY_PROOF_PLAINTEXT_LENGTH);
        _simplePlaintextJson = MathematicalCollectionSerializer
            .zpGroupElementsToJson(simplePlaintext);
        putExceptionData(
            "Simple plaintext used to generate ciphertext (JSON format) [Java]",
            _simplePlaintextJson);

        SimplePlaintextCiphertextPair ciphertextPair =
            ProofsTestDataGenerator.encryptSimplePlaintext(parentPublicKey,
                simplePlaintext);

        _primaryPublicKey = publicKeyPair.getPrimaryPublicKey();
        _primaryPublicKeyJson = _primaryPublicKey.toJson();
        putExceptionData("Primary ElGamal public key (JSON format) [Java]",
            _primaryPublicKeyJson);

        _witness = ciphertextPair.getWitness();
        _witnessJson = _witness.getExponent().toJson();
        putExceptionData("Witness (JSON format) [Java]", _witnessJson);

        _primaryCiphertext = ciphertextPair.getPrimaryCiphertext();
        ElGamalComputationsValues elGamalComputationsElements =
            new ElGamalComputationsValues(
                _primaryCiphertext.getElements());
        _primaryCiphertextJson = elGamalComputationsElements.toJson();
        putExceptionData("Primary ciphertext (JSON format) [Java]",
            _primaryCiphertextJson);

        _secondaryPublicKey = publicKeyPair.getSecondaryPublicKey();
        _secondaryPublicKeyJson = _secondaryPublicKey.toJson();
        putExceptionData(
            "Secondary ElGamal public key (JSON format) [Java]",
            _secondaryPublicKeyJson);

        _secondaryCiphertext = ciphertextPair.getSecondaryCiphertext();
        elGamalComputationsElements = new ElGamalComputationsValues(
            _secondaryCiphertext.getElements());
        _secondaryCiphertextJson = elGamalComputationsElements.toJson();
        putExceptionData("Secondary ciphertext (JSON format) [Java]",
            _secondaryCiphertextJson);

        _javaPreComputedValues =
            _preComputer.preComputeSimplePlaintextEqualityProof(
                _primaryPublicKey, _secondaryPublicKey);
        _javaPreComputedValuesJson = MathematicalCollectionSerializer
            .proofPreComputedValuesToJson(_javaPreComputedValues);

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

    private String generateProofUsingJava() throws CommandException {

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

        try {
            return _prover.createSimplePlaintextEqualityProof(
                _primaryCiphertext, _primaryPublicKey, _witness,
                _secondaryCiphertext, _secondaryPublicKey).toJson();
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not generate simple plaintext equality proof, using Java.",
                e);
        }
    }

    private void verifyProofUsingJavaScript(final String proofJson)
            throws CommandException {

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

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

    private String generateProofUsingJavaScript() throws CommandException {

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

        try {
            return (String) getInvocable().invokeMethod(_namespace,
                ProofConstants.JS_PROOF_GENERATION_FUNCTION,
                _zpSubgroupJson, _primaryPublicKeyJson, _witnessJson,
                _primaryCiphertextJson, _secondaryPublicKeyJson,
                _secondaryCiphertextJson);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder()
                .addMessage(
                    "Attempt by script engine to call non-existent function "
                        + ProofConstants.JS_SIMPLE_PLAINTEXT_EQUALITY_PROOF_TEST_NAMESPACE
                        + "." + ProofConstants.JS_PROOF_GENERATION_FUNCTION
                        + " in file "
                        + ProofConstants.JS_SIMPLE_PLAINTEXT_EQUALITY_PROOF_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not generate simple plaintext equality proof, using JavaScript.",
                e);
        }
    }

    private void verifyProofUsingJava(final String proofJson)
            throws CommandException {

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

        putExceptionData(
            JS_GENERATED_SIMPLE_PLAINTEXT_EQUALITY_PROOF_EXCEPTION_DATA_KEY,
            proofJson);

        try {
            boolean verified = _verifier
                .verifySimplePlaintextEqualityProof(_primaryCiphertext,
                    _primaryPublicKey, _secondaryCiphertext,
                    _secondaryPublicKey, Proof.fromJson(proofJson));

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

    private String generatePreComputedProofUsingJava()
            throws CommandException {

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

        removeExceptionData(
            JS_PRE_COMPUTED_SIMPLE_PLAINTEXT_EQUALITY_PROOF_VALUES_EXCEPTION_DATA_KEY);
        putExceptionData(
            JAVA_PRE_COMPUTED_SIMPLE_PLAINTEXT_EQUALITY_PROOF_VALUES_EXCEPTION_DATA_KEY,
            _javaPreComputedValuesJson);

        try {
            return _prover
                .createSimplePlaintextEqualityProof(_primaryCiphertext,
                    _primaryPublicKey, _witness, _secondaryCiphertext,
                    _secondaryPublicKey, _javaPreComputedValues)
                .toJson();
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not generate simple plaintext 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 simple plaintext equality proof, using JavaScript...");

        try {
            getInvocable().invokeMethod(_namespace,
                ProofConstants.JS_PROOF_VERIFICATION_FUNCTION,
                _zpSubgroupJson, _primaryPublicKeyJson,
                _primaryCiphertextJson, _secondaryPublicKeyJson,
                _secondaryCiphertextJson, proofJson);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder().addMessage(
                "Attempt by script engine to call non-existent function "
                    + ProofConstants.JS_SIMPLE_PLAINTEXT_EQUALITY_PROOF_TEST_NAMESPACE
                    + "." + ProofConstants.JS_PROOF_VERIFICATION_FUNCTION
                    + " in resources file "
                    + ProofConstants.JS_SIMPLE_PLAINTEXT_EQUALITY_PROOF_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not verify Java generated pre-computed simple plaintext equality proof, using JavaScript.",
                e);
        }
    }

    private String generatePreComputedProofUsingJavaScript()
            throws CommandException {

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

        try {
            return (String) getInvocable().invokeMethod(_namespace,
                ProofConstants.JS_PROOF_GENERATION_FUNCTION,
                _zpSubgroupJson, _primaryPublicKeyJson, _witnessJson,
                _primaryCiphertextJson, _secondaryPublicKeyJson,
                _secondaryCiphertextJson, _jsPreComputedValuesJson);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder()
                .addMessage(
                    "Attempt by script engine to call non-existent function "
                        + ProofConstants.JS_SIMPLE_PLAINTEXT_EQUALITY_PROOF_TEST_NAMESPACE
                        + "." + ProofConstants.JS_PROOF_GENERATION_FUNCTION
                        + " in file "
                        + ProofConstants.JS_SIMPLE_PLAINTEXT_EQUALITY_PROOF_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not generate simple plaintext equality proof, via pre-computed values, using JavaScript.",
                e);
        }
    }

    private void verifyPreComputedProofUsingJava(final String proofJson)
            throws CommandException {

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

        putExceptionData(
            JS_GENERATED_SIMPLE_PLAINTEXT_EQUALITY_PROOF_EXCEPTION_DATA_KEY,
            proofJson);
        removeExceptionData(
            JAVA_PRE_COMPUTED_SIMPLE_PLAINTEXT_EQUALITY_PROOF_VALUES_EXCEPTION_DATA_KEY);
        putExceptionData(
            JS_PRE_COMPUTED_SIMPLE_PLAINTEXT_EQUALITY_PROOF_VALUES_EXCEPTION_DATA_KEY,
            _jsPreComputedValuesJson);

        try {
            boolean verified = _verifier
                .verifySimplePlaintextEqualityProof(_primaryCiphertext,
                    _primaryPublicKey, _secondaryCiphertext,
                    _secondaryPublicKey, Proof.fromJson(proofJson));

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

    }
}
