<?php
require_once dirname(dirname(__DIR__)) . '/das/methods/vendor/autoload.php';

use MathPHP\Statistics\Average;
use MathPHP\Statistics\Descriptive;
use MathPHP\Statistics\RandomVariable;
use MathPHP\Probability\Distribution\Continuous;

class Describe
{
    public static $decimals = 4;

    public static function isNumber($data)
    {
        foreach($data as $value)
        {
            if(!is_numeric($value)) return false;
        }

        return true;
    }

    public static function setDecimal($decimal)
    {
        self::$decimals = $decimal;
    }

    /**
     * Get data that is not null
     *
     * @access public
     * @return array
     */
    public static function notNull($array)
    {
        $arr = array_filter($array, function($v) {return (empty($v) && $v == '0') || !empty($v);});
        return array_values($arr);
    }

    /*
     * Return the length of the array.
     *
     *
     * @param array  $result
     * @access public
     * @return void
     */
    public static function getDataLength($result)
    {
        $colData = array();
        foreach($result as $key => $value)
        {
            if($value !== '' and $value !== null)
            {
                $colData[$key] = $value;
            }
        }

        end($colData);
        return max(0, key($colData) + 1);
    }
    /**
     * Check data, return true if not null.
     *
     * @access public
     * @return bool
     */
    public static function checkNotNull($value)
    {
        return (empty($value) && $value == '0') || !empty($value);
    }

    /**
     * Sum method.
     *
     * @param  array $cols
     * @access public
     * @return array
     */
    public static function sum($data, $round = true)
    {
        if(!self::isNumber($data)) return '-';
        $sum = 0;
        foreach($data as $value)
        {
            $sum += $value;
        }
        return $round ? round($sum, self::$decimals) : $sum;
    }

    /**
     * Mean method.
     *
     * @param  array $cols
     * @access public
     * @return array
     */
    public static function mean($data, $round = true)
    {
        if(!self::isNumber($data)) return '-';

        $mean = Average::mean($data);

        return $round ? round($mean, self::$decimals) : $mean;
    }

    /**
     * Trim mean method.
     *
     * @param  array $cols
     * @access public
     * @return array
     */
    public static function trimMean($data, $percent, $round = true)
    {
        asort($data);
        $n = count($data);
        $index = round($n * $percent);
        $index = max(1, $index);
        array_splice($data, 0, $index);
        array_splice($data, -1 * $index);

        return self::mean($data, $round);
    }

    /**
     * Median method.
     *
     * @param  array $cols
     * @access public
     * @return array
     */
    public static function median($data, $round = true)
    {
        if(!self::isNumber($data)) return '-';

        $median = Average::median($data);

        return $round ? round($median, self::$decimals) : $median;
    }

    /**
     * Row mean method.
     * @param array $cols
     * @param string $keyName
     * @param bool $round
     * @access public
     * @return array
     */
    public static function rowMean($datas, $round = true)
    {
        // TODO
    }

    /**
     * Row standard deviation method.
     * @param array $cols
     * @param string $keyName
     * @param bool $round
     * @access public
     * @return array
     */
    public static function rowSD($datas, $round = true)
    {
        // TODO
    }

    /**
     * Max method.
     *
     * @param  array $cols
     * @access public
     * @return array
     */
    public static function max($data, $round = true)
    {
        if(!self::isNumber($data)) return '-';

        $max = max($data);
        return $round ? round($max, self::$decimals) : $max;
    }

    /**
     * Min method.
     *
     * @param  array $cols
     * @access public
     * @return array
     */
    public static function min($data, $round = true)
    {
        if(!self::isNumber($data)) return '-';

        $min = min($data);
        return $round ? round($min, self::$decimals) : $min;
    }

    /**
     * Middle method.
     *
     * @param array $cols
     * @param int   $pos -1:lower quartile 0:middle 1:upper quartile
     * @access public
     * @return array
     */
    public static function mid($data, $pos = 0, $round = true)
    {
        // TODO
    }

    /**
     * Sample standard method.
     *
     * @param array $cols
     * @access public
     * @return array
     */
    public static function standard($data, $round = true)
    {
        $n = count($data);
        if(!self::isNumber($data) or $n < 2) return '-';

        $σ² = self::variance($data, false);
        $σ  = sqrt($σ²);

        return $round ? round($σ, self::$decimals) : $σ;
    }

