<?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\Plugin;

use Piwik\API\Request;
use Piwik\API\Request as ApiRequest;
use Piwik\Common;
use Piwik\DataTable;
use Piwik\Period;
use Piwik\Piwik;
use Piwik\Plugins\API\Filter\DataComparisonFilter;
use Piwik\View\ViewInterface;
use Piwik\ViewDataTable\Config as VizConfig;
use Piwik\ViewDataTable\Manager as ViewDataTableManager;
use Piwik\ViewDataTable\Request as ViewDataTableRequest;
use Piwik\ViewDataTable\RequestConfig as VizRequest;

/**
 * The base class of all report visualizations.
 *
 * ViewDataTable instances load analytics data via Piwik's Reporting API and then output some
 * type of visualization of that data.
 *
 * Visualizations can be in any format. HTML-based visualizations should extend
 * {@link Visualization}. Visualizations that use other formats, such as visualizations
 * that output an image, should extend ViewDataTable directly.
 *
 * ### Creating ViewDataTables
 *
 * ViewDataTable instances are not created via the new operator, instead the {@link Piwik\ViewDataTable\Factory}
 * class is used.
 *
 * The specific subclass to create is determined, first, by the **viewDataTable** query parameter.
 * If this parameter is not set, then the default visualization type for the report being
 * displayed is used.
 *
 * ### Configuring ViewDataTables
 *
 * **Display properties**
 *
 * ViewDataTable output can be customized by setting one of many available display
 * properties. Display properties are stored as fields in {@link Piwik\ViewDataTable\Config} objects.
 * ViewDataTables store a {@link Piwik\ViewDataTable\Config} object in the {@link $config} field.
 *
 * Display properties can be set at any time before rendering.
 *
 * **Request properties**
 *
 * Request properties are similar to display properties in the way they are set. They are,
 * however, not used to customize ViewDataTable instances, but in the request to Piwik's
 * API when loading analytics data.
 *
 * Request properties are set by setting the fields of a {@link Piwik\ViewDataTable\RequestConfig} object stored in
 * the {@link $requestConfig} field. They can be set at any time before rendering.
 * Setting them after data is loaded will have no effect.
 *
 * **Customizing how reports are displayed**
 *
 * Each individual report should be rendered in its own controller method. There are two
 * ways to render a report within its controller method. You can either:
 *
 * 1. manually create and configure a ViewDataTable instance
 * 2. invoke {@link Piwik\Plugin\Controller::renderReport} and configure the ViewDataTable instance
 *    in the {@hook ViewDataTable.configure} event.
 *
 * ViewDataTable instances are configured by setting and modifying display properties and request
 * properties.
 *
 * ### Creating new visualizations
 *
 * New visualizations can be created by extending the ViewDataTable class or one of its
 * descendants. To learn more [read our guide on creating new visualizations](/guides/visualizing-report-data#creating-new-visualizations).
 *
 * ### Examples
 *
 * **Manually configuring a ViewDataTable**
 *
 *     // a controller method that displays a single report
 *     public function myReport()
 *     {
 *         $view = \Piwik\ViewDataTable\Factory::build('table', 'MyPlugin.myReport');
 *         $view->config->show_limit_control = true;
 *         $view->config->translations['myFancyMetric'] = "My Fancy Metric";
 *         // ...
 *         return $view->render();
 *     }
 *
 * **Using {@link Piwik\Plugin\Controller::renderReport}**
 *
 * First, a controller method that displays a single report:
 *
 *     public function myReport()
 *     {
 *         return $this->renderReport(__FUNCTION__);`
 *     }
 *
 * Then the event handler for the {@hook ViewDataTable.configure} event:
 *
 *     public function configureViewDataTable(ViewDataTable $view)
 *     {
 *         switch ($view->requestConfig->apiMethodToRequestDataTable) {
 *             case 'MyPlugin.myReport':
 *                 $view->config->show_limit_control = true;
 *                 $view->config->translations['myFancyMetric'] = "My Fancy Metric";
 *                 // ...
 *                 break;
 *         }
 *     }
 *
 * **Using custom configuration objects in a new visualization**
 *
 *     class MyVisualizationConfig extends Piwik\ViewDataTable\Config
 *     {
 *         public $my_new_property = true;
 *     }
 *
 *     class MyVisualizationRequestConfig extends Piwik\ViewDataTable\RequestConfig
 *     {
 *         public $my_new_property = false;
 *     }
 *
 *     class MyVisualization extends Piwik\Plugin\ViewDataTable
 *     {
 *         public static function getDefaultConfig()
 *         {
 *             return new MyVisualizationConfig();
 *         }
 *
 *         public static function getDefaultRequestConfig()
 *         {
 *             return new MyVisualizationRequestConfig();
 *         }
 *     }
 *
 *
 * @api
 */
