/*
 * 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.rng.examples.jmh.core;

import org.apache.commons.rng.UniformRandomProvider;
import org.apache.commons.rng.core.source32.IntProvider;
import org.apache.commons.rng.sampling.PermutationSampler;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OperationsPerInvocation;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;

import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

/**
 * Executes benchmark to compare the speed of random number generators to create
 * an int value in a range.
 */
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@State(Scope.Benchmark)
@Fork(value = 1, jvmArgs = { "-server", "-Xms128M", "-Xmx128M" })
public class RngNextIntInRangeBenchmark {
    /** The value. Must NOT be final to prevent JVM optimisation! */
    private int intValue;

    /**
     * The upper range for the {@code int} generation.
     */
    @State(Scope.Benchmark)
    public static class IntRange {
        /**
         * The upper range for the {@code int} generation.
         *
         * <p>Note that the while loop uses a rejection algorithm. From the Javadoc for
         * java.util.Random:</p>
         *
         * <pre>
         * "The probability of a value being rejected depends on n. The
         * worst case is n=2^30+1, for which the probability of a reject is 1/2,
         * and the expected number of iterations before the loop terminates is 2."
         * </pre>
         */
        @Param({"16", "17", "256", "257", "4096", "4097",
                // Worst case power-of-2: (1 << 30)
                "1073741824",
                // Worst case: (1 << 30) + 1
                "1073741825", })
        private int n;

        /**
         * Gets the upper bound {@code n}.
         *
         * @return the upper bound
         */
        public int getN() {
            return n;
        }
    }

    /**
     * The data used for the shuffle benchmark.
     */
    @State(Scope.Benchmark)
    public static class IntData {
        /**
         * The size of the data.
         */
        @Param({ "4", "16", "256", "4096", "16384" })
        private int size;

        /** The data. */
        private int[] data;

        /**
         * Gets the data.
         *
         * @return the data
         */
        public int[] getData() {
            return data;
        }

        /**
         * Create the data.
         */
        @Setup
        public void setup() {
            data = PermutationSampler.natural(size);
        }
    }

    /**
     * The source generator.
     */
    @State(Scope.Benchmark)
    public static class Source {
        /**
         * The name of the generator.
         */
        @Param({ "jdk", "jdkPow2", "lemire", "lemirePow2", "lemire31", "lemire31Pow2"})
        private String name;

        /** The random generator. */
        private UniformRandomProvider rng;

        /**
         * Gets the random generator.
         *
         * @return the generator
         */
        public UniformRandomProvider getRng() {
            return rng;
        }

        /** Create the generator. */
        @Setup
        public void setup() {
            final long seed = ThreadLocalRandom.current().nextLong();
            if ("jdk".equals(name)) {
                rng = new JDK(seed);
            } else if ("jdkPow2".equals(name)) {
                rng = new JDKPow2(seed);
            } else if ("lemire".equals(name)) {
                rng = new Lemire(seed);
            } else if ("lemirePow2".equals(name)) {
                rng = new LemirePow2(seed);
            } else if ("lemire31".equals(name)) {
                rng = new Lemire31(seed);
            } else if ("lemire31Pow2".equals(name)) {
                rng = new Lemire31Pow2(seed);
            }
        }
    }

    /**
     * Implement the SplitMix algorithm from {@link java.util.SplittableRandom
     * SplittableRandom} to output 32-bit int values.
     *
     * <p>This is a base generator to test nextInt(int) methods.
     */
    abstract static class SplitMix32 extends IntProvider {
        /**
         * The golden ratio, phi, scaled to 64-bits and rounded to odd.
         */
        private static final long GOLDEN_RATIO = 0x9e3779b97f4a7c15L;

        /** The state. */
        protected long state;

        /**
         * Create a new instance.
         *
         * @param seed the seed
         */
        SplitMix32(long seed) {
            state = seed;
        }

