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

use Piwik\Common;
use Piwik\Container\StaticContainer;
use Piwik\Piwik;
use Piwik\Plugins\CoreVisualizations\FeatureFlags\SparklinesRedesign;
use Piwik\Plugins\FeatureFlags\FeatureFlagManager;
use Piwik\View\ViewInterface;

/**
 * Renders a sparkline image given a PHP data array.
 * Using the Sparkline PHP Graphing Library sparkline.org
 */
class Sparkline implements ViewInterface
{
    public const DEFAULT_WIDTH = 200;
    public const DEFAULT_HEIGHT = 50;
    public const DEFAULT_LINE_THICKNESS = 1;
    public const REDESIGN_LINE_THICKNESS = 2;
    // Sparklines are small decorative graphics (100x25 displayed, 200x50 rendered by default).
    // These caps bound the size of the image generated server-side from caller-supplied
    // width/height request params, to avoid generating needlessly large images.
    public const MAX_WIDTH = 500;
    public const MAX_HEIGHT = 250;


    /**
     * Width of the sparkline
     * @var int
     */
    protected $width = self::DEFAULT_WIDTH;
    /**
     * Height of sparkline
     * @var int
     */
    protected $height = self::DEFAULT_HEIGHT;
    private $serieses = [];
    /**
     * @var \Davaxi\Sparkline
     */
    private $sparkline;

    /**
     * Array with format: array( x, y, z, ... )
     * @param array ...$data
     */
    public function setValues(...$data)
    {
        $this->serieses = $data;
    }

    public function addSeries(array $values)
    {
        $this->serieses[] = $values;
    }

    public function main()
    {
        try {
            $sparkline = new \Davaxi\Sparkline();
        } catch (\Exception $exception) {
            // Ignore GD not installed exception
            return;
        }


        $thousandSeparator = Piwik::translate('Intl_NumberSymbolGroup');
        $decimalSeparator = Piwik::translate('Intl_NumberSymbolDecimal');

        $sparkline->setData(); // remove default series
        foreach ($this->serieses as $seriesIndex => $series) {
            $values = [];
            $hasFloat = false;

            foreach ($series as $value) {
                // replace localized decimal separator
                $value = str_replace($thousandSeparator, '', $value);
                $value = str_replace($decimalSeparator, '.', $value);

                // sanitize value
                $value = filter_var($value, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION | FILTER_FLAG_ALLOW_SCIENTIFIC);

                if (empty($value) || !is_numeric($value)) {
                    $value = 0;
                }

                $values[] = $value;

                if (is_float($value + 0)) { // coerce to int/float type before checking
                    $hasFloat = true;
                }
            }

            // the sparkline lib used converts everything to integers (see the FormatTrait.php file) which means float
            // numbers that are close to 1.0 or 0.0 will get floored. this can happen in the average page generation time
            // report, and cause some values which are, eg, around ~.9 to appear as 0 in the sparkline. to workaround this, we
            // scale the values.
            if ($hasFloat) {
                $values = array_map(function ($x) {
                    return $x * 1000.0;
                }, $values);
            }

            $sparkline->addSeries($values);
            $this->setSparklineColors($sparkline, $seriesIndex);
        }

        $sparkline->setWidth($this->getWidth());
        $sparkline->setHeight($this->getHeight());
        $sparkline->setLineThickness($this->getLineThickness());
        $sparkline->setPadding('5');

        $this->sparkline = $sparkline;
    }

    /**
     * Returns the width of the sparkline
     * @return int
     */
    public function getWidth()
    {
        return $this->width;
    }

    /**
     * Sets the width of the sparkline
     * @param int $width
     */
    public function setWidth($width)
    {
        if (!is_numeric($width) || $width <= 0) {
            return;
        }
        if ($width > self::MAX_WIDTH) {
            $this->width = self::MAX_WIDTH;
        } else {
            $this->width = (int)$width;
        }
    }

    /**
     * Returns the height of the sparkline
     * @return int
     */
    public function getHeight()
    {
        return $this->height;
    }

    /**
     * Sets the height of the sparkline
     * @param int $height
     */
    public function setHeight($height)
    {
        if (!is_numeric($height) || $height <= 0) {
            return;
        }
        if ($height > self::MAX_HEIGHT) {
            $this->height = self::MAX_HEIGHT;
        } else {
            $this->height = (int)$height;
        }
    }

    /**
     * Sets the sparkline colors
     *
     * @param \Davaxi\Sparkline $sparkline
     */
    private function setSparklineColors($sparkline, $seriesIndex)
    {
        $colors = Common::getRequestVar('colors', false, 'json');

        $defaultColors = array(
            'backgroundColor' => '#ffffff',
            'lineColor' => '#162C4A',
            'minPointColor' => '#ff7f7f',
            'maxPointColor' => '#75BF7C',
            'lastPointColor' => '#55AAFF',
            'fillColor' => '#ffffff',
        );

        if (empty($colors) || !is_array($colors)) {
            $colors = $defaultColors; //set default color, if no color passed
        } else {
            $colors = array_merge($defaultColors, $colors); //set default color key, if no key set.
        }

        if ($this->shouldApplyColor($colors['backgroundColor'])) {
            $sparkline->setBackgroundColorHex($colors['backgroundColor']);
        } else {
            $sparkline->deactivateBackgroundColor();
        }

        if (is_array($colors['lineColor'])) {
            $sparkline->setLineColorHex($colors['lineColor'][$seriesIndex] ?? $defaultColors['lineColor'], $seriesIndex);

            // set point colors to same as line colors so they can be better differentiated
            $colors['minPointColor'] = $colors['maxPointColor'] = $colors['lastPointColor'] = $colors['lineColor'][$seriesIndex] ?? $defaultColors['lineColor'];
        } else {
            $sparkline->setLineColorHex($colors['lineColor']);
        }

        if ($this->shouldApplyColor($colors['fillColor'])) {
            $sparkline->setFillColorHex($colors['fillColor']);
        } else {
            $sparkline->deactivateFillColor();
        }
        if ($this->shouldApplyColor($colors['minPointColor'])) {
            $sparkline->addPoint("minimum", 5, $colors['minPointColor'], $seriesIndex);
        }
        if ($this->shouldApplyColor($colors['maxPointColor'])) {
            $sparkline->addPoint("maximum", 5, $colors['maxPointColor'], $seriesIndex);
        }
        if ($this->shouldApplyColor($colors['lastPointColor'])) {
            $sparkline->addPoint("last", 5, $colors['lastPointColor'], $seriesIndex);
        }
    }

    private function getLineThickness(): int
    {
        if ($this->isSparklinesRedesignEnabled()) {
            return self::REDESIGN_LINE_THICKNESS;
        }

        return self::DEFAULT_LINE_THICKNESS;
    }

    private function isSparklinesRedesignEnabled(): bool
    {
        return StaticContainer::get(FeatureFlagManager::class)->isFeatureActive(SparklinesRedesign::class);
    }

    private function shouldApplyColor($color): bool
    {
        return is_string($color)
            && strtolower($color) !== '#ffffff';
    }

    public function render()
    {
        if (!$this->sparkline instanceof \Davaxi\Sparkline) {
            return;
        }

        if (0 === $this->sparkline->getSeriesCount()) {
            // ensure to have at least one series & point in sparkline to avoid possible php notices/errors
            // a sparkline will then be displayed with a zero line
            $this->sparkline->addSeries([0]);
        }

        $this->sparkline->display();
        $this->sparkline->destroy();
    }
}