abstract class ViewDataTable implements ViewInterface
{
    public const ID = '';

    /**
     * DataTable loaded from the API for this ViewDataTable.
     *
     * @var DataTable|null
     */
    protected $dataTable = null;

    /**
     * Contains display properties for this visualization.
     *
     * @var \Piwik\ViewDataTable\Config
     */
    public $config;

    /**
     * Contains request properties for this visualization.
     *
     * @var \Piwik\ViewDataTable\RequestConfig
     */
    public $requestConfig;

    /**
     * @var ViewDataTableRequest
     */
    protected $request;

    private $isComparing = null;

    /**
     * Constructor. Initializes display and request properties to their default values.
     * Posts the {@hook ViewDataTable.configure} event which plugins can use to configure the
     * way reports are displayed.
     */
    public function __construct($controllerAction, $apiMethodToRequestDataTable, $overrideParams = array())
    {
        if (strpos($controllerAction, '.') === false) {
            $controllerName = '';
            $controllerAction = '';
        } else {
            list($controllerName, $controllerAction) = explode('.', $controllerAction);
        }

        $this->requestConfig = static::getDefaultRequestConfig();
        $this->config        = static::getDefaultConfig();
        $this->config->subtable_controller_action = $controllerAction;
        $this->config->setController($controllerName, $controllerAction);

        $this->request = new ViewDataTableRequest($this->requestConfig);

        $this->requestConfig->idSubtable = Common::getRequestVar('idSubtable', false, 'int');
        $this->config->self_url          = Request::getBaseReportUrl($controllerName, $controllerAction);

        $this->requestConfig->apiMethodToRequestDataTable = $apiMethodToRequestDataTable;

        $report = ReportsProvider::factory($this->requestConfig->getApiModuleToRequest(), $this->requestConfig->getApiMethodToRequest());

        if (!empty($report)) {
            /** @var Report $report */
            $subtable = $report->getActionToLoadSubTables();
            if (!empty($subtable)) {
                $this->config->subtable_controller_action = $subtable;
            }

            $this->config->show_goals = $report->hasGoalMetrics();

            $relatedReports = $report->getRelatedReports();
            if (!empty($relatedReports)) {
                foreach ($relatedReports as $relatedReport) {
                    if (!$relatedReport) {
                        continue;
                    }

                    $relatedReportName = $relatedReport->getName();

                    $this->config->addRelatedReport(
                        $relatedReport->getModule() . '.' . $relatedReport->getAction(),
                        $relatedReportName
                    );
                }
            }

            $metrics = $report->getMetrics();
            if (!empty($metrics)) {
                $this->config->addTranslations($metrics);
            }

            $processedMetrics = $report->getProcessedMetrics();
            if (!empty($processedMetrics)) {
                $this->config->addTranslations($processedMetrics);
            }

            $dimension = $report->getDimension();
            if (!empty($dimension)) {
                $this->config->addTranslations(['label' => $dimension->getName()]);
            }

            $this->config->title = $report->getName();

            $report->configureView($this);
        }

        /**
         * Triggered during {@link ViewDataTable} construction. Subscribers should customize
         * the view based on the report that is being displayed.
         *
         * This event is triggered before view configuration properties are overwritten by saved settings or request
         * parameters. Use this to define default values.
         *
         * Plugins that define their own reports must subscribe to this event in order to
         * specify how the Piwik UI should display the report.
         *
         * **Example**
         *
         *     // event handler
         *     public function configureViewDataTable(ViewDataTable $view)
         *     {
         *         switch ($view->requestConfig->apiMethodToRequestDataTable) {
         *             case 'VisitTime.getVisitInformationPerServerTime':
         *                 $view->config->enable_sort = true;
         *                 $view->requestConfig->filter_limit = 10;
         *                 break;
         *         }
         *     }
         *
         * @param ViewDataTable $view The instance to configure.
         */
        Piwik::postEvent('ViewDataTable.configure', array($this));

        $this->assignRelatedReportsTitle();

        $this->config->show_footer_icons = (false == $this->requestConfig->idSubtable);

        // the exclude low population threshold value is sometimes obtained by requesting data.
        // to avoid issuing unnecessary requests when display properties are determined by metadata,
        // we allow it to be a closure.
        if (
            isset($this->requestConfig->filter_excludelowpop_value)
            && $this->requestConfig->filter_excludelowpop_value instanceof \Closure
        ) {
            $function = $this->requestConfig->filter_excludelowpop_value;
            $this->requestConfig->filter_excludelowpop_value = $function();
        }

        $this->overrideViewPropertiesWithParams($overrideParams);
        $this->overrideViewPropertiesWithQueryParams();

        /**
         * Triggered after {@link ViewDataTable} construction. Subscribers should customize
         * the view based on the report that is being displayed.
         *
         * This event is triggered after all view configuration values have been overwritten by saved settings or
         * request parameters. Use this if you need to work with the final configuration values.
         *
         * Plugins that define their own reports can subscribe to this event in order to
         * specify how the Piwik UI should display the report.
         *
         * **Example**
         *
         *     // event handler
         *     public function configureViewDataTableEnd(ViewDataTable $view)
         *     {
         *         if ($view->requestConfig->apiMethodToRequestDataTable == 'VisitTime.getVisitInformationPerServerTime'
         *             && $view->requestConfig->flat == 1) {
         *                 $view->config->show_header_message = 'You are viewing this report flattened';
         *         }
         *     }
         *
         * @param ViewDataTable $view The instance to configure.
         */
        Piwik::postEvent('ViewDataTable.configure.end', array($this));
    }

