<?php
/**
 * The file of monte carlo plan of ZenDAS.
 *
 * @copyright   Copyright 2009-2023 禅道软件（青岛）有限公司(ZenTao Software (Qingdao) Co., Ltd. www.zentao.net)
 * @license     ZPL(https://zpl.pub/page/zplv12.html) or AGPL(https://www.gnu.org/licenses/agpl-3.0.en.html)
 * @author      qixinzhi <qixinzhi@easycorp.ltd>
 * @package     montecarlo
 * @link        http://www.zentao.net
 */
?>

<?php
require_once dirname(dirname(__DIR__)) . '/zendasmath/basic/describe.php';
require_once dirname(dirname(__DIR__)) . '/zendasmath/basic/combination.php';
require_once __DIR__ . '/mathcheck.php';

class Plan
{
    /**
     * 方案。
     *
     * @var array
     * @access public
     */
    public $plan;
    /**
     * 预测公式。
     *
     * @var string
     * @access public
     */
    public $forecasts;
    /**
     * 预测模拟值。
     *
     * @var array
     * @access public
     */
    public $forecastValue;
    /**
     * 要求预测模拟值。
     *
     * @var array
     * @access public
     */
    public $requestValue;
    /**
     * 是否为固定值。
     *
     * @var bool[]
     * @access public
     */
    public $fixed;
    /**
     * 基本统计量相关参数。
     *
     * @var object
     * @access public
     */
    public $basicStatistic;
    /**
     * 概率密度相关参数。
     *
     * @var object
     * @access public
     */
    public $probabilityDensity;
    /**
     * 累积分布相关参数。
     *
     * @var object
     * @access public
     */
    public $cumulative;

    /**
     * 统计量圆整位数。
     *
     * @var int
     * @access public
     */
    public $decimals;

    public $requestStatistic;

    public $exportKey;

    public function __construct($plan, $forecasts, $hypothesis, $times, $optQuest = true)
    {
        $this->plan          = $plan;
        $this->forecastValue = array();
        $this->requestValue  = array();
        $this->fixed         = array();
        $this->times         = $times;
        $this->decimals      = 4;
        $this->exportKey     = 'export';

        $this->basicStatistic     = new stdclass();
        $this->probabilityDensity = new stdclass();
        $this->cumulative         = new stdclass();
        $this->requestStatistic   = array();

        $formatForecasts = array();

        foreach($forecasts as $key => $forecast)
        {
            if($key == 'export' || $forecast != $forecasts['export'])
            {
                $formatForecasts[$key] = $forecast;
            }

            if($key != 'export' && $forecast == $forecasts['export'])
            {
                $this->exportKey = $key;
            }

            if($key != 'export')
            {
                $this->fixed[$key] = false;
                $this->requestStatistic[$key] = new stdclass();
                $this->requestValue[$key] = array();
            }
        }

        if($optQuest)
        {
            foreach($formatForecasts as $key => $forecast)
            {
                foreach($plan as $decisionVar => $decisionValue)
                {
                    $forecast = str_replace($decisionVar, $decisionValue, $forecast);
                }
                $formatForecasts[$key] = $forecast;
            }
        }
        $this->forecasts = $formatForecasts;
        $this->checkFixed($hypothesis);
        $this->simulateForecast($hypothesis);
    }

    /**
     * 均值。
     *
     * @param  bool    $round
     * @param  bool    $cache
     * @access public
     * @return void
     */
    public function mean($round = true, $cache = true, $key = 'export')
    {
        $values = $this->getValues($key);
        $value  = $this->fixed[$key] ? $values : Describe::mean($values, false);

        return $this->handleStatistic('mean', $value, $round, $cache, $key);
    }

    /**
     * 中位数。
     *
     * @param  bool    $round
     * @param  bool    $cache
     * @access public
     * @return void
     */
    public function median($round = true, $cache = true, $key = 'export')
    {
        $values = $this->getValues($key);
        $value  = $this->fixed[$key] ? $values : Describe::median($values, false);

        return $this->handleStatistic('median', $value, $round, $cache, $key);
    }

    /**
     * 方差。
     *
     * @param  bool    $round
     * @param  bool    $cache
     * @access public
     * @return void
     */
    public function variance($round = true, $cache = true, $key = 'export')
    {
        $values = $this->getValues($key);
        $value  = $this->fixed[$key] ? '-' : Describe::variance($values, false);

        return $this->handleStatistic('variance', $value, $round, $cache, $key);
    }