        @Override
        public int next() {
            long key = state += GOLDEN_RATIO;
            // 32 high bits of Stafford variant 4 mix64 function as int:
            // http://zimbry.blogspot.com/2011/09/better-bit-mixing-improving-on.html
            key = (key ^ (key >>> 33)) * 0x62a9d9ed799705f5L;
            return (int) (((key ^ (key >>> 28)) * 0xcb24d0a5c88c35b3L) >>> 32);
        }

        /**
         * Check the value is strictly positive.
         *
         * @param n the value
         */
        void checkStrictlyPositive(int n) {
            if (n <= 0) {
                throw new IllegalArgumentException("not strictly positive: " + n);
            }
        }
    }

    /**
     * Implement the nextInt(int) method of the JDK excluding the case for a power-of-2 range.
     */
    static class JDK extends SplitMix32 {
        /**
         * Create a new instance.
         *
         * @param seed the seed
         */
        JDK(long seed) {
            super(seed);
        }

        @Override
        public int nextInt(int n) {
            checkStrictlyPositive(n);

            int bits;
            int val;
            do {
                bits = next() >>> 1;
                val = bits % n;
            } while (bits - val + n - 1 < 0);

            return val;
        }
    }

    /**
     * Implement the nextInt(int) method of the JDK with a case for a power-of-2 range.
     */
    static class JDKPow2 extends SplitMix32 {
        /**
         * Create a new instance.
         *
         * @param seed the seed
         */
        JDKPow2(long seed) {
            super(seed);
        }

        @Override
        public int nextInt(int n) {
            checkStrictlyPositive(n);

            final int nm1 = n - 1;
            if ((n & nm1) == 0) {
                // Power of 2
                return next() & nm1;
            }

            int bits;
            int val;
            do {
                bits = next() >>> 1;
                val = bits % n;
            } while (bits - val + nm1 < 0);

            return val;
        }
    }

    /**
     * Implement the nextInt(int) method of Lemire (2019).
     *
     * @see <a href="https://arxiv.org/abs/1805.10941SplittableRandom"> Lemire
     * (2019): Fast Random Integer Generation in an Interval</a>
     */
    static class Lemire extends SplitMix32 {
        /** 2^32. */
        static final long POW_32 = 1L << 32;

        /**
         * Create a new instance.
         *
         * @param seed the seed
         */
        Lemire(long seed) {
            super(seed);
        }

        @Override
        public int nextInt(int n) {
            checkStrictlyPositive(n);

            long m = (next() & 0xffffffffL) * n;
            long l = m & 0xffffffffL;
            if (l < n) {
                // 2^32 % n
                final long t = POW_32 % n;
                while (l < t) {
                    m = (next() & 0xffffffffL) * n;
                    l = m & 0xffffffffL;
                }
            }
            return (int) (m >>> 32);
        }
    }

    /**
     * Implement the nextInt(int) method of Lemire (2019) with a case for a power-of-2 range.
     */
    static class LemirePow2 extends SplitMix32 {
        /** 2^32. */
        static final long POW_32 = 1L << 32;

        /**
         * Create a new instance.
         *
         * @param seed the seed
         */
        LemirePow2(long seed) {
            super(seed);
        }

        @Override
        public int nextInt(int n) {
            checkStrictlyPositive(n);

            final int nm1 = n - 1;
            if ((n & nm1) == 0) {
                // Power of 2
                return next() & nm1;
            }

            long m = (next() & 0xffffffffL) * n;
            long l = m & 0xffffffffL;
            if (l < n) {
                // 2^32 % n
                final long t = POW_32 % n;
                while (l < t) {
                    m = (next() & 0xffffffffL) * n;
                    l = m & 0xffffffffL;
                }
            }
            return (int) (m >>> 32);
        }
    }

    /**
     * Implement the nextInt(int) method of Lemire (2019) modified to 31-bit arithmetic to use
     * an int modulus operation.
     */
    static class Lemire31 extends SplitMix32 {
        /** 2^32. */
        static final long POW_32 = 1L << 32;