    private function assignRelatedReportsTitle()
    {
        if (!empty($this->config->related_reports_title)) {
            // title already assigned by a plugin
            return;
        }
        if (count($this->config->related_reports) == 1) {
            $this->config->related_reports_title = Piwik::translate('General_RelatedReport') . ':';
        } else {
            $this->config->related_reports_title = Piwik::translate('General_RelatedReports') . ':';
        }
    }

    /**
     * Returns the default config instance.
     *
     * Visualizations that define their own display properties should override this method and
     * return an instance of their new {@link Piwik\ViewDataTable\Config} descendant.
     *
     * See the last example {@link ViewDataTable here} for more information.
     *
     * @return \Piwik\ViewDataTable\Config
     */
    public static function getDefaultConfig()
    {
        return new VizConfig();
    }

    /**
     * Returns the default request config instance.
     *
     * Visualizations that define their own request properties should override this method and
     * return an instance of their new {@link Piwik\ViewDataTable\RequestConfig} descendant.
     *
     * See the last example {@link ViewDataTable here} for more information.
     *
     * @return \Piwik\ViewDataTable\RequestConfig
     */
    public static function getDefaultRequestConfig()
    {
        return new VizRequest();
    }

    protected function loadDataTableFromAPI()
    {
        if (!is_null($this->dataTable)) {
            // data table is already there
            // this happens when setDataTable has been used
            return $this->dataTable;
        }

        $extraParams = [];
        if ($this->isComparing()) {
            $extraParams['compare'] = '1';
        }

        $this->dataTable = $this->request->loadDataTableFromAPI($extraParams);

        return $this->dataTable;
    }

