/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.commons.text;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.Validate;

/**
 * Generates random Unicode strings containing the specified number of code points.
 * Instances are created using a builder class, which allows the
 * callers to define the properties of the generator. See the documentation for the
 * {@link Builder} class to see available properties.
 *
 * <pre>
 * // Generates a 20 code point string, using only the letters a-z
 * RandomStringGenerator generator = RandomStringGenerator.builder()
 *     .withinRange('a', 'z').build();
 * String randomLetters = generator.generate(20);
 * </pre>
 * <pre>
 * // Using Apache Commons RNG for randomness
 * UniformRandomProvider rng = RandomSource.create(...);
 * // Generates a 20 code point string, using only the letters a-z
 * RandomStringGenerator generator = RandomStringGenerator.builder()
 *     .withinRange('a', 'z')
 *     .usingRandom(rng::nextInt) // uses Java 8 syntax
 *     .build();
 * String randomLetters = generator.generate(20);
 * </pre>
 * <p>
 * {@code RandomStringGenerator} instances are thread-safe when using the
 * default random number generator (RNG). If a custom RNG is set by calling the method
 * {@link Builder#usingRandom(TextRandomProvider) Builder.usingRandom(TextRandomProvider)}, thread-safety
 * must be ensured externally.
 * </p>
 * @since 1.1
 */
public final class RandomStringGenerator {

    /**
     * A builder for generating {@code RandomStringGenerator} instances.
     *
     * <p>The behavior of a generator is controlled by properties set by this
     * builder. Each property has a default value, which can be overridden by
     * calling the methods defined in this class, prior to calling {@link #build()}.</p>
     *
     * <p>All the property setting methods return the {@code Builder} instance to allow for method chaining.</p>
     *
     * <p>The minimum and maximum code point values are defined using {@link #withinRange(int, int)}. The
     * default values are {@code 0} and {@link Character#MAX_CODE_POINT} respectively.</p>
     *
     * <p>The source of randomness can be set using {@link #usingRandom(TextRandomProvider)},
     * otherwise {@link ThreadLocalRandom} is used.</p>
     *
     * <p>The type of code points returned can be filtered using {@link #filteredBy(CharacterPredicate...)},
     * which defines a collection of tests that are applied to the randomly generated code points.
     * The code points will only be included in the result if they pass at least one of the tests.
     * Some commonly used predicates are provided by the {@link CharacterPredicates} enum.</p>
     *
     * <p>This class is not thread safe.</p>
     * @since 1.1
     */
    public static class Builder implements org.apache.commons.text.Builder<RandomStringGenerator> {

        /**
         * The default maximum code point allowed: {@link Character#MAX_CODE_POINT}
         * ({@value}).
         */
        public static final int DEFAULT_MAXIMUM_CODE_POINT = Character.MAX_CODE_POINT;

        /**
         * The default string length produced by this builder: {@value}.
         */
        public static final int DEFAULT_LENGTH = 0;

        /**
         * The default minimum code point allowed: {@value}.
         */
        public static final int DEFAULT_MINIMUM_CODE_POINT = 0;

        /**
         * The minimum code point allowed.
         */
        private int minimumCodePoint = DEFAULT_MINIMUM_CODE_POINT;

        /**
         * The maximum code point allowed.
         */
        private int maximumCodePoint = DEFAULT_MAXIMUM_CODE_POINT;

        /**
         * Filters for code points.
         */
        private Set<CharacterPredicate> inclusivePredicates;

        /**
         * The source of randomness.
         */
        private TextRandomProvider random;

        /**
         * The source of provided characters.
         */
        private List<Character> characterList;

        /**
         * Builds the {@code RandomStringGenerator} using the properties specified.
         *
         * @return The configured {@code RandomStringGenerator}
         */
        @Override
        public RandomStringGenerator build() {
            return new RandomStringGenerator(minimumCodePoint, maximumCodePoint, inclusivePredicates,
                    random, characterList);
        }

        /**
         * Limits the characters in the generated string to those that match at
         * least one of the predicates supplied.
         *
         * <p>
         * Passing {@code null} or an empty array to this method will revert to the
         * default behavior of allowing any character. Multiple calls to this
         * method will replace the previously stored predicates.
         * </p>
         *
         * @param predicates
         *            the predicates, may be {@code null} or empty
         * @return {@code this}, to allow method chaining
         */
        public Builder filteredBy(final CharacterPredicate... predicates) {
            if (ArrayUtils.isEmpty(predicates)) {
                inclusivePredicates = null;
                return this;
            }

            if (inclusivePredicates == null) {
                inclusivePredicates = new HashSet<>();
            } else {
                inclusivePredicates.clear();
            }

            Collections.addAll(inclusivePredicates, predicates);

            return this;
        }

