<?php

namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical\Distributions;

use PhpOffice\PhpSpreadsheet\Calculation\ArrayEnabled;
use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;
use PhpOffice\PhpSpreadsheet\Calculation\MathTrig\Combinations;

class Binomial
{
    use ArrayEnabled;

    /**
     * BINOMDIST.
     *
     * Returns the individual term binomial distribution probability. Use BINOMDIST in problems with
     *        a fixed number of tests or trials, when the outcomes of any trial are only success or failure,
     *        when trials are independent, and when the probability of success is constant throughout the
     *        experiment. For example, BINOMDIST can calculate the probability that two of the next three
     *        babies born are male.
     *
     * @param mixed $value Integer number of successes in trials
     *                      Or can be an array of values
     * @param mixed $trials Integer umber of trials
     *                      Or can be an array of values
     * @param mixed $probability Probability of success on each trial as a float
     *                      Or can be an array of values
     * @param mixed $cumulative Boolean value indicating if we want the cdf (true) or the pdf (false)
     *                      Or can be an array of values
     *
     * @return array|float|string If an array of numbers is passed as an argument, then the returned result will also be an array
     *            with the same dimensions
     */
    public static function distribution(mixed $value, mixed $trials, mixed $probability, mixed $cumulative)
    {
        if (is_array($value) || is_array($trials) || is_array($probability) || is_array($cumulative)) {
            return self::evaluateArrayArguments([self::class, __FUNCTION__], $value, $trials, $probability, $cumulative);
        }

        try {
            $value = DistributionValidations::validateInt($value);
            $trials = DistributionValidations::validateInt($trials);
            $probability = DistributionValidations::validateProbability($probability);
            $cumulative = DistributionValidations::validateBool($cumulative);
        } catch (Exception $e) {
            return $e->getMessage();
        }

        if (($value < 0) || ($value > $trials)) {
            return ExcelError::NAN();
        }

        if ($cumulative) {
            return self::calculateCumulativeBinomial($value, $trials, $probability);
        }
        /** @var float $comb */
        $comb = Combinations::withoutRepetition($trials, $value);

        return $comb * $probability ** $value
            * (1 - $probability) ** ($trials - $value);
    }

    /**
     * BINOM.DIST.RANGE.
     *
     * Returns returns the Binomial Distribution probability for the number of successes from a specified number
     *     of trials falling into a specified range.
     *
     * @param mixed $trials Integer number of trials
     *                      Or can be an array of values
     * @param mixed $probability Probability of success on each trial as a float
     *                      Or can be an array of values
     * @param mixed $successes The integer number of successes in trials
     *                      Or can be an array of values
     * @param mixed $limit Upper limit for successes in trials as null, or an integer
     *                           If null, then this will indicate the same as the number of Successes
     *                      Or can be an array of values
     *
     * @return array|float|int|string If an array of numbers is passed as an argument, then the returned result will also be an array
     *            with the same dimensions
     */
    public static function range(mixed $trials, mixed $probability, mixed $successes, mixed $limit = null): array|string|float|int
    {
        if (is_array($trials) || is_array($probability) || is_array($successes) || is_array($limit)) {
            return self::evaluateArrayArguments([self::class, __FUNCTION__], $trials, $probability, $successes, $limit);
        }

        $limit = $limit ?? $successes;

        try {
            $trials = DistributionValidations::validateInt($trials);
            $probability = DistributionValidations::validateProbability($probability);
            $successes = DistributionValidations::validateInt($successes);
            $limit = DistributionValidations::validateInt($limit);
        } catch (Exception $e) {
            return $e->getMessage();
        }

        if (($successes < 0) || ($successes > $trials)) {
            return ExcelError::NAN();
        }
        if (($limit < 0) || ($limit > $trials) || $limit < $successes) {
            return ExcelError::NAN();
        }

        $summer = 0;
        for ($i = $successes; $i <= $limit; ++$i) {
            /** @var float $comb */
            $comb = Combinations::withoutRepetition($trials, $i);
            $summer += $comb * $probability ** $i
                * (1 - $probability) ** ($trials - $i);
        }

        return $summer;
    }