    /**
     * Returns the viewDataTable ID for this DataTable visualization.
     *
     * Derived classes should not override this method. They should instead declare a const ID field
     * with the viewDataTable ID.
     *
     * @throws \Exception
     * @return string
     */
    public static function getViewDataTableId()
    {
        $id = static::ID;

        if (empty($id)) {
            $message = sprintf('ViewDataTable %s does not define an ID. Set the ID constant to fix this issue', get_called_class());
            throw new \Exception($message);
        }

        return $id;
    }

    /**
     * Returns `true` if this instance's or any of its ancestors' viewDataTable IDs equals the supplied ID,
     * `false` if otherwise.
     *
     * Can be used to test whether a ViewDataTable object is an instance of a certain visualization or not,
     * without having to know where that visualization is.
     *
     * @param  string $viewDataTableId The viewDataTable ID to check for, eg, `'table'`.
     * @return bool
     */
    public function isViewDataTableId($viewDataTableId)
    {
        $myIds = ViewDataTableManager::getIdsWithInheritance(get_called_class());

        return in_array($viewDataTableId, $myIds);
    }

    /**
     * Returns the DataTable loaded from the API.
     *
     * @return DataTable
     * @throws \Exception if not yet loaded.
     */
    public function getDataTable()
    {
        if (is_null($this->dataTable)) {
            throw new \Exception("The DataTable object has not yet been created");
        }

        return $this->dataTable;
    }

    /**
     * To prevent calling an API multiple times, the DataTable can be set directly.
     * It won't be loaded from the API in this case.
     *
     * @param DataTable $dataTable The DataTable to use.
     * @return void
     */
    public function setDataTable($dataTable)
    {
        $this->dataTable = $dataTable;
    }

    /**
     * Checks that the API returned a normal DataTable (as opposed to DataTable\Map)
     * @throws \Exception
     * @return void
     */
    protected function checkStandardDataTable()
    {
        Piwik::checkObjectTypeIs($this->dataTable, array('\Piwik\DataTable'));
    }

    /**
     * Requests all needed data and renders the view.
     *
     * @return string The result of rendering.
     */
    public function render()
    {
        return '';
    }

    protected function getDefaultDataTableCssClass()
    {
        return 'dataTableViz' . Piwik::getUnnamespacedClassName(get_class($this));
    }

    /**
     * Returns the list of view properties that can be overridden by query parameters.
     *
     * @return array
     */
    protected function getOverridableProperties()
    {
        return array_merge($this->config->overridableProperties, $this->requestConfig->overridableProperties);
    }

    private function overrideViewPropertiesWithQueryParams()
    {
        $properties = $this->getOverridableProperties();

        foreach ($properties as $name) {
            if (property_exists($this->requestConfig, $name)) {
                $this->requestConfig->$name = $this->getPropertyFromQueryParam($name, $this->requestConfig->$name);
            } elseif (property_exists($this->config, $name)) {
                $this->config->$name = $this->getPropertyFromQueryParam($name, $this->config->$name);
            }
        }

        // handle special 'columns' query parameter
        $columns = Common::getRequestVar('columns', false);

        if (false !== $columns) {
            $this->config->columns_to_display = Piwik::getArrayFromApiParameter($columns);
            array_unshift($this->config->columns_to_display, 'label');
        }
    }

    protected function getPropertyFromQueryParam($name, $defaultValue)
    {
        $type = is_numeric($defaultValue) ? 'int' : null;
        $value = Common::getRequestVar($name, $defaultValue, $type);
        // convert comma separated values to arrays if needed
        if (is_array($defaultValue)) {
            $value = Piwik::getArrayFromApiParameter($value);
        }
        return $value;
    }