    /**
     * Sample variance method.
     *
     * @param array $cols
     * @access public
     * @return array
     */
    public static function variance($data, $round = true)
    {
        $n = count($data);
        if(!self::isNumber($data) or $n < 2) return '-';

        $sum = array_sum($data);
        $mean = $sum / count($data);

        $varSum = 0;
        foreach($data as $value)
        {
            $varSum += ($value - $mean) ** 2;
        }

        $σ² = $varSum / ($n - 1);

        return $round ? round($σ², self::$decimals) : $σ²;
    }

    /**
     * Percentage number function.
     *
     * @param array $data
     * @param float $percent
     * @param bool $round
     * @access public
     * @return float
     */
    public static function percentData($data, $percent, $round = true)
    {
        $result = null;
        $n = count($data);
        if($n === 0) return $result;

        sort($data);

        $index = $percent * ($n + 1) - 1;
        if($index < 0) $index = 0;
        if($index > $n - 1) $index = $n - 1;

        if(is_int($index))
        {
            $result = $data[$index];
        }
        else
        {
            $pre = (int)$index;
            $decimal = $index - $pre;
            $result = $decimal == 0 ? $data[$pre] : $data[$pre] + ($data[$pre + 1] - $data[$pre]) * $decimal;
        }

        return $round ? round($result, self::$decimals) : $result;
    }

    /**
     * Q1 Q2 Q3 calculate function.
     * @param array $data
     * @param int $pos
     * @param bool $round = true
     * @access public
     * @return array
     */
    public static function midData($data, $pos, $round = true)
    {
        $percent = array('-1' => 1 / 4, '0' => 1 / 2, '1' => 3 / 4);

        return self::percentData($data, $percent[$pos], $round);
    }

    public static function box($data, $name, $round = true)
    {
        $result = new stdclass();

        $result->q1    = self::percentData($data, 0.25, false);
        $result->q2    = self::percentData($data, 0.5, false);
        $result->q3    = self::percentData($data, 0.75, false);
        $result->p10   = self::percentData($data, 0.1, false);
        $result->p90   = self::percentData($data, 0.9, false);
        $result->min   = min($data);
        $result->max   = max($data);
        $result->iqr   = $result->q3 - $result->q1;
        $result->mean  = self::mean($data);
        $result->data  = $data;
        $result->count = count($data);
        $result->name  = $name;
        $result->LL    = $result->q1 - 1.5 * $result->iqr;
        $result->UL    = $result->q3 + 1.5 * $result->iqr;;

        $lower = $result->LL;
        $upper = $result->UL;

        $lowerfence = $result->max;
        $upperfence = $result->min;
        $outliers   = array('x' => array(), 'y' => array());
        foreach($data as $value)
        {
            if($value === NULL) continue;
            if($value < $lowerfence and $value >= $lower)
            {
                $lowerfence = $value;
            }
            if($value > $upperfence and $value <= $upper)
            {
                $upperfence = $value;
            }
        }
        foreach($data as $index => $value)
        {
            if($value === NULL) continue;
            if($value > $upperfence or $value < $lowerfence)
            {
                $outliers['x'][$index] = $name;
                $outliers['y'][$index] = $value;
            }
        }

        $result->lowerfence = $lowerfence;
        $result->upperfence = $upperfence;
        $result->outliers   = $outliers;
        $result->whisker    = "$lowerfence, $upperfence";

        return $result;
    }

    /**
     * Skewness method.
     *
     * @param array $data
     * @param bool $round
     * @access public
     * @return array
     */
    public static function skewness($data, $round = true)
    {
        if(count($data) < 3) return '-';

        $result = RandomVariable::skewness($data);
        return $round ? round($result, self::$decimals) : $result;
    }

    /**
     * Kurtosis method.
     *
     * @param array $data
     * @param bool $round
     * @access public
     * @return array
     */
    public static function kurtosis($data, $round = true)
    {
        if(count($data) < 4) return '-';

        $result = RandomVariable::kurtosis($data);
        return $round ? round($result, self::$decimals) : $result;
    }

    /**
     * log  method.
     *
     * @param  array    $data
     * @param  bool     $round
     * @static
     * @access public
     * @return void
     */
    public static function log($data, $round = true)
    {
        $result = array();
        foreach($data as $index => $value)
        {
            if(!is_numeric($value) || $value == 0)
            {
                $result[$index] = '-';
            }
            else
            {
                $logValue = log($value);
                $result[$index] = $round ? round($logValue, self::$decimals) : $logValue;
            }
        }

        return $result;
    }

