<?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\CoreVisualizations\Visualizations;

use Piwik\Common;
use Piwik\DataTable;
use Piwik\Plugin\Metric;
use Piwik\Plugins\AbTesting\Columns\Metrics\ProcessedMetric;
use Piwik\Plugins\CoreVisualizations\Metrics\Formatter\Numeric;
use Piwik\Piwik;
use Piwik\Plugin\Visualization;
use Piwik\SettingsPiwik;

/**
 * This is an abstract visualization that should be the base of any 'graph' visualization.
 * This class defines certain visualization properties that are specific to all graph types.
 * Derived visualizations can decide for themselves whether they should support individual
 * properties.
 *
 * @property Graph\Config $config
 */
abstract class Graph extends Visualization
{
    public const ID = 'graph';

    public $selectableRows = array();

    public static function getDefaultConfig()
    {
        return new Graph\Config();
    }

    public static function getDefaultRequestConfig()
    {
        $config = parent::getDefaultRequestConfig();
        $config->addPropertiesThatShouldBeAvailableClientSide(array('columns'));

        return $config;
    }

    public function beforeRender()
    {
        if ($this->config->show_goals) {
            $this->config->translations['nb_conversions'] = Piwik::translate('Goals_ColumnConversions');
            $this->config->translations['revenue'] = Piwik::translate('General_TotalRevenue');
        }
    }

    public function beforeLoadDataTable()
    {
        // TODO: this should not be required here. filter_limit should not be a view property, instead HtmlTable should use 'limit' or something,
        //       and manually set request_parameters_to_modify['filter_limit'] based on that. (same for filter_offset).
        $this->requestConfig->request_parameters_to_modify['filter_limit'] = false;

        if ($this->config->max_graph_elements) {
            $this->requestConfig->request_parameters_to_modify['filter_truncate'] = $this->config->max_graph_elements - 1;
        }

        // Only default to formatting metrics if the request hasn't already been set to not format metrics
        if (!isset($this->requestConfig->request_parameters_to_modify['format_metrics'])) {
            $this->requestConfig->request_parameters_to_modify['format_metrics'] = 1;
        }

        // if addTotalRow was called in GenerateGraphHTML, add a row containing totals of
        // different metrics
        if ($this->config->add_total_row) {
            $this->requestConfig->request_parameters_to_modify['totals'] = 1;
            $this->requestConfig->request_parameters_to_modify['keep_totals_row'] = 1;
            $this->requestConfig->request_parameters_to_modify['keep_totals_row_label'] = Piwik::translate('General_Total');
        }

        if (!empty($this->config->columns_to_display)) {
            $metrics = $this->removeUnavailableMetrics($this->config->columns_to_display);
            if (empty($metrics)) {
                if (!empty($this->config->selectable_columns)) {
                    $this->config->columns_to_display = array(reset($this->config->selectable_columns));
                } else {
                    $this->config->columns_to_display = array('nb_visit');
                }
                $this->requestConfig->request_parameters_to_modify['columns'] = 'nb_visits';
                $this->requestConfig->request_parameters_to_modify['columns_to_display'] = 'nb_visits';
            }
        }

        $this->metricsFormatter = new Numeric();
    }

    /**
     * Determines what rows are selectable and stores them in the selectable_rows property in
     * a format the SeriesPicker JavaScript class can use.
     */
    public function determineWhichRowsAreSelectable(): void
    {
        if ($this->config->row_picker_match_rows_by === false) {
            return;
        }

        // collect all selectable rows
        $self = $this;

        $this->dataTable->filter(function ($dataTable) use ($self) {

            $identifier = $self->config->row_picker_match_rows_by;

            /** @var DataTable $dataTable */
            foreach ($dataTable->getRows() as $row) {
                $rowLabel = $row->getColumn('label');
                $rowIdentifier = $row->hasColumn($identifier) ? $row->getColumn($identifier) : $row->getMetadata($identifier);

                if (false === $rowLabel || false === $rowIdentifier) {
                    continue;
                }

                $rowIdentifier = (string) $rowIdentifier; // ensure we always have the same type

                // build config
                if (!isset($self->selectableRows[$rowIdentifier])) {
                    $self->selectableRows[$rowIdentifier] = [
                        'label'     => $rowLabel,
                        'matcher'   => $rowIdentifier,
                        'displayed' => $self->isRowVisible($rowLabel, $rowIdentifier)
                    ];
                }
            }
        });
    }

    public function isRowVisible($rowLabel, $rowIdentifier): bool
    {
        if (false !== $this->config->row_picker_match_rows_by) {
            return is_array($this->config->rows_to_display) &&
                (in_array($rowLabel, $this->config->rows_to_display) || in_array($rowIdentifier, $this->config->rows_to_display));
        }

        return true;
    }

