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

namespace Piwik\Plugins\CoreVisualizations\Visualizations\Sparklines;
use Piwik\Common;
use Piwik\DataTable\Filter\CalculateEvolutionFilter;
use Piwik\Metrics;
use Piwik\NoAccessException;
use Piwik\Period\Range;
use Piwik\Site;
use Piwik\Url;

/**
 * DataTable Visualization that derives from Sparklines.
 */
class Config extends \Piwik\ViewDataTable\Config
{
    /**
     * Holds metrics / column names that will be used to fetch data from the configured $requestConfig API.
     * Default value: array
     */
    private $sparkline_metrics = array();

    /**
     * Holds the actual sparkline entries based on fetched data that will be used in the template.
     * @var array
     */
    private $sparklines = array();

    /**
     * If false, will not link them with any evolution graph
     * @var bool
     */
    private $evolutionGraphLinkable = true;

    /**
     * Adds possibility to set html attributes on the sparklines title / headline. For example can be used
     * to set an angular directive
     * @var string
     */
    public $title_attributes = array();

    /**
     * Defines custom parameters that will be appended to the sparkline image urls
     */
    public $custom_parameters = [];

    /**
     * If supplied, this function is used to compute the evolution percent displayed next to non-comparison sparkline views.
     *
     * The function is passed three parameters:
     * - an array mapping column names with column values ['column' => 123]
     * - an array of \Piwik\Plugin\Metrics objects available for the report - useful for formatting values
     *
     * compute_evolution(array, array)
     *
     * @var callable
     */
    public $compute_evolution = null;

    public function __construct()
    {
        parent::__construct();

        $this->translations = Metrics::getDefaultMetricTranslations();
    }

    /**
     * @ignore
     * @return array
     */
    public function getSparklineMetrics()
    {
        return $this->sparkline_metrics;
    }

    /**
     * @ignore
     * @return bool
     */
    public function hasSparklineMetrics()
    {
        return !empty($this->sparkline_metrics);
    }

    /**
     * Removes an existing sparkline entry. Especially useful in dataTable filters in case sparklines should be not
     * displayed depending on the fetched data.
     *
     * Example:
     * $config->addSparklineMetric('nb_users');
     * $config->filters[] = function ($dataTable) use ($config) {
     *   if ($dataTable->getFirstRow()->getColumn('nb_users') == 0) {
     *      // do not show a sparkline if there are no recorded users
     *      $config->removeSparklineMetric('nb_users');
     *   }
     * }
     *
     * @param array|string $metricNames The name of the metrics in the same format they were used when added via
     *                                  {@link addSparklineMetric}
     */
    public function removeSparklineMetric($metricNames)
    {
        foreach ($this->sparkline_metrics as $index => $metric) {
            if ($metric['columns'] === $metricNames) {
                array_splice($this->sparkline_metrics, $index, 1);

                break;
            }
        }
    }

    /**
     * Replaces an existing sparkline entry with different columns. Especially useful in dataTable filters in case
     * sparklines should be not displayed depending on the fetched data.
     *
     * Example:
     * $config->addSparklineMetric('nb_users');
     * $config->filters[] = function ($dataTable) use ($config) {
     *   if ($dataTable->getFirstRow()->getColumn('nb_users') == 0) {
     *      // instead of showing the sparklines for users, show a placeholder if there are no recorded users
     *      $config->replaceSparklineMetric(array('nb_users'), '');
     *   }
     * }
     *
     * @param array|string $metricNames The name of the metrics in the same format they were used when added via
     *                                  {@link addSparklineMetric}
     * @param array|string $replacementColumns The removed columns will be replaced with these columns
     */
    public function replaceSparklineMetric($metricNames, $replacementColumns)
    {
        foreach ($this->sparkline_metrics as $index => $metric) {
            if ($metric['columns'] === $metricNames) {
                $this->sparkline_metrics[$index]['columns'] = $replacementColumns;
            }
        }
    }

    /**
     * Adds a new sparkline.
     *
     * It will show a sparkline image, the value of the resolved metric name and a descrption. Optionally, multiple
     * values can be shown after a sparkline image by passing multiple metric names
     * (eg array('nb_visits', 'nb_actions')). The data will be requested from the configured api method see
     * {@link \Piwik\ViewDataTable\RequestConfig::$apiMethodToRequestDataTable}.
     *
     * Example:
     * $config->addSparklineMetric('nb_visits');
     * $config->addTranslation('nb_visits', 'Visits');
     * Results in: [sparkline image] X visits
     *
     * Example:
     * $config->addSparklineMetric(array('nb_visits', 'nb_actions'));
     * $config->addTranslations(array('nb_visits' => 'Visits', 'nb_actions' => 'Actions'));
     * Results in: [sparkline image] X visits, Y actions
     *
     * @param string|array $metricName  Either one metric name (eg 'nb_visits') or an array of metric names
     * @param int|null $order  Defines the order. The lower the order the earlier the sparkline will be displayed.
     *                         By default the sparkline will be appended to the end.
     * @param array $graphParams The params to use when changing an associated evolution graph. By default this is determined
     *                           from the sparkline URL, but sometimes the sparkline API method may not match the evolution graph API method.
     */
    public function addSparklineMetric($metricName, $order = null, $graphParams = null)
    {
        $this->sparkline_metrics[] = array(
            'columns' => $metricName,
            'order'   => $order,
            'graphParams' => $graphParams,
        );
    }

