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

/**
 * Class that implements {@link CryptoAPIRandomString}.
 * <p>
 * Note that the {@link SecureRandom} that is used to generate the random
 * strings is stored in this class and cannot be modified.
 * <P>
 * This class generates random strings from a given alphabet. The alphabet
 * should not contain repeated elements. At the moment only alphabets with
 * length power of two are considered. The following two alphabets are provided
 * in {@link com.scytl.cryptolib.commons.constants.Constants}:
 * <ul>
 * <li>32 characters (
 * {@link com.scytl.cryptolib.primitives.securerandom.constants.SecureRandomConstants#ALPHABET_BASE32}
 * ): ABCDEFGHIJKLMNOPQRSTUVWXYZ234567
 * <li>64 characters (
 * {@link com.scytl.cryptolib.primitives.securerandom.constants.SecureRandomConstants#ALPHABET_BASE64}
 * ): ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/
 * </ul>
 * <P>
 * Instances of this class are immutable.
 */
public final class CryptoRandomString implements CryptoAPIRandomString {

    private final SecureRandom _secureRandom;

    private final String _alphabet;

    private final int _alphabetPowerOfTwo;

    /**
     * Instantiates a random generator with specified parameters.
     * 
     * @param secureRandom
     *            The instance of SecureRandom that should be wrapped within
     *            this class
     * @param alphabet
     *            The alphabet to be used to generate the random strings.
     *            <p>
     *            It should be noted that if the given alphabet is {@code null}
     *            or of a specific length which is not considered (see class
     *            description), a {@link CryptoLibException} is thrown.
     */
    CryptoRandomString(final SecureRandom secureRandom,
            final String alphabet) {

        _secureRandom = secureRandom;
        _alphabet = alphabet;

        validateAlphabet();
        _alphabetPowerOfTwo = getPowerOfTwo();

    }

    private void validateAlphabet() {

        if (_alphabet == null) {
            throw new CryptoLibException(new IllegalArgumentException(
                "The given alphabet is null"));
        } else if (_alphabet.length() == 0) {
            throw new CryptoLibException(new IllegalArgumentException(
                "The given alphabet cannot be an empty string"));
        } else if (alphabetHasRepeatedCharacters()) {
            throw new CryptoLibException(new IllegalArgumentException(
                "The given alphabet has repeated characters"));
        } else if (!alphabetLengthIsPowerOfTwo()) {
            throw new CryptoLibException(new IllegalArgumentException(
                "The alphabet length should be a power of two."));
        }
    }

    /**
     * @return The power of two of the alphabet length.
     */
    private int getPowerOfTwo() {
        return (int) (Math.log10(_alphabet.length()) / Math.log10(2));
    }

    /**
     * @return True if the alphabet length is a power of two. False otherwise.
     */
    private boolean alphabetLengthIsPowerOfTwo() {
        return (_alphabet.length() & (_alphabet.length() - 1)) == 0;
    }

    private boolean alphabetHasRepeatedCharacters() {

        Set<Character> setUniqueChars = new HashSet<Character>();

        for (char c : _alphabet.toCharArray()) {

            if (!setUniqueChars.add(c)) {
                return true;
            }
        }

        return false;

    }

    @Override
    public String nextRandom(final int lengthInChars)
            throws GeneralCryptoLibException {

        Validate.inRange(lengthInChars, 1,
            SecureRandomConstants.MAXIMUM_GENERATED_STRING_LENGTH,
            "Length in characters", "",
            "maximum allowed value for secure random Strings");

        int numberOfBytes =
            (int) Math.ceil((_alphabetPowerOfTwo * lengthInChars)
                / (float) Byte.SIZE);

        byte[] bytes = new byte[numberOfBytes];
        _secureRandom.nextBytes(bytes);
        String bitArray = new BigInteger(1, bytes).toString(2);

        int bytesLength = numberOfBytes * Byte.SIZE;

        StringBuilder stringBuilder =
            new StringBuilder(bitArray).reverse();

        while (stringBuilder.length() < bytesLength) {
            stringBuilder.append('0');
        }

        bitArray = stringBuilder.reverse().toString();

        String c;

        StringBuilder stringBuilderGeneratedString = new StringBuilder();

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

            c =
                bitArray.substring(i * _alphabetPowerOfTwo, (i + 1)
                    * _alphabetPowerOfTwo);

            stringBuilderGeneratedString
                .append(_alphabet.toCharArray()[Integer.parseInt(c, 2)]);
        }

        String generatedString = stringBuilderGeneratedString.toString();

        return generatedString;
    }

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

        Validate.isPositive(lengthInChars, "Length in characters");
        validateBlackList(lengthInChars, blackList);

        String generatedString = nextRandom(lengthInChars);

        while (blackList.contains(generatedString)) {
            generatedString = nextRandom(lengthInChars);
        }

        return generatedString;
    }

    private void validateBlackList(final int length,
            final Set<String> blackList) throws GeneralCryptoLibException {

        Validate.notNull(blackList, "Blacklist");

        if (!blackList.isEmpty()) {
            validatePossibilityToGenerateElementsNotFromBlacklist(length,
                blackList.size());
            validateBlacklistOnlyContainsCharactersFromTheAlphabet(blackList);
            validateBlacklistContainsCorrectStringLength(blackList, length);
        }
    }

    private void validatePossibilityToGenerateElementsNotFromBlacklist(
            final int requiredLength, final int blacklistSize)
            throws GeneralCryptoLibException {

        double logSetSize =
            Math.log10(blacklistSize) / Math.log10(_alphabet.length());
        if (logSetSize >= requiredLength) {
            throw new GeneralCryptoLibException(
                "The black list contains all possible generated values.");
        }
    }

    private void validateBlacklistOnlyContainsCharactersFromTheAlphabet(
            final Set<String> blackList) throws GeneralCryptoLibException {

        Set<Character> setUniqueCharsBlackList = new HashSet<Character>();

        for (String string : blackList) {

            char[] arrayOfChars = string.toCharArray();

            for (char arrayOfChar : arrayOfChars) {
                setUniqueCharsBlackList.add(arrayOfChar);
            }
        }

        String errorMessage =
            "Blacklist contains characters that are not in alphabet";
        if (setUniqueCharsBlackList.size() > _alphabet.length()) {
            throw new GeneralCryptoLibException(errorMessage);
        }
        for (Character c : setUniqueCharsBlackList) {
            if (!_alphabet.contains(String.valueOf(c))) {
                throw new GeneralCryptoLibException(errorMessage);
            }
        }
    }

    private void validateBlacklistContainsCorrectStringLength(
            final Set<String> blackList, final int totalLength)
            throws GeneralCryptoLibException {

        for (String element : blackList) {
            if (element.length() != totalLength) {
                throw new GeneralCryptoLibException(
                    "Blacklist strings length should be equal to "
                        + totalLength);
            }
        }
    }
}
