<?php
require_once LIB_ROOT . '/dataframe/checkdata.php';

class SPC
{
    /**
     * Build chart result.
     *
     * @param object $param
     * @access public
     * @return object
     */
    public static function buildChartResult($param)
    {
        global $dasLang;
        global $config;

        $style = new stdclass();
        $style->line = array('width' => 1,   'color' => '#005EE9');
        $style->CL   = array('width' => 0.5, 'color' => '#2D9A91', 'dash' => '4px,2px');
        $style->UCL  = array('width' => 0.5, 'color' => '#A71515', 'dash' => '4px,2px');
        $style->LCL  = array('width' => 0.5, 'color' => '#A71515', 'dash' => '4px,2px');
        $style->ref  = array('width' => 0.5, 'color' => '#fb2b2b', 'dash' => '4px,2px');

        $annotationPos = 'left';

        $lang      = $param->lang;
        $points    = $param->points;
        $pointSize = isset($param->pointSize) ? $param->pointSize : $config->default->pointSize;
        $name      = $param->name;
        $lineBegin = $param->lineBegin + 1;
        $lineEnd   = $param->lineEnd + 1;
        $CL        = $param->CL;
        $CLName    = isset($param->CLName) ? $param->CLName : $lang->mean;
        $ULCLs     = $param->ULCLs;
        $tests     = isset($param->testsResults) ? $param->testsResults : '';
        $title     = $param->title;
        $xTitle    = $param->xTitle;
        $yTitle    = $param->yTitle;
        $ref       = isset($param->ref) ? $param->ref : array();

        $keys   = array_keys($points);
        $values = array_values($points);

        $keys[] = count($points);
        array_shift($keys);

        $points     = array_combine($keys, $values);
        $pointCount = count($points);

        $data        = array();
        $annotations = array();

        $result               = new stdclass();
        $result->type         = 'chart';
        $result->title        = '';
        $result->data         = array();
        $result->data['type'] = 'scatter';

        /* line-point chart  */
        $lineData = array();
        $lineData['x']      = array_keys($points);
        $lineData['y']      = array_values($points);
        $lineData['name']   = $name;
        $lineData['type']   = 'scatter';
        $lineData['marker'] = array('size' => $pointSize);
        $lineData['line']   = $style->line;

        $data[] = $lineData;

        /* CL line */
        list($annoCL, $CLdata) = self::buildLimitLine($CL, $CLName, $pointCount, $lineBegin, $lineEnd, $style->CL, [$annotationPos, 'middle']);

        $annotations[] = $annoCL;
        $data[]        = $CLdata;

        /* ref line */
        foreach($ref as $refValue)
        {
            list($annoRef, $refData) = self::buildRefLine($refValue, $lineBegin, $lineEnd, $style->ref, [$annotationPos, 'middle']);
            $annotations[] = $annoRef;
            $data[]        = $refData;
        }

        $standard = null;
        /* UCL LCL line */
        foreach($ULCLs as $sl => $ULCL)
        {
            list($UCL, $LCL) = $ULCL;

            if($standard === null)
            {
                $value = is_array($UCL) ? array_filter($UCL) : array($UCL);
                $mean  = is_array($CL) ? array_filter($CL) : array($CL);
                $value = end($value);
                $mean  = end($mean);
                $standard = ($value - $mean) / $sl;
            }

            $UCLname = $sl == 3 ? "+{$sl}SL/UCL" : "+{$sl}SL";
            $LCLname = $sl == 3 ? "-{$sl}SL/LCL" : "-{$sl}SL";

            list($annoUCL, $UCLdata) = self::buildLimitLine($UCL, $UCLname, $pointCount, $lineBegin, $lineEnd, $style->UCL, [$annotationPos, 'bottom']);
            list($annoLCL, $LCLdata) = self::buildLimitLine($LCL, $LCLname, $pointCount, $lineBegin, $lineEnd, $style->LCL, [$annotationPos, 'top']);

            $annotations[] = $annoUCL;
            $annotations[] = $annoLCL;

            $data[] = $UCLdata;
            $data[] = $LCLdata;
        }

        if(!empty($tests))
        {
            foreach($tests as $rule => $pointIndexes)
            {
                $indexes = array();
                $values  = array();
                foreach ($pointIndexes as $index)
                {
                    $index += 1;
                    $indexes[] = $index;
                    $values[]  = $points[$index];
                }

                $testData = array();
                $testData['x']          = $indexes;
                $testData['y']          = $values;
                $testData['mode']       = 'markers';
                $testData['name']       = "{$dasLang->spc->tests}$rule";
                $testData['marker']     = array('color' => '#fb2b2b', 'size' => $pointSize + 6, 'line' => array('width' => 6, 'color' => '#d8dbde'));
                $testData['showlegend'] = true;

                $data[] = $testData;
            }
        }

        $result->tests          = $tests;
        $result->xaxisName      = '';
        $result->yaxisName      = $name;
        $result->data['data']   = json_encode($data);

        $mean = round($mean, 2);
        $standard = round($standard, 2);

        $layout = array();
        $layout['title']       = array('text' => "$title <br> ($CLName: $mean  {$dasLang->spc->sd}: $standard)", 'yanchor' => 'top', 'y' => 0.80, 'font' => array('size' => 12));
        $layout['xaxis']       = array('title' => $xTitle, 'dtick' => 1);
        $layout['yaxis']       = array('title' => $yTitle);
        $layout['annotations'] = $annotations;

        $result->data['layout'] = $layout;

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

        return $result;
    }



