/*
 * 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.function;

import java.util.ArrayList;
import java.util.Arrays;
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;

/**
 * Class which performs the PHI function of Maurer's Unified Zero-Knowledge
 * Proof of Knowledge (ZK-PoK) scheme.
 * 
 * @param <T>
 *            the type which parameterizes the group elements of the
 *            mathematical group that will be used during this exchange.
 */
public class PhiFunction<T> {

    private final int _numInputs;

    private final int _numOutputs;

    private final List<? extends GroupElement<T>> _baseElements;

    private final int[][][] _computationRules;

    /**
     * Creates an instance of a {@code PhiFunction} and initializes it by
     * provided values.
     * <P>
     * This constructor defines the generic type {@code <E>} as a type which
     * extends GroupElement{@code <T>}.
     * 
     * @param group
     *            the mathematical group on which this PHI function operates.
     * @param numInputs
     *            the number of secret inputs that the calculatePhi method of
     *            this class should accept.
     * @param numOutputs
     *            the number of outputs that will be generated by the
     *            calculatePhi method of this class.
     * @param baseElements
     *            a list of base elements which are members of the received
     *            mathematical group.
     * @param computationRules
     *            the computational rules that should be followed when executing
     *            the PHI function. The first value in each pair of indexes
     *            within the computational rules must be within the range [1..n]
     *            (where n is the number of base elements received), and the
     *            second value in each pair of indexes must be within the range
     *            [1...number of inputs]. See the
     *            {@link #calculatePhi(java.util.List)} for a detailed
     *            description of this array and how it is used.
     * @throws GeneralCryptoLibException
     *             if arguments are invalid.
     */
    public <E extends GroupElement<T>> PhiFunction(
            final MathematicalGroup<E> group, final int numInputs,
            final int numOutputs, final List<E> baseElements,
            final int[][][] computationRules)
            throws GeneralCryptoLibException {

        validateInputs(group, numInputs, numOutputs, baseElements,
            computationRules);

        _numInputs = numInputs;
        _numOutputs = numOutputs;
        _baseElements = new ArrayList<>(baseElements);
        _computationRules =
            Arrays.copyOf(computationRules, computationRules.length);
    }

    /**
     * Performs the PHI function on the received array of inputs, using the
     * array of base elements and the computational rules that were specified
     * when this object was constructed.
     * <P>
     * The algorithm for calculating the output values is as follows:
     * <ul>
     * <li>For each list of lists of pairs of indexes (in other words, for each
     * element in the first dimension of the computation rules):
     * <ul>
     * <li>For each pair of indexes (i,j) within the current list of pairs:
     * <ul>
     * <li>Take the base element with index i and exponentiate it to the input
     * with index j. Save the result as a partial result.</li>
     * </ul>
     * </li>
     * <li>Once all of the pairs of the current list have been processed,
     * multiply all of the partial results. This result is the i-th output</li>
     * </ul>
     * </li>
     * </ul>
     * The computational rules are implemented as a 3-dimensional array. It is
     * effectively an array of arrays of pairs of indexes. In other words, each
     * element in the first dimension of the computational rules is an array of
     * pairs of indexes. Each element in the second dimension of the computation
     * rules is a pair of indexes. Finally, each element in the third dimension
     * of the computational rules specifies an index in the list of base
     * elements or the list of secret inputs.
     * <P>
     * Note: The values in the computational rules are indexed from 1, but the
     * base elements and the secret inputs are indexed from 0. Therefore a pair
     * of indexes which are specified as [1,1] actually refers to the base
     * element with index 0, and the secret input with index 0.
     * 
     * @param inputs
     *            an array of exponents.
     * @return an array (of size 1 or more) containing the calculated outputs
     *         from the function.
     * @throws GeneralCryptoLibException
     *             if {@code inputs} list is null or contains the invalid number
     *             of elements.
     */
    public List<ZpGroupElement> calculatePhi(final List<Exponent> inputs)
            throws GeneralCryptoLibException {

        Validate.notNullOrEmptyAndNoNulls(inputs,
            "List of phi function inputs");
        Validate.equals(inputs.size(), _numInputs,
            "Number of phi function inputs", "number of required inputs");

        List<ZpGroupElement> result = new ArrayList<>();
        ZpGroupElement partialResult;
        ZpGroupElement resultForThisListOfPairs;

        for (int i = 0; i < _computationRules.length; i++) {

            int numPairsInList = _computationRules[i].length;

            resultForThisListOfPairs = (ZpGroupElement) _baseElements
                .get(_computationRules[i][0][0] - 1)
                .exponentiate(inputs.get(_computationRules[i][0][1] - 1));

            for (int j = 1; j < numPairsInList; j++) {

                partialResult = (ZpGroupElement) _baseElements
                    .get(_computationRules[i][j][0] - 1).exponentiate(
                        inputs.get(_computationRules[i][j][1] - 1));

                resultForThisListOfPairs =
                    resultForThisListOfPairs.multiply(partialResult);
            }
            result.add(resultForThisListOfPairs);
        }
        return result;
    }

    /**
     * Returns the number of outputs generated by the PhiFunction.
     * 
     * @return the number of outputs.
     */
    public int getNumberOfOutputs() {
        return _numOutputs;
    }

    /**
     * Returns the number of secret input values expected by the PhiFunction.
     * 
     * @return the number of expected secret input values.
     */
    public int getNumberOfSecrets() {
        return _numInputs;
    }

    private <E extends GroupElement<T>> void validateInputs(
            final MathematicalGroup<E> group, final int numInputs,
            final int numOutputs, final List<E> baseElements,
            final int[][][] computationRules)
            throws GeneralCryptoLibException {

        Validate.notNull(group, "Mathematical group");
        Validate.isPositive(numInputs, "Number of phi function inputs");
        Validate.isPositive(numOutputs, "Number of phi function outputs");
        Validate.notNullOrEmptyAndNoNulls(baseElements,
            "List of base elements");
        Validate.notNull(computationRules, "Computation rules");
        Validate.isPositive(computationRules.length,
            "Length of computation rules");
        Validate.equals(computationRules.length, numOutputs,
            "Length of computation rules",
            "number of phi function outputs");
        validateComputationRules(numInputs, numOutputs, baseElements,
            computationRules);
    }

    private <E extends GroupElement<T>> void validateComputationRules(
            final int numInputs, final int numOutputs,
            final List<E> baseElements, final int[][][] computationRules)
            throws GeneralCryptoLibException {

        // Validate that both of the values in each of the
        // pairs of rules have values in the correct ranges
        int numBaseElements = baseElements.size();
        for (int i = 0; i < numOutputs; i++) {
            for (int j = 0; j < computationRules[i].length; j++) {

                // Validate the first value of the pair
                int pairValue1 = computationRules[i][j][0];
                if ((pairValue1 < 1) || (pairValue1 > numBaseElements)) {
                    throw new GeneralCryptoLibException(
                        "Invalid first value in index pair - should be in the range 1 to "
                            + numBaseElements + ", but it was "
                            + pairValue1);
                }

                // Validate the second value of the pair
                int pairValue2 = computationRules[i][j][1];
                if ((pairValue2 < 1) || (pairValue2 > numInputs)) {
                    throw new GeneralCryptoLibException(
                        "Invalid second value in index pair - should be in the range 1 to "
                            + numInputs + ", but it was " + pairValue2);
                }
            }
        }
    }
}