        /**
         * Limits the characters in the generated string to those who match at
         * supplied list of Character.
         *
         * <p>
         * Passing {@code null} or an empty array to this method will revert to the
         * default behavior of allowing any character. Multiple calls to this
         * method will replace the previously stored Character.
         * </p>
         *
         * @param chars set of predefined Characters for random string generation
         *            the Character can be, may be {@code null} or empty
         * @return {@code this}, to allow method chaining
         * @since 1.2
         */
        public Builder selectFrom(final char... chars) {
            characterList = new ArrayList<>();
            if (chars != null) {
                for (final char c : chars) {
                    characterList.add(c);
                }
            }
            return this;
        }

        /**
         * Overrides the default source of randomness.  It is highly
         * recommended that a random number generator library like
         * <a href="https://commons.apache.org/proper/commons-rng/">Apache Commons RNG</a>
         * be used to provide the random number generation.
         *
         * <p>
         * When using Java 8 or later, {@link TextRandomProvider} is a
         * functional interface and need not be explicitly implemented:
         * </p>
         * <pre>
         * {@code
         *     UniformRandomProvider rng = RandomSource.create(...);
         *     RandomStringGenerator gen = RandomStringGenerator.builder()
         *         .usingRandom(rng::nextInt)
         *         // additional builder calls as needed
         *         .build();
         * }
         * </pre>
         *
         * <p>
         * Passing {@code null} to this method will revert to the default source of
         * randomness.
         * </p>
         *
         * @param random
         *            the source of randomness, may be {@code null}
         * @return {@code this}, to allow method chaining
         */
        public Builder usingRandom(final TextRandomProvider random) {
            this.random = random;
            return this;
        }

        /**
         * Sets the array of minimum and maximum char allowed in the
         * generated string.
         *
         * For example:
         * <pre>
         * {@code
         *     char [][] pairs = {{'0','9'}};
         *     char [][] pairs = {{'a','z'}};
         *     char [][] pairs = {{'a','z'},{'0','9'}};
         * }
         * </pre>
         *
         * @param pairs array of characters array, expected is to pass min, max pairs through this arg.
         * @return {@code this}, to allow method chaining.
         */
        public Builder withinRange(final char[]... pairs) {
            characterList = new ArrayList<>();
            if (pairs != null) {
                for (final char[] pair : pairs) {
                    Validate.isTrue(pair.length == 2, "Each pair must contain minimum and maximum code point");
                    final int minimumCodePoint = pair[0];
                    final int maximumCodePoint = pair[1];
                    Validate.isTrue(minimumCodePoint <= maximumCodePoint, "Minimum code point %d is larger than maximum code point %d", minimumCodePoint,
                            maximumCodePoint);

                    for (int index = minimumCodePoint; index <= maximumCodePoint; index++) {
                        characterList.add((char) index);
                    }
                }
            }
            return this;

        }

        /**
         * Sets the minimum and maximum code points allowed in the
         * generated string.
         *
         * @param minimumCodePoint
         *            the smallest code point allowed (inclusive)
         * @param maximumCodePoint
         *            the largest code point allowed (inclusive)
         * @return {@code this}, to allow method chaining
         * @throws IllegalArgumentException
         *             if {@code maximumCodePoint >}
         *             {@link Character#MAX_CODE_POINT}
         * @throws IllegalArgumentException
         *             if {@code minimumCodePoint < 0}
         * @throws IllegalArgumentException
         *             if {@code minimumCodePoint > maximumCodePoint}
         */
        public Builder withinRange(final int minimumCodePoint, final int maximumCodePoint) {
            Validate.isTrue(minimumCodePoint <= maximumCodePoint,
                    "Minimum code point %d is larger than maximum code point %d", minimumCodePoint, maximumCodePoint);
            Validate.isTrue(minimumCodePoint >= 0, "Minimum code point %d is negative", minimumCodePoint);
            Validate.isTrue(maximumCodePoint <= Character.MAX_CODE_POINT,
                    "Value %d is larger than Character.MAX_CODE_POINT.", maximumCodePoint);

            this.minimumCodePoint = minimumCodePoint;
            this.maximumCodePoint = maximumCodePoint;
            return this;
        }
    }

    /**
     * Constructs a new builder.
     * @return a new builder.
     *
     * @since 1.11.0
     */
    public static Builder builder() {
        return new Builder();
    }

    /**
     * The smallest allowed code point (inclusive).
     */
    private final int minimumCodePoint;

    /**
     * The largest allowed code point (inclusive).
     */
    private final int maximumCodePoint;

    /**
     * Filters for code points.
     */
    private final Set<CharacterPredicate> inclusivePredicates;

    /**
     * The source of randomness for this generator.
     */
    private final TextRandomProvider random;

    /**
     * The source of provided characters.
     */
    private final List<Character> characterList;

