/*
 * 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.sampling.distribution;

import org.apache.commons.rng.UniformRandomProvider;
import org.apache.commons.rng.sampling.distribution.DiscreteSampler;
import org.apache.commons.rng.sampling.distribution.KempSmallMeanPoissonSampler;
import org.apache.commons.rng.sampling.distribution.LargeMeanPoissonSampler;
import org.apache.commons.rng.sampling.distribution.SmallMeanPoissonSampler;
import org.apache.commons.rng.simple.RandomSource;

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.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.TimeUnit;
import java.util.function.Supplier;

/**
 * Executes benchmark to compare the speed of generation of Poisson distributed random numbers.
 */
@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 PoissonSamplersPerformance {
    /**
     * The value for the baseline generation of an {@code int} value.
     *
     * <p>This must NOT be final!</p>
     */
    private int value;

    /**
     * The mean for the call to {@link Math#exp(double)}.
     */
    @State(Scope.Benchmark)
    public static class Means {
        /**
         * The Poisson mean. This is set at a level where the small mean sampler is to be used
         * in preference to the large mean sampler.
         */
        @Param({"0.25",
                "0.5",
                "1",
                "2",
                "4",
                "8",
                "16",
                "32"})
        private double mean;

        /**
         * Gets the mean.
         *
         * @return the mean
         */
        public double getMean() {
            return mean;
        }
    }

    /**
     * The {@link DiscreteSampler} samplers to use for testing. Creates the sampler for each
     * {@link RandomSource} in the default
     * {@link org.apache.commons.rng.examples.jmh.RandomSources RandomSources}.
     */
    @State(Scope.Benchmark)
    public static class Sources {
        /**
         * RNG providers.
         *
         * <p>Use different speeds.</p>
         *
         * @see <a href="https://commons.apache.org/proper/commons-rng/userguide/rng.html">
         *      Commons RNG user guide</a>
         */
        @Param({"WELL_44497_B",
                //"ISAAC",
                "XO_RO_SHI_RO_128_PLUS"})
        private String randomSourceName;

        /**
         * The sampler type.
         */
        @Param({"SmallMeanPoissonSampler",
                "KempSmallMeanPoissonSampler",
                "BoundedKempSmallMeanPoissonSampler",
                "KempSmallMeanPoissonSamplerP50",
                "KempSmallMeanPoissonSamplerBinarySearch",
                "KempSmallMeanPoissonSamplerGuideTable",
                "LargeMeanPoissonSampler",
                "TinyMeanPoissonSampler"})
        private String samplerType;

        /**
         * The Poisson mean. This is set at a level where the small mean sampler is to be used
         * in preference to the large mean sampler.
         */
        @Param({"0.25",
                "0.5",
                "1",
                "2",
                "4",
                "8",
                "16",
                "32",
                "64"})
        private double mean;

        /** RNG. */
        private UniformRandomProvider generator;

        /** The factory. */
        private Supplier<DiscreteSampler> factory;

        /** The sampler. */
        private DiscreteSampler sampler;

        /**
         * @return The RNG.
         */
        public UniformRandomProvider getGenerator() {
            return generator;
        }

        /**
         * Gets the sampler.
         *
         * @return The sampler.
         */
        public DiscreteSampler getSampler() {
            return sampler;
        }

        /** Instantiates sampler. */
        @Setup
        public void setup() {
            final RandomSource randomSource = RandomSource.valueOf(randomSourceName);
            generator = randomSource.create();
            if ("SmallMeanPoissonSampler".equals(samplerType)) {
                factory = () -> SmallMeanPoissonSampler.of(generator, mean);
            } else if ("KempSmallMeanPoissonSampler".equals(samplerType)) {
                factory = () -> KempSmallMeanPoissonSampler.of(generator, mean);
            } else if ("BoundedKempSmallMeanPoissonSampler".equals(samplerType)) {
                factory = () -> new BoundedKempSmallMeanPoissonSampler(generator, mean);
            } else if ("KempSmallMeanPoissonSamplerP50".equals(samplerType)) {
                factory = () -> new KempSmallMeanPoissonSamplerP50(generator, mean);
            } else if ("KempSmallMeanPoissonSamplerBinarySearch".equals(samplerType)) {
                factory = () -> new KempSmallMeanPoissonSamplerBinarySearch(generator, mean);
            } else if ("KempSmallMeanPoissonSamplerGuideTable".equals(samplerType)) {
                factory = () -> new KempSmallMeanPoissonSamplerGuideTable(generator, mean);
            } else if ("LargeMeanPoissonSampler".equals(samplerType)) {
                factory = () -> LargeMeanPoissonSampler.of(generator, mean);
            } else if ("TinyMeanPoissonSampler".equals(samplerType)) {
                factory = () -> new TinyMeanPoissonSampler(generator, mean);
            }
            sampler = factory.get();
        }

        /**
         * Creates a new instance of the sampler.
         *
         * @return The sampler.
         */
        public DiscreteSampler createSampler() {
            return factory.get();
        }
    }

    /**
     * Kemp sampler for the <a href="https://mathworld.wolfram.com/PoissonDistribution.html">Poisson
     * distribution</a>.
     *
     * <ul>
     *  <li>
     *   For small means, a Poisson process is simulated using uniform deviates, as
     *   described in Kemp, A, W, (1981) Efficient Generation of Logarithmically Distributed
     *   Pseudo-Random Variables. Journal of the Royal Statistical Society. Vol. 30, No. 3, pp. 249-253.
     *  </li>
     * </ul>
     *
     * <p>Note: This is similar to {@link KempSmallMeanPoissonSampler} but the sample is
     * bounded by 1000 * mean.</p>
     *
     * @see <a href="https://www.jstor.org/stable/2346348">Kemp, A.W. (1981) JRSS Vol. 30, pp. 249-253</a>
     */
    static class BoundedKempSmallMeanPoissonSampler
        implements DiscreteSampler {
        /** Underlying source of randomness. */
        private final UniformRandomProvider rng;
        /**
         * Pre-compute {@code Math.exp(-mean)}.
         * Note: This is the probability of the Poisson sample {@code p(x=0)}.
         */
        private final double p0;
        /** Pre-compute {@code 1000 * mean} as the upper limit of the sample. */
        private final int limit;
        /**
         * The mean of the Poisson sample.
         */
        private final double mean;

        /**
         * @param rng  Generator of uniformly distributed random numbers.
         * @param mean Mean.
         * @throws IllegalArgumentException if {@code mean <= 0} or {@code mean > 700}.
         */
        BoundedKempSmallMeanPoissonSampler(UniformRandomProvider rng,
                                            double mean) {
            if (mean <= 0) {
                throw new IllegalArgumentException();
            }

            p0 = Math.exp(-mean);
            if (p0 == 0) {
                throw new IllegalArgumentException();
            }
            // The returned sample is bounded by 1000 * mean
            limit = (int) Math.ceil(1000 * mean);
            this.rng = rng;
            this.mean = mean;
        }

        /** {@inheritDoc} */
        @Override
        public int sample() {
            // Note on the algorithm:
            // - X is the unknown sample deviate (the output of the algorithm)
            // - x is the current value from the distribution
            // - p is the probability of the current value x, p(X=x)
            // - u is effectively the cumulative probability that the sample X
            //   is equal or above the current value x, p(X>=x)
            // So if p(X>=x) > p(X=x) the sample must be above x, otherwise it is x
            double u = rng.nextDouble();
            int x = 0;
            double p = p0;
            while (u > p) {
                u -= p;
                // Compute the next probability using a recurrence relation.
                // p(x+1) = p(x) * mean / (x+1)
                p *= mean / ++x;
                // The algorithm listed in Kemp (1981) does not check that the rolling probability
                // is positive. This check is added to ensure a simple bounds in the event that
                // p == 0
                if (x == limit) {
                    return x;
                }
            }
            return x;
        }
    }

    /**
     * Kemp sampler for the Poisson distribution.
     *
     * <p>Note: This is a modification of the original algorithm by Kemp. It implements a hedge
     * on the cumulative probability set at 50%. This saves computation in half of the samples.</p>
     */
    static class KempSmallMeanPoissonSamplerP50
        implements DiscreteSampler {
        /** The value of p=0.5. */
        private static final double ONE_HALF = 0.5;
        /**
         * The threshold that defines the cumulative probability for the long tail of the
         * Poisson distribution. Above this threshold the recurrence relation that computes the
         * next probability must check that the p-value is not zero.
         */
        private static final double LONG_TAIL_THRESHOLD = 0.999;

        /** Underlying source of randomness. */
        private final UniformRandomProvider rng;
        /**
         * Pre-compute {@code Math.exp(-mean)}.
         * Note: This is the probability of the Poisson sample {@code p(x=0)}.
         */
        private final double p0;
        /**
         * The mean of the Poisson sample.
         */
        private final double mean;
        /**
         * Pre-compute the cumulative probability for all samples up to and including x.
         * This is F(x) = sum of {@code p(X<=x)}.
         *
         * <p>The value is computed at approximately 50% allowing the algorithm to choose to start
         * at value (x+1) approximately half of the time.
         */
        private final double fx;
        /**
         * Store the value (x+1) corresponding to the next value after the cumulative probability is
         * above 50%.
         */
        private final int x1;
        /**
         * Store the probability value p(x+1), allowing the algorithm to start from the point x+1.
         */
        private final double px1;

        /**
         * Create a new instance.
         *
         * <p>This is valid for means as large as approximately {@code 744}.</p>
         *
         * @param rng  Generator of uniformly distributed random numbers.
         * @param mean Mean.
         * @throws IllegalArgumentException if {@code mean <= 0} or {@code Math.exp(-mean) == 0}.
         */
        KempSmallMeanPoissonSamplerP50(UniformRandomProvider rng,
                                       double mean) {
            if (mean <= 0) {
                throw new IllegalArgumentException();
            }

            this.rng = rng;
            p0 = Math.exp(-mean);
            this.mean = mean;

            // Pre-compute a hedge value for the cumulative probability at approximately 50%.
            // This is only done when p0 is less than the long tail threshold.
            // The result is that the rolling probability computation should never hit the
            // long tail where p reaches zero.
            if (p0 <= LONG_TAIL_THRESHOLD) {
                // Check the edge case for no probability
                if (p0 == 0) {
                    throw new IllegalArgumentException();
                }

                double p = p0;
                int x = 0;
                // Sum is cumulative probability F(x) = sum p(X<=x)
                double sum = p;
                while (sum < ONE_HALF) {
                    // Compute the next probability using a recurrence relation.
                    // p(x+1) = p(x) * mean / (x+1)
                    p *= mean / ++x;
                    sum += p;
                }
                fx = sum;
                x1 = x + 1;
                px1 = p * mean / x1;
            } else {
                // Always start at zero.
                // Note: If NaN is input as the mean this path is executed and the sample is always zero.
                fx = 0;
                x1 = 0;
                px1 = p0;
            }
        }

        /** {@inheritDoc} */
        @Override
        public int sample() {
            // Note on the algorithm:
            // - X is the unknown sample deviate (the output of the algorithm)
            // - x is the current value from the distribution
            // - p is the probability of the current value x, p(X=x)
            // - u is effectively the cumulative probability that the sample X
            //   is equal or above the current value x, p(X>=x)
            // So if p(X>=x) > p(X=x) the sample must be above x, otherwise it is x
            final double u = rng.nextDouble();

            if (u <= fx) {
                // Sample from the lower half of the distribution starting at zero
                return sampleBeforeLongTail(u, 0, p0);
            }

            // Sample from the upper half of the distribution starting at cumulative probability fx.
            // This is reached when u > fx and sample X > x.

            // If below the long tail threshold then omit the check on the asymptote of p -> zero
            if (u <= LONG_TAIL_THRESHOLD) {
                return sampleBeforeLongTail(u - fx, x1, px1);
            }

            return sampleWithinLongTail(u - fx, x1, px1);
        }

        /**
         * Compute the sample assuming it is <strong>not</strong> in the long tail of the distribution.
         *
         * <p>This avoids a check on the next probability value assuming that the cumulative probability
         * is at a level where the long tail of the Poisson distribution will not be reached.
         *
         * @param u the remaining cumulative probability ({@code p(X>x)})
         * @param x the current sample value X
         * @param p the current probability of the sample (p(X=x))
         * @return the sample X
         */
        private int sampleBeforeLongTail(double u, int x, double p) {
            while (u > p) {
                // Update the remaining cumulative probability
                u -= p;
                // Compute the next probability using a recurrence relation.
                // p(x+1) = p(x) * mean / (x+1)
                p *= mean / ++x;
                // The algorithm listed in Kemp (1981) does not check that the rolling probability
                // is positive (non-zero). This is omitted here on the assumption that the cumulative
                // probability will not be in the long tail where the probability asymptotes to zero.
            }
            return x;
        }

        /**
         * Compute the sample assuming it is in the long tail of the distribution.
         *
         * <p>This requires a check on the next probability value which is expected to asymptote to zero.
         *
         * @param u the remaining cumulative probability
         * @param x the current sample value X
         * @param p the current probability of the sample (p(X=x))
         * @return the sample X
         */
        private int sampleWithinLongTail(double u, int x, double p) {
            while (u > p) {
                // Update the remaining cumulative probability
                u -= p;
                // Compute the next probability using a recurrence relation.
                // p(x+1) = p(x) * mean / (x+1)
                p *= mean / ++x;
                // The algorithm listed in Kemp (1981) does not check that the rolling probability
                // is positive. This check is added to ensure no errors when the limit of the summation
                // 1 - sum(p(x)) is above 0 due to cumulative error in floating point arithmetic when
                // in the long tail of the distribution.
                if (p == 0) {
                    return x;
                }
            }
            return x;
        }
    }

    /**
     * Kemp sampler for the Poisson distribution.
     *
     * <p>Note: This is a modification of the original algorithm by Kemp. It stores the
     * cumulative probability table for repeat use. The table is searched using a binary
     * search algorithm.</p>
     */
    static class KempSmallMeanPoissonSamplerBinarySearch
        implements DiscreteSampler {
        /**
         * Store the cumulative probability table size for integer means so that 99.99%
         * of the Poisson distribution is covered. This is done until the table size is
         * 2 * mean.
         *
         * <p>At higher mean the expected range is mean + 4 * sqrt(mean). To avoid the sqrt
         * the conservative limit of 2 * mean is used.
         */
        private static final int[] TABLE_SIZE = {
            /* mean 1 to 10. */
            8, 10, 12, 14, 16, 18, 20, 22, 24, 25,
            /* mean 11 to 20. */
            27, 29, 30, 32, 33, 35, 36, 38, 39, 41,
        };

        /** Underlying source of randomness. */
        private final UniformRandomProvider rng;
        /**
         * The mean of the Poisson sample.
         */
        private final double mean;
        /**
         * Store the cumulative probability for all samples up to and including x.
         * This is F(x) = sum of {@code p(X<=x)}.
         *
         * <p>This table is initialized to store cumulative probabilities for x up to 2 * mean
         * or 99.99% (whichever is larger).
         */
        private final double[] fx;
        /**
         * Store the value x corresponding to the last stored cumulative probability.
         */
        private int lastX;
        /**
         * Store the probability value p(x) corresponding to last stored cumulative probability,
         * allowing the algorithm to start from the point x.
         */
        private double px;

        /**
         * Create a new instance.
         *
         * <p>This is valid for means as large as approximately {@code 744}.</p>
         *
         * @param rng  Generator of uniformly distributed random numbers.
         * @param mean Mean.
         * @throws IllegalArgumentException if {@code mean <= 0} or {@code Math.exp(-mean) == 0}.
         */
        KempSmallMeanPoissonSamplerBinarySearch(UniformRandomProvider rng,
                                                double mean) {
            if (mean <= 0) {
                throw new IllegalArgumentException();
            }

            px = Math.exp(-mean);
            if (px > 0) {
                this.rng = rng;
                this.mean = mean;

                // Initialise the cumulative probability table.
                // The supported mean where p(x=0) > 0 sets a limit of around 744 so this will always be
                // possible.
                final int upperMean = (int) Math.ceil(mean);
                fx = new double[(upperMean < TABLE_SIZE.length) ? TABLE_SIZE[upperMean] : upperMean * 2];
                fx[0] = px;
            } else {
                // This will catch NaN mean values
                throw new IllegalArgumentException();
            }
        }

        /** {@inheritDoc} */
        @Override
        public int sample() {
            // Note on the algorithm:
            // - X is the unknown sample deviate (the output of the algorithm)
            // - x is the current value from the distribution
            // - p is the probability of the current value x, p(X=x)
            // - u is effectively the cumulative probability that the sample X
            //   is equal or above the current value x, p(X>=x)
            // So if p(X>=x) > p(X=x) the sample must be above x, otherwise it is x
            final double u = rng.nextDouble();

            if (u <= fx[lastX]) {
                // Binary search within known cumulative probability table.
                // Find x so that u > f[x-1] and u <= f[x].
                // This is a looser search than Arrays.binarySearch:
                // - The output is x = upper.
                // - The pre-condition check ensures u <= f[upper] at the start.
                // - The table stores probabilities where f[0] is non-zero.
                // - u should be >= 0 (or the random generator is broken).
                // - It avoids comparisons using Double.doubleToLongBits.
                // - It avoids the low likelihood of equality between two doubles so uses
                //   only 1 compare per loop.
                // - It uses a weighted middle anticipating that the cumulative distribution
                //   is skewed as the expected use case is a small mean.
                int lower = 0;
                int upper = lastX;
                while (lower < upper) {
                    // Weighted middle is 1/4 of the range between lower and upper
                    final int mid = (3 * lower + upper) >>> 2;
                    final double midVal = fx[mid];
                    if (u > midVal) {
                        // Change lower such that
                        // u > f[lower - 1]
                        lower = mid + 1;
                    } else {
                        // Change upper such that
                        // u <= f[upper]
                        upper = mid;
                    }
                }
                return upper;
            }

            // The sample is above x
            int x1 = lastX + 1;

            // Fill the cumulative probability table if possible
            while (x1 < fx.length) {
                // Compute the next probability using a recurrence relation.
                // p(x+1) = p(x) * mean / (x+1)
                px = nextProbability(px, x1);
                // Compute the next cumulative probability f(x+1) and update
                final double sum = fx[lastX] + px;
                fx[++lastX] = sum;
                // Check if this is the correct sample
                if (u <= sum) {
                    return lastX;
                }
                x1 = lastX + 1;
            }

            // The sample is above the range of the cumulative probability table.
            // Compute using the Kemp method.
            // This requires the remaining cumulative probability and the probability for x+1.
            return sampleWithinLongTail(u - fx[lastX], x1, nextProbability(px, x1));
        }

        /**
         * Compute the next probability using a recurrence relation.
         *
         * <pre>
         * p(x + 1) = p(x) * mean / (x + 1)
         * </pre>
         *
         * @param p the probability of x
         * @param x1 the value of x+1
         * @return the probability of x+1
         */
        private double nextProbability(double p, int x1) {
            return p * mean / x1;
        }

        /**
         * Compute the sample assuming it is in the long tail of the distribution.
         *
         * <p>This requires a check on the next probability value which is expected to asymptote to zero.
         *
         * @param u the remaining cumulative probability
         * @param x the current sample value X
         * @param p the current probability of the sample (p(X=x))
         * @return the sample X
         */
        private int sampleWithinLongTail(double u, int x, double p) {
            while (u > p) {
                // Update the remaining cumulative probability
                u -= p;
                p = nextProbability(p, ++x);
                // The algorithm listed in Kemp (1981) does not check that the rolling probability
                // is positive. This check is added to ensure no errors when the limit of the summation
                // 1 - sum(p(x)) is above 0 due to cumulative error in floating point arithmetic when
                // in the long tail of the distribution.
                if (p == 0) {
                    return x;
                }
            }
            return x;
        }
    }
    /**
     * Sampler for the <a href="https://mathworld.wolfram.com/PoissonDistribution.html">Poisson
     * distribution</a>.
     *
     * <p>Note: This is a modification of the original algorithm by Kemp. It stores the
     * cumulative probability table for repeat use. The table is computed dynamically and
     * searched using a guide table.</p>
     */
    static class KempSmallMeanPoissonSamplerGuideTable implements DiscreteSampler {
        /**
         * Store the cumulative probability table size for integer means so that 99.99% of the
         * Poisson distribution is covered. This is done until the table size is 2 * mean.
         *
         * <p>At higher mean the expected range is mean + 4 * sqrt(mean). To avoid the sqrt
         * the conservative limit of 2 * mean is used.
         */
        private static final int[] TABLE_SIZE = {
            /* mean 1 to 10. */
            8, 10, 12, 14, 16, 18, 20, 22, 24, 25,
            /* mean 11 to 20. */
            27, 29, 30, 32, 33, 35, 36, 38, 39, 41, };

        /** Underlying source of randomness. */
        private final UniformRandomProvider rng;
        /**
         * The mean of the Poisson sample.
         */
        private final double mean;
        /**
         * Store the cumulative probability for all samples up to and including x. This is
         * F(x) = sum of {@code p(X<=x)}.
         *
         * <p>This table is initialized to store cumulative probabilities for x up to 2 * mean
         * or 99.99% (whichever is larger).
         */
        private final double[] cumulativeProbability;
        /**
         * Store the value x corresponding to the last stored cumulative probability.
         */
        private int tabulatedX;
        /**
         * Store the probability value p(x), allowing the algorithm to start from the point x.
         */
        private double probabilityX;
        /**
         * The inverse cumulative probability guide table. This is a map between the cumulative
         * probability (f(x)) and the value x. It is used to set the initial point for search
         * of the cumulative probability table.
         *
         * <p>The index into the table is obtained using {@code f(x) * guideTable.length}. The value
         * stored at the index is value {@code x+1} such that it is the exclusive upper bound
         * on the sample value for searching the cumulative probability table. It requires the
         * table search is towards zero.</p>
         *
         * <p>Note: Using x+1 ensures that the table can be zero filled upon initialisation and
         * any index with zero has yet to be allocated.</p>
         *
         * <p>The guide table should never be used when the input f(x) is above the current range of
         * the cumulative probability table. This would create an index that has not been
         * allocated a mapping.
         */
        private final int[] guideTable;

        /**
         * Create a new instance.
         *
         * <p>This is valid for means as large as approximately {@code 744}.</p>
         *
         * @param rng Generator of uniformly distributed random numbers.
         * @param mean Mean.
         * @throws IllegalArgumentException if {@code mean <= 0} or
         * {@code Math.exp(-mean) == 0}.
         */
        KempSmallMeanPoissonSamplerGuideTable(UniformRandomProvider rng, double mean) {
            if (mean <= 0) {
                throw new IllegalArgumentException("mean is not strictly positive: " + mean);
            }

            probabilityX = Math.exp(-mean);
            if (probabilityX > 0) {
                this.rng = rng;
                this.mean = mean;

                // Initialise the cumulative probability table.
                // The supported mean where p(x=0) > 0 sets a limit of around 744 so the cast to int
                // will always be possible.
                final int upperMean = (int) Math.ceil(mean);
                final int size = (upperMean < TABLE_SIZE.length) ? TABLE_SIZE[upperMean] : upperMean * 2;
                cumulativeProbability = new double[size];
                cumulativeProbability[0] = probabilityX;

                guideTable = new int[cumulativeProbability.length + 1];
                initializeGuideTable(probabilityX);
            } else {
                // This will catch NaN mean values
                throw new IllegalArgumentException("No probability for mean " + mean);
            }
        }

        /**
         * Initialise the cumulative probability guide table. All guide indices at or below the
         * index corresponding to the given probability will be set to 1.
         *
         * @param p0 the probability for x=0
         */
        private void initializeGuideTable(double p0) {
            for (int index = getGuideTableIndex(p0); index >= 0; index--) {
                guideTable[index] = 1;
            }
        }

        /**
         * Fill the inverse cumulative probability guide table. Set the index corresponding to the
         * given probability to x+1 establishing an exclusive upper bound on x for the probability.
         * All unused guide indices below the index will also be set to x+1.
         *
         * @param p the cumulative probability
         * @param x the sample value x
         */
        private void updateGuideTable(double p, int x) {
            // Always set the current index as the guide table is the exclusive upper bound
            // for searching the cumulative probability table for any value p.
            // Then fill any lower positions that are not allocated.
            final int x1 = x + 1;
            int index = getGuideTableIndex(p);
            do {
                guideTable[index--] = x1;
            } while (index > 0 && guideTable[index] == 0);
        }

        /**
         * Gets the guide table index for the probability. This is obtained using
         * {@code p * (guideTable.length - 1)} so is inside the length of the table.
         *
         * @param p the cumulative probability
         * @return the guide table index
         */
        private int getGuideTableIndex(double p) {
            // Note: This is only ever called when p is in the range of the cumulative
            // probability table. So assume 0 <= p <= 1.
            return (int) (p * (guideTable.length - 1));
        }

        /** {@inheritDoc} */
        @Override
        public int sample() {
            // Note on the algorithm:
            // 1. Compute a cumulative probability with a uniform deviate (u).
            // 2. If the probability lies within the tabulated cumulative probabilities
            //    then find the sample value.
            // 3. If possible expand the tabulated cumulative probabilities up to the value u.
            // 4. If value u exceeds the capacity for the tabulated cumulative probabilities
            //    then compute the sample value dynamically without storing the probabilities.

            // Compute a probability
            final double u = rng.nextDouble();

            // Check the tabulated cumulative probability table
            if (u <= cumulativeProbability[tabulatedX]) {
                // Initialise the search using a guide table to find an initial guess.
                // The table provides an upper bound on the sample for a known cumulative probability.
                int sample = guideTable[getGuideTableIndex(u)] - 1;
                // If u is above the sample probability (this occurs due to truncation)
                // then return the next value up.
                if (u > cumulativeProbability[sample]) {
                    return sample + 1;
                }
                // Search down
                while (sample != 0 && u <= cumulativeProbability[sample - 1]) {
                    sample--;
                }
                return sample;
            }

            // The sample is above the tabulated cumulative probability for x
            int x1 = tabulatedX + 1;

            // Fill the cumulative probability table if possible
            while (x1 < cumulativeProbability.length) {
                probabilityX = nextProbability(probabilityX, x1);
                // Compute the next cumulative probability f(x+1) and update
                final double sum = cumulativeProbability[tabulatedX] + probabilityX;
                cumulativeProbability[++tabulatedX] = sum;
                updateGuideTable(sum, tabulatedX);
                // Check if this is the correct sample
                if (u <= sum) {
                    return tabulatedX;
                }
                x1 = tabulatedX + 1;
            }

            // The sample is above the range of the cumulative probability table.
            // Compute using the Kemp method.
            // This requires the remaining cumulative probability and the probability for x+1.
            return sampleWithinLongTail(u - cumulativeProbability[tabulatedX], x1, nextProbability(probabilityX, x1));
        }

        /**
         * Compute the next probability using a recurrence relation.
         *
         * <pre>
         * p(x + 1) = p(x) * mean / (x + 1)
         * </pre>
         *
         * @param px the probability of x
         * @param x1 the value of x+1
         * @return the probability of x+1
         */
        private double nextProbability(double px, int x1) {
            return px * mean / x1;
        }

        /**
         * Compute the sample assuming it is in the long tail of the distribution.
         *
         * <p>This requires a check on the next probability value which is expected to
         * asymptote to zero.
         *
         * @param u the remaining cumulative probability
         * @param x the current sample value X
         * @param p the current probability of the sample (p(X=x))
         * @return the sample X
         */
        private int sampleWithinLongTail(double u, int x, double p) {
            // Note on the algorithm:
            // - X is the unknown sample deviate (the output of the algorithm)
            // - x is the current value from the distribution
            // - p is the probability of the current value x, p(X=x)
            // - u is effectively the cumulative probability that the sample X
            // is equal or above the current value x, p(X>=x)
            // So if p(X>=x) > p(X=x) the sample must be above x, otherwise it is x
            while (u > p) {
                // Update the remaining cumulative probability
                u -= p;
                p = nextProbability(p, ++x);
                // The algorithm listed in Kemp (1981) does not check that the rolling
                // probability is positive. This check is added to ensure no errors when the
                // limit of the summation 1 - sum(p(x)) is above 0 due to cumulative error in
                // floating point arithmetic when in the long tail of the distribution.
                if (p == 0) {
                    break;
                }
            }
            return x;
        }
    }

    /**
     * Sampler for the <a href="https://mathworld.wolfram.com/PoissonDistribution.html">Poisson distribution</a>.
     *
     * <ul>
     *  <li>
     *   For small means, a Poisson process is simulated using uniform deviates, as
     *   described in Knuth (1969). Seminumerical Algorithms. The Art of Computer Programming,
     *   Volume 2. Addison Wesley.
     *  </li>
     * </ul>
     *
     * <p>This sampler is suitable for {@code mean < 20}.</p>
     *
     * <p>Sampling uses {@link UniformRandomProvider#nextInt()} and 32-bit integer arithmetic.</p>
     *
     * @see <a href="https://en.wikipedia.org/wiki/Poisson_distribution#Generating_Poisson-distributed_random_variables">
     * Poisson random variables</a>
     */
    static class TinyMeanPoissonSampler
        implements DiscreteSampler {
        /** Pre-compute Poisson probability p(n=0) mapped to the range of a 32-bit unsigned fraction. */
        private final long p0;
        /** Underlying source of randomness. */
        private final UniformRandomProvider rng;

        /**
         * @param rng  Generator of uniformly distributed random numbers.
         * @param mean Mean.
         * @throws IllegalArgumentException if {@code mean <= 0} or {@code Math.exp(-mean) * (1L << 32)}
         * is not positive.
         */
        TinyMeanPoissonSampler(UniformRandomProvider rng,
                               double mean) {
            this.rng = rng;
            if (mean <= 0) {
                throw new IllegalArgumentException();
            }
            // Math.exp(-mean) is the probability of a Poisson distribution for n=0 (p(n=0)).
            // This is mapped to a 32-bit range as the numerator of a 32-bit fraction
            // for use in optimised 32-bit arithmetic.
            p0 = (long) (Math.exp(-mean) * 0x100000000L);
            if (p0 == 0) {
                throw new IllegalArgumentException("No p(x=0) probability for mean: " + mean);
            }
        }

        /** {@inheritDoc} */
        @Override
        public int sample() {
            int n = 0;
            // The unsigned 32-bit sample represents the fraction x / 2^32 where x is [0,2^32-1].
            // The upper bound is exclusive hence the fraction is a uniform deviate from [0,1).
            long r = nextUnsigned32();
            // The limit is the probability p(n=0) converted to an unsigned fraction.
            while (r >= p0) {
                // Compute the fraction:
                //  r       [0,2^32)      2^32
                // ----  *  --------   /  ----
                // 2^32       2^32        2^32
                // This rounds down the fraction numerator when the lower 32-bits are discarded.
                r = (r * nextUnsigned32()) >>> 32;
                n++;
            }
            // Ensure the value is positive in the worst case scenario of a broken
            // generator that returns 0xffffffff for each sample. This will cause
            // the integer counter to overflow 2^31-1 but not exceed 2^32. The fraction
            // multiplication effectively turns r into a counter decrementing from 2^32-1
            // to zero.
            return (n >= 0) ? n : Integer.MAX_VALUE;
        }

        /**
         * Get the next unsigned 32-bit random integer.
         *
         * @return the random integer
         */
        private long nextUnsigned32() {
            return rng.nextInt() & 0xffffffffL;
        }
    }

    // Benchmarks methods below.

    /**
     * Baseline for the JMH timing overhead for production of an {@code int} value.
     *
     * @return the {@code int} value
     */
    @Benchmark
    public int baselineInt() {
        return value;
    }

    /**
     * Baseline for {@link Math#exp(double)}.
     *
     * @param mean the mean
     * @return the value
     */
    @Benchmark
    public double baselineExp(Means mean) {
        return Math.exp(-mean.getMean());
    }

    /**
     * Run the sampler.
     *
     * @param sources Source of randomness.
     * @return the sample value
     */
    @Benchmark
    public int sample(Sources sources) {
        return sources.getSampler().sample();
    }

    /**
     * Run the sampler.
     *
     * @param sources Source of randomness.
     * @return the sample value
     */
    @Benchmark
    public int singleSample(Sources sources) {
        return sources.createSampler().sample();
    }
}