    /**
     * Defaults the selectable_columns property if it has not been set and then transforms
     * it into something the SeriesPicker JavaScript class can use.
     */
    public function afterAllFiltersAreApplied()
    {
        $this->determineWhichRowsAreSelectable();

        // set default selectable columns, if none specified
        $selectableColumns = $this->config->selectable_columns;
        if (false === $selectableColumns) {
            $this->generateSelectableColumns();
        }

        $this->ensureValidColumnsToDisplay();

        $this->addTranslations();

        $this->config->selectable_rows = array_values($this->selectableRows);
    }

    protected function addTranslations(): void
    {
        if ($this->config->add_total_row) {
            $totalTranslation = Piwik::translate('General_Total');
            $this->selectableRows['total'] = [
                'label'     => $totalTranslation,
                'matcher'   => 'total',
                'displayed' => $this->isRowVisible($totalTranslation, 'total')
            ];
        }

        if ($this->config->show_goals) {
            $this->config->addTranslations([
                'nb_conversions' => Piwik::translate('Goals_ColumnConversions'),
                'revenue'        => Piwik::translate('General_TotalRevenue')
            ]);
        }

        $transformed = [];
        foreach ($this->config->selectable_columns as $column) {
            $transformed[] = [
                'column'      => $column,
                'translation' => @$this->config->translations[$column],
                'displayed'   => in_array($column, $this->config->columns_to_display)
            ];
        }
        $this->config->selectable_columns = $transformed;
    }

    protected function generateSelectableColumns()
    {
        $defaultColumns = $this->getDefaultColumnsToDisplay();
        if ($this->config->show_goals) {
            $goalMetrics       = array('nb_conversions', 'revenue');
            $defaultColumns    = array_merge($defaultColumns, $goalMetrics);
        }

        // Use the subset of default columns that are actually present in the datatable
        $allColumns = $this->getDataTable()->getColumns();
        $selectableColumns = array_intersect($defaultColumns, $allColumns);

        // If there are no default columns, just strip out the 'label' column and use all the others
        if (empty($selectableColumns)) {
            $selectableColumns = $this->removeLabelFromArray($allColumns);
        }

        $this->config->selectable_columns = $selectableColumns;
    }

    private function removeLabelFromArray($theArray)
    {
        if (!empty($theArray) && is_array($theArray)) {
            $key = array_search('label', $theArray);
            if ($key !== false) {
                unset($theArray[$key]);
                $theArray = array_values($theArray);
            }
        }

        return $theArray;
    }

    protected function ensureValidColumnsToDisplay()
    {
        $columnsToDisplay = $this->config->columns_to_display;

        // Remove 'label' from columns to display if present
        $columnsToDisplay = $this->removeLabelFromArray($columnsToDisplay);

        // Strip out any columns_to_display that are not in the dataset
        $allColumns = [];
        if ($this->report) {
            $allColumns = $this->report->getAllMetrics();
        }
        $allColumns = array_merge($allColumns, $this->getDataTable()->getColumns());

        $dataTable = $this->getDataTable();
        if ($dataTable instanceof DataTable\Map) {
            $dataTable = $dataTable->getFirstRow();
        }

        /** @var ProcessedMetric[] $extraProcessedMetrics */
        $extraProcessedMetrics = $dataTable->getMetadata(DataTable::EXTRA_PROCESSED_METRICS_METADATA_NAME);
        if (!empty($extraProcessedMetrics)) {
            $extraProcessedMetricNames = array_map(function (Metric $m) {
                return $m->getName();
            }, $extraProcessedMetrics);
            $allColumns = array_merge($allColumns, $extraProcessedMetricNames);
        }

        $allColumns = array_unique($allColumns);

        // If the datatable has no data, use the default columns (there must be data for evolution graphs or else nothing displays)
        if (empty($allColumns)) {
            $allColumns = $this->getDefaultColumnsToDisplay();
        }

        $this->config->columns_to_display = $this->removeUnavailableMetrics(array_intersect($columnsToDisplay, $allColumns));
    }

    private function getDefaultColumnsToDisplay()
    {
        return array(
            'nb_visits',
            'nb_actions',
            'nb_uniq_visitors',
            'nb_users'
        );
    }

    private function removeUnavailableMetrics($metrics)
    {
        $currentPeriod = Common::getRequestVar('period', false);

        if (!SettingsPiwik::isUniqueVisitorsEnabled($currentPeriod)) {
            $metrics = array_diff($metrics, ['nb_uniq_visitors', 'nb_users']);
        }

        return $metrics;
    }
}