    /**
     * Build tests results
     * @param array  $testsResults
     * @param string $title
     */
    public static function buildTestsResult($testsResults, $title = '', $cols = array())
    {
        global $dasLang;

        $html = '';
        $allPoints = array();
        if(empty($testsResults))
        {
            $html .= "<p class='text-muted'>{$dasLang->spc->noOutlier}</p>";
        }
        else
        {
            foreach($testsResults as $rule => $points)
            {
                $points = array_map(function ($val) {
                    return $val + 1;
                }, $points);
                $ruleName = $dasLang->spc->testsRules[$rule];
                $html .= "<p>{$dasLang->spc->pointsNotPassTestRule} {$rule}：<u>{$ruleName}</u></p>";
                $html .= '<pre>' . implode(', ', $points) . '</pre>';

                $allPoints = array_merge($allPoints, $points);
            }
        }

        if(!is_array($cols)) $cols = array($cols);
        $highlight = self::setHighlight($allPoints, $cols);
        $html = "<h4>{$title} {$dasLang->spc->testsResults} $highlight</h4>" . $html;

        $textResult = new stdclass();

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

    /**
     * setHighlight
     *
     * @param  array    $points
     * @param  array    $cols
     * @static
     * @access private
     * @return string
     */
    private static function setHighlight($points, $cols)
    {
        if(empty($points)) return '';

        $colRows = array();
        $points = array_unique($points);
        $points = array_values($points);
        foreach($cols as $col) $colRows[$col] = $points;

        return getHighlight($colRows);
    }

    /**
     * Check spc tests rules
     * @see https://support.minitab.com/zh-cn/minitab/19/help-and-how-to/quality-and-process-improvement/control-charts/how-to/variables-charts-for-subgroups/xbar-r-chart/perform-the-analysis/xbar-r-options/select-tests-for-special-causes/
     * @param array $points
     * @param array $stdULCLs
     * @param array $tests
     * @return array Array of rule id to points indexes
     */
    public static function checkTestsRules($points, $stdULCLs, $tests)
    {
        $results = array();
        if(!is_array($tests) || empty($tests)) return $results;

        foreach($tests as $testRule)
        {
            $checkFuncName  = "checkTestRule$testRule";
            if(!method_exists(__CLASS__, $checkFuncName)) continue;
            $indexesNotPass = self::$checkFuncName($points, $stdULCLs);
            if(is_array($indexesNotPass) && !empty($indexesNotPass))
            {
                sort($indexesNotPass);
                $results[$testRule] = $indexesNotPass;
            }
        }

        return $results;
    }

    /**
     * Test Rule 1: 1 point > 3 standard deviations from center line
     * 1个点距离中心线超过3个标准差
     * @param array $points
     * @param array $stdULCLs
     * @return array return points indexes list which not pass the test rule
     */
    public static function checkTestRule1($points, $stdULCLs)
    {
        $indexes = array();
        $UCL3    = $stdULCLs['3'][0];
        $LCL3    = $stdULCLs['3'][1];
        foreach($points as $index => $value)
        {
            $subUCL3 = is_array($UCL3) ? $UCL3[$index] : $UCL3;
            $subLCL3 = is_array($LCL3) ? $LCL3[$index] : $LCL3;
            if($value > $subUCL3 || $value < $subLCL3) $indexes[] = $index;
        }
        return $indexes;
    }

    /**
     * Test Rule 2: 9 points in a row on same side of center line
     * 9个连续的点在中心线同侧
     * @param array $points
     * @param array $stdULCLs
     * @return array return points indexes list which not pass the test rule
     */
    public static function checkTestRule2($points, $stdULCLs)
    {
        if(count($points) < 9) return;

        $indexes         = array();
        $CL              = $stdULCLs['C'];
        $lastSide        = '';
        $sameSideIndexes = array();
        foreach($points as $index => $value)
        {
            $subCL = is_array($CL) ? $CL[$index] : $CL;
            $side  = $value > $subCL ? 'up' : 'low';
            if(empty($lastSide)) $lastSide = $side;

            if($lastSide == $side)
            {
                $sameSideIndexes[] = $index;
                if(count($sameSideIndexes) === 9) $indexes = array_merge($indexes, $sameSideIndexes);
                if(count($sameSideIndexes) > 9)   $indexes[] = $index;
            }
            else
            {
                $lastSide = $side;
                $sameSideIndexes = array($index);
            }
        }
        return $indexes;
    }

    /**
     * Test Rule 3: 2 out of 3 points > 2 standard deviations from center line (same side)
     * 3个连续的点，至少有2个点落在中心线同侧，且距离中心线超过2个标准差
     * @param array $points
     * @param array $stdULCLs
     * @return array return points indexes list which not pass the test rule
     */
    public static function checkTestRule3($points, $stdULCLs, $stdDev = '2', $continuesPoints = 3, $sameSideOutPoints = 2)
    {
        $pointsCount = count($points);
        if($pointsCount < $continuesPoints) return;

        $indexes    = array();
        $CL         = $stdULCLs['C'];
        $UCL        = $stdULCLs[$stdDev][0];
        $LCL        = $stdULCLs[$stdDev][1];
        $pointsInfo = array();

        foreach($points as $index => $value)
        {
            $subCL     = is_array($CL) ? $CL[$index] : $CL;
            $subUCL    = is_array($UCL) ? $UCL[$index] : $UCL;
            $subLCL    = is_array($LCL) ? $LCL[$index] : $LCL;
            $side      = $value > $subCL ? 'up' : 'low';
            $outOfLine = $side == 'up' ? ($value > $subUCL) : ($value < $subLCL);
            $pointsInfo[$index] = array('side' => $side, 'out' => $outOfLine);
        }

        for($i = $continuesPoints - 1; $i < $pointsCount; $i++)
        {
            $upIndexes  = array();
            $lowIndexes = array();
            $allIndexes = array();
            for($j = 0; $j < $continuesPoints; $j++)
            {
                $index = $i - $j;
                $info  = $pointsInfo[$index];

                $allIndexes[] = $index;
                if(!$info['out'])              continue;
                if($info['side'] == 'up')      $upIndexes[]  = $index;
                elseif($info['side'] == 'low') $lowIndexes[] = $index;
            }
            if(count($upIndexes)  >= $sameSideOutPoints)     $indexes = array_merge($indexes, $allIndexes);
            elseif(count($lowIndexes) >= $sameSideOutPoints) $indexes = array_merge($indexes, $allIndexes);
        }

        return array_values(array_unique($indexes));
    }

    /**
     * Test Rule 4: 4 out of 5 points > 1 standard deviations from center line (same side)
     * 5个连续的点，至少有4个点落在中心线同侧，且距离中心线超过1个标准差
     * @param array $points
     * @param array $stdULCLs
     * @return array return points indexes list which not pass the test rule
     */
    public static function checkTestRule4($points, $stdULCLs)
    {
        return self::checkTestRule3($points, $stdULCLs, '1', 5, 4);
    }

    /**
     * Test Rule 5: 6 points in a row, all increasing or all decreasing
     * 6个连续的点，递增或递减
     * @param array $points
     * @param array $stdULCLs
     * @return array return points indexes list which not pass the test rule
     */
    public static function checkTestRule5($points, $stdULCLs)
    {
        if(count($points) < 6) return;

        $indexes          = array();
        $lastTrend        = '';
        $sameTrandIndexes = array();
        $lastValue        = NULL;
        foreach($points as $index => $value)
        {
            if(!$index)
            {
                $lastValue = $value;
                continue;
            }
            $newTrand = $value > $lastValue ? 'increasing' : 'decreasing';
            if($lastTrend == $newTrand)
            {
                $sameTrandIndexes[] = $index;
                if(count($sameTrandIndexes) === 6) $indexes = array_merge($indexes, $sameTrandIndexes);
                if(count($sameTrandIndexes) > 6)   $indexes[] = $index;
            }
            else
            {
                $sameTrandIndexes = array($index);
            }
            $lastValue = $value;
            $lastTrend = $newTrand;
        }
        return $indexes;
    }

    /**
     * Test Rule 6: 14 points in a row, alternating up and down
     * 14个连续的点，相邻点上下交替
     * @param array $points
     * @param array $stdULCLs
     * @return array return points indexes list which not pass the test rule
     */
    public static function checkTestRule6($points, $stdULCLs)
    {
        if(count($points) < 14) return;

        $CL               = $stdULCLs['C'];
        $indexes          = array();
        $lastSide         = '';
        $sameSideIndexes  = array();
        foreach($points as $index => $value)
        {
            $subCL   = is_array($CL) ? $CL[$index] : $CL;
            $newSide = $value > $subCL ? 'up' : 'low';
            if($lastSide != $newSide)
            {
                $sameSideIndexes[] = $index;
                if(count($sameSideIndexes) === 14) $indexes = array_merge($indexes, $sameSideIndexes);
                if(count($sameSideIndexes) > 14)   $indexes[] = $index;
            }
            else
            {
                $sameSideIndexes = array($index);
            }
            $lastSide  = $newSide;
        }
        return $indexes;
    }

    /**
     * Test Rule 7: 15 points in a row within 1 standard deviation of center line
     * 15个连续的点，落在中心线两侧，且距离中心线1个标准差以内
     * @param array $points
     * @param array $stdULCLs
     * @return array return points indexes list which not pass the test rule
     */
    public static function checkTestRule7($points, $stdULCLs, $continuesPoints = 15, $stdDev = '1', $devSide = 'in')
    {
        $pointsCount = count($points);
        if($pointsCount < $continuesPoints) return;

        $indexes    = array();
        $CL         = $stdULCLs['C'];
        $UCL        = $stdULCLs[$stdDev][0];
        $LCL        = $stdULCLs[$stdDev][1];
        $pointsInfo = array();

        foreach($points as $index => $value)
        {
            $subCL              = is_array($CL) ? $CL[$index] : $CL;
            $subUCL             = is_array($UCL) ? $UCL[$index] : $UCL;
            $subLCL             = is_array($LCL) ? $LCL[$index] : $LCL;
            $side               = $value > $CL ? 'up' : 'low';
            $outOfDevSide       = ($side == 'up' ? ($value > $subUCL) : ($value < $subLCL));
            $pointsInfo[$index] = array('side' => $side, 'out' => $outOfDevSide, 'in' => !$outOfDevSide);
        }

        for($i = $continuesPoints - 1; $i < $pointsCount; $i++)
        {
            $upIndexes  = array();
            $lowIndexes = array();
            for($j = 0; $j < $continuesPoints; $j++)
            {
                $index = $i - $j;
                $info  = $pointsInfo[$index];

                if(!$info[$devSide])           break;
                if($info['side'] == 'up')      $upIndexes[] = $index;
                elseif($info['side'] == 'low') $lowIndexes[] = $index;
            }
            if(!empty($upIndexes) && !empty($lowIndexes) && (count($upIndexes) + count($lowIndexes)) === $continuesPoints) $indexes = array_merge($indexes, $upIndexes, $lowIndexes);
        }

        return array_values(array_unique($indexes));
    }

    /**
     * Test Rule 8: 8 points in a row > 1 standard deviation from center line (either side)
     * 8个连续的点，落在中心线两侧，且距离中心线1个标准差以外
     * @param array $points
     * @param array $stdULCLs
     * @return array return points indexes list which not pass the test rule
     */
    public static function checkTestRule8($points, $stdULCLs)
    {
        return self::checkTestRule7($points, $stdULCLs, 8, '1', 'out');
    }

    /**
     * Add MR datas to dataframe.
     *
     * @param object $dataframe
     * @param string $dataframe
     * @access public
     * @return object
     */
    public static function addMRDatas($dataframe, $vars)
    {
        foreach($vars as $var)
        {
            $dataframe = self::addMRData($dataframe, $var);
        }
        return $dataframe;
    }

    /**
     * Add MR data to dataframe.
     *
     * @param object $dataframe
     * @param string $key
     * @access public
     * @return object
     */
    public static function addMRData($dataframe, $var)
    {
        global $dasLang;
        $key = 'mr' . $var;

        $type = 'number';
        $mrs = current($dataframe->SPCMR(array($var), $type));

        $mrIndex = 0;
        $data = [];
        foreach($mrs as $i => $mr)
        {
            $data[$mrIndex] = null;
            $mrIndex += 1;
            foreach($mr as $mrValue)
            {
                $data[$mrIndex] = $mrValue;
                $mrIndex += 1;
            }

            if(count($mr) == 0 && $i + 1 == count($mrs))
            {
                $data[$mrIndex] = null;
                $mrIndex += 1;
            }
        }

        return self::addData($dataframe, $key, $dasLang->IMR->range, $data);
    }

    /**
     * Add data to dataframe.
     *
     * @param object $dataframe
     * @param string $key
     * @access public
     * @return object
     */
    public static function addData($dataframe, $key, $name, $data)
    {
        global $dasLang;
        $dataframe->columns[$key] = $name;

        foreach(array_values($data) as $index => $item)
        {
            $dataframe->data[$index][$key] = $item;
        }

        return $dataframe;
    }

    /**
     * Get limit line data from limit param.
     *
     * @param string|array $cl
     * @param int          $count
     * @param int          $start
     * @param int          $end
     * @access private
     * @return array
     */
    private static function getLimitData($cl, $count, $start, $end)
    {
        if(!is_array($cl)) $cl = array_fill(0, $count, $cl);

        $x = array();
        $y = array();
        foreach($cl as $i => $item)
        {
            // copy every item.
            $y[] = $item;
            $y[] = $item;

            // copy index
            $x[] = max(1, $i - 0.5 + $start);
            $x[] = min($i + 0.5 + $start, $end);
        }

        $notNull = array_filter($cl);
        $value   = end($notNull);

        return array($x, $y, $value);
    }

    /**
     * Build limit line data config for chart.
     *
     * @param string|array $cl
     * @param string       $name
     * @param int          $count
     * @param int          $start
     * @param int          $end
     * @param array        $lineStyle
     * @access private
     * @return array
     */
    private static function buildLimitLine($cl, $name, $count, $start, $end, $lineStyle, $anchor = ['middle', 'bottom'])
    {
        list($x, $y, $value) = self::getLimitData($cl, $count, $start, $end);

        $annotation = array();
        $annotation['x']         = $end;
        $annotation['y']         = $value;
        $annotation['xanchor']   = $anchor[0];
        $annotation['yanchor']   = $anchor[1];
        $annotation['text']      = $name . ':' . round($value, 2);
        $annotation['showarrow'] = false;

        $data = array();
        $data['x']    = $x;
        $data['y']    = $y;
        $data['type'] = 'scatter';
        $data['mode'] = 'lines';
        $data['name'] = $name;
        $data['line'] = $lineStyle;

        return array($annotation, $data);
    }

    /**
     * Build ref line data config for chart.
     *
     * @param string|array $ref
     * @param int          $start
     * @param int          $end
     * @param array        $lineStyle
     * @access private
     * @return array
     */
    private static function buildRefLine($ref, $start, $end, $lineStyle, $anchor = ['middle', 'bottom'])
    {
        global $dasLang;
        $annotation = array();
        $annotation['x']         = $end;
        $annotation['y']         = $ref;
        $annotation['xanchor']   = $anchor[0];
        $annotation['yanchor']   = $anchor[1];
        $annotation['text']      = round($ref, 2);
        $annotation['showarrow'] = false;

        $data = array();
        $data['x']    = array($start, $end);
        $data['y']    = array($ref, $ref);
        $data['type'] = 'scatter';
        $data['mode'] = 'lines';
        $data['name'] = $dasLang->spc->ref. ':' . $ref;
        $data['line'] = $lineStyle;

        return array($annotation, $data);
    }
}