    /**
     * Constructs the generator.
     *
     * @param minimumCodePoint
     *            smallest allowed code point (inclusive)
     * @param maximumCodePoint
     *            largest allowed code point (inclusive)
     * @param inclusivePredicates
     *            filters for code points
     * @param random
     *            source of randomness
     * @param characterList list of predefined set of characters.
     */
    private RandomStringGenerator(final int minimumCodePoint, final int maximumCodePoint,
                                  final Set<CharacterPredicate> inclusivePredicates, final TextRandomProvider random,
                                  final List<Character> characterList) {
        this.minimumCodePoint = minimumCodePoint;
        this.maximumCodePoint = maximumCodePoint;
        this.inclusivePredicates = inclusivePredicates;
        this.random = random;
        this.characterList = characterList;
    }

    /**
     * Generates a random string, containing the specified number of code points.
     *
     * <p>
     * Code points are randomly selected between the minimum and maximum values defined
     * in the generator.
     * Surrogate and private use characters are not returned, although the
     * resulting string may contain pairs of surrogates that together encode a
     * supplementary character.
     * </p>
     * <p>
     * Note: the number of {@code char} code units generated will exceed
     * {@code length} if the string contains supplementary characters. See the
     * {@link Character} documentation to understand how Java stores Unicode
     * values.
     * </p>
     *
     * @param length
     *            the number of code points to generate
     * @return The generated string
     * @throws IllegalArgumentException
     *             if {@code length < 0}
     */
    public String generate(final int length) {
        if (length == 0) {
            return StringUtils.EMPTY;
        }
        Validate.isTrue(length > 0, "Length %d is smaller than zero.", length);

        final StringBuilder builder = new StringBuilder(length);
        long remaining = length;

        do {
            final int codePoint;
            if (characterList != null && !characterList.isEmpty()) {
                codePoint = generateRandomNumber(characterList);
            } else {
                codePoint = generateRandomNumber(minimumCodePoint, maximumCodePoint);
            }
            switch (Character.getType(codePoint)) {
            case Character.UNASSIGNED:
            case Character.PRIVATE_USE:
            case Character.SURROGATE:
                continue;
            default:
            }

            if (inclusivePredicates != null) {
                boolean matchedFilter = false;
                for (final CharacterPredicate predicate : inclusivePredicates) {
                    if (predicate.test(codePoint)) {
                        matchedFilter = true;
                        break;
                    }
                }
                if (!matchedFilter) {
                    continue;
                }
            }

            builder.appendCodePoint(codePoint);
            remaining--;

        } while (remaining != 0);

        return builder.toString();
    }

    /**
     * Generates a random string, containing between the minimum (inclusive) and the maximum (inclusive)
     * number of code points.
     *
     * @param minLengthInclusive
     *            the minimum (inclusive) number of code points to generate
     * @param maxLengthInclusive
     *            the maximum (inclusive) number of code points to generate
     * @return The generated string
     * @throws IllegalArgumentException
     *             if {@code minLengthInclusive < 0}, or {@code maxLengthInclusive < minLengthInclusive}
     * @see RandomStringGenerator#generate(int)
     * @since 1.2
     */
    public String generate(final int minLengthInclusive, final int maxLengthInclusive) {
        Validate.isTrue(minLengthInclusive >= 0, "Minimum length %d is smaller than zero.", minLengthInclusive);
        Validate.isTrue(minLengthInclusive <= maxLengthInclusive,
                "Maximum length %d is smaller than minimum length %d.", maxLengthInclusive, minLengthInclusive);
        return generate(generateRandomNumber(minLengthInclusive, maxLengthInclusive));
    }

    /**
     * Generates a random number within a range, using a {@link ThreadLocalRandom} instance
     * or the user-supplied source of randomness.
     *
     * @param minInclusive
     *            the minimum value allowed
     * @param maxInclusive
     *            the maximum value allowed
     * @return The random number.
     */
    private int generateRandomNumber(final int minInclusive, final int maxInclusive) {
        if (random != null) {
            return random.nextInt(maxInclusive - minInclusive + 1) + minInclusive;
        }
        return ThreadLocalRandom.current().nextInt(minInclusive, maxInclusive + 1);
    }

    /**
     * Generates a random number within a range, using a {@link ThreadLocalRandom} instance
     * or the user-supplied source of randomness.
     *
     * @param characterList predefined char list.
     * @return The random number.
     */
    private int generateRandomNumber(final List<Character> characterList) {
        final int listSize = characterList.size();
        if (random != null) {
            return String.valueOf(characterList.get(random.nextInt(listSize))).codePointAt(0);
        }
        return String.valueOf(characterList.get(ThreadLocalRandom.current().nextInt(0, listSize))).codePointAt(0);
    }
}