    /**
     * Adds a placeholder. In this case nothing will be shown, neither a sparkline nor any description. This can be
     * useful if you want to have some kind of separator. Eg if you want to have a sparkline on the left side but
     * not sparkline on the right side.
     *
     * @param int|null $order   Defines the order. The lower the order the earlier the sparkline will be displayed.
     *                          By default the sparkline will be appended to the end.
     */
    public function addPlaceholder($order = null)
    {
        $this->sparklines[] = array(
            'url' => '',
            'metrics' => array(),
            'order' => $this->getSparklineOrder($order),

            // adding this group ensures the sparkline will be placed between individual sparklines, and not in their own group together
            'group' => 'placeholder' . count($this->sparklines),
        );
    }

    /**
     * Add a new sparkline to be displayed to the view.
     *
     * Each sparkline can consist of one or multiple metrics. One metric consists of a value and a description. By
     * default the value is shown first, then the description. The description can optionally contain a '%s' in case
     * the value shall be displayed within the description. If multiple metrics are given, they will be separated by
     * a comma.
     *
     * @param array $requestParamsForSparkline You need to at least set a module / action eg
     *                                         array('columns' => array('nb_visit'), 'module' => '', 'action' => '')
     * @param int|float|string|array $value Either the metric value or an array of values.
     * @param string|array $description Either one description or an array of descriptions. If an array, both
     *                                         $value and $description need the same amount of array entries.
     *                                         $description[0] should be the description for $value[0].
     *                                         $description should be already translated. If $value should appear
     *                                         somewhere within the text a `%s` can be used in the translation.
     * @param array|null $evolution            Optional array containing at least the array keys 'currentValue' and
     *                                         'pastValue' which are needed to calculate the correct percentage.
     *                                         An optional 'tooltip' can be set as well. Eg
     *                                         array('currentValue' => 10, 'pastValue' => 20,
     *                                               'tooltip' => '10 visits in 2015-07-26 compared to 20 visits in 2015-07-25')
     * @param int $order                       Defines the order. The lower the order the earlier the sparkline will be
     *                                         displayed. By default the sparkline will be appended to the end.
     * @param string $title                    The title of this specific sparkline. It is displayed on the left above the sparkline image.
     * @param string $group                    The ID of the group for this sparkline.
     * @param int $seriesIndices               The indexes of each series displayed in the sparkline. This determines what color is used for each series. Mainly used for comparison.
     * @param array $graphParams               The params to use when changing an associated evolution graph. By default this is determined
     *                                         from the sparkline URL, but sometimes the sparkline API method may not match the evolution graph API method.
     * @throws \Exception In case an evolution parameter is set but has wrong data structure
     */
    public function addSparkline($requestParamsForSparkline, $metricInfos, $description, $evolution = null, $order = null, $title = null, $group = '', $seriesIndices = null, $graphParams = null)
    {
        $metrics = array();

        if ($description === null && is_array($metricInfos)) {
            $metrics = $metricInfos;
        } else {
            $value = $metricInfos;

            if (is_array($value)) {
                $values = $value;
            } else {
                $values = array($value);
            }

            if (!is_array($description)) {
                $description = array($description);
            }

            if (count($values) === count($description)) {
                foreach ($values as $index => $value) {
                    $metrics[] = array(
                        'value' => $value,
                        'description' => $description[$index]
                    );
                }
            } else {
                $msg  = 'The number of values and descriptions need to be the same to add a sparkline. ';
                $msg .= 'Values: ' . implode(', ', $values). ' Descriptions: ' . implode(', ', $description);
                throw new \Exception($msg);
            }
        }

        if (!empty($requestParamsForSparkline['columns'])
            && is_array($requestParamsForSparkline['columns'])
            && count($requestParamsForSparkline['columns']) === count($metrics)) {
            $columns = array_values($requestParamsForSparkline['columns']);
        } elseif (!empty($requestParamsForSparkline['columns'])
                  && is_string($requestParamsForSparkline['columns'])
                  && count($metrics) === 1) {
            $columns = array($requestParamsForSparkline['columns']);
        } else{
            $columns = array();
        }

        foreach ($metrics as $index => $metricInfo) {
            $metrics[$index]['column'] = isset($columns[$index]) ? $columns[$index] : '';
        }

        if (empty($metrics)) {
            return;
        }

        $groupedMetrics = [];
        foreach ($metrics as $metricInfo) {
            $metricGroup = isset($metricInfo['group']) ? $metricInfo['group'] : '';
            $groupedMetrics[$metricGroup][] = $metricInfo;
        }

        $sparkline = array(
            'url' => $this->getUrlSparkline($requestParamsForSparkline),
            'metrics' => $groupedMetrics,
            'order' => $this->getSparklineOrder($order),
            'title' => $title,
            'group' => $group,
            'seriesIndices' => $seriesIndices,
            'graphParams' => $graphParams,
        );

        if (!empty($evolution)) {
            if (!is_array($evolution) ||
                !array_key_exists('currentValue', $evolution) ||
                !array_key_exists('pastValue', $evolution)
            ) {
                throw new \Exception('In order to show an evolution in the sparklines view a currentValue and pastValue array key needs to be present');
            }

            $evolutionPercent = CalculateEvolutionFilter::calculate($evolution['currentValue'], $evolution['pastValue'], $precision = 1);

            // do not display evolution if evolution percent is 0 and current value is 0
            if ($evolutionPercent != 0 || $evolution['currentValue'] != 0) {
                $sparkline['evolution'] = array(
                    'percent' => $evolutionPercent,
                    'tooltip' => !empty($evolution['tooltip']) ? $evolution['tooltip'] : null,
                    'trend' => $evolution['currentValue'] - $evolution['pastValue'],
                );
            }

        }

        $this->sparklines[] = $sparkline;
    }

