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

/**
 * Simulate
 */
class simulate
{
    /**
     * 假设变量参数。
     * Param of variables.
     *
     * @var array
     * @access public
     */
    public $param;

    /**
     * 预测变量公式。
     * Formula.
     *
     * @var string
     * @access public
     */
    public $formula;

    /**
     * 运行次数。
     * Run times.
     *
     * @var int
     * @access public
     */
    public $times;

    /**
     * 错误信息。
     * Error mesage.
     *
     * @var string
     * @access public
     */
    public $errorMsg;

    /**
     * 模拟结果数据。
     * Data of result.
     *
     * @var array
     * @access public
     */
    public $data;

    /**
     * 累计结果数据。
     * Data of cumulate.
     *
     * @var array
     * @access public
     */
    public $cumulate;

    /**
     * 目标。
     * Target.
     *
     * @var array
     * @access public
     */
    public $target;

    /**
     * 检查状态。
     * Check status.
     *
     * @var bool
     * @access public
     */
    public $check;

    /**
     * 原始错误信息。
     * Origin error message.
     *
     * @var string
     * @access public
     */
    public $error;

    /**
     * 目标寻优。
     * Quest best plan.
     *
     * @var bool
     * @access public
     */
    public $optQuest;

    /**
     * 决策方案穷举。
     * Combination of plan.
     *
     * @var array
     * @access public
     */
    public $plans;

    /**
     * 决策方案穷举键。
     * Combination key of plan.
     *
     * @var array
     * @access public
     */
    public $planKeys;

    public $decision;

    public $request;

    public $plansResult;

    public $bestFit;

    public $bestFitPlan;

    public $requestValues;

    public $requestBools;

    public $satisfyRequest;

    public $allFixed;

    /**
     * Constructor
     *
     * @param array  $param
     * @param string $formula
     * @param int    $times
     * @param string $error
     * @param array  $target
     * @param array  $decision
     * @access public
     * @return void
     */
    public function __construct($param, $formula, $times, $error, $target = null, $decision = array(), $request = array())
    {
        foreach($param as $var => $config)
        {
            if($config['type'] == 'dist')
            {
                $param[$var]['method'] = eval("return self::{$config['method']};");
            }
        }

        $this->param    = $param;
        $this->formula  = $formula;
        $this->times    = $times;
        $this->errorMsg = '';
        $this->data     = array();
        $this->cumulate = array(0);
        $this->target   = $target; // array('type' => 'max', 'stat' => 'mean')
        $this->decision = $decision;
        $this->request  = $request; // array(array('stat' => 'mean', 'condition' => 'gt', 'value' => 10))

        $this->optQuest = (!empty($target) and !empty($decision));
        $this->check    = true;

        if($this->optQuest)
        {
            $decisionValues  = array_map(fn($val) => $val['values'], $decision);
            if(count($decisionValues) == 1)
            {
                $this->plans = current($decisionValues);
            }
            else
            {
                $com             = new combination($decisionValues);
                $this->plans     = $com->getCombination();
            }
            $this->planKeys  = array_keys($decision);

            $this->plansResult = array();

            foreach($this->plans as $plan)
            {
                $formula = $this->convertFormula($plan);
                $this->check = $this->checkFormula($formula);

                if(!$this->check) break;
            }
        }else
        {
            $this->check = $this->checkFormula($this->formula);
        }

        $this->error = $this->check ? '' : $error;
    }

    /**
     * Run Monte Carlo simulate.
     *
     * @access public
     * @return void
     */
    public function run()
    {
        $errorLevel = error_reporting(0);
        if(!$this->check) return $this->errorMsg;

        if($this->optQuest)
        {
            $targetValues  = array();
            $requestBools  = array();
            $requestValues = array();
            foreach($this->plans as $plan)
            {
                list($data, $cumulate, $fixed) = $this->getData($plan);

                $res = new stdclass;

                $res->data     = $data;
                $res->cumulate = $cumulate;
                $res->fixed    = $fixed;

                $this->plansResult[$plan] = $res;

                $targetValues[$plan]  = $this->getTargetValue($data, $this->target['stat']);
                list($bools, $values)  = $this->getRequestBool($data);
                $requestBools[$plan]  = $bools;
                $requestValues[$plan] = $values;
            }
            $filterTargets = array();
            foreach($targetValues as $index => $value)
            {
                if(array_sum($requestBools[$index]) == count($this->request)) $filterTargets[$index] = $value;
            }

            $bestFit     = $this->target['type'](count($filterTargets) == 0 ? $targetValues : $filterTargets);
            $bestFitPlan = array_search($bestFit, $targetValues);
            $bestResult  = $this->plansResult[$bestFitPlan];

            $data     = $bestResult->data;
            $cumulate = $bestResult->cumulate;
            $fixed    = $bestResult->fixed;

            $this->bestFit        = $bestFit;
            $this->bestFitPlan    = $bestFitPlan;
            $this->requestValues  = $requestValues[$bestFitPlan];
            $this->requestBools   = $requestBools[$bestFitPlan];
            $this->satisfyRequest = count($filterTargets) != 0;
        }
        else
        {
            list($data, $cumulate, $fixed) = $this->getData();
        }

        $this->data     = $data;
        $this->cumulate = $cumulate;
        $this->allFixed = $fixed;
    }