    /**
     * Returns `true` if this instance will request a single DataTable, `false` if requesting
     * more than one.
     *
     * @return bool
     */
    public function isRequestingSingleDataTable()
    {
        $requestArray = $this->request->getRequestArray() + $_GET + $_POST;
        $date   = Common::getRequestVar('date', null, 'string', $requestArray);
        $period = Common::getRequestVar('period', null, 'string', $requestArray);
        $idSite = Common::getRequestVar('idSite', null, 'string', $requestArray);

        if (
            Period::isMultiplePeriod($date, $period)
            || strpos($idSite, ',') !== false
            || $idSite == 'all'
        ) {
            return false;
        }

        return true;
    }

    /**
     * Returns `true` if this visualization can display some type of data or not.
     *
     * New visualization classes should override this method if they can only visualize certain
     * types of data. The evolution graph visualization, for example, can only visualize
     * sets of DataTables. If the API method used results in a single DataTable, the evolution
     * graph footer icon should not be displayed.
     *
     * @param  ViewDataTable $view Contains the API request being checked.
     * @return bool
     */
    public static function canDisplayViewDataTable(ViewDataTable $view)
    {
        return $view->config->show_all_views_icons;
    }

    private function overrideViewPropertiesWithParams($overrideParams)
    {
        if (empty($overrideParams)) {
            return;
        }

        foreach ($overrideParams as $key => $value) {
            if (property_exists($this->requestConfig, $key)) {
                $this->requestConfig->$key = $value;
            } elseif (property_exists($this->config, $key)) {
                $this->config->$key = $value;
            } elseif ($key != 'enable_filter_excludelowpop') {
                $this->config->custom_parameters[$key] = $value;
            }
        }
    }

    /**
     * Display a meaningful error message when any invalid parameter is being set.
     *
     * @param $overrideParams
     * @throws
     */
    public function throwWhenSettingNonOverridableParameter($overrideParams)
    {
        $nonOverridableParams = $this->getNonOverridableParams($overrideParams);
        if (count($nonOverridableParams) > 0) {
            throw new \Exception(sprintf(
                "Setting parameters %s is not allowed. Please report this bug to the Matomo team.",
                implode(" and ", $nonOverridableParams)
            ));
        }
    }

    /**
     * @param $overrideParams
     * @return array
     */
    public function getNonOverridableParams($overrideParams)
    {
        $paramsCannotBeOverridden = array();
        foreach ($overrideParams as $paramName => $paramValue) {
            if (property_exists($this->requestConfig, $paramName)) {
                $allowedParams = $this->requestConfig->overridableProperties;
            } elseif (property_exists($this->config, $paramName)) {
                $allowedParams = $this->config->overridableProperties;
            } else {
                // setting Config.custom_parameters is always allowed
                continue;
            }

            if (!in_array($paramName, $allowedParams)) {
                $paramsCannotBeOverridden[] = $paramName;
            }
        }
        return $paramsCannotBeOverridden;
    }

    /**
     * Returns true if both this current visualization supports comparison, and if comparison query parameters
     * are present in the URL.
     *
     * @return bool
     */
    public function isComparing()
    {
        if (
            !$this->supportsComparison()
            || $this->config->disable_comparison
        ) {
            return false;
        }

        $request = $this->request->getRequestArray();
        $request = ApiRequest::getRequestArrayFromString($request);

        $result = DataComparisonFilter::isCompareParamsPresent($request);
        return $result;
    }

    /**
     * Implementations should override this method if they support a special comparison view. By
     * default, it is assumed visualizations do not support comparison.
     *
     * @return bool
     */
    public function supportsComparison()
    {
        return false;
    }

    public function getRequestArray()
    {
        $requestArray = $this->request->getRequestArray();
        return ApiRequest::getRequestArrayFromString($requestArray);
    }
}
