<?php

/**
 * Matomo - free/libre analytics platform
 *
 * @link    https://matomo.org
 * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 */

namespace Piwik\Plugins\CoreHome\DataTableRowAction;

use Exception;
use Piwik\API\DataTablePostProcessor;
use Piwik\API\Request;
use Piwik\Common;
use Piwik\DataTable\DataTableInterface;
use Piwik\DataTable\Map;
use Piwik\Date;
use Piwik\Metrics;
use Piwik\NumberFormatter;
use Piwik\Period\Factory as PeriodFactory;
use Piwik\Piwik;
use Piwik\Plugin\ViewDataTable;
use Piwik\Plugins\API\Filter\DataComparisonFilter;
use Piwik\Plugins\CoreVisualizations\Visualizations\Graph\Config as GraphConfig;
use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph\Config as JqplotGraphConfig;
use Piwik\Plugins\CoreVisualizations\Visualizations\JqplotGraph\Evolution as EvolutionViz;
use Piwik\ViewDataTable\Factory;
use Piwik\ViewDataTable\Manager as ViewDataTableManager;

/**
 * ROW EVOLUTION
 * The class handles the popover that shows the evolution of a single row in a data table
 */
class RowEvolution
{
    /**
     * The current site id
     * @var int
     */
    protected $idSite;

    /**
     * The api method to get the data. Format: Plugin.apiAction
     * @var string
     */
    protected $apiMethod;

    /**
     * The label of the requested row
     * @var string
     */
    protected $label;

    /**
     * The requested period
     * @var string
     */
    protected $period;

    /**
     * The requested date
     * @var Date
     */
    protected $date;

    /**
     * The request segment
     * @var string
     */
    protected $segment;

    /**
     * The metrics that are available for the requested report and period
     * @var array<string, array{name: string, min?: number, max?: number, change?: string, logo?: string}>
     */
    protected $availableMetrics;

    /**
     * The name of the dimension of the current report
     * @var string
     */
    protected $dimension;

    /**
     * The data
     * @var Map
     */
    protected $dataTable;

    /**
     * The label of the current record
     * @var string
     */
    protected $rowLabel;

    /**
     * The icon of the current record
     * @var string|false
     */
    protected $rowIcon;

    /**
     * The type of graph that has been requested last
     * @var string
     */
    protected $graphType;

    /**
     * The metrics for the graph that has been requested last
     * @var array<string, array{name: string, min?: number, max?: number, change?: string, logo?: string}>
     */
    protected $graphMetrics;

    /**
     * Whether or not to show all metrics in the evolution graph when to popover opens
     * @var bool
     */
    protected $initiallyShowAllMetrics = false;

    /**
     * The constructor
     * Initialize some local variables from the request
     * @param int $idSite
     * @param Date $date ($this->date from controller)
     * @param string $graphType
     * @throws Exception
     */
    public function __construct($idSite, $date, $graphType = 'graphEvolution')
    {
        $this->apiMethod = Common::getRequestVar('apiMethod', '', 'string');
        if (empty($this->apiMethod)) {
            throw new Exception("Parameter apiMethod not set.");
        }

        $label = DataTablePostProcessor::getLabelFromRequest($_GET);
        if (!is_array($label)) {
            throw new Exception("Expected label to be an array, got instead: " . $label);
        }
        $this->label = Common::unsanitizeInputValue($label[0]);

        if ($this->label === '') {
            throw new Exception("Parameter label not set.");
        }

        $this->period = Common::getRequestVar('period', '', 'string');
        PeriodFactory::checkPeriodIsEnabled($this->period);

        if ($this->period != 'range' && !($date instanceof Date)) {
            throw new Exception("Expected date to be an instance of \\Piwik\\Date");
        }

        $this->idSite = $idSite;
        $this->graphType = $graphType;

        if ($this->period != 'range') {
            // handle day, week, month and year: display last X periods
            //handle cache if exist
            $cache = ViewDataTableManager::getViewDataTableParameters(
                Piwik::getCurrentUserLogin(),
                'CoreHome.getRowEvolutionGraph'
            );
            $lastDay = (isset($cache['evolution_' . $this->period . '_last_n']) ? $cache['evolution_' . $this->period . '_last_n'] : null);
            $end = $date->toString();
            [$this->date, $lastN] = EvolutionViz::getDateRangeAndLastN($this->period, $end, $lastDay);
        }
        $this->segment = \Piwik\API\Request::getRawSegmentFromRequest();

        $this->loadEvolutionReport();
    }