    /**
     * reciprocal
     *
     * @param  array    $data
     * @param  bool     $round
     * @static
     * @access public
     * @return void
     */
    public static function reciprocal($data, $round = true)
    {
        $result = array();
        foreach($data as $index => $value)
        {
            if(!is_numeric($value) || $value == 0)
            {
                $result[$index] = '-';
            }
            else
            {
                $reciprocal = 1 /$value;
                $result[$index] = $round ? round($reciprocal, self::$decimals) : $reciprocal;
            }
        }

        return $result;
    }

    /**
     * square
     *
     * @param  array    $data
     * @param  bool     $round
     * @static
     * @access public
     * @return void
     */
    public static function square($data, $round = true)
    {
        $result = array();
        foreach($data as $index => $value)
        {
            if(!is_numeric($value))
            {
                $result[$index] = '-';
            }
            else
            {
                $square = pow($value, 2);
                $result[$index] = $round ? round($square, self::$decimals) : $square;
            }
        }

        return $result;
    }

    /**
     * A square method.
     * https://support.minitab.com/zh-cn/minitab/21/help-and-how-to/statistics/basic-statistics/how-to/normality-test/methods-and-formulas/methods-and-formulas/#anderson-darling-statistic-a2
     *
     * @param array $data
     * @param bool $round
     * @access public
     * @return array
     */
    public static function Asquare($data, $round = true)
    {
        $n = count($data);
        if($n < 2) return null;

        $mean           = self::mean($data, false);
        $standard       = self::standard($data, false);
        $standardNormal = new Continuous\StandardNormal();

        sort($data);

        $sum = 0;
        foreach($data as $index => $value)
        {
            if(!is_numeric($value)) continue;
            $index += 1;
            $part1 = 2 * $index  - 1;
            /* The cumulative distribution function of the standard normal distribution */
            $cdf1  = $standardNormal->cdf(($value - $mean) / $standard);
            $cdf2  = $standardNormal->cdf(($data[$n - $index] - $mean) / $standard);
            $part2 = log($cdf1);
            $part3 = log(1 - $cdf2);

            $sum += $part1 * ($part2 + $part3);
        }

        $result = -1 * $n - 1 / $n * $sum;
        return $round ? round($result, self::$decimals) : $result;
    }

    /**
     * P value method.
     * https://support.minitab.com/zh-cn/minitab/21/help-and-how-to/statistics/basic-statistics/how-to/normality-test/methods-and-formulas/methods-and-formulas/#p-value
     *
     * @param array $data
     * @param bool $round
     * @access public
     * @return array
     */
    public static function Pvalue($data, $round = true)
    {
        $Asquare = self::Asquare($data, false);
        $n = count($data);
        $A = $Asquare * (1.0 + (0.75 / $n) + (2.25 / ($n * $n)));
        $p = 0;
        if($A > 0.6 && $A < 13)
        {
            $p = pow(M_E, 1.2937 - 5.709 * $A + 0.0186 * $A * $A);
        }
        if($A > 0.34 && $A < 0.6)
        {
            $p = pow(M_E, 0.9177 - 4.279 * $A - 1.38 * $A * $A);
        }
        if($A > 0.2 && $A < 0.34)
        {
            $p = 1.0 - pow(M_E, -8.318 + 42.796 * $A - 59.938 * $A * $A);
        }
        if($A < 0.2)
        {
            $p = 1.0 - pow(M_E, -13.436 + 101.14 * $A - 223.73 * $A * $A);
        }

        $result = $p;

        return $round ? round($result, self::$decimals) : $result;
    }

    /**
     * Assign a fractional average ranking to data - ("1 2.5 2.5 4" ranking)
     * https://en.wikipedia.org/wiki/Ranking
     *
     * Similar to R: rank(values, ties.method='average')
     *
     * @param array $data
     * @access public
     * @return array
     */
    public static function fractionalRanking($data)
    {
        $Xs = $data;
        \sort($Xs);

        // Determine ranks - some items might show up multiple times, so record each successive rank.
        $ordinalRanking = [];
        foreach ($Xs as $rank => $x) {
            $ordinalRanking[\strval($x)][] = $rank + 1;
        }

        // Determine average rank of each value. Necessary when values show up multiple times.
        // Rank will not change if value only shows up once.
        $rg = \array_map(
            function (array $x) {
                return \array_sum($x) / \count($x);
            },
            $ordinalRanking
        );

        // Map ranks to values in order they were originally input
        return \array_map(
            function ($value) use ($rg) {
                return $rg[\strval($value)];
            },
            $data
        );
    }
}
