/*
 * 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.proofs.bg.zero;

import java.math.BigInteger;

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

import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.mathematical.groups.impl.Exponent;
import com.scytl.ov.mixing.commons.beans.proofs.ZeroProofAnswer;
import com.scytl.ov.mixing.commons.beans.proofs.ZeroProofInitialMessage;
import com.scytl.ov.mixing.commons.proofs.bg.commitments.CommitmentParams;
import com.scytl.ov.mixing.commons.proofs.bg.commitments.PublicCommitment;
import com.scytl.ov.mixing.commons.tools.ExponentTools;
import com.scytl.ov.mixing.commons.tools.GroupTools;
import com.scytl.ov.mixing.commons.tools.MultiExponentiation;
import com.scytl.ov.mixing.commons.tools.RandomOracleHash;

public class ZeroProofVerifier {

    private static final Logger LOG = LoggerFactory.getLogger("std");

    private final CommitmentParams params;

    private final PublicCommitment[] cA;

    private final PublicCommitment[] cB;

    private final int n;

    private final int m;

    private final RandomOracleHash rO;

    private final BigInteger groupOrder;

    private final Exponent challengeInnerProduct;

    private final MultiExponentiation multiExponentiation;

    public ZeroProofVerifier(final int m, final int n, final CommitmentParams params, final PublicCommitment[] cA,
            final PublicCommitment[] cB, final BigInteger groupOrder, final Exponent challengeInnerProduct,
            final MultiExponentiation multiExponentiation) {

        this.params = params;
        this.n = n;
        if (m == 1) {
        	this.m = 2;
        } else {
        	this.m = m;
        }

        this.groupOrder = groupOrder;

        this.cA = new PublicCommitment[this.m + 1];
        System.arraycopy(cA, 0, this.cA, 1, this.m);

        this.cB = new PublicCommitment[this.m + 1];
        System.arraycopy(cB, 0, this.cB, 0, this.m);
        this.rO = new RandomOracleHash(groupOrder);
        this.rO.addDataToRO(cA);
        this.rO.addDataToRO(cB);

        this.challengeInnerProduct = challengeInnerProduct;

        this.multiExponentiation = multiExponentiation;
    }

    public boolean verify(final ZeroProofInitialMessage initial, final ZeroProofAnswer answer) {

        boolean correct = true;

        final PublicCommitment[] cD = initial.getCommitmentPublicD();
        cA[0] = initial.getCommitmentPublicA0();
        cB[m] = initial.getCommitmentPublicBM();

        // Checking that things are group elements

        if (!GroupTools.isGroupElement(cA[0].getElement())) {
            LOG.error("ERROR(Zero Argument): cA0 is not a group element");
            correct = false;
        }

        if (!GroupTools.isGroupElement(cB[m].getElement())) {
            LOG.error("ERROR(Zero Argument): cBm is not a group element");
            correct = false;
        }

        if ((cD.length != 2 * m + 1) && (m != 1)) {

            LOG.error("ERROR(Zero Argument): cD does not have the expected length");
            correct = false;
        }

        for (int i = 0; i < cD.length && correct; i++) {

            if (!GroupTools.isGroupElement(cD[i].getElement())) {
                LOG.error("ERROR(Zero Argument): cD[" + i + "] is not a group element");
                correct = false;
            }
        }

        try {

            // Checking that cD[m+1] is a commitment to 0
            if (!cD[m + 1].verifyOpening( //
                new Exponent[] {new Exponent(groupOrder, BigInteger.ZERO) }, //
                new Exponent(groupOrder, BigInteger.ZERO), params, multiExponentiation) && (m != 1)) {

                correct = false;
                LOG.error("ERROR(Zero Argument): cD[m+1] is not a commitment to 0 with randomness 0");
            }

        } catch (GeneralCryptoLibException ex) {
            throw new CryptoLibException(ex);
        }

        // Checking that things are valid exponents
        if (answer.getExponentsA().length != n) {
            correct = false;
            LOG.error("ERROR(Zero Argument): a doesn't have the expected length");
        }
        for (int i = 0; (i < n) && correct; i++) {

            if (!ExponentTools.isExponent(answer.getExponentsA()[i])) {
                correct = false;
                LOG.error("ERROR(Zero Argument): a[" + i + "] is not a valid exponent");
            }
            if (!hasSameOrder(answer.getExponentsA()[i])) {
                correct = false;
                LOG.error("ERROR(Zero Argument): a[" + i + "] is not a group element");
            }
        }

        if (answer.getExponentsB().length != n) {
            correct = false;
            LOG.error("ERROR(Zero Argument): b doesn't have the expected length");
        }
        for (int i = 0; (i < n) && correct; i++) {

            if (!ExponentTools.isExponent(answer.getExponentsB()[i])) {
                correct = false;
                LOG.error("ERROR(Zero Argument): b[" + i + "] is not a valid exponent");
            }
            if (!hasSameOrder(answer.getExponentsB()[i])) {
                correct = false;
                LOG.error("ERROR(Zero Argument): b[" + i + "] is not a group element");
            }

        }

        if (!ExponentTools.isExponent(answer.getExponentR())) {
            correct = false;
            LOG.error("ERROR(Zero Argument): r is not a valid exponent");
        }
        if (!hasSameOrder(answer.getExponentR())) {
            correct = false;
            LOG.error("ERROR(Zero Argument): r is not a group element");
        }

        if (!ExponentTools.isExponent(answer.getExponentS())) {
            correct = false;
            LOG.error("ERROR(Zero Argument): s is not a valid exponent");
        }
        if (!hasSameOrder(answer.getExponentS())) {
            correct = false;
            LOG.error("ERROR(Zero Argument): s is not a group element");
        }

        if (!ExponentTools.isExponent(answer.getExponentT())) {
            correct = false;
            LOG.error("ERROR(Zero Argument): t is not a valid exponent");
        }
        if (!hasSameOrder(answer.getExponentT())) {
            correct = false;
            LOG.error("ERROR(Zero Argument): t is not a group element");
        }

        // Done with the membership checks. Now checking commitments match

        // We first compute the commitments from the challenge

        PublicCommitment comCAR = cA[0];
        PublicCommitment comCBS = cB[m];
        PublicCommitment comCABT = cD[0];

        PublicCommitment[] inputStatement = new PublicCommitment[m];
        System.arraycopy(cA, 1, inputStatement, 0, m);

        rO.addDataToRO(initial);
        final Exponent challengeX = rO.getHash();

        Exponent accumulator = challengeX;

        try {

            for (int i = 1; i < m + 1; i++) {
                comCAR = comCAR.multiply(cA[i].exponentiate(accumulator));
                comCBS = comCBS.multiply(cB[m - i].exponentiate(accumulator));
                comCABT = comCABT.multiply(cD[i].exponentiate(accumulator));
                accumulator = accumulator.multiply(challengeX);

            }

            for (int i = m + 1; i < 2 * m + 1; i++) {
                comCABT = comCABT.multiply(cD[i].exponentiate(accumulator));
                accumulator = accumulator.multiply(challengeX);
            }

        } catch (GeneralCryptoLibException ex) {
            throw new CryptoLibException(ex);
        }

        // Now we check the openings

        if (!comCAR.verifyOpening(answer.getExponentsA(), answer.getExponentR(), params, multiExponentiation)) {
            LOG.error("ERROR(Zero Argument): the commitment to a is incorrect");
            correct = false;
        }
        if (!comCBS.verifyOpening(answer.getExponentsB(), answer.getExponentS(), params, multiExponentiation)) {
            LOG.error("ERROR(Zero Argument): the commitment to b is incorrect");
            correct = false;
        }
        if (!comCABT.verifyOpening( //
            new Exponent[] { //
                    ExponentTools.innerProduct(answer.getExponentsA(), answer.getExponentsB(), groupOrder,
                        challengeInnerProduct) }, //
            answer.getExponentT(), //
            params, multiExponentiation)) {
            LOG.error("ERROR(Zero Argument): the commitment to a inner b is incorrect");
            correct = false;
        }

        if (correct) {
            LOG.info("The Zero Argument was verified successfully!");
        }
        return correct;
    }

    private boolean hasSameOrder(Exponent exponent) {
        return groupOrder.equals(exponent.getQ());
    }
}