    /**
     * 标准差。
     *
     * @param  bool    $round
     * @param  bool    $cache
     * @access public
     * @return void
     */
    public function standard($round = true, $cache = true, $key = 'export')
    {
        $values = $this->getValues($key);
        $value  = $this->fixed[$key] ? '-' : Describe::standard($values, false);

        return $this->handleStatistic('standard', $value, $round, $cache, $key);
    }

    /**
     * 最大值。
     *
     * @param  bool    $round
     * @param  bool    $cache
     * @access public
     * @return void
     */
    public function max($round = true, $cache = true, $key = 'export')
    {
        $values = $this->getValues($key);
        $value  = $this->fixed[$key] ? $values : Describe::max($values, false);

        return $this->handleStatistic('max', $value, $round, $cache, $key);
    }

    /**
     * 最小值。
     *
     * @param  bool    $round
     * @param  bool    $cache
     * @access public
     * @return void
     */
    public function min($round = true, $cache = true, $key = 'export')
    {
        $values = $this->getValues($key);
        $value  = $this->fixed[$key] ? $values : Describe::min($values, false);

        return $this->handleStatistic('min', $value, $round, $cache, $key);
    }

    /**
     * 蒙特卡洛峰度。
     *
     * @param  bool    $round
     * @param  bool    $cache
     * @access public
     * @return void
     */
    public function kurtosis($round = true, $cache = true, $key = 'export')
    {
        if($this->fixed[$key]) $value = '-';
        if(!$this->fixed[$key])
        {
            $values = $this->getValues($key);
            $mean     = $this->mean(false, true, $key);
            $standard = $this->standard(false, true, $key);

            $n = count($values);
            $sum = 0;
            foreach($values as $item)
            {
                $sum += ($item - $mean) ** 4;
            }

            $value = $sum / ($n * ($standard ** 4));
        }

        return $this->handleStatistic('kurtosis', $value, $round, $cache, $key);
    }

    /**
     * 蒙特卡洛偏度。
     *
     * @param  bool    $round
     * @param  bool    $cache
     * @access public
     * @return void
     */
    public function skewness($round = true, $cache = true, $key = 'export')
    {
        if($this->fixed[$key]) $value = '-';
        if(!$this->fixed[$key])
        {
            $values = $this->getValues($key);
            $mean     = $this->mean(false, true, $key);
            $standard = $this->standard(false, true, $key);

            $n = count($values);
            $sum = 0;
            foreach($values as $item)
            {
                $sum += ($item - $mean) ** 3;
            }

            $value = $sum / ($n * ($standard ** 3));
        }

        return $this->handleStatistic('skewness', $value, $round, $cache, $key);
    }

    /**
     * 处理概率密度相关的参数。
     *
     * @param  int    $split
     * @access public
     * @return viod
     */
    public function handleProbabilityDensityParams($split = 100)
    {
        $times = $this->times;
        if($this->fixed['export'])
        {
            $fixedValue = $this->forecastValue;
            $this->probabilityDensity->intervals = array($fixedValue);
            $this->probabilityDensity->groupData = array("$fixedValue" => $times);
            $this->probabilityDensity->pd        = array("$fixedValue" => 1);
            return;
        }

        $split = min($split, $times);
        /* 计算整体区间和步长。*/
        $min  = $this->min(false);
        $max  = $this->max(false);
        $base = $max - $min;
        $base = $base === 0 ? 1 : $base;
        $step = $base / $split;
        $intervals = array();
        for($i = 0; $i <= $split; $i += 1) $intervals[] = (string)round($min + $i * $step, 4);

        /* 遍历数据，计算每个数据所属的区间。*/
        $dataList      = $this->forecastValue;
        $groupData     = array();
        foreach($dataList as $value)
        {
            /* 计算当前值应该落在哪个区间。*/
            $intervalIndex = $min + floor(($value - $min) / $step) * $step;
            $intervalIndex = (string)round($intervalIndex, 4);

            if(!isset($groupData[$intervalIndex])) $groupData[$intervalIndex] = 0;
            $groupData[$intervalIndex] += 1;
        }
        ksort($groupData);

        /* 计算概率密度数据。*/
        $pd = array(); // 概率密度数组。
        foreach($groupData as $key => $count) $pd[$key] = $count / $times;

        $this->probabilityDensity->intervals = $intervals;
        $this->probabilityDensity->groupData = $groupData;
        $this->probabilityDensity->pd        = $pd;
    }

