<?php
require_once LIB_ROOT . '/zendasmath/basic/describe.php';
require_once LIB_ROOT . '/dataframe/checkdata.php';
require_once LIB_ROOT . '/dataframe/plotly.php';
require_once LIB_ROOT . '/dataframe/validatedata.php';

/* Box group plot method 基本量统计 */
class boxPlotMethod
{
    /* Method name 分析方法内部名称 */
    public static $name = 'boxPlot';

    /* Method settings 方法设置参数定义 */
    public static $settings = array();

    /* Diagram config 图表配置项 */
    public static $config = array();

    /* Callback for basic statistic method 基本量统计计算回调函数 */
    public static function func($dataframe, $settings)
    {
        global $dasLang;
        global $lang;

        $selectedCol = $settings['type'] == 'single' ? $settings['singleYaxis'] : $settings['groupYaxis'];
        $titleResult = $dataframe->getTextResult($dataframe->getChartTitle($selectedCol, $dasLang->box->basic));
        $titleResult->children = 1;

        $result = array();

        /* Outlier table. */
        $checkResult       = new stdclass();
        $checkResult->type = 'table';
        $checkResult->data = array();
        $checkData         = array();
        $columns           = array();

        $data = array();

        $type        = $settings['type'];
        $singleYaxis = $settings['singleYaxis'];
        $groupYaxis  = $settings['groupYaxis'];
        $groups      = $settings['groups'];
        $display     = $settings['display'];
        $label       = $settings['label'];
        $yName       = $settings['yname'];

        if($type == 'group')
        {
            $check = new checkData();
            $check->setData($dataframe->colData($groupYaxis), $dataframe->columns[$groupYaxis], self::$settings['groupYaxis']['validate']);
            $check->setDataList($dataframe->colsData($groups), $dataframe->colsName($groups), self::$settings['groups']['validate']);

            $checkRes = $check->check();
            if(is_string($checkRes)) return ValidateData::result($checkRes);
        }
        else
        {
            $check = new checkData();
            $check->setDataList($dataframe->colsData($singleYaxis), $dataframe->colsName($singleYaxis), self::$settings['singleYaxis']['validate']);

            $checkRes = $check->check();
            if(is_string($checkRes)) return ValidateData::result($checkRes);
        }

        if($type == 'single')
        {
            $names = array();
            $boxes = array();
            $testPoints = array();

            foreach($singleYaxis as $index => $yitem)
            {
                $y       = $dataframe->col($yitem, 'number');
                if(count($y->trimdata) < 2) return array();
                $name    = $dataframe->columns[$yitem];
                $box     = Describe::box($y->trimdata, $name);

                $boxes[$name] = $box;

                if(!empty($box->outliers['y']))
                {
                    $checkData[$name] = $box->outliers['y'];
                    $rows = array_keys($box->outliers['y']);
                    $rows = array_map(function($v){return $v + 1;}, $rows);
                    $testPoints[$yitem] = $rows;
                }
            }

            if(is_array($label))
            {
                $result[] = $titleResult;
                $result[] = self::buildTableResult($label, $boxes);
            }

            $textResult = $dataframe->getTextResult($dataframe->getChartTitle($singleYaxis, $lang->perfanalysis->methods->graphic['boxPlot']));
            $textResult->name     = 'single';
            $textResult->children = !empty($checkData) ? 2 : 1;

            $result[] = $textResult;
            $result[] = self::buildChartResult($boxes, $display, null, $yName);

            if(!empty($checkData)) $result[] = self::buildOutlierResult($checkData, $testPoints);
        }
        else
        {
            $yData = $dataframe->col($groupYaxis, 'number')->trimdata;
            $yaxisName = isset($dataframe->columns[$groupYaxis]) ? $dataframe->columns[$groupYaxis] : "C$groupYaxis";

            [$xGroup, $yGroup] = self::buildGroupData($dataframe, $groupYaxis, $groups, $result);
            list($boxes, $checkData, $testPoints) = self::buildBoxGroupData($dataframe, $xGroup, $yGroup, $groupYaxis);

            $tableNameType = count($groups) == 1 ? 'single' : 'group';

            if(is_array($label))
            {
                $result[] = $titleResult;
                $result[] = self::buildTableResult($label, $boxes, $tableNameType);
            }

            $textResult = $dataframe->getTextResult($dataframe->getChartTitle(array('x' => $groups, 'y' => $groupYaxis), $lang->perfanalysis->methods->graphic['boxPlot'], 'versus'));
            $textResult->name     = 'group';
            $textResult->children = !empty($checkData) ? 2 : 1;

            $result[] = $textResult;
            $result[] = self::buildChartResult($boxes, null, 'group', $yName, $yaxisName, $yData);
            if(!empty($checkData)) $result[] = self::buildOutlierResult($checkData, $testPoints);
        }

        return $result;
    }

