<?php

namespace TablePress\PhpOffice\PhpSpreadsheet\Calculation\Statistical;

use TablePress\PhpOffice\PhpSpreadsheet\Calculation\Exception;
use TablePress\PhpOffice\PhpSpreadsheet\Calculation\Functions;
use TablePress\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(...$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($valueSet, $value, $significance = 3)
	{
		$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(...$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($value, $valueSet, $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)
		);
	}
}
