/*
 * 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.primitives.securerandom.factory;

import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.Set;

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.primitives.securerandom.constants.SecureRandomConstants;
import com.scytl.cryptolib.securerandom.cryptoapi.CryptoAPIRandomInteger;

/**
 * Class that implements {@link CryptoAPIRandomInteger}.
 * <p>
 * Note that the {@link SecureRandom} that is used to generate the random
 * integers is stored in this class and cannot be modified.
 * <P>
 * Instances of this class are immutable.
 */
public final class CryptoRandomInteger implements CryptoAPIRandomInteger {

    private final SecureRandom _secureRandom;

    /**
     * Instantiates a random generator as a wrapper of the specifier parameter.
     * 
     * @param secureRandom
     *            The instance of SecureRandom that should be wrapped within
     *            this class.
     */
    CryptoRandomInteger(final SecureRandom secureRandom) {

        _secureRandom = secureRandom;

    }

    @Override
    public BigInteger nextRandomByBits(final int lengthInBits)
            throws GeneralCryptoLibException {

        Validate.isPositive(lengthInBits, "Length in bits");

        int numberOfBytes = getNumberOfBytesFromBitLength(lengthInBits);
        int modBytes = getModOfBytesFromBitLength(lengthInBits);

        byte[] bytes = generateRandomBits(numberOfBytes, modBytes);

        return new BigInteger(1, bytes);
    }

    @Override
    public BigInteger nextRandom(final int lengthInDigits)
            throws GeneralCryptoLibException {

        Validate
            .inRange(
                lengthInDigits,
                1,
                SecureRandomConstants.MAXIMUM_GENERATED_BIG_INTEGER_DIGIT_LENGTH,
                "Length in digits", "",
                "maximum allowed value for secure random BigIntegers");

        BigInteger n =
            BigInteger.TEN.pow(lengthInDigits).subtract(BigInteger.ONE);

        int bitLength = n.bitLength();

        int numberOfBytes = getNumberOfBytesFromBitLength(bitLength);
        int modBytes = getModOfBytesFromBitLength(bitLength);

        BigInteger generatedInteger;

        do {

            byte[] bytes = generateRandomBits(numberOfBytes, modBytes);
            generatedInteger = new BigInteger(1, bytes);

        } while (isHigherThan(generatedInteger, n));

        return generatedInteger;

    }

    private boolean isHigherThan(final BigInteger generatedInteger, final BigInteger n) {
        return generatedInteger.compareTo(n) > 0;
    }

    @Override
    public BigInteger nextRandom(final int lengthInDigits,
            final Set<BigInteger> blackList)
            throws GeneralCryptoLibException {

        Validate.isPositive(lengthInDigits, "Length in digits");

        if (!validateBlackList(lengthInDigits, blackList)) {
            throw new GeneralCryptoLibException(
                "The black list should be strictly less than total amount of elements.");
        }

        BigInteger bigInteger;

        do {
            bigInteger = nextRandom(lengthInDigits);
        } while (blackList.contains(bigInteger));

        return bigInteger;

    }

    @Override
    public BigInteger getRandomProbablePrimeBigInteger(
            final int lengthInBits) throws GeneralCryptoLibException {

        Validate.notLessThan(lengthInBits, 2, "Length in bits",
            "minimum allowed value for probable prime BigIntegers");

        try {
            return BigInteger.probablePrime(lengthInBits, _secureRandom);
        } catch (ArithmeticException e) {
            throw new CryptoLibException(
                "Failed to generate a random probable prime. "
                    + "Error message was " + e.getMessage(), e);
        }
    }

    @Override
    public BigInteger getRandomProbablePrimeBigIntegerWithSetCertainty(
            final int lengthInBits, final int certainty)
            throws GeneralCryptoLibException {

        Validate.notLessThan(lengthInBits, 2, "Length in bits",
            "minimum allowed value for probable prime BigIntegers");
        Validate.isPositive(certainty, "Certainty");

        try {
            return new BigInteger(lengthInBits, certainty, _secureRandom);
        } catch (ArithmeticException e) {
            throw new CryptoLibException(
                "Failed to generate a random probable prime. "
                    + "Error message was " + e.getMessage(), e);
        }
    }

    private boolean validateBlackList(final int length,
            final Set<BigInteger> blackList) {

        boolean setIsCorrect = true;

        if (blackList == null) {
            setIsCorrect = false;
        } else if (!blackList.isEmpty()
            && hasEqualOrMoreElements(length, blackList.size())) {
            setIsCorrect = false;
        }

        return setIsCorrect;
    }

    private boolean hasEqualOrMoreElements(final int length, final int size) {
        return Math.log10(size + 1d) > length;
    }

    /**
     * This method generates a byte array with the required number of bits
     * <em>required</em>. The number of bits are obtained from the
     * {@code numberOfBytes} and {@code modBytes} parameters.
     * 
     * @param numberOfBytes
     *            The number of bytes to be randomly generated.
     * @param modBytes
     *            If different from 0, specifies how many bits are to be
     *            discarded from the last byte.
     * @return A byte array only with the required bits.
     */
    private byte[] generateRandomBits(final int numberOfBytes,
            final int modBytes) {
        byte[] bytes = new byte[numberOfBytes];
        _secureRandom.nextBytes(bytes);

        if (modBytes != 0) {
            for (int k = modBytes; k < Byte.SIZE; k++) {
                bytes[0] &= (byte) ~(1 << k);
            }
        }
        return bytes;
    }

    /**
     * Provides the number of bytes required to comprise {@code bitLength} bits.
     * 
     * @param bitLength
     *            The number of bits.
     * @return The number of bytes.
     */
    private int getNumberOfBytesFromBitLength(final int bitLength) {
        int numberOfBytes = bitLength / Byte.SIZE;
        int modBytes = getModOfBytesFromBitLength(bitLength);

        if (modBytes != 0) {
            numberOfBytes++;
        }

        return numberOfBytes;
    }

    /**
     * Returns the modulo of {@code bitLength div 8}.
     * 
     * @param bitLength
     *            The number of bits.
     * @return The modulo.
     */
    private int getModOfBytesFromBitLength(final int bitLength) {
        return bitLength % Byte.SIZE;
    }
}
