/*
 * 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.proofs.maurer.factory;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;

import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.commons.utils.validations.Validate;
import com.scytl.cryptolib.mathematical.groups.GroupElement;
import com.scytl.cryptolib.mathematical.groups.MathematicalGroup;
import com.scytl.cryptolib.mathematical.groups.impl.Exponent;
import com.scytl.cryptolib.mathematical.groups.impl.ZpGroupElement;
import com.scytl.cryptolib.primitives.messagedigest.configuration.ConfigMessageDigestAlgorithmAndProvider;
import com.scytl.cryptolib.proofs.maurer.configuration.ConfigProofHashCharset;
import com.scytl.cryptolib.proofs.maurer.function.PhiFunction;
import com.scytl.cryptolib.proofs.proof.Proof;
import com.scytl.cryptolib.proofs.utils.HashBuilder;

/**
 * Class which acts as a verifier in a Zero-Knowledge Proof of Knowledge
 * (ZK-PoK) exchange using Maurer's unified PHI function.
 *
 * @param <T>
 *            the type which parameterizes the group elements of the
 *            mathematical group that will be used during this exchange.
 * @param <E>
 *            the type of the group elements of the MathematicalGroup which will
 *            be used during this exchange. E must be a type which extends
 *            GroupElement{@code <T>}.
 */
public final class Verifier<T, E extends GroupElement<T>> {

    private final MathematicalGroup<E> _group;

    private final PhiFunction<T> _phiFunction;

    private final ConfigMessageDigestAlgorithmAndProvider _configMessageDigestAlgorithmAndProvider;

    private final ConfigProofHashCharset _configProofHashCharset;

    /**
     * Creates an instance of a Verifier and initializes it by the provided
     * arguments.
     *
     * @param group
     *            the mathematical group to be used during this exchange.
     * @param phiFunction
     *            the PHI function to be used by the prover during the exchange.
     * @param configMessageDigestAlgorithmAndProvider
     *            the message digest policy.
     * @param charset
     *            the charset policy.
     * @throws GeneralCryptoLibException
     *             if {@code group} or {@code phiFunction} is null.
     */
    public Verifier(
            final MathematicalGroup<E> group,
            final PhiFunction<T> phiFunction,
            final ConfigMessageDigestAlgorithmAndProvider configMessageDigestAlgorithmAndProvider,
            final ConfigProofHashCharset charset)
            throws GeneralCryptoLibException {

        Validate.notNull(group, "Mathematical group");
        Validate.notNull(phiFunction, "Phi function.");

        _group = group;

        _phiFunction = phiFunction;

        _configMessageDigestAlgorithmAndProvider =
            configMessageDigestAlgorithmAndProvider;

        _configProofHashCharset = charset;
    }

    /**
     * Verifies the received proof as being true or false.
     *
     * @param publicValues
     *            a list of group elements.
     * @param proof
     *            the {@link com.scytl.cryptolib.proofs.proof.Proof} that needs
     *            to be verified.
     * @param data
     *            some data encoded as a String.
     * @return true if proof has been successfully verified, false otherwise.
     * @throws GeneralCryptoLibException
     *             if {@code Verifier} was initialized by invalid values.
     */
    public boolean verify(final List<E> publicValues, final Proof proof,
            final String data) throws GeneralCryptoLibException {

        validateVerifyInputs(publicValues, proof, data);

        Exponent proofHashValue = proof.getHashValue();

        List<ZpGroupElement> phiOutputs =
            _phiFunction.calculatePhi(proof.getValuesList());

        List<GroupElement<T>> computedValues =
            calculateComputedValues(publicValues, phiOutputs,
                proofHashValue);

        Exponent calculatedHash;
        calculatedHash = calculateHash(publicValues, computedValues, data);

        return proofHashValue.equals(calculatedHash);
    }

    /**
     * Generates a hash (converted to an exponent) from a collection of objects.
     * <P>
     * There are a number of ways to generate a hash from a collection of
     * objects. The approach that is followed in this system uses the output
     * from the {@code toString()} method of each of the objects to create a
     * "super string" representing all of the objects, which is later converted
     * to bytes, and finally to an exponent.
     * <P>
     * There is an equivalent method in the {@link Prover}, which must use the
     * same approach for the system to work.
     */
    private Exponent calculateHash(final List<E> publicValues,
            final List<GroupElement<T>> computedValues, final String data)
            throws GeneralCryptoLibException {

        HashBuilder hashBuilder =
            new HashBuilder(_configMessageDigestAlgorithmAndProvider,
                _configProofHashCharset);

        byte[] hashAsByteArray;
        hashAsByteArray =
            hashBuilder.buildHashForProofs(publicValues, computedValues,
                data);

        return new Exponent(_group.getQ(), new BigInteger(1, hashAsByteArray));
    }

    @SuppressWarnings("unchecked")
    private List<GroupElement<T>> calculateComputedValues(
            final List<E> publicValues,
            final List<ZpGroupElement> phiOutputs, final Exponent c)
            throws GeneralCryptoLibException {

        List<GroupElement<T>> computedValues = new ArrayList<>();

        new ArrayList<GroupElement<T>>();
        int m = _phiFunction.getNumberOfOutputs();

        for (int i = 0; i < m; i++) {
            computedValues.add(publicValues.get(i)
                .exponentiate(c.negate())
                .multiply((GroupElement<T>) phiOutputs.get(i)));
        }

        return computedValues;
    }

    private void validateVerifyInputs(final List<E> publicValues,
            final Proof proof, final String data)
            throws GeneralCryptoLibException {

        validatePublicValues(publicValues);

        validateProof(proof);

        validateInputString(data);
    }

    private void validateInputString(final String data)
            throws GeneralCryptoLibException {

        Validate.notNullOrBlank(data, "Data string");
    }

    private void validatePublicValues(final List<E> publicValues)
            throws GeneralCryptoLibException {

        Validate.notNullOrEmptyAndNoNulls(publicValues,
            "List of public values");
        Validate.equals(publicValues.size(),
            _phiFunction.getNumberOfOutputs(), "Number of public values",
            "number of phi function outputs");

        checkThatPublicValuesAreGroupMembers(publicValues);
    }

    private void checkThatPublicValuesAreGroupMembers(
            final List<E> publicValues) throws GeneralCryptoLibException {
        for (E element : publicValues) {
            if (!_group.isGroupMember(element)) {
                throw new GeneralCryptoLibException(
                    "All elements of list of public values must be elements of mathematical group associated with proof.");
            }
        }
    }

    private void validateProof(final Proof proof)
            throws GeneralCryptoLibException {

        Validate.notNull(proof, "Proof");

        Validate.equals(proof.getValuesList().size(),
            _phiFunction.getNumberOfSecrets(), "Number of proof values",
            "number of phi function secrets");

        BigInteger groupQ = _group.getQ();

        validateExponent(proof.getHashValue(), groupQ);

        for (Exponent exponentFromProof : proof.getValuesList()) {
            validateExponent(exponentFromProof, groupQ);
        }
    }

    private void validateExponent(final Exponent e, final BigInteger groupQ)
            throws GeneralCryptoLibException {

        if (!(e.getQ().equals(groupQ)) || !exponentValueWithinRange(e)) {
            throw new GeneralCryptoLibException(
                "Invalid exponent found while validating proof; q must match q (order) of group, value must be less than q");
        }
    }

    private boolean exponentValueWithinRange(final Exponent e) {
        return (_group.getQ().compareTo(e.getValue()) > 0)
            && (BigInteger.ZERO.compareTo(e.getValue()) < 1);
    }
}