    /**
     * Describe info.
     *
     * @access public
     * @return object
     */
    public function describe()
    {
        $data = $this->data;
        if(empty($data))
        {
            return null;
        }

        $strData = array();
        foreach($data as $value)
        {
            $strData[] = (string)$value;
        }
        $strData = array_flip($strData); // exchange key and value to remove duplicates.
        $strData = array_flip($strData); // exchange key and value to restore array.
        if(count($strData) <= 1) $this->allFixed = true;

        $fixedValue = current($data);

        $result = new stdclass();

        $result->times     = $this->times;
        $result->mean      = $this->allFixed ? $fixedValue : Describe::mean($data);
        $result->median    = $this->allFixed ? $fixedValue : Describe::median($data);
        $result->variance  = $this->allFixed ? 0 : Describe::variance($data);
        $result->standard  = $this->allFixed ? 0 : Describe::standard($data);
        $result->skewness  = $this->allFixed ? '-' : $this->skewness($result->mean, $result->standard);
        $result->kurtosis  = $this->allFixed ? '-' : $this->kurtosis($result->mean, $result->standard);
        $result->max       = $this->allFixed ? $fixedValue : Describe::max($data);
        $result->min       = $this->allFixed ? $fixedValue : Describe::min($data);

        return $result;
    }

    public function chart($split = 100)
    {
        if(empty($this->data)) return array();
        $split = min($split, $this->times);

        $data = $this->data;
        $max = Describe::max($data, false);
        $min = Describe::min($data, false);

        $base = $max - $min;
        $base = $base == 0 ? 1 : $base;
        $unit = $base / $split;

        $chartData = array();
        foreach($data as $value)
        {
            $index = round($min + floor(($value - $min) / $unit) * $unit, 4);
            $key   = "$index";
            $chartData[$key][] = $value;
        }

        ksort($chartData);
        $pdf    = array();
        foreach($chartData as $key => $arr)
        {
            $count = count($arr);
            $pdf[$key] = $count / $this->times;
        }

        $cdf    = array();
        for($i = 0; $i <= $split; $i++)
        {
            $index = round($min + $i * $unit, 4);
            $index = "$index";
            $value = isset($pdf[$index]) ? $pdf[$index] : 0;
            $cdf[$index] = empty($cdf) ? $value : end($cdf) + $value;
        }

        $result            = new stdclass();
        $result->pdf       = $pdf;
        $result->cdf       = $cdf;
        $result->chartData = $chartData;

        return $result;
    }

    /**
     * Check formula.
     *
     * @param string $formula
     * @access public
     * @return bool
     */
    public function checkFormula($formula)
    {
        @trigger_error('init', E_USER_NOTICE);
        $errorLevel = error_reporting(0);
        try{
            $execStr = '';
            foreach($this->param as $var => $config)
            {
                $value    = $config['type'] == 'dist' ? $config['method']->rand() : $config['value'];
                $execStr .= '$' . $var . ' = ' . $value . ';';
                $formula  = str_replace($var, '$' . $var, $formula);
            }
            $execStr .= "return $formula;";
            $value = eval($execStr);
        }
        catch (ParseError $e){
            $this->errorMsg = "Syntax error: ". $e->getMessage();
            return false;
        }
        catch (Error $e){
            $this->errorMsg = "Execute error: ". $e->getMessage();
            return false;
        }
        error_reporting($errorLevel);
        $error = error_get_last();
        if(isset($error['message']) and $error['message'] != 'init')
        {
            $this->errorMsg = $error['message'];
            return false;
        }
        return true;
    }

    /**
     * Normal distribution random.
     *
     * @param float $mu
     * @param float $sigma
     * @access public
     * @return object
     */
    public static function normal($mu, $sigma, $min = null, $max = null)
    {
        return new normal($mu, $sigma, $min, $max);
    }

