/*
 * 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.mathematical.groups.utils;

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

import com.scytl.cryptolib.api.exceptions.CryptoLibException;
import com.scytl.cryptolib.api.exceptions.GeneralCryptoLibException;
import com.scytl.cryptolib.commons.utils.validations.Validate;
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.impl.ZpSubgroup;
import com.scytl.cryptolib.primitives.securerandom.factory.CryptoRandomInteger;
import com.scytl.cryptolib.securerandom.cryptoapi.CryptoAPIRandomInteger;

/**
 * Provides some utilities for creating and working with mathematical groups.
 */
public final class GroupUtils {

    /**
     * A {@link BigInteger} with the value two.
     */
    public static final BigInteger BIG_INTEGER_TWO = new BigInteger("2");

    private static final int SHORT_EXPONENT_BIT_LENGTH = 256;

    /**
     * Builds a {@link ZpSubgroup} from the p(modulus) and g(order) parameters.
     * <P>
     * NOTE: This method builds a particular type of quadratic residue group
     * with property that:
     * <P>
     * P = (Q * 2) + 1
     * <P>
     * This property holds for all groups generated using this method, but this
     * property does not hold for all mathematical groups.
     *
     * @param p
     *            the p parameter (modulus) of the group.
     * @param g
     *            the generator of the group.
     * @return the generated {@link ZpSubgroup}
     * @throws GeneralCryptoLibException
     *             if arguments are invalid.
     */
    public ZpSubgroup buildQuadraticResidueGroupFromPAndG(
            final BigInteger p, final BigInteger g)
            throws GeneralCryptoLibException {

        Validate.notNull(p, "Zp subgroup p parameter");
        Validate.notNull(g, "Zp subgroup generator");

        BigInteger q = p.subtract(BigInteger.ONE).divide(BIG_INTEGER_TWO);

        return new ZpSubgroup(g, p, q);
    }

    /**
     * Given an input list containing possible group members, this method cycles
     * through that list, checking if each element is a group member. This
     * method attempts to find the specified number of group members. Once that
     * number has been found, the method will return a new list containing the
     * specified number of group members.
     * <p>
     *
     * @param inputList
     *            a list containing possible group members.
     * @param group
     *            the group for which the specified number of group members are
     *            required.
     * @param numMembersRequired
     *            the number of group members of the specified group that should
     *            be added to the output list.
     * @return a new list containing the specified number of group elements,
     *         which have been read from the input list.
     * @throws GeneralCryptoLibException
     *             if the required number of group members are not found in the
     *             input list.
     */
    public List<BigInteger> extractNumberOfGroupMembersFromListOfPossibleMembers(
            final List<BigInteger> inputList, final ZpSubgroup group,
            final int numMembersRequired) throws GeneralCryptoLibException {

        validateExtractionInput(inputList, group, numMembersRequired);

        List<BigInteger> outputList = new ArrayList<>();

        int numGroupMembersFound = 0;

        for (BigInteger value : inputList) {

            ZpGroupElement possibleGroupMember;
            try {
                possibleGroupMember = new ZpGroupElement(value, group);
            } catch (GeneralCryptoLibException e) {
                continue;
            }

            if (group.isGroupMember(possibleGroupMember)) {
                outputList.add(value);
                numGroupMembersFound++;
            }

            if (numGroupMembersFound == numMembersRequired) {
                break;
            }
        }

        if (numGroupMembersFound != numMembersRequired) {
            throw new GeneralCryptoLibException(
                "Error - did not find the required number of group members in the given list. The required number of was "
                    + numMembersRequired
                    + ", number of members found was  "
                    + numGroupMembersFound);
        }
        return outputList;
    }

    /**
     * Generates a uniformly distributed random Exponent for the received
     * mathematical group.
     * <P>
     * The value of the created exponent will be between {@code 0} and
     * {@code q-1}.
     *
     * @param group
     *            a mathematical group.
     * @param cryptoRandomInteger
     *            a generator.
     * @return a random Exponent.
     * @throws GeneralCryptoLibException
     *             if arguments are invalid.
     */
    public Exponent getRandomExponent(final MathematicalGroup<?> group,
            final CryptoAPIRandomInteger cryptoRandomInteger)
            throws GeneralCryptoLibException {

        Validate.notNull(group, "Zp subgroup");

        BigInteger value =
            getRandomExponentValue(group.getQ().bitLength(), group.getQ(),
                cryptoRandomInteger);

        return new Exponent(group.getQ(), value);
    }

    /**
     * Generates a short uniformly distributed random {@code Exponent} for the
     * received mathematical group.
     * <P>
     * The value of the created exponent will be between {@code 0} and
     * {@code 2^}{@value #SHORT_EXPONENT_BIT_LENGTH}{@code -1} bit length.
     *
     * @param group
     *            a mathematical group.
     * @param cryptoRandomInteger
     *            a generator.
     * @return a random Exponent.
     * @throws GeneralCryptoLibException
     *             if a group is null.
     * @throws CryptoLibException
     *             if the bit length of the group parameter q is smaller than
     *             {@value #SHORT_EXPONENT_BIT_LENGTH}.
     */
    public Exponent getRandomShortExponent(
            final MathematicalGroup<?> group,
            final CryptoRandomInteger cryptoRandomInteger)
            throws GeneralCryptoLibException {

        Validate.notNull(group, "Zp subgroup");

        if (group.getQ().bitLength() < SHORT_EXPONENT_BIT_LENGTH) {
            throw new CryptoLibException(
                "Cannot generate a random value because the bit length of the group parameter q is smaller than "
                    + SHORT_EXPONENT_BIT_LENGTH);
        }

        BigInteger value =
            getRandomExponentValue(SHORT_EXPONENT_BIT_LENGTH,
                group.getQ(), cryptoRandomInteger);

        return new Exponent(group.getQ(), value);
    }

    private BigInteger getRandomExponentValue(final int bitLength,
            final BigInteger q,
            final CryptoAPIRandomInteger cryptoRandomInteger)
            throws GeneralCryptoLibException {

        BigInteger random;

        while (true) {
            random = cryptoRandomInteger.nextRandomByBits(bitLength);

            if (random.compareTo(q) < 0) {
                return random;
            }
        }
    }

    private void validateExtractionInput(
            final List<BigInteger> inputList, final ZpSubgroup group,
            final int numMembersRequired) throws GeneralCryptoLibException {

        Validate.notNullOrEmptyAndNoNulls(inputList, "Specified list");
        Validate.notNull(group, "Zp subgroup");

        int numMembersInExistingList = inputList.size();
        if ((numMembersRequired < 1)
            || (numMembersRequired > numMembersInExistingList)) {
            throw new CryptoLibException(
                "Number of required elements specified "
                    + numMembersRequired
                    + " is not between 1 and number of elements in specified input list "
                    + numMembersInExistingList);
        }
    }
}
