<?php

namespace PhpOffice\PhpSpreadsheet\Calculation\Statistical;

use PhpOffice\PhpSpreadsheet\Calculation\Exception;
use PhpOffice\PhpSpreadsheet\Calculation\Functions;
use PhpOffice\PhpSpreadsheet\Calculation\Information\ExcelError;

class Percentiles
{
    public const RANK_SORT_DESCENDING = 0;

    public const RANK_SORT_ASCENDING = 1;

    /**
     * PERCENTILE.
     *
     * Returns the nth percentile of values in a range..
     *
     * Excel Function:
     *        PERCENTILE(value1[,value2[, ...]],entry)
     *
     * @param mixed $args Data values
     *
     * @return float|string The result, or a string containing an error
     */
    public static function PERCENTILE(mixed ...$args)
    {
        $aArgs = Functions::flattenArray($args);

        // Calculate
        $entry = array_pop($aArgs);

        try {
            $entry = StatisticalValidations::validateFloat($entry);
        } catch (Exception $e) {
            return $e->getMessage();
        }

        if (($entry < 0) || ($entry > 1)) {
            return ExcelError::NAN();
        }

        $mArgs = self::percentileFilterValues($aArgs);
        $mValueCount = count($mArgs);
        if ($mValueCount > 0) {
            sort($mArgs);
            /** @var float[] $mArgs */
            $count = Counts::COUNT($mArgs);
            $index = $entry * ($count - 1);
            $indexFloor = floor($index);
            $iBase = (int) $indexFloor;
            if ($index == $indexFloor) {
                return $mArgs[$iBase];
            }
            $iNext = $iBase + 1;
            $iProportion = $index - $iBase;

            return $mArgs[$iBase] + (($mArgs[$iNext] - $mArgs[$iBase]) * $iProportion);
        }

        return ExcelError::NAN();
    }

    /**
     * PERCENTRANK.
     *
     * Returns the rank of a value in a data set as a percentage of the data set.
     * Note that the returned rank is simply rounded to the appropriate significant digits,
     *      rather than floored (as MS Excel), so value 3 for a value set of  1, 2, 3, 4 will return
     *      0.667 rather than 0.666
     *
     * @param mixed $valueSet An array of (float) values, or a reference to, a list of numbers
     * @param mixed $value The number whose rank you want to find
     * @param mixed $significance The (integer) number of significant digits for the returned percentage value
     *
     * @return float|string (string if result is an error)
     */
    public static function PERCENTRANK(mixed $valueSet, mixed $value, mixed $significance = 3): string|float
    {
        $valueSet = Functions::flattenArray($valueSet);
        $value = Functions::flattenSingleValue($value);
        $significance = ($significance === null) ? 3 : Functions::flattenSingleValue($significance);

        try {
            $value = StatisticalValidations::validateFloat($value);
            $significance = StatisticalValidations::validateInt($significance);
        } catch (Exception $e) {
            return $e->getMessage();
        }

        /** @var array<float|int|numeric-string> */
        $valueSet = self::rankFilterValues($valueSet);
        $valueCount = count($valueSet);
        if ($valueCount == 0) {
            return ExcelError::NA();
        }
        sort($valueSet, SORT_NUMERIC);

        $valueAdjustor = $valueCount - 1;
        if (($value < $valueSet[0]) || ($value > $valueSet[$valueAdjustor])) {
            return ExcelError::NA();
        }

        $pos = array_search($value, $valueSet);
        if ($pos === false) {
            /** @var float[] $valueSet */
            $pos = 0;
            $testValue = $valueSet[0];
            while ($testValue < $value) {
                $testValue = $valueSet[++$pos];
            }
            --$pos;
            $pos += (($value - $valueSet[$pos]) / ($testValue - $valueSet[$pos]));
        }

        return round(((float) $pos) / $valueAdjustor, $significance);
    }

    /**
     * QUARTILE.
     *
     * Returns the quartile of a data set.
     *
     * Excel Function:
     *        QUARTILE(value1[,value2[, ...]],entry)
     *
     * @param mixed $args Data values
     *
     * @return float|string The result, or a string containing an error
     */
    public static function QUARTILE(mixed ...$args)
    {
        $aArgs = Functions::flattenArray($args);
        $entry = array_pop($aArgs);

        try {
            $entry = StatisticalValidations::validateFloat($entry);
        } catch (Exception $e) {
            return $e->getMessage();
        }

        $entry = floor($entry);
        $entry /= 4;
        if (($entry < 0) || ($entry > 1)) {
            return ExcelError::NAN();
        }

        return self::PERCENTILE($aArgs, $entry);
    }

    /**
     * RANK.
     *
     * Returns the rank of a number in a list of numbers.
     *
     * @param mixed $value The number whose rank you want to find
     * @param mixed $valueSet An array of float values, or a reference to, a list of numbers
     * @param mixed $order Order to sort the values in the value set
     *
     * @return float|string The result, or a string containing an error (0 = Descending, 1 = Ascending)
     */
    public static function RANK(mixed $value, mixed $valueSet, mixed $order = self::RANK_SORT_DESCENDING)
    {
        $value = Functions::flattenSingleValue($value);
        $valueSet = Functions::flattenArray($valueSet);
        $order = ($order === null) ? self::RANK_SORT_DESCENDING : Functions::flattenSingleValue($order);

        try {
            $value = StatisticalValidations::validateFloat($value);
            $order = StatisticalValidations::validateInt($order);
        } catch (Exception $e) {
            return $e->getMessage();
        }

        /** @var array<float|int|numeric-string> */
        $valueSet = self::rankFilterValues($valueSet);
        if ($order === self::RANK_SORT_DESCENDING) {
            rsort($valueSet, SORT_NUMERIC);
        } else {
            sort($valueSet, SORT_NUMERIC);
        }

        $pos = array_search($value, $valueSet);
        if ($pos === false) {
            return ExcelError::NA();
        }

        return ++$pos;
    }

    /**
     * @param mixed[] $dataSet
     *
     * @return mixed[]
     */
    protected static function percentileFilterValues(array $dataSet): array
    {
        return array_filter(
            $dataSet,
            fn ($value): bool => is_numeric($value) && !is_string($value)
        );
    }

    /**
     * @param mixed[] $dataSet
     *
     * @return mixed[]
     */
    protected static function rankFilterValues(array $dataSet): array
    {
        return array_filter(
            $dataSet,
            fn ($value): bool => is_numeric($value)
        );
    }
}