    /**
     * Triangular distribution random.
     *
     * @param float $left
     * @param float $mode
     * @param float $right
     * @access public
     * @return object
     */
    public static function triangular($left, $mode, $right)
    {
        return new triangular($left, $mode, $right);
    }

    /**
     * Kurtosis of distribution.
     *
     * @param float $mean
     * @param float $standard
     * @access private
     * @return float
     */
    private function kurtosis($mean, $standard)
    {
        $n = count($this->data);
        $sum = 0;
        foreach($this->data as $value)
        {
            $sum += ($value - $mean) ** 4;
        }

        return round($sum / ($n * ($standard ** 4)), 4);
    }

    /**
     * Skewness of distribution.
     *
     * @param float $mean
     * @param float $standard
     * @access private
     * @return float
     */
    private function skewness($mean, $standard)
    {
        $n = count($this->data);
        $sum = 0;

        foreach($this->data as $value)
        {
            $sum += ($value - $mean) ** 3;
        }

        return round($sum / ($n * ($standard ** 3)), 4);
    }

    /**
     * Covert formula from plan variable.
     *
     * @param string $plan
     * @access private
     * @return string
     */
    private function convertFormula($plan)
    {
        $planVars = explode(',', $plan);
        $formula  = $this->formula;
        foreach($this->planKeys as $index => $key)
        {
            if(strpos($formula, $key) === false) continue;
            $formula = str_replace($key, $planVars[$index], $formula);
        }

        return $formula;
    }

    /**
     * Get target value.
     *
     * @param array $data
     * @param string $stat
     * @access private
     * @return float
     */
    private function getTargetValue($data, $stat)
    {
        $value = null;
        switch($stat)
        {
            case 'mean':
                $value = Describe::mean($data);
                break;
            case 'median':
                $value = Describe::median($data);
                break;
            case 'max':
                $value = Describe::max($data);
                break;
            case 'min':
                $value = Describe::min($data);
                break;
        }

        return $value;
    }

    /**
     * Get request bool.
     *
     * @param array $data
     * @access private
     * @return float
     */
    private function getRequestBool($data)
    {
        $flags  = array();
        $values = array();
        foreach($this->request as $request)
        {
            $value = $this->getTargetValue($data, $request['stat']);
            switch($request['condition'])
            {
                case 'gt':
                    $flags[] = $value > $request['value'] ? 1 : 0;
                    break;
                case 'lt':
                    $flags[] = $value < $request['value'] ? 1 : 0;
                    break;
                case 'gte':
                    $flags[] = $value >= $request['value'] ? 1 : 0;
                    break;
                case 'lte':
                    $flags[] = $value <= $request['value'] ? 1 : 0;
                    break;
                case 'eq':
                    $flags[] = $value = $request['value'] ? 1 : 0;
                    break;
                case 'ne':
                    $flags[] = $value != $request['value'] ? 1 : 0;
                    break;
            }

            $values[] = $value;
        }

        return array($flags, $values);
    }

    /**
     * Get simulate data.
     *
     * @param string $plan
     * @access private
     * @return array
     */
    private function getData($plan = null)
    {
        $formula = empty($plan) ? $this->formula : $this->convertFormula($plan);

        $errorLevel = error_reporting(0);

        $data     = array();
        $cumulate = array(0);
        $fixed    = $this->isFixed($formula);

        $fixedValue = $fixed ? $this->calculateValue($formula) : null;
        for($time = 1; $time <= $this->times; $time++)
        {
            $value      = $fixed ? $fixedValue : $this->calculateValue($formula);
            $data[]     = $value;
            $cumulate[] = end($this->cumulate) + $value;
        }

        array_shift($this->cumulate);
        error_reporting($errorLevel);

        return array($data, $cumulate, $fixed);
    }

    /**
     * Calculate formula value.
     *
     * @param string $formula
     * @access private
     * @return float
     */
    private function calculateValue($formula)
    {
        $execStr = '';
        foreach($this->param as $var => $config)
        {
            if(strpos($formula, $var) === false) continue;

            $value    = $config['type'] == 'dist' ? $config['method']->rand() : $config['value'];
            $execStr .= '$' . $var . ' = ' . $value . ';';
            $formula  = str_replace($var, '$' . $var, $formula);
        }
        $execStr .= "return $formula;";
        $value    = eval($execStr);

        return $value;
    }

    /**
     * Check formula is fixed or not.
     *
     * @param string $formula
     * @access private
     * @return bool
     */
    private function isFixed($formula)
    {
        foreach($this->param as $var => $config)
        {
            if($config['type'] == 'dist' and strpos($formula, $var) !== false) return false;
        }

        return true;
    }
}