    /**
     * Render the popover
     * @param \Piwik\Plugins\CoreHome\Controller $controller
     * @param \Piwik\View (the popover_rowevolution template)
     * @return string
     */
    public function renderPopover($controller, $view)
    {
        // render main evolution graph
        $this->graphType = 'graphEvolution';
        $this->graphMetrics = $this->availableMetrics;
        $view->graph = $this->getRowEvolutionGraphFromController($controller);

        // render metrics overview
        $view->metrics = $this->getMetricsToggles();

        // available metrics text
        $metricsText = Piwik::translate('RowEvolution_AvailableMetrics');
        $popoverTitle = '';
        if ($this->rowLabel) {
            $icon = $this->rowIcon ? '<img height="16px" src="' . $this->rowIcon . '" alt="">' : '';
            $rowLabel = str_replace('/', '<wbr>/', str_replace('&', '<wbr>&', $this->rowLabel));
            $metricsText = sprintf(Piwik::translate('RowEvolution_MetricsFor'), $this->dimension . ': ' . $icon . ' ' . $rowLabel);
            $popoverTitle = $icon . ' ' . $this->rowLabel;
        }

        $view->availableMetricsText = $metricsText;
        $view->popoverTitle = $popoverTitle;

        return $view->render();
    }

    /**
     * @param string|false|null $column
     * @return void
     */
    protected function loadEvolutionReport($column = false)
    {
        [$apiModule, $apiAction] = explode('.', $this->apiMethod);

        // getQueryStringFromParameters expects sanitised query parameter values
        $parameters = array(
            'method'    => 'API.getRowEvolution',
            'label'     => $this->label,
            'apiModule' => $apiModule,
            'apiAction' => $apiAction,
            'idSite'    => $this->idSite,
            'period'    => $this->period,
            'date'      => $this->date,
            'format'    => 'original',
            'serialize' => '0',
        );

        if (!empty($this->segment)) {
            $parameters['segment'] = $this->segment;
        }

        if ($column !== false) {
            $parameters['column'] = $column;
        }

        $unmodifiedSeriesLabels = [];
        $isComparing = DataComparisonFilter::isCompareParamsPresent();
        if ($isComparing) {
            $compareDates = Common::getRequestVar('compareDates', [], 'array');
            $comparePeriods = Common::getRequestVar('comparePeriods', [], 'array');
            $compareSegments = Common::getRequestVar('compareSegments', [], 'array');

            $totalSeriesCount = (count($compareSegments) + 1) * (count($comparePeriods) + 1);

            for ($i = 0; $i < $totalSeriesCount; ++$i) {
                $unmodifiedSeriesLabels[] = DataComparisonFilter::getPrettyComparisonLabelFromSeriesIndex($i);
            }

            $parameters['compare'] = 1;

            foreach ($comparePeriods as $index => $period) {
                $date = $compareDates[$index];

                if ($period == 'range') {
                    $comparePeriods[$index] = 'day';
                } else {
                    [$newDate, $lastN] = EvolutionViz::getDateRangeAndLastN($period, $date);
                    $compareDates[$index] = $newDate;
                }
            }

            $parameters['compareDates'] = $compareDates;
            $parameters['comparePeriods'] = $comparePeriods;
        }

        $parameters = array_filter($parameters, function ($value) {
            return $value !== null && $value !== false;
        });

        /** @var array{label: string, reportData: Map, metadata: array{metrics: array, dimension: string, columns: array}} $report */
        $report = Request::processRequest('API.getRowEvolution', $parameters);

        // at this point the report data will reference the comparison series labels for the changed compare periods/dates. We don't
        // want to show this to users because they will not recognize the changed periods, so we have to replace them.
        if ($isComparing) {
            $dataTables = $report['reportData']->getDataTables();
            $modifiedSeriesLabels = reset($dataTables)->getMetadata('comparisonSeries');
            $seriesMap = array_combine($modifiedSeriesLabels, $unmodifiedSeriesLabels);

            foreach ($report['metadata']['metrics'] as $key => $metricInfo) {
                foreach ($seriesMap as $modified => $unmodified) {
                    $report['metadata']['metrics'][$key]['name'] = str_replace($modified, $unmodified, $report['metadata']['metrics'][$key]['name']);
                }
            }
        }

        $this->extractEvolutionReport($report);
    }