        /**
         * Create a new instance.
         *
         * @param seed the seed
         */
        Lemire31(long seed) {
            super(seed);
        }

        @Override
        public int nextInt(int n) {
            checkStrictlyPositive(n);

            long m = (nextInt() & 0x7fffffffL) * n;
            long l = m & 0x7fffffffL;
            if (l < n) {
                // 2^31 % n
                final long t = (Integer.MIN_VALUE - n) % n;
                while (l < t) {
                    m = (nextInt() & 0x7fffffffL) * n;
                    l = m & 0x7fffffffL;
                }
            }
            return (int) (m >>> 31);
        }
    }

    /**
     * Implement the nextInt(int) method of Lemire (2019) modified to 31-bit arithmetic to use
     * an int modulus operation, with a case for a power-of-2 range.
     */
    static class Lemire31Pow2 extends SplitMix32 {
        /** 2^32. */
        static final long POW_32 = 1L << 32;

        /**
         * Create a new instance.
         *
         * @param seed the seed
         */
        Lemire31Pow2(long seed) {
            super(seed);
        }

        @Override
        public int nextInt(int n) {
            checkStrictlyPositive(n);

            final int nm1 = n - 1;
            if ((n & nm1) == 0) {
                // Power of 2
                return next() & nm1;
            }

            long m = (nextInt() & 0x7fffffffL) * n;
            long l = m & 0x7fffffffL;
            if (l < n) {
                // 2^31 % n
                final long t = (Integer.MIN_VALUE - n) % n;
                while (l < t) {
                    m = (nextInt() & 0x7fffffffL) * n;
                    l = m & 0x7fffffffL;
                }
            }
            return (int) (m >>> 31);
        }
    }

    /**
     * Baseline for a JMH method call returning an {@code int}.
     *
     * @return the value
     */
    @Benchmark
    public int baselineInt() {
        return intValue;
    }

    /**
     * Exercise the {@link UniformRandomProvider#nextInt()} method.
     *
     * @param range the range
     * @param source Source of randomness.
     * @return the int
     */
    @Benchmark
    public int nextIntN(IntRange range, Source source) {
        return source.getRng().nextInt(range.getN());
    }

    /**
     * Exercise the {@link UniformRandomProvider#nextInt()} method in a loop.
     *
     * @param range the range
     * @param source Source of randomness.
     * @return the int
     */
    @Benchmark
    @OperationsPerInvocation(65_536)
    public int nextIntNloop65536(IntRange range, Source source) {
        int sum = 0;
        for (int i = 0; i < 65_536; i++) {
            sum += source.getRng().nextInt(range.getN());
        }
        return sum;
    }

    /**
     * Exercise the {@link UniformRandomProvider#nextInt(int)} method by shuffling
     * data.
     *
     * @param data the data
     * @param source Source of randomness.
     * @return the shuffle data
     */
    @Benchmark
    public int[] shuffle(IntData data, Source source) {
        final int[] array = data.getData();
        shuffle(array, source.getRng());
        return array;
    }

    /**
     * Perform a Fischer-Yates shuffle.
     *
     * @param array the array
     * @param rng the random generator
     */
    private static void shuffle(int[] array, UniformRandomProvider rng) {
        for (int i = array.length - 1; i > 0; i--) {
            // Swap index with any position down to 0
            final int j = rng.nextInt(i);
            final int tmp = array[j];
            array[j] = array[i];
            array[i] = tmp;
        }
    }

    /**
     * Exercise the {@link UniformRandomProvider#nextInt(int)} method by creating
     * random indices for shuffling data.
     *
     * @param data the data
     * @param source Source of randomness.
     * @return the sum
     */
    @Benchmark
    public int pseudoShuffle(IntData data, Source source) {
        int sum = 0;
        for (int i = data.getData().length - 1; i > 0; i--) {
            sum += source.getRng().nextInt(i);
        }
        return sum;
    }
}
