/*
 * 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 static java.util.Collections.singletonList;

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.ElGamalEncrypterValues;
import com.scytl.cryptolib.elgamal.bean.ElGamalKeyPair;
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.primitives.primes.utils.PrimitivesTestDataGenerator;
import com.scytl.cryptolib.primitives.securerandom.constants.SecureRandomConstants;
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.test.tools.utils.CommonTestDataGenerator;

/**
 * Tests the Java-JavaScript interoperation for OR zero knowledge proof of
 * knowledge applications.
 */
public class TestORProof extends Command {

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

    private static final String JS_GENERATED_OR_PROOF_EXCEPTION_DATA_KEY =
        "OR zero knowledge proof (JSON format) [JS]";

    private static final String JAVA_GENERATED_PRE_COMPUTED_OR_PROOF_VALUES_EXCEPTION_DATA_KEY =
        "Pre-computed OR proof values (JSON format) [Java]";

    private static final String JS_GENERATED_PRE_COMPUTED_OR_PROOF_VALUES_EXCEPTION_DATA_KEY =
        "Pre-computed OR 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> _elements;

    private String _elementsJson;

    private int _index;

    private ElGamalPublicKey _publicKey;

    private String _publicKeyJson;

    private Ciphertext _ciphertext;

    private String _ciphertextJson;

    private Witness _witness;

    private String _witnessJson;

    private String _data;

    private ProofPreComputedValues _javaPreComputedValues;

    private String _javaPreComputedValuesJson;

    private String _jsPreComputedValuesJson;