    /**
     * NEGBINOMDIST.
     *
     * Returns the negative binomial distribution. NEGBINOMDIST returns the probability that
     *        there will be number_f failures before the number_s-th success, when the constant
     *        probability of a success is probability_s. This function is similar to the binomial
     *        distribution, except that the number of successes is fixed, and the number of trials is
     *        variable. Like the binomial, trials are assumed to be independent.
     *
     * @param mixed $failures Number of Failures as an integer
     *                      Or can be an array of values
     * @param mixed $successes Threshold number of Successes as an integer
     *                      Or can be an array of values
     * @param mixed $probability Probability of success on each trial as a float
     *                      Or can be an array of values
     *
     * @return array|float|string The result, or a string containing an error
     *         If an array of numbers is passed as an argument, then the returned result will also be an array
     *            with the same dimensions
     *
     * TODO Add support for the cumulative flag not present for NEGBINOMDIST, but introduced for NEGBINOM.DIST
     *      The cumulative default should be false to reflect the behaviour of NEGBINOMDIST
     */
    public static function negative(mixed $failures, mixed $successes, mixed $probability): array|string|float
    {
        if (is_array($failures) || is_array($successes) || is_array($probability)) {
            return self::evaluateArrayArguments([self::class, __FUNCTION__], $failures, $successes, $probability);
        }

        try {
            $failures = DistributionValidations::validateInt($failures);
            $successes = DistributionValidations::validateInt($successes);
            $probability = DistributionValidations::validateProbability($probability);
        } catch (Exception $e) {
            return $e->getMessage();
        }

        if (($failures < 0) || ($successes < 1)) {
            return ExcelError::NAN();
        }
        if (Functions::getCompatibilityMode() == Functions::COMPATIBILITY_GNUMERIC) {
            if (($failures + $successes - 1) <= 0) {
                return ExcelError::NAN();
            }
        }
        /** @var float $comb */
        $comb = Combinations::withoutRepetition($failures + $successes - 1, $successes - 1);

        return $comb
            * ($probability ** $successes) * ((1 - $probability) ** $failures);
    }

    /**
     * BINOM.INV.
     *
     * Returns the smallest value for which the cumulative binomial distribution is greater
     *        than or equal to a criterion value
     *
     * @param mixed $trials number of Bernoulli trials as an integer
     *                      Or can be an array of values
     * @param mixed $probability probability of a success on each trial as a float
     *                      Or can be an array of values
     * @param mixed $alpha criterion value as a float
     *                      Or can be an array of values
     *
     * @return array|int|string If an array of numbers is passed as an argument, then the returned result will also be an array
     *            with the same dimensions
     */
    public static function inverse(mixed $trials, mixed $probability, mixed $alpha): array|string|int
    {
        if (is_array($trials) || is_array($probability) || is_array($alpha)) {
            return self::evaluateArrayArguments([self::class, __FUNCTION__], $trials, $probability, $alpha);
        }

        try {
            $trials = DistributionValidations::validateInt($trials);
            $probability = DistributionValidations::validateProbability($probability);
            $alpha = DistributionValidations::validateFloat($alpha);
        } catch (Exception $e) {
            return $e->getMessage();
        }

        if ($trials < 0) {
            return ExcelError::NAN();
        } elseif (($alpha < 0.0) || ($alpha > 1.0)) {
            return ExcelError::NAN();
        }

        $successes = 0;
        while ($successes <= $trials) {
            $result = self::calculateCumulativeBinomial($successes, $trials, $probability);
            if ($result >= $alpha) {
                break;
            }
            ++$successes;
        }

        return $successes;
    }

    private static function calculateCumulativeBinomial(int $value, int $trials, float $probability): float|int
    {
        $summer = 0;
        for ($i = 0; $i <= $value; ++$i) {
            /** @var float $comb */
            $comb = Combinations::withoutRepetition($trials, $i);
            $summer += $comb * $probability ** $i
                * (1 - $probability) ** ($trials - $i);
        }

        return $summer;
    }
}