    /**
     * Generate ploty data
     *
     * @param object $dataframe
     * @param array $xGroup
     * @param array $yGroup
     * @access public
     * @return array
     */
    public static function buildBoxGroupData($dataframe, $xGroup, $yGroup, $yaxis)
    {
        $boxes     = array();
        $checkData = array();
        $outliers  = array();

        if(empty($xGroup))
        {
            foreach($yGroup['data'] as $key)
            {
                $y = $dataframe->col($yaxis, 'number', fn($row) => $row[$yGroup['col']] == $key);

                $box        = Describe::box($y->trimdata, $key);
                //$box->name  = $dataframe->columns[$yGroup['col']] . ':' . $key;
                $box->name  = $key;
                $box->xName = $dataframe->columns[$yaxis];

                $boxes[$key] = $box;
                if(!empty($box->outliers['y']))
                {
                    $checkData[$key] = $box->outliers['y'];
                    $outliers = array_merge($outliers, $box->outliers['y']);
                }
            }
        }
        else
        {
            foreach($yGroup['data'] as $yKey)
            {
                $xName = $dataframe->columns[$yGroup['col']] . ':' . $yKey;
                foreach($xGroup['names'] as $xKeys)
                {
                    $y = $dataframe->col($yaxis, 'number', fn($row) => self::buildFilter($row, $xGroup['cols'], $xKeys) && $row[$yGroup['col']] == $yKey);
                    if($y->numCount < 2) continue;

                    $name = array();
                    foreach($xKeys as $index => $xKey)
                    {
                        $col = $xGroup['cols'][$index];
                        $colName = $dataframe->columns[$col];
                        $name[] = $colName . ':' . $xKey;
                    }
                    $name = implode(',', $name);

                    $box = Describe::box($y->trimdata, $name);
                    $box->xName = $xName;
                    $boxes[$name . '-' . $yKey] = $box;

                    if(!empty($box->outliers['y']))
                    {
                        $checkData[$name] = $box->outliers['y'];
                        $outliers = array_merge($outliers, $box->outliers['y']);
                    }
                }
            }
        }

        $yData = $dataframe->col($yaxis, 'number');
        $testPoints = array();
        foreach($outliers as $outlier)
        {
            $testPoints[] = array_search($outlier, $yData->trimdata) + 1;
        }
        $testPoints = array($yaxis => $testPoints);
        return array($boxes, $checkData, $testPoints);
    }

    public static function buildGroupData($dataframe, $yaxis, $groups, $result)
    {
        /* Build unique group array. */
        $uniqueGroups = array();
        foreach($groups as $groupCol)
        {
            $groupData = $dataframe->col($groupCol, 'any');
            $uniqueGroups[] = array('col' => $groupCol, 'data' =>array_unique($groupData->trimdata));
        }

        /* let subgroup num max be y axis. */
        // uasort($uniqueGroups, fn($a, $b) => count($a['data']) - count($b['data']));

        $yGroup = array_pop($uniqueGroups);

        if(count($uniqueGroups) === 0) return array(null, $yGroup);

        $xGroup = array();
        while(count($uniqueGroups) > 0)
        {
            $group  = array_pop($uniqueGroups);
            $pre    = $xGroup;

            if(empty($pre))
            {
                $xGroup = array('cols' => array($group['col']), 'names' => array());
                foreach($group['data'] as $key)
                {
                    $xGroup['names'][] = array($key);
                }
                continue;
            }

            $pre['cols'][]  = $group['col'];
            $xGroup = array('cols' => $pre['cols'], 'names' => array());
            foreach($group['data'] as $key)
            {
                foreach($pre['names'] as $item)
                {
                    $item[]   = $key;
                    $xGroup['names'][] = $item;
                }
            }
        }

        return array($xGroup, $yGroup);
    }

    public static function buildOutlierResult($checkData, $testPoints = array())
    {
        global $dasLang;

        $html = '';

        foreach($checkData as $key => $points)
        {
            $ruleName = $key . $dasLang->box->outlier;
            $html .= "<p>{$ruleName}</p>";
            $html .= '<pre>' . implode(', ', $points) . '</pre>';
        }

        $highlight = getHighlight($testPoints);
        $html = "<h4>{$dasLang->box->outlier} $highlight</h4>" . $html;

        $textResult = new stdclass();

        $textResult->type  = 'text';
        $textResult->title = '';
        $textResult->data  = array();
        $textResult->data['content'] = $html;
        $textResult->data['type']    = 'html';
        return $textResult;
    }

