/*
 * 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
 *
 *      https://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.statistics.distribution;

import org.apache.commons.numbers.gamma.RegularizedBeta;

/**
 * Implementation of the binomial distribution.
 *
 * <p>The probability mass function of \( X \) is:
 *
 * <p>\[ f(k; n, p) = \binom{n}{k} p^k (1-p)^{n-k} \]
 *
 * <p>for \( n \in \{0, 1, 2, \dots\} \) the number of trials,
 * \( p \in [0, 1] \) the probability of success,
 * \( k \in \{0, 1, \dots, n\} \) the number of successes, and
 *
 * <p>\[ \binom{n}{k} = \frac{n!}{k! \, (n-k)!} \]
 *
 * <p>is the binomial coefficient.
 *
 * @see <a href="https://en.wikipedia.org/wiki/Binomial_distribution">Binomial distribution (Wikipedia)</a>
 * @see <a href="https://mathworld.wolfram.com/BinomialDistribution.html">Binomial distribution (MathWorld)</a>
 */
public final class BinomialDistribution extends AbstractDiscreteDistribution {
    /** 1/2. */
    private static final float HALF = 0.5f;

    /** The number of trials. */
    private final int numberOfTrials;
    /** The probability of success. */
    private final double probabilityOfSuccess;
    /** Cached value for pmf(x=0). */
    private final double pmf0;
    /** Cached value for pmf(x=n). */
    private final double pmfn;

    /**
     * @param trials Number of trials.
     * @param p Probability of success.
     */
    private BinomialDistribution(int trials,
                                 double p) {
        probabilityOfSuccess = p;
        numberOfTrials = trials;
        // Special pmf cases where the power function is more accurate:
        //   (n choose k) == 1 for k=0, k=n
        //   pmf = p^k (1-p)^(n-k)
        // Note: This handles the edge case of n=0: pmf(k=0) = 1, else 0
        if (probabilityOfSuccess >= HALF) {
            pmf0 = Math.pow(1 - probabilityOfSuccess, numberOfTrials);
        } else {
            pmf0 = Math.exp(numberOfTrials * Math.log1p(-probabilityOfSuccess));
        }
        pmfn = Math.pow(probabilityOfSuccess, numberOfTrials);
    }

    /**
     * Creates a binomial distribution.
     *
     * @param trials Number of trials.
     * @param p Probability of success.
     * @return the distribution
     * @throws IllegalArgumentException if {@code trials < 0}, or if {@code p < 0}
     * or {@code p > 1}.
     */
    public static BinomialDistribution of(int trials,
                                          double p) {
        if (trials < 0) {
            throw new DistributionException(DistributionException.NEGATIVE,
                                            trials);
        }
        ArgumentUtils.checkProbability(p);
        // Avoid p = -0.0 to avoid returning -0.0 for some probability computations.
        return new BinomialDistribution(trials, Math.abs(p));
    }

    /**
     * Gets the number of trials parameter of this distribution.
     *
     * @return the number of trials.
     */
    public int getNumberOfTrials() {
        return numberOfTrials;
    }

    /**
     * Gets the probability of success parameter of this distribution.
     *
     * @return the probability of success.
     */
    public double getProbabilityOfSuccess() {
        return probabilityOfSuccess;
    }

    /** {@inheritDoc} */
    @Override
    public double probability(int x) {
        if (x < 0 || x > numberOfTrials) {
            return 0;
        } else if (x == 0) {
            return pmf0;
        } else if (x == numberOfTrials) {
            return pmfn;
        }
        return Math.exp(SaddlePointExpansionUtils.logBinomialProbability(x,
                        numberOfTrials, probabilityOfSuccess,
                        1.0 - probabilityOfSuccess));
    }

    /** {@inheritDoc} **/
    @Override
    public double logProbability(int x) {
        if (numberOfTrials == 0) {
            return (x == 0) ? 0.0 : Double.NEGATIVE_INFINITY;
        } else if (x < 0 || x > numberOfTrials) {
            return Double.NEGATIVE_INFINITY;
        }
        // Special cases for x=0, x=n
        // are handled in the saddle point expansion
        return SaddlePointExpansionUtils.logBinomialProbability(x,
                numberOfTrials, probabilityOfSuccess,
                1.0 - probabilityOfSuccess);
    }

    /** {@inheritDoc} */
    @Override
    public double cumulativeProbability(int x) {
        if (x < 0) {
            return 0.0;
        } else if (x >= numberOfTrials) {
            return 1.0;
        } else if (x == 0) {
            return pmf0;
        }
        return RegularizedBeta.complement(probabilityOfSuccess,
                                          x + 1.0, (double) numberOfTrials - x);
    }

    /** {@inheritDoc} */
    @Override
    public double survivalProbability(int x) {
        if (x < 0) {
            return 1.0;
        } else if (x >= numberOfTrials) {
            return 0.0;
        } else if (x == numberOfTrials - 1) {
            return pmfn;
        }
        return RegularizedBeta.value(probabilityOfSuccess,
                                     x + 1.0, (double) numberOfTrials - x);
    }

    /**
     * {@inheritDoc}
     *
     * <p>For number of trials \( n \) and probability of success \( p \), the mean is \( np \).
     */
    @Override
    public double getMean() {
        return numberOfTrials * probabilityOfSuccess;
    }

    /**
     * {@inheritDoc}
     *
     * <p>For number of trials \( n \) and probability of success \( p \), the variance is \( np (1 - p) \).
     */
    @Override
    public double getVariance() {
        final double p = probabilityOfSuccess;
        return numberOfTrials * p * (1 - p);
    }

    /**
     * {@inheritDoc}
     *
     * <p>The lower bound of the support is always 0 except for the probability
     * parameter {@code p = 1}.
     *
     * @return 0 or the number of trials.
     */
    @Override
    public int getSupportLowerBound() {
        return probabilityOfSuccess < 1.0 ? 0 : numberOfTrials;
    }

    /**
     * {@inheritDoc}
     *
     * <p>The upper bound of the support is the number of trials except for the
     * probability parameter {@code p = 0}.
     *
     * @return number of trials or 0.
     */
    @Override
    public int getSupportUpperBound() {
        return probabilityOfSuccess > 0.0 ? numberOfTrials : 0;
    }

    /** {@inheritDoc} */
    @Override
    int getMedian() {
        // Overridden for the probability(int, int) method.
        // This is intentionally not a public method.
        // Can be floor or ceiling of np. For the probability in a range use the floor
        // as this only used for values >= median+1.
        return (int) (numberOfTrials * probabilityOfSuccess);
    }
}
