package net.ranides.assira.math;

import net.ranides.assira.collection.lists.IntArrayList;
import net.ranides.assira.collection.lists.IntList;

import java.awt.*;
import java.util.Random;

/**
 * Simple helper generating random values for wide range of standard types.
 * It is just facade: it can be seeded by any implementation of {@link Random}
 */
public class Randomizer {

    private static final String CHARACTERS = "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDFGHJKLZXCVBNM1234567890      ";

    private final Random rng;

    /**
     * Creates new randomizer which retrieves random bits from specified PRNG
     *
     * @param rng rng
     */
    public Randomizer(Random rng) {
        this.rng = rng;
    }

    /**
     * Reimplementation of
     * <a href="https://tfetimes.com/wp-content/uploads/2015/04/ProgrammingPearls2nd.pdf">Knuth's algorithm</a>.
     * It generates count distinct numbers from range [begin,end). <br>Complexity: {@code O(count)}.
     *
     * @param count count
     * @param begin begin
     * @param end end
     * @return numbers
     */
    public IntList uniqueInts(int count, int begin, int end) {
        int n = end - begin;
        IntList result = new IntArrayList(count);
        for(int i=0; i<n; i++) {
            if( Long.remainderUnsigned(rng.nextLong(), n-i) < count) {
                result.push(begin + i);
                count--;
            }
        }
        return result;
    }

    /**
     * Returns number from range [0,1). That means: zero inclusive, 1 exclusive.
     * @return float
     */
    public float nextFloat() {
        return rng.nextFloat();
    }

    /**
     * Returns number from range [0,upper). That means: zero inclusive, "upper" exclusive.
     *
     * @param upper upper
     * @return number
     */
    public float nextFloat(float upper) {
        return upper * rng.nextFloat();
    }

    /**
     * Returns number from range [lower,upper). That means: "lower" inclusive, "upper" exclusive.
     *
     * @param lower lower
     * @param upper upper
     * @return number
     */
    public float nextFloat(float lower, float upper) {
        if (lower >= upper || Double.isInfinite(lower) || Double.isInfinite(upper) || Double.isNaN(lower) || Double.isNaN(upper)) {
            throw new IllegalRange(lower, upper);
        }
        float u = rng.nextFloat();
        return u * upper + (1.0f - u) * lower;
    }

    /**
     * Returns number from range [0,1). That means: zero inclusive, 1 exclusive.
     *
     * @return number
     */
    public double nextDouble() {
        return rng.nextDouble();
    }

    /**
     * Returns number from range [0,upper). That means: zero inclusive, "upper" exclusive.
     *
     * @param upper upper
     * @return number
     */
    public double nextDouble(double upper) {
        return upper * rng.nextDouble();
    }

    /**
     * Returns number from range [lower,upper). That means: "lower" inclusive, "upper" exclusive.
     *
     * @param lower lower
     * @param upper upper
     * @return number
     */
    public double nextDouble(double lower, double upper) {
        if (lower >= upper || Double.isInfinite(lower) || Double.isInfinite(upper) || Double.isNaN(lower) || Double.isNaN(upper)) {
            throw new IllegalRange(lower, upper);
        }
        double u = rng.nextDouble();
        return u * upper + (1.0 - u) * lower;
    }

    /**
     * Returns byte value
     * @return byte
     */
    public byte nextByte() {
        return (byte)nextInt(Byte.MIN_VALUE, Byte.MAX_VALUE);
    }

    /**
     * Returns byte value from range [0,upper). That means: zero inclusive, "upper" exclusive.
     * Please note, that it means that negative values can't be returned
     *
     * @param upper upper
     * @return byte
     */
    public byte nextByte(byte upper) {
        return (byte)nextInt(upper);
    }

    /**
     * Returns byte from range [lower,upper). That means: "lower" inclusive, "upper" exclusive.
     * Please note, that it means that negative values can be returned, if "lower" is negative
     *
     * @param lower lower
     * @param upper upper
     * @return byte
     */
    public byte nextByte(byte lower, byte upper) {
        return (byte)nextInt(lower, upper);
    }

    /**
     * Returns short value
     * @return short
     */
    public short nextShort() {
        return (short)nextInt(Short.MIN_VALUE, Short.MAX_VALUE);
    }

    /**
     * Returns short value from range [0,upper). That means: zero inclusive, "upper" exclusive.
     * Please note, that it means that negative values can't be returned
     *
     * @param upper upper
     * @return short
     */
    public short nextShort(short upper) {
        return (short)nextInt(upper);
    }

