<?php
include 'column.php';

require_once dirname(__DIR__) . '/zendasmath/basic/describe.php';
require_once dirname(__DIR__) . '/zendasmath/distribution/gamma.php';

/**
 * Dataframe
 */
class dataframe
{
    /**
     * 名称
     *
     * Name
     *
     * @var string
     * @access public
     */
    public $name;

    /**
     * 列
     *
     * Columns
     *
     * @var array
     * @access public
     */
    public $columns;

    /**
     * 行定义
     *
     * Index
     *
     * @var array
     * @access public
     */
    public $index;

    /**
     * 类型
     *
     * Dtypes
     *
     * @var array
     * @access public
     */
    public $dtypes;

    /**
     * 数据（行×列）
     *
     * Data (row x column)
     *
     * @var array
     * @access public
     */
    public $data;

    public $decimals;

    public $pointSize;

    public $colWidths;

    public $fields;

    public $rows;

    /**
     * 构造方法。
     *
     * The construct function.
     *
     * @access public
     * @return void
     */
    public function __construct()
    {
        global $config;
        $this->decimals  = $config->default->decimals;
        $this->pointSize = $config->default->pointSize;

        $this->name    = '';
        $this->columns = array();
        $this->index   = array();
        $this->dtypes  = array();
        $this->data    = array();
    }

    public function notEmptyCols()
    {
        $columns = array();
        foreach(array_keys($this->columns) as $index)
        {
            $data = $this->colData($index);
            if(count($data) != 0) $columns[$index] = $this->columns[$index];
        }

        return $columns;
    }

    /**
     * Col
     *
     * @param int    $index
     * @param string $type
     * @param bool $filter
     * @param bool $keepEmpty
     * @access public
     * @return object
     */
    public function col($index, $type = 'any', $filter = true, $keepEmpty = false)
    {
        return new Column($this, $index, $type, $filter, $keepEmpty);
    }

    /**
     * ColData
     *
     * @param int    $index
     * @param string $type
     * @param bool $filter
     * @param bool $keepEmpty
     * @access public
     * @return object
     */
    public function colData($index, $type = 'any', $filter = true, $keepEmpty = false)
    {
        $col = $this->col($index, $type, $filter, $keepEmpty);

        return $col->trimdata;
    }

    /**
     * Cols
     *
     * @param array  $cols
     * @param string $type
     * @access public
     * @return array
     */
    public function cols($cols, $type)
    {
        $result = array();
        foreach($cols as $col)
        {
            $result[$col] = new Column($this, $col, $type);
        }
        return $result;
    }

    /**
     * ColsData
     *
     * @param array  $cols
     * @param string $type
     * @access public
     * @return array
     */
    public function colsData($cols, $type = 'any')
    {
        $result = array();
        foreach($cols as $col)
        {
            $column       = new Column($this, $col, $type);
            $result[$col] = $column->trimdata;
        }
        return $result;
    }

    /**
     * ColsName
     *
     * @param array $cols
     * @access public
     * @return array
     */
    public function colsName($cols)
    {
        $result = array();
        foreach($cols as $col)
        {
            $result[$col] = $this->columns[$col];
        }

        return $result;
    }

    /**
     * $max is the longest data column length in selected columns, use array_slice to cut excess rows in $dataframe->data.
     *
     *
     * @param array  $results
     * @access public
     * @return void
     */
    public function sliceDataframe($dataframe, $cols, $type)
    {
        $cloneDF = clone($dataframe);
        if(is_array($cols))
        {
            $results = $this->cols($cols, $type);
        }
        else
        {
            $results = $this->col($cols, $type);
        }

        // max值的计算用原始数据，因为如果在使用cols的时候传入type=number，会把空转换为0，导致数据多
        $max = 0;
        if(is_array($results))
        {
            foreach($results as $result)
            {
                $max = max($max, $this->getDataLength($result->originData));
            }
        }
        /* $this->col() return object. */
        elseif(is_object($results))
        {
            $max = max($max, $this->getDataLength($results->originData));
        }

        $cloneDF->data = array_slice($cloneDF->data, 0, $max + 1);
        return $cloneDF;
    }

