/*
 * 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.mathematical.groups.utils.GroupUtils;
import com.scytl.cryptolib.primitives.messagedigest.configuration.ConfigMessageDigestAlgorithmAndProvider;
import com.scytl.cryptolib.proofs.bean.ProofPreComputedValues;
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;
import com.scytl.cryptolib.securerandom.cryptoapi.CryptoAPIRandomInteger;

/**
 * Class which acts as a prover 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
 *            {@link MathematicalGroup} that will be used during this exchange.
 * @param <E>
 *            The type of the group elements of the {@link MathematicalGroup}
 *            that will be used during this exchange. E must be a type which
 *            extends GroupElement{@code <T>}.
 */
public final class Prover<T, E extends GroupElement<T>> {

    private final MathematicalGroup<E> _group;

    private final PhiFunction<T> _phiFunction;

    private final ConfigMessageDigestAlgorithmAndProvider _configMessageDigestAlgorithmAndProvider;

    private final ConfigProofHashCharset _configProofHashCharset;

    /**
     * Creates a proofs prover, that can be used for constructing proofs.
     *
     * @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 arguments are invalid.
     */
    public Prover(
            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;
    }

    /**
     * Performs prover steps in a ZK-PoK exchange using Maurer's unified PHI
     * function. This involves generating a {@link Proof}, using the received
     * inputs and this Prover's internal fields. This method starts with
     * pre-computed values for the proof generation.
     * 
     * @param publicValues
     *            a list of group elements.
     * @param privateValues
     *            a list of exponents, this list is used as the secret values in
     *            the exchange.
     * @param data
     *            some data encoded as a String.
     * @param preComputedValues
     *            the pre-computed values of the proof generation.
     * @return the generated proof.
     * @throws GeneralCryptoLibException
     *             if arguments are invalid.
     */
    public Proof prove(final List<E> publicValues,
            final List<Exponent> privateValues, final String data,
            final ProofPreComputedValues preComputedValues)
            throws GeneralCryptoLibException {

        validateInput(publicValues, privateValues, data);

        Exponent hash =
            generateHash(publicValues, preComputedValues.getPhiOutputs(),
                data);

        List<Exponent> proofValues =
            generateValuesList(privateValues, hash,
                preComputedValues.getExponents());

        return new Proof(hash, proofValues);
    }

    /**
     * Pre-computes the values needed for proof generation.
     * 
     * @param cryptoRandomInteger
     *            the random number generator used to generate the proof
     *            secrets.
     * @return the pre-computed values.
     * @throws GeneralCryptoLibException
     *             if the pre-computation process fails.
     */
    public ProofPreComputedValues preCompute(
            final CryptoAPIRandomInteger cryptoRandomInteger)
            throws GeneralCryptoLibException {

        List<Exponent> randomExponents =
            calculateRandomExponents(_phiFunction.getNumberOfSecrets(),
                cryptoRandomInteger);

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

        return new ProofPreComputedValues(randomExponents, phiOutputs);
    }

    private List<Exponent> generateValuesList(
            final List<Exponent> privateValues,
            final Exponent hashAsExponent,
            final List<Exponent> randomExponents)
            throws GeneralCryptoLibException {

        int numInputs = privateValues.size();

        List<Exponent> proofValues = new ArrayList<>();
        BigInteger exponentValue;

        for (int i = 0; i < numInputs; i++) {

            exponentValue =
                (hashAsExponent.getValue().multiply(privateValues.get(i)
                    .getValue())).add(randomExponents.get(i).getValue());

            proofValues.add(new Exponent(_group.getQ(), exponentValue));
        }

        return proofValues;
    }

    /**
     * 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 toString() method of each of the objects to create a construct 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 Verifier}, which must use the
     * same approach for the system to work.
     */
    private Exponent generateHash(final List<E> publicValues,
            final List<ZpGroupElement> phiOutputs, final String data)
            throws GeneralCryptoLibException {

        HashBuilder hashBuilder;
        hashBuilder =
            new HashBuilder(this._configMessageDigestAlgorithmAndProvider,
                this._configProofHashCharset);

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

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

    private List<Exponent> calculateRandomExponents(final int numInputs,
            final CryptoAPIRandomInteger cryptoRandomInteger)
            throws GeneralCryptoLibException {

        List<Exponent> randomExponents = new ArrayList<>();

        GroupUtils utils = new GroupUtils();

        for (int i = 0; i < numInputs; i++) {
            randomExponents.add(utils.getRandomExponent(_group,
                cryptoRandomInteger));
        }

        return randomExponents;
    }

    private void validateInput(final List<E> publicValues,
            final List<Exponent> privateValues, final String data)
            throws GeneralCryptoLibException {

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

        Validate.notNullOrEmptyAndNoNulls(privateValues,
            "List of private values");
        Validate.equals(privateValues.size(),
            _phiFunction.getNumberOfSecrets(), "Number of private values",
            "number of phi function secrets");

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