    /**
     * Returns short from range [lower,upper). That means: "lower" inclusive, "upper" exclusive.
     * Please note, that it means that negative values can be returned, if "lower" is negative
     *
     * @param lower lower
     * @param upper upper
     * @return short
     */
    public short nextShort(short lower, short upper) {
        return (short)nextInt(lower, upper);
    }

    /**
     * Returns int value
     * @return int
     */
    public int nextInt() {
        return rng.nextInt();
    }

    /**
     * Returns int value from range [0,upper). That means: zero inclusive, "upper" exclusive.
     * Please note, that it means that negative values can't be returned
     *
     * @param upper upper
     * @return int
     */
    public int nextInt(int upper) {
        return rng.nextInt(upper);
    }

    /**
     * Returns int from range [lower,upper). That means: "lower" inclusive, "upper" exclusive.
     * Please note, that it means that negative values can be returned, if "lower" is negative
     *
     * @param lower lower
     * @param upper upper
     * @return int
     */
    public int nextInt(int lower, int upper) {
        if(lower >= upper) {
            throw new IllegalRange(lower, upper);
        }
        return lower + rng.nextInt(upper - lower);
    }

    /**
     * Returns long value
     * @return long
     */
    public long nextLong() {
        return rng.nextLong();
    }

    /**
     * Returns long value from range [0,upper). That means: zero inclusive, "upper" exclusive.
     * Please note, that it means that negative values can't be returned
     *
     * @param upper upper
     * @return long
     */
    public long nextLong(long upper) {
        if(upper <=0) {
            throw new IllegalRange(0, upper);
        }
        long r = rng.nextLong();
        long m = upper - 1;

        // power of two
        if ((upper & m) == 0L) {
            return r & m;
        }
        // reject over-represented candidates
        for (long u = r >>> 1; u + m - (r = u % upper) < 0L; u = rng.nextLong() >>> 1);
        return r;
    }

    /**
     * Returns long from range [lower,upper). That means: "lower" inclusive, "upper" exclusive.
     * Please note, that it means that negative values can be returned, if "lower" is negative
     *
     * @param lower lower
     * @param upper upper
     * @return long
     */
    public long nextLong(long lower, long upper) {
        if(lower >= upper) {
            throw new IllegalRange(lower, upper);
        }
        long max = (upper - lower) + 1;
        if (max <= 0) {
            // the range is too wide to fit in a positive long (larger than 2^63); as it covers
            // more than half the long range, we use directly a simple rejection method
            while (true) {
                long r = rng.nextLong();
                if (r >= lower && r <= upper) {
                    return r;
                }
            }
        } else if (max < Integer.MAX_VALUE){
            return lower + rng.nextInt((int) max);
        } else {
            return lower + nextLong(max);
        }
    }

    /**
     * Returns String of specified "size" containing chars from allowed list.
     * @param size size
     * @param chars chars
     * @return String
     */
    public String text(int size, String chars) {
        char[] buffer = new char[size];
        for(int i=0; i<size; i++) { buffer[i] = character(chars); }
        return new String(buffer);
    }

    /**
     * Returns String of specified "size" containing alphanumeric chars and spaces.
     * @param size size
     * @return String
     */
    public String text(int size) {
        return text(size, CHARACTERS);
    }

    /**
     * Returns fully saturated color, with random hue and brightness from specified range.
     * @param min min brightness
     * @param max max brightness
     * @return color
     */
    public Color color(float min, float max) {
        return Color.getHSBColor(nextFloat(), 1, nextFloat(min,max));
    }

    /**
     * Returns some intermediate color between two specified edge colors.
     * @param min edge color
     * @param max edge color
     * @return color
     */
    public Color color(Color min, Color max) {
        return new Color(
                nextInt(min.getRed(), max.getRed()),
                nextInt(min.getGreen(), max.getGreen()),
                nextInt(min.getBlue(), max.getBlue())
        );
    }

    /**
     * Returns random alphanumeric character (including space)
     * @return char
     */
    public char character() {
        return character(CHARACTERS);
    }

    /**
     * Returns random character from specified set.
     * @param chars chars
     * @return char
     */
    public char character(String chars) {
        return chars.charAt( nextInt(chars.length()) );
    }

    private static class IllegalRange extends IllegalArgumentException {

        public IllegalRange(Number lower, Number upper) {
            super("Illegal range: [" + lower + ";" + upper + ")");
        }
    }

}