    /**
     * @param array $report
     * @return void
     */
    protected function extractEvolutionReport($report)
    {
        $this->dataTable = $report['reportData'];
        $this->rowLabel = $this->extractPrettyLabel($report);
        $this->rowIcon = !empty($report['logo']) ? $report['logo'] : false;
        $this->availableMetrics = $report['metadata']['metrics'];
        $this->dimension = $report['metadata']['dimension'];
    }

    /**
     * Generic method to get an evolution graph or a sparkline for the row evolution popover.
     * Do as much as possible from outside the controller.
     * @param string|bool $graphType
     * @param array|bool $metrics
     * @return ViewDataTable
     */
    public function getRowEvolutionGraph($graphType = false, $metrics = false)
    {
        // set up the view data table
        $view = Factory::build(
            $graphType ? : $this->graphType,
            $this->apiMethod,
            $controllerAction = 'CoreHome.getRowEvolutionGraph',
            $forceDefault = true
        );
        $view->setDataTable($this->dataTable);

        if (!empty($this->graphMetrics)) { // In row Evolution popover, this is empty
            $view->config->columns_to_display = array_keys($metrics ? : $this->graphMetrics);
        }

        $view->requestConfig->request_parameters_to_modify['label'] = '';
        $view->config->show_goals = false;
        $view->config->show_search = false;
        $view->config->show_all_views_icons = false;
        $view->config->show_related_reports  = false;
        $view->config->show_footer_message   = false;

        foreach ($this->availableMetrics as $metric => $metadata) {
            $view->config->translations[$metric] = $metadata['name'];
        }

        if ($view->config instanceof GraphConfig) {
            $view->config->show_series_picker = false;
        }

        if ($view->config instanceof JqplotGraphConfig) {
            $view->config->external_series_toggle          = 'RowEvolutionSeriesToggle';
            $view->config->external_series_toggle_show_all = $this->initiallyShowAllMetrics;
        }

        return $view;
    }

    /**
     * Prepare metrics toggles with spark lines
     * @return array
     */
    protected function getMetricsToggles()
    {
        $i = 0;
        $metrics = array();
        foreach ($this->availableMetrics as $metric => $metricData) {
            $unit = Metrics::getUnit($metric, $this->idSite);
            $change = isset($metricData['change']) ? $metricData['change'] : false;

            [$first, $last] = $this->getFirstAndLastDataPointsForMetric($metric);
            $fractionDigits = max($this->getFractionDigits($first), $this->getFractionDigits($last));

            $details = Piwik::translate('RowEvolution_MetricBetweenText', array(
                NumberFormatter::getInstance()->format($first, $fractionDigits) . $unit,
                NumberFormatter::getInstance()->format($last, $fractionDigits) . $unit,
            ));

            if ($change !== false) {
                $lowerIsBetter = Metrics::isLowerValueBetter($metric);
                if (substr($change, 0, 1) == '+') {
                    $changeClass = $lowerIsBetter ? 'bad' : 'good';
                    $changeImage = $lowerIsBetter ? 'arrow_up_red' : 'arrow_up';
                } elseif (substr($change, 0, 1) == '-') {
                    $changeClass = $lowerIsBetter ? 'good' : 'bad';
                    $changeImage = $lowerIsBetter ? 'arrow_down_green' : 'arrow_down';
                } else {
                    $changeClass = 'neutral';
                    $changeImage = false;
                }

                $change = '<span class="' . $changeClass . '">'
                    . ($changeImage ? '<img src="plugins/MultiSites/images/' . $changeImage . '.png" /> ' : '')
                    . $change . '</span>';

                $details .= ', ' . Piwik::translate('RowEvolution_MetricChangeText', $change);
            }

            // set metric min/max text (used as tooltip for details)
            $max = isset($metricData['max']) ? $metricData['max'] : 0;
            $min = isset($metricData['min']) ? $metricData['min'] : 0;
            $minmax = Piwik::translate('RowEvolution_MetricMinMax', array(
                $metricData['name'],
                NumberFormatter::getInstance()->formatNumber($min, $fractionDigits, $fractionDigits) . $unit,
                NumberFormatter::getInstance()->formatNumber($max, $fractionDigits, $fractionDigits) . $unit,
            ));

            $newMetric = array(
                'label'     => $metricData['name'],
                'details'   => $details,
                'minmax'    => $minmax,
                'sparkline' => $this->getSparkline($metric),
            );
            // Multi Rows, each metric can be for a particular row and display an icon
            if (!empty($metricData['logo'])) {
                $newMetric['logo'] = $metricData['logo'];
            }

            // TODO: this check should be determined by metric metadata, not hardcoded here
            if (
                $metric == 'nb_users'
                && $first == 0
                && $last == 0
            ) {
                $newMetric['hide'] = true;
            }

            $metrics[] = $newMetric;
            $i++;
        }

        return $metrics;
    }