    public static function buildTableResult($label, $boxes, $type = 'single')
    {
        $result        = new stdclass();
        $result->type  = 'table';
        $result->title = '';
        $result->data  = array();

        $data = array();

        foreach($boxes as $name => $box)
        {
            $row = $type == 'single' ? array($name) : array("{$box->name},{$box->xName}");

            foreach($label as $target)
            {
                if(in_array($target, $label)) $row[] = $box->$target;
            }

            $data[] = $row;
        }

        $result->data['data'] = $data;
        $columns = array();
        $columns[] = null;
        foreach($label as $target)
        {
            $columns[] = array('label' => self::$settings['label']['enumOptions'][$target], 'type' => 'number');
        }
        $result->data['columns'] = $columns;

        return $result;

    }

    public static function buildChartResult($boxes, $display, $boxmode = null, $yName = '', $yaxisName = '', $yData = array())
    {
        global $dasLang;

        $result = new stdclass();
        $result->type  = 'chart';
        $result->title = '';

        $result->data = array();
        $result->data['type']   = 'box';

        $data = array();

        if($boxmode == 'group')
        {
            $classify = array();
            foreach($boxes as $box)
            {
                $key = $box->xName;
                if(!isset($classify[$key])) $classify[$key] = array();
                $classify[$key][] = $box;
            }

            foreach($classify as $name => $classifyBoxes)
            {
                $data[] = self::buildBoxData($name, $classifyBoxes);

                foreach($classifyBoxes as $box)
                {
                    if(!empty($box->outliers['y'])) $data[] = self::buildOutlierData($box->outliers, $yaxisName, $yData);
                }
            }
        }
        else
        {
            foreach($boxes as $box)
            {
                $data[] = self::buildindependenceBoxData($box);
                $data[] = self::buildOutlierData($box->outliers, $box->name, $box->data);
            }
        }

        $annotations = array();
        if(is_array($display))
        {
            $mean = array_map(fn($val) => $val->mean, $boxes);
            $mid  = array_map(fn($val) => $val->q2, $boxes);
            $xs   = array_map(fn($val) => $val->name, $boxes);

            foreach($display as $item)
            {
                $type = strtok($item, "-");
                $mode = strtok("-");
                $y = $type == 'mean' ? $mean : $mid;
                $name = self::$settings['display']['enumOptions'];

                $data[] = array('hoverinfo' => 'y', 'x' => array_values($xs), 'y' => array_values($y), 'type' => 'scatter', 'mode' => $mode, 'name' => $name[$item]);
            }
        }

        $result->data['data'] = json_encode($data);
        $result->data['layout']['yaxis'] = array('title' => $yName);
        if($boxmode == 'group') $result->data['layout']['boxmode'] = 'group';

        $grid   = $dasLang->config->gridConfig;
        $legend = $dasLang->config->legendConfig;
        $legend['defaultValue'] = 'true';
        $result->data['config'] = array('grid' => $grid, 'legend' => $legend);

        return $result;
    }

    public static function buildBoxData($name, $boxes)
    {
        $data = array();
        global $config;

        $data['hoverinfo']  = 'y';
        $data['x']          = array_values(array_map(fn($val) => $val->name, $boxes));
        //$data['y']          = array_values(array_map(fn($val) => $val->data, $boxes));
        $data['q1']         = array_values(array_map(fn($val) => $val->q1, $boxes));
        $data['q3']         = array_values(array_map(fn($val) => $val->q3, $boxes));
        $data['median']     = array_values(array_map(fn($val) => $val->q2, $boxes));
        $data['lowerfence'] = array_values(array_map(fn($val) => $val->lowerfence, $boxes));
        $data['upperfence'] = array_values(array_map(fn($val) => $val->upperfence, $boxes));
        $data['name']       = $name;
        $data['type']       = 'box';
        $data['boxpoints']  = 'outliers';
        $data['marker']     = array('symbol' => 'asterisk-open', 'size' => $config->default->pointSize);

        return $data;
    }

    public static function buildIndependenceBoxData($box)
    {
        $data = array();
        global $config;

        $data['hoverinfo']  = 'y';
        $data['x']          = array($box->name);
        $data['q1']         = array($box->q1);
        $data['q3']         = array($box->q3);
        $data['median']     = array($box->q2);
        $data['lowerfence'] = array($box->lowerfence);
        $data['upperfence'] = array($box->upperfence);
        $data['name']       = $box->name;
        $data['type']       = 'box';
        $data['marker']     = array('symbol' => 'asterisk-open', 'size' => $config->default->pointSize);

        return $data;
    }