    /**
     * If there are sparklines and evolution graphs on one page, we try to connect them so that when you click on a
     * sparkline, the evolution graph will update and show the evolution for that sparkline metric. In some cases
     * we might falsely connect sparklines with an evolution graph that don't belong together. In this case you can
     * mark all sparklines as "not linkable". This will prevent the sparklines being linked with an evolution graph.
     */
    public function setNotLinkableWithAnyEvolutionGraph()
    {
        $this->evolutionGraphLinkable = false;
    }

    /**
     * Detect whether sparklines are linkable with an evolution graph. {@link setNotLinkableWithAnyEvolutionGraph()}
     */
    public function areSparklinesLinkable()
    {
        return $this->evolutionGraphLinkable;
    }

    /**
     * @return array
     * @ignore
     */
    public function getSortedSparklines()
    {
        usort($this->sparklines, function ($a, $b) {
            if ($a['order'] == $b['order']) {
                return 0;
            }
            return ($a['order'] < $b['order']) ? -1 : 1;
        });

        $sparklines = [];
        foreach ($this->sparklines as $sparkline) {
            $group = $sparkline['group'];
            $sparklines[$group][] = $sparkline;
        }
        return $sparklines;
    }

    private function getSparklineOrder($order)
    {
        if (!isset($order)) {
            // make sure to append to the end if nothing set (in the order they are added)
            $order = 999 + count($this->sparklines);
        }

        return (int) $order;
    }

    /**
     * Returns a URL to a sparkline image for a report served by the current plugin.
     *
     * The result of this URL should be used with the [sparkline()](/api-reference/Piwik/View#twig) twig function.
     *
     * The current site ID and period will be used.
     *
     * @param array $customParameters The array of query parameter name/value pairs that
     *                                should be set in result URL.
     * @return string The generated URL.
     */
    private function getUrlSparkline($customParameters = array())
    {
        $customParameters['viewDataTable'] = 'sparkline';

        $params = $this->getGraphParamsModified($customParameters);

        // convert array values to comma separated
        foreach ($params as &$value) {
            if (is_array($value)) {
                $value = rawurlencode(implode(',', $value));
            }
        }
        $url = Url::getCurrentQueryStringWithParametersModified($params);
        return $url;
    }

    /**
     * Returns the array of new processed parameters once the parameters are applied.
     * For example: if you set range=last30 and date=2008-03-10,
     *  the date element of the returned array will be "2008-02-10,2008-03-10"
     *
     * Parameters you can set:
     * - range: last30, previous10, etc.
     * - date: YYYY-MM-DD, today, yesterday
     * - period: day, week, month, year
     *
     * @param array $paramsToSet array( 'date' => 'last50', 'viewDataTable' =>'sparkline' )
     * @throws \Piwik\NoAccessException
     * @return array
     */
    private function getGraphParamsModified($paramsToSet = array())
    {
        if (!isset($paramsToSet['period'])) {
            $period = Common::getRequestVar('period');
        } else {
            $period = $paramsToSet['period'];
        }

        if ($period == 'range') {
            return $paramsToSet;
        }

        if (!isset($paramsToSet['range'])) {
            $range = 'last30';
        } else {
            $range = $paramsToSet['range'];
        }

        if (!isset($paramsToSet['idSite'])) {
            $idSite = Common::getRequestVar('idSite');
        } else {
            $idSite = $paramsToSet['idSite'];
        }

        if (!isset($paramsToSet['date'])) {
            $endDate = Common::getRequestVar('date', 'yesterday', 'string');
        } else {
            $endDate = $paramsToSet['date'];
        }

        $site = new Site($idSite);

        if (is_null($site)) {
            throw new NoAccessException("Website not initialized, check that you are logged in and/or using the correct token_auth.");
        }

        if (!isset($paramsToSet['date'])
            || !Range::isMultiplePeriod($paramsToSet['date'], $period)
        ) {
            $paramDate = Range::getRelativeToEndDate($period, $range, $endDate, $site);
        } else {
            $paramDate = $paramsToSet['date'];
        }

        $params = array_merge($paramsToSet, array('date' => $paramDate));
        return $params;
    }

}