    /**
     * Get the img tag for a sparkline showing a single metric
     *
     * @param string $metric
     * @return string
     */
    protected function getSparkline($metric)
    {
        // sparkline is always echoed, so we need to buffer the output
        $view = $this->getRowEvolutionGraph($graphType = 'sparkline', $metrics = array($metric => $metric));

        ob_start();
        $view->render();
        $spark = ob_get_contents();
        ob_end_clean();

        // undo header change by sparkline renderer
        Common::sendHeader('Content-type: text/html');

        // base64 encode the image and put it in an img tag
        $spark = base64_encode($spark);
        return '<img width="100" height="25" src="data:image/png;base64,' . $spark . '" />';
    }

    /**
     * Use the available metrics for the metrics of the last requested graph.
     * @return void
     */
    public function useAvailableMetrics()
    {
        $this->graphMetrics = $this->availableMetrics;
    }

    /**
     * @param string|int $metric
     * @return array<int|float>
     */
    private function getFirstAndLastDataPointsForMetric($metric)
    {
        $first = 0;
        $firstTable = $this->dataTable->getFirstRow();
        if (!empty($firstTable)) {
            $row = $firstTable->getFirstRow();
            if (!empty($row)) {
                $first = floatval($row->getColumn($metric));
            }
        }

        $last = 0;
        $lastTable = $this->dataTable->getLastRow();
        if (!empty($lastTable)) {
            $row = $lastTable->getFirstRow();
            if (!empty($row)) {
                $last = floatval($row->getColumn($metric));
            }
        }

        return array($first, $last);
    }

    /**
     * @param array $report
     * @return string
     */
    protected function extractPrettyLabel($report)
    {
        // By default, use the specified label
        $rowLabel = Common::sanitizeInputValue($report['label']);

        /** @var \Piwik\DataTable\Map $dataTableMap */
        $dataTableMap = $report['reportData'];

        // If the dataTable specifies a label_html, use this instead
        if ($dataTableMap instanceof DataTableInterface) {
            $labelPretty = $dataTableMap->getColumn('label_html');
            $labelPretty = array_filter($labelPretty, 'strlen');
            $labelPretty = current($labelPretty);
            if (!empty($labelPretty)) {
                return $labelPretty;
            }
        }
        return $rowLabel;
    }

    /**
     * @param string|int|float $value
     */
    private function getFractionDigits($value): int
    {
        $value = (string) $value;
        $fraction = substr(strrchr($value, ".") ?: '', 1);
        return strlen($fraction);
    }

    /**
     * @return string|null
     */
    protected function getRowEvolutionGraphFromController(\Piwik\Plugins\CoreHome\Controller $controller)
    {
        return $controller->getRowEvolutionGraph($fetch = true, $rowEvolution = $this);
    }
}
