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

import org.apache.commons.rng.UniformRandomProvider;

/**
 * Sampling from a <a href="https://en.wikipedia.org/wiki/Beta_distribution">beta distribution</a>.
 *
 * <p>Uses Cheng's algorithms for beta distribution sampling:</p>
 *
 * <blockquote>
 * <pre>
 * R. C. H. Cheng,
 * "Generating beta variates with nonintegral shape parameters",
 * Communications of the ACM, 21, 317-322, 1978.
 * </pre>
 * </blockquote>
 *
 * <p>Sampling uses {@link UniformRandomProvider#nextDouble()}.</p>
 *
 * @since 1.0
 */
public class ChengBetaSampler
    extends SamplerBase
    implements SharedStateContinuousSampler {
    /** Natural logarithm of 4. */
    private static final double LN_4 = Math.log(4.0);

    /** The appropriate beta sampler for the parameters. */
    private final SharedStateContinuousSampler delegate;

    /**
     * Base class to implement Cheng's algorithms for the beta distribution.
     */
    private abstract static class BaseChengBetaSampler
            implements SharedStateContinuousSampler {
        /**
         * Flag set to true if {@code a} is the beta distribution alpha shape parameter.
         * Otherwise {@code a} is the beta distribution beta shape parameter.
         *
         * <p>From the original Cheng paper this is equal to the result of: {@code a == a0}.</p>
         */
        protected final boolean aIsAlphaShape;
        /**
         * First shape parameter.
         * The meaning of this is dependent on the {@code aIsAlphaShape} flag.
         */
        protected final double a;
        /**
         * Second shape parameter.
         * The meaning of this is dependent on the {@code aIsAlphaShape} flag.
         */
        protected final double b;
        /** Underlying source of randomness. */
        protected final UniformRandomProvider rng;
        /**
         * The algorithm alpha factor. This is not the beta distribution alpha shape parameter.
         * It is the sum of the two shape parameters ({@code a + b}.
         */
        protected final double alpha;
        /** The logarithm of the alpha factor. */
        protected final double logAlpha;

        /**
         * @param rng Generator of uniformly distributed random numbers.
         * @param aIsAlphaShape true if {@code a} is the beta distribution alpha shape parameter.
         * @param a Distribution first shape parameter.
         * @param b Distribution second shape parameter.
         */
        BaseChengBetaSampler(UniformRandomProvider rng, boolean aIsAlphaShape, double a, double b) {
            this.rng = rng;
            this.aIsAlphaShape = aIsAlphaShape;
            this.a = a;
            this.b = b;
            alpha = a + b;
            logAlpha = Math.log(alpha);
        }

        /**
         * @param rng Generator of uniformly distributed random numbers.
         * @param source Source to copy.
         */
        BaseChengBetaSampler(UniformRandomProvider rng,
                             BaseChengBetaSampler source) {
            this.rng = rng;
            aIsAlphaShape = source.aIsAlphaShape;
            a = source.a;
            b = source.b;
            // Compute algorithm factors.
            alpha = source.alpha;
            logAlpha = source.logAlpha;
        }

        /** {@inheritDoc} */
        @Override
        public String toString() {
            return "Cheng Beta deviate [" + rng.toString() + "]";
        }

        /**
         * Compute the sample result X.
         *
         * <blockquote>
         * If a == a0 deliver X = W/(b + W); otherwise deliver X = b/(b + W).
         * </blockquote>
         *
         * <p>The finalisation step is shared between the BB and BC algorithm (as step 5 of the
         * BB algorithm and step 6 of the BC algorithm).</p>
         *
         * @param w Algorithm value W.
         * @return the sample value
         */
        protected double computeX(double w) {
            // Avoid (infinity / infinity) producing NaN
            final double tmp = Math.min(w, Double.MAX_VALUE);
            return aIsAlphaShape ? tmp / (b + tmp) : b / (b + tmp);
        }
    }

    /**
     * Computes one sample using Cheng's BB algorithm, when beta distribution {@code alpha} and
     * {@code beta} shape parameters are both larger than 1.
     */
    private static final class ChengBBBetaSampler extends BaseChengBetaSampler {
        /** 1 + natural logarithm of 5. */
        private static final double LN_5_P1 = 1 + Math.log(5.0);

        /** The algorithm beta factor. This is not the beta distribution beta shape parameter. */
        private final double beta;
        /** The algorithm gamma factor. */
        private final double gamma;

        /**
         * @param rng Generator of uniformly distributed random numbers.
         * @param aIsAlphaShape true if {@code a} is the beta distribution alpha shape parameter.
         * @param a min(alpha, beta) shape parameter.
         * @param b max(alpha, beta) shape parameter.
         */
        ChengBBBetaSampler(UniformRandomProvider rng, boolean aIsAlphaShape, double a, double b) {
            super(rng, aIsAlphaShape, a, b);
            beta = Math.sqrt((alpha - 2) / (2 * a * b - alpha));
            gamma = a + 1 / beta;
        }

        /**
         * @param rng Generator of uniformly distributed random numbers.
         * @param source Source to copy.
         */
        private ChengBBBetaSampler(UniformRandomProvider rng,
                                   ChengBBBetaSampler source) {
            super(rng, source);
            // Compute algorithm factors.
            beta = source.beta;
            gamma = source.gamma;
        }

        @Override
        public double sample() {
            double r;
            double w;
            double t;
            do {
                // Step 1:
                final double u1 = rng.nextDouble();
                final double u2 = rng.nextDouble();
                final double v = beta * (Math.log(u1) - Math.log1p(-u1));
                w = a * Math.exp(v);
                final double z = u1 * u1 * u2;
                r = gamma * v - LN_4;
                final double s = a + r - w;
                // Step 2:
                if (s + LN_5_P1 >= 5 * z) {
                    break;
                }

                // Step 3:
                t = Math.log(z);
                if (s >= t) {
                    break;
                }
                // Step 4:
            } while (r + alpha * (logAlpha - Math.log(b + w)) < t);

            // Step 5:
            return computeX(w);
        }

        @Override
        public SharedStateContinuousSampler withUniformRandomProvider(UniformRandomProvider rng) {
            return new ChengBBBetaSampler(rng, this);
        }
    }

    /**
     * Computes one sample using Cheng's BC algorithm, when at least one of beta distribution
     * {@code alpha} or {@code beta} shape parameters is smaller than 1.
     */
    private static final class ChengBCBetaSampler extends BaseChengBetaSampler {
        /** 1/2. */
        private static final double ONE_HALF = 1d / 2;
        /** 1/4. */
        private static final double ONE_QUARTER = 1d / 4;

        /** The algorithm beta factor. This is not the beta distribution beta shape parameter. */
        private final double beta;
        /** The algorithm delta factor. */
        private final double delta;
        /** The algorithm k1 factor. */
        private final double k1;
        /** The algorithm k2 factor. */
        private final double k2;

        /**
         * @param rng Generator of uniformly distributed random numbers.
         * @param aIsAlphaShape true if {@code a} is the beta distribution alpha shape parameter.
         * @param a max(alpha, beta) shape parameter.
         * @param b min(alpha, beta) shape parameter.
         */
        ChengBCBetaSampler(UniformRandomProvider rng, boolean aIsAlphaShape, double a, double b) {
            super(rng, aIsAlphaShape, a, b);
            // Compute algorithm factors.
            beta = 1 / b;
            delta = 1 + a - b;
            // The following factors are divided by 4:
            // k1 = delta * (1 + 3b) / (18a/b - 14)
            // k2 = 1 + (2 + 1/delta) * b
            k1 = delta * (1.0 / 72.0 + 3.0 / 72.0 * b) / (a * beta - 7.0 / 9.0);
            k2 = 0.25 + (0.5 + 0.25 / delta) * b;
        }

        /**
         * @param rng Generator of uniformly distributed random numbers.
         * @param source Source to copy.
         */
        private ChengBCBetaSampler(UniformRandomProvider rng,
                                   ChengBCBetaSampler source) {
            super(rng, source);
            beta = source.beta;
            delta = source.delta;
            k1 = source.k1;
            k2 = source.k2;
        }

        @Override
        public double sample() {
            double w;
            while (true) {
                // Step 1:
                final double u1 = rng.nextDouble();
                final double u2 = rng.nextDouble();
                // Compute Y and Z
                final double y = u1 * u2;
                final double z = u1 * y;
                if (u1 < ONE_HALF) {
                    // Step 2:
                    if (ONE_QUARTER * u2 + z - y >= k1) {
                        continue;
                    }
                } else {
                    // Step 3:
                    if (z <= ONE_QUARTER) {
                        final double v = beta * (Math.log(u1) - Math.log1p(-u1));
                        w = a * Math.exp(v);
                        break;
                    }

                    // Step 4:
                    if (z >= k2) {
                        continue;
                    }
                }

                // Step 5:
                final double v = beta * (Math.log(u1) - Math.log1p(-u1));
                w = a * Math.exp(v);
                if (alpha * (logAlpha - Math.log(b + w) + v) - LN_4 >= Math.log(z)) {
                    break;
                }
            }

            // Step 6:
            return computeX(w);
        }

        @Override
        public SharedStateContinuousSampler withUniformRandomProvider(UniformRandomProvider rng) {
            return new ChengBCBetaSampler(rng, this);
        }
    }

    /**
     * Creates a sampler instance.
     *
     * @param rng Generator of uniformly distributed random numbers.
     * @param alpha Distribution first shape parameter.
     * @param beta Distribution second shape parameter.
     * @throws IllegalArgumentException if {@code alpha <= 0} or {@code beta <= 0}
     */
    public ChengBetaSampler(UniformRandomProvider rng,
                            double alpha,
                            double beta) {
        this(of(rng, alpha, beta));
    }

    /**
     * @param delegate Beta sampler.
     */
    private ChengBetaSampler(SharedStateContinuousSampler delegate) {
        super(null);
        this.delegate = delegate;
    }

    /** {@inheritDoc} */
    @Override
    public double sample() {
        return delegate.sample();
    }

    /** {@inheritDoc} */
    @Override
    public String toString() {
        return delegate.toString();
    }

    /**
     * {@inheritDoc}
     *
     * @since 1.3
     */
    @Override
    public SharedStateContinuousSampler withUniformRandomProvider(UniformRandomProvider rng) {
        return delegate.withUniformRandomProvider(rng);
    }

    /**
     * Creates a new beta distribution sampler.
     *
     * @param rng Generator of uniformly distributed random numbers.
     * @param alpha Distribution first shape parameter.
     * @param beta Distribution second shape parameter.
     * @return the sampler
     * @throws IllegalArgumentException if {@code alpha <= 0} or {@code beta <= 0}
     * @since 1.3
     */
    public static SharedStateContinuousSampler of(UniformRandomProvider rng,
                                                  double alpha,
                                                  double beta) {
        InternalUtils.requireStrictlyPositive(alpha, "alpha");
        InternalUtils.requireStrictlyPositive(beta, "beta");

        // Choose the algorithm.
        final double a = Math.min(alpha, beta);
        final double b = Math.max(alpha, beta);
        final boolean aIsAlphaShape = a == alpha;

        return a > 1 ?
            // BB algorithm
            new ChengBBBetaSampler(rng, aIsAlphaShape, a, b) :
            // The BC algorithm is deliberately invoked with reversed parameters
            // as the argument order is: max(alpha,beta), min(alpha,beta).
            // Also invert the 'a is alpha' flag.
            new ChengBCBetaSampler(rng, !aIsAlphaShape, b, a);
    }
}