    /**
     * 处理累积分布相关的参数。
     *
     * @access public
     * @return viod
     */
    public function handleCumulativeParams()
    {
        $intervals = $this->probabilityDensity->intervals;
        $pd        = $this->probabilityDensity->pd;

        $cd = array();
        $endValue = 0;
        foreach($intervals as $key)
        {
            $endValue = isset($pd[$key]) ? $pd[$key] : 0;
            $cd[$key] = $endValue + end($cd);
        }

        $this->cumulative->cd = $cd;
    }

    /**
     * 检查是否为固定值，如果模拟值前10个为相同的值，那么可以认定为当前方案的模拟值全为固定值。
     *
     * @param  int    $hypothesisList
     * @access private
     * @return void
     */
    private function checkFixed($hypothesisList)
    {
        foreach(array_keys($this->forecasts) as $key)
        {
            $values = array();
            for($index = 0; $index < 10; $index += 1)
            {
                $values[] = $this->calculateForecast($key, $index, $hypothesisList);
            }

            $unique =  array_unique($values);

            $this->fixed[$key] = count($unique) == 1;
            if($key == 'export') $this->fixed[$this->exportKey] = $this->fixed[$key];
        }
    }

    /**
     * 处理预测变量公式，计算得到预测的模拟值。
     *
     * @param  int    $hypothesisList
     * @access private
     * @return void
     */
    private function simulateForecast($hypothesisList)
    {
        foreach(array_keys($this->forecasts) as $key)
        {
            if($this->fixed[$key])
            {
                $simulateValue = $this->calculateForecast($key, 0, $hypothesisList);
            }
            else
            {
                $simulateValue = array();
                for($index = 0; $index < $this->times; $index += 1)
                {
                    $simulateValue[] = $this->calculateForecast($key, $index, $hypothesisList);
                }
            }

            if($key == 'export') $this->forecastValue      = $simulateValue;
            if($key != 'export') $this->requestValue[$key] = $simulateValue;
        }
    }

    /**
     * 带入假设变量的模拟值到预测公式中，计算预测值。
     *
     * @param  int    $hypothesisList
     * @access private
     * @return void
     */
    private function calculateForecast($key, $index, $hypothesisList)
    {
        /* 计算具体的预测模拟值。*/
        $forecast = $this->forecasts[$key];
        $forecast = MathCheck::replaceFormula($forecast);
        foreach($hypothesisList as $hypoVar => $hypothesis)
        {
            $value = $hypothesis->type == 'dist' ? $hypothesis->data[$index] : $hypothesis->value;
            $forecast = str_replace($hypoVar, $value, $forecast);
        }

        $value = eval("return {$forecast};");

        return $value;
    }

    /**
     * 处理统计量数值圆整以及缓存。
     *
     * @param  int    $name
     * @param  int    $value
     * @param  int    $round
     * @param  int    $cache
     * @param  int    $key
     * @access private
     * @return void
     */
    private function handleStatistic($name, $value, $round, $cache, $key)
    {
        if($key == 'export')
        {
            if($cache && isset($this->basicStatistic->$name)) return $this->roundDigit($this->basicStatistic->$name, $round);

            $this->basicStatistic->$name = $value;
            return $this->roundDigit($this->basicStatistic->$name, $round);
        }

        if($cache && isset($this->requestStatistic[$key]->$name)) return $this->roundDigit($this->requestStatistic[$key]->$name, $round);

        $this->requestStatistic[$key]->$name = $value;

        return $this->roundDigit($this->requestStatistic[$key]->$name, $round);
    }

    /**
     * 根据公式的变量获取模拟值数组。
     *
     * @param  int    $key
     * @access private
     * @return void
     */
    private function getValues($key)
    {
        $key = $this->exportKey == $key ? 'export' : $key;
        return $key == 'export' ? $this->forecastValue : $this->requestValue[$key];
    }

    /**
     * 圆整数值。
     *
     * @param  float    $digit
     * @param  bool     $round
     * @param  int      $decimals
     * @access private
     * @return void
     */
    private function roundDigit($digit, $round = true, $decimals = null)
    {
        $decimals = empty($decimals) ? $this->decimals : $decimals;
        if(is_numeric($digit) && $round)
        {
            $digit = round($digit, $decimals);
            $digit = $this->toString($digit, $decimals);
        }
        return $digit;
    }

    /**
     * Number to string without scientific notation.
     *
     * @param float $num
     * @param int $decimals
     * @access public
     * @return string
     */
    private function toString($num, $decimals)
    {
        return sprintf("%1\$.{$decimals}f", $num);
    }
}