    /**
     * Default constructor.
     *
     * @throws CommandException
     *             if the script engine cannot evaluate theOR zero knowledge
     *             proof of knowledge interoperation tests implemented in
     *             JavaScript.
     */
    public TestORProof() throws CommandException {
        super(ProofConstants.JS_OR_PROOF_TEST_PATH);

        _namespace =
            _engine.get(ProofConstants.JS_OR_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 OR zero knowledge proof, verifies the result using
     * JavaScript, generates an OR 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
     * the OR 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 OR ZERO KNOWLEDGE PROOF WAS SUCCESSFUL.");
    }

    private void generateInputData()
            throws GeneralCryptoLibException, CommandException {
        _elements = MathematicalTestDataGenerator.getZpGroupElements(
            _zpSubgroup, ProofConstants.OR_PROOF_NUMBER_OF_ELEMENTS);
        _elementsJson = MathematicalCollectionSerializer
            .zpGroupElementsToJson(_elements);
        putExceptionData("Elements (JSON format) [Java]", _elementsJson);

        _index = CommonTestDataGenerator.getInt(0, _elements.size() - 1);
        putExceptionData("Index [Java]", Integer.toString(_index));

        ElGamalKeyPair pair =
            ElGamalTestDataGenerator.getKeyPair(_zpSubgroup, 1);
        _publicKey = pair.getPublicKeys();
        _publicKeyJson = _publicKey.toJson();
        putExceptionData("ElGamal public key (JSON format) [Java]",
            _publicKeyJson);

        ElGamalEncrypterValues encrypterValues =
            (ElGamalEncrypterValues) ElGamalTestDataGenerator
                .encryptGroupElements(_publicKey,
                    singletonList(_elements.get(_index)));
        _ciphertext = encrypterValues;
        _ciphertextJson = encrypterValues.getComputationValues().toJson();
        putExceptionData("Ciphertext (JSON format) [Java]",
            _ciphertextJson);

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

        int numChars = CommonTestDataGenerator.getInt(1,
            SecureRandomConstants.MAXIMUM_GENERATED_STRING_LENGTH);
        _data = PrimitivesTestDataGenerator.getString64(numChars);
        putExceptionData("Data [Java]", _data);

        _javaPreComputedValues =
            _preComputer.preComputeORProof(_publicKey, _elements.size());
        _javaPreComputedValuesJson = MathematicalCollectionSerializer
            .proofPreComputedValuesToJson(_javaPreComputedValues);

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

    private String generateProofUsingJava() throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Generating OR proof, using Java...");

        try {
            return _prover.createORProof(_ciphertext, _publicKey, _witness,
                _index, _elements, _data).toJson();
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not generate OR proof, using Java.", e);
        }
    }

    private void verifyProofUsingJavaScript(final String proofJson)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Verifying OR proof, using JavaScript...");

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

    private String generateProofUsingJavaScript() throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Generating OR proof, using JavaScript...");

        try {
            return (String) getInvocable().invokeMethod(_namespace,
                ProofConstants.JS_PROOF_GENERATION_FUNCTION,
                _zpSubgroupJson, _publicKeyJson, _ciphertextJson,
                _witnessJson, _elementsJson, _index, _data);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder().addMessage(
                "Attempt by script engine to call non-existent function "
                    + ProofConstants.JS_OR_PROOF_TEST_NAMESPACE + "."
                    + ProofConstants.JS_PROOF_GENERATION_FUNCTION
                    + " in file " + ProofConstants.JS_OR_PROOF_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not generate OR proof, using JavaScript.", e);
        }
    }

    private void verifyProofUsingJava(final String proofJson)
            throws CommandException {

        LOGGER.info(CommonConstants.LOGGER_PROMPT
            + "Verifying OR proof, using Java...");

        putExceptionData(JS_GENERATED_OR_PROOF_EXCEPTION_DATA_KEY,
            proofJson);

        try {
            boolean verified = _verifier.verifyORProof(_ciphertext,
                _publicKey, _elements, _data, Proof.fromJson(proofJson));

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

    private String generatePreComputedProofUsingJava()
            throws CommandException {

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

        putExceptionData(
            JAVA_GENERATED_PRE_COMPUTED_OR_PROOF_VALUES_EXCEPTION_DATA_KEY,
            _javaPreComputedValuesJson);

        try {
            return _prover.createORProof(_ciphertext, _publicKey, _witness,
                _index, _elements, _data, _javaPreComputedValues).toJson();
        } catch (GeneralCryptoLibException e) {
            throw buildExceptionWithData(
                "Could not generate OR proof, via pre-computed values, using Java.",
                e);
        }
    }

    private void verifyPreComputedProofUsingJavaScript(
            final String proofJson) throws CommandException {

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

        try {
            getInvocable().invokeMethod(_namespace,
                ProofConstants.JS_PROOF_VERIFICATION_FUNCTION,
                _zpSubgroupJson, _publicKeyJson, _ciphertextJson,
                _elementsJson, _data, proofJson);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder()
                .addMessage(
                    "Attempt by script engine to call non-existent function "
                        + ProofConstants.JS_OR_PROOF_TEST_NAMESPACE + "."
                        + ProofConstants.JS_PROOF_VERIFICATION_FUNCTION
                        + " in resources file "
                        + ProofConstants.JS_OR_PROOF_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not verify Java generated pre-computed OR proof, using JavaScript.",
                e);
        }
    }

    private String generatePreComputedProofUsingJavaScript()
            throws CommandException {

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

        removeExceptionData(
            JAVA_GENERATED_PRE_COMPUTED_OR_PROOF_VALUES_EXCEPTION_DATA_KEY);
        putExceptionData(
            JS_GENERATED_PRE_COMPUTED_OR_PROOF_VALUES_EXCEPTION_DATA_KEY,
            _jsPreComputedValuesJson);

        try {
            return (String) getInvocable().invokeMethod(_namespace,
                ProofConstants.JS_PROOF_GENERATION_FUNCTION,
                _zpSubgroupJson, _publicKeyJson, _ciphertextJson,
                _witnessJson, _elementsJson, _index, _data,
                _jsPreComputedValuesJson);
        } catch (NoSuchMethodException e) {
            throw new CommandException.Builder().addMessage(
                "Attempt by script engine to call non-existent function "
                    + ProofConstants.JS_OR_PROOF_TEST_NAMESPACE + "."
                    + ProofConstants.JS_PROOF_GENERATION_FUNCTION
                    + " in file " + ProofConstants.JS_OR_PROOF_TEST_PATH)
                .addCause(e).build();
        } catch (ScriptException e) {
            throw buildExceptionWithData(
                "Could not generate OR proof, via pre-computed values, using JavaScript.",
                e);
        }
    }

    private void verifyPreComputedProofUsingJava(final String proofJson)
            throws CommandException {

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

        putExceptionData(JS_GENERATED_OR_PROOF_EXCEPTION_DATA_KEY,
            proofJson);

        try {
            boolean verified = _verifier.verifyORProof(_ciphertext,
                _publicKey, _elements, _data, Proof.fromJson(proofJson));

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