    /*
     * Return the length of the array to remove excess null data.
     *
     *
     * @param array  $result
     * @access public
     * @return void
     */
    public 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));
    }

    /*
     * Get chart title.
     *
     *
     * @param array|int $rows
     * @param string    $chartName
     * @param string    $type  of|versus
     * @access public
     * @return void
     */
    public function getChartTitle($rows, $chartName, $type = 'of')
    {
        global $lang;
        global $app;

        if($type == 'of')
        {
            $name  = $this->getColumnNames($rows);
            $title = $name . ' ' . $lang->of . ' ' . $chartName;
            if($app->getClientLang() == 'en') $title = $chartName . ' ' . $lang->of . ' ' . $name;
        }
        elseif($type == 'versus')
        {
            $xName = $this->getColumnNames($rows['x']);
            $yName = $this->getColumnNames($rows['y']);
            $title = $chartName . ': ' . $yName . ' ' . $lang->versus . ' ' . $xName;
        }

        return $title;
    }

    /*
     * Get chart title.
     *
     *
     * @param array|int $rows
     * @param string    $chartName
     * @param string    $type  of|versus
     * @access public
     * @return void
     */
    public function getColumnNames($rows, $isImplode = true)
    {
        $name = array();
        if(is_array($rows))
        {
            foreach($rows as $row) $name[] = $this->columns[$row];
        }
        else
        {
            $name[] = $this->columns[$rows];
        }

        if($isImplode) $name = implode(',', $name);
        return $name;
    }

    /*
     * Add chart title.
     *
     *
     * @param array  $rows
     * @access public
     * @return void
     */
    public function getTextResult($title, $content = '', $type = 'text')
    {
        $textResult        = new stdclass();
        $textResult->type  = 'text';
        $textResult->title = $title;
        $textResult->data  = array();
        $textResult->data['content'] = $content;
        $textResult->data['type']    = $type;

        return $textResult;
    }

    /**
     * Array
     *
     * @param int    $col1
     * @param string $type1
     * @param int    $col2
     * @param string $type2
     * @access public
     * @return array
     */
    public function array($col1, $type1, $col2, $type2)
    {
        $data = array();
        foreach($this->data as $row)
        {
            $xvalue = $type1 == 'number' ? column::cast2Number($row[$col1]) : $row[$col1];
            $yvalue = $type2 == 'number' ? column::cast2Number($row[$col2]) : $row[$col2];
            if($xvalue === NULL or $yvalue === NULL) continue;

            $data[] = array($xvalue, $yvalue);
        }

        return $data;
    }

    /**
     * Get values of col2 where value of col1 is number.
     *
     * @param int    $col1
     * @param int    $col2
     * @param string $type2
     * @access public
     * @return array
     */
    public function xValues($col1, $col2, $type2)
    {
        $data = array();
        foreach($this->data as $row)
        {
            if(column::cast2Number($row[$col1]) !== NULL)
            {
                $data[] = ($type2 == 'number') ? column::cast2Number($row[$col2]) : $row[$col2];
            }
        }

        return $data;
    }

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

    /**
     * Row mean method.
     * @param array $cols
     * @param string $keyName
     * @param bool $round
     * @access public
     * @return array
     */
    public function rowMean($cols, $keyName, $round = true)
    {
        foreach($this->data as $index => $data)
        {
            $row      = array();
            $numCount = 0;
            foreach($cols as $col)
            {
                if(!isset($data[$col])) continue;
                $num = Column::cast2Number($data[$col]);
                if($num !== NULL)
                {
                    $numCount += 1;
                    $row[] = $num;
                }
            }

            $mean = empty($row) ? NULL : array_sum($row) / $numCount;
            $this->data[$index][$keyName] = is_numeric($mean) && $round ? round($mean, $this->decimals) : $mean;
        }
        $this->columns[$keyName] = '';

        return $this->col($keyName);
    }

    /**
     * Row standard deviation method.
     * @param array $cols
     * @param string $keyName
     * @param bool $round
     * @access public
     * @return array
     */
    public function rowSD($cols, $keyName, $round = true)
    {
        foreach($this->data as $index => $data)
        {
            $row      = array();
            $numCount = 0;
            foreach($cols as $col)
            {
                if(!isset($data[$col])) continue;
                $num = Column::cast2Number($data[$col]);
                if($num !== NULL)
                {
                    $numCount += 1;
                    $row[] = $num;
                }
            }

            $σ = $numCount < 2 ? NULL : Describe::standard($row);
            $this->data[$index][$keyName] = is_numeric($σ) && $round ? round($σ, $this->decimals) : $σ;
        }
        $this->columns[$keyName] = '';

        return $this->col($keyName);
    }

    /**
     * C₄ method;
     *
     *
     * @param int $n
     * @access public
     * @return float
     */
    public function C₄($n)
    {
        $Γ₁ = gamma::gamma($n / 2);
        $Γ₂ = gamma::gamma(($n - 1) / 2);

        return sqrt(2 / ($n - 1)) * $Γ₁ / $Γ₂;
    }

    /**
     * C₅ method;
     *
     *
     * @param int $n
     * @access public
     * @return float
     */
    public function C₅($n)
    {
        $c4 = $this->C₄($n);
        return sqrt(1 - $c4 ** 2);
    }

    /**
     * 无偏常量 d2()
     *
     * @param int $n
     * @access public
     * @return float
     */
    public function D₂($n)
    {
        $table = array();
        $table['2'] = 1.128;
        $table['3'] = 1.693;
        $table['4'] = 2.059;
        $table['5'] = 2.326;
        $table['6'] = 2.534;
        $table['7'] = 2.704;
        $table['8'] = 2.847;
        $table['9'] = 2.97;
        $table['10'] = 3.078;
        $table['11'] = 3.173;
        $table['12'] = 3.258;
        $table['13'] = 3.336;
        $table['14'] = 3.407;
        $table['15'] = 3.472;
        $table['16'] = 3.532;
        $table['17'] = 3.588;
        $table['18'] = 3.64;
        $table['19'] = 3.689;
        $table['20'] = 3.735;
        $table['21'] = 3.778;
        $table['22'] = 3.819;
        $table['23'] = 3.858;
        $table['24'] = 3.895;
        $table['25'] = 3.931;
        $table['26'] = 3.964;
        $table['27'] = 3.997;
        $table['28'] = 4.027;
        $table['29'] = 4.057;
        $table['30'] = 4.086;
        $table['31'] = 4.113;
        $table['32'] = 4.139;
        $table['33'] = 4.165;
        $table['34'] = 4.189;
        $table['35'] = 4.213;
        $table['36'] = 4.236;
        $table['37'] = 4.259;
        $table['38'] = 4.28;
        $table['39'] = 4.301;
        $table['40'] = 4.322;
        $table['41'] = 4.341;
        $table['42'] = 4.361;
        $table['43'] = 4.379;
        $table['44'] = 4.398;
        $table['45'] = 4.415;
        $table['46'] = 4.433;
        $table['47'] = 4.45;
        $table['48'] = 4.466;
        $table['49'] = 4.482;
        $table['50'] = 4.498;

        if($n <= 50) return $table[$n];

        // https://support.minitab.com/zh-cn/minitab/21/help-and-how-to/quality-and-process-improvement/control-charts/how-to/variables-charts-for-subgroups/xbar-r-chart/methods-and-formulas/unbiasing-constants-d2-d3-and-d4/
        return 3.4873 + 0.0250141 * $n - 0.00009823 * pow($n, 2);
    }

    /**
     * 无偏常量 d3()
     *
     * @param int $n
     * @access public
     * @return float
     */
    public function D₃($n)
    {
        $table = array();

        $table['2'] = 0.8525;
        $table['2'] = 0.8525;
        $table['3'] = 0.8884;
        $table['4'] = 0.8798 ;
        $table['5'] = 0.8641;
        $table['6'] = 0.848;
        $table['7'] = 0.8332;
        $table['8'] = 0.8198;
        $table['9'] = 0.8078;
        $table['10'] = 0.7971;
        $table['11'] = 0.7873;
        $table['12'] = 0.7785;
        $table['13'] = 0.7704;
        $table['14'] = 0.763;
        $table['15'] = 0.7562;
        $table['16'] = 0.7499;
        $table['17'] = 0.7441;
        $table['18'] = 0.7386;
        $table['19'] = 0.7335;
        $table['20'] = 0.7287;
        $table['21'] = 0.7242 ;
        $table['22'] = 0.7199;
        $table['23'] = 0.7159;
        $table['24'] = 0.7121;
        $table['25'] = 0.7084;

        if($n <= 25) return $table[$n];

        return 0.80818 - 0.0051871 * $n + 0.00005098 * pow($n, 2) - 0.00000019 * pow($n, 3);
    }

    public function roundDigit($digit, $round = true, $decimals = null)
    {
        $decimals = empty($decimals) ? $this->decimals : $decimals;
        if(is_numeric($digit) && $round)
        {
            if(is_nan($digit)) return '-';
            $digit = round($digit, $decimals);
            $digit = $this->toString($digit, $decimals);
        }
        return $digit;
    }
}