    public static function buildOutlierData($outliers, $boxName, $yData)
    {
        global $dasLang;
        global $config;

        $outX = $outliers['x'];
        $outY = $outliers['y'];

        $data = getHoverTemplateY(array(), array('yname' => $boxName, 'custom' => getDisOrderCustom($outY, $yData)));

        $data['name']      = $dasLang->box->outlier;
        $data['x']         = array_values($outX);
        $data['y']         = array_values($outY);
        $data['mode']      = 'markers';
        $data['type']      = 'scatter';
        $data['marker']    = array('symbol' => 'asterisk-open', 'size' => $config->default->pointSize);

        return $data;
    }

    public static function checkRowEqual($dataframe, $yaxis, $groups)
    {
        $y     = $dataframe->col($yaxis, self::columnType('groupYaxis'));
        $group = $dataframe->cols($groups, self::columnType('groups'));

        $count = count($y->data);
        if($count == 0) return false;

        foreach($group as $item)
        {
            if($count != count($item->data)) return false;
        }

        return true;
    }

    public static function buildFilter($row, $cols, $keys)
    {
        $exp = true;
        foreach($cols as $index => $col)
        {
            $key = $keys[$index];
            $exp &= $row[$col] == $key;
        }
        return $exp;
    }

    /**
     * Get settings.
     *
     * @access public
     * @return object
     */
    public static function getSettings()
    {
        global $dasLang;

        /* Basic settings */
        self::$settings['type']        = array('name' => 'type',        'label' => $dasLang->box->type, 'defaultValue' => 'single');
        self::$settings['singleYaxis'] = array('name' => 'singleYaxis', 'label' => $dasLang->box->yaxis,       'required' => true);
        self::$settings['groupYaxis']  = array('name' => 'groupYaxis',  'label' => $dasLang->boxGroup->yaxis,  'required' => true);
        self::$settings['groups']      = array('name' => 'groups',      'label' => $dasLang->boxGroup->groups, 'required' => true);
        self::$settings['display']     = array('name' => 'display',     'label' => $dasLang->box->display);
        self::$settings['label']       = array('name' => 'label',       'label' => $dasLang->box->label, 'defaultValue' => array_keys($dasLang->box->labelEnum));
        self::$settings['yname']       = array('name' => 'yname',       'label' => $dasLang->yAxisName);

        /* Type settings */
        self::$settings['type']        += array('type' => 'enum', 'enumOptions' => $dasLang->box->typeEnum);
        self::$settings['singleYaxis'] += array('type' => 'list', 'listType' => 'column', 'columnType' => 'number');
        self::$settings['groupYaxis']  += array('type' => 'column', 'columnType' => 'number');
        self::$settings['groups']      += array('type' => 'list', 'listType' => 'column', 'columnType' => 'string');
        self::$settings['display']     += array('type' => 'list', 'listType' => 'enum', 'enumOptions' => $dasLang->box->displayEnum);
        self::$settings['label']       += array('type' => 'list', 'listType' => 'enum', 'enumOptions' => $dasLang->box->labelEnum);
        self::$settings['yname']       += array('type' => 'any');

        /* Limit settings */
        self::$settings['singleYaxis'] += array('conditions' => array('type' => 'single'));
        self::$settings['groupYaxis']  += array('conditions' => array('type' => 'group'));
        self::$settings['groups']      += array('conditions' => array('type' => 'group'), 'listLength' => '0,4');
        self::$settings['display']     += array('conditions' => array('type' => 'single'));
        //self::$settings['label']       += array('conditions' => array('type' => 'single'));

        /* Col grid settings */
        self::$settings['type']        += array('col' => 6);
        self::$settings['display']     += array('col' => 3);
        self::$settings['label']       += array('col' => 3);

        /* More settings */
        self::$settings['yname']       += array('paramType' => 'more');

        /* Data validate */
        self::$settings['singleYaxis'] += array('validate' => array('continuous', 'number'));
        self::$settings['groupYaxis']  += array('validate' => array('continuous', 'number', 'rowEqual'));
        self::$settings['groups']      += array('validate' => array('continuous', 'rowEqual', 'columnUnique >= 2'));

        return self::$settings;
    }

    /**
     * Get config.
     *
     * @access public
     * @return object
     */
    public static function getConfig()
    {
        global $dasLang;
        $lang = $dasLang->config;
        self::$config['grid']   = $lang->gridConfig;
        self::$config['legend'] = $lang->legendConfig;
        // self::$config['title']  = array('name' => 'title', 'label' => $lang->title, 'type' => 'checkbox', 'enumOptions' => array('yaxis' => $lang->titleEnum['yaxis']), 'defaultValue' => '');

        return self::$config;
    }

    /**
     * Get Setting column type.
     *
     * @param string $index
     * @access public
     * @return string
     */
    public static function columnType($index)
    {
        return self::$settings[$index]['columnType'];
    }
}
