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

use CpChart\Data;
use CpChart\Image;
use Piwik\Container\StaticContainer;
use Piwik\NumberFormatter;
use Piwik\Piwik;
use Piwik\BaseFactory;

/**
 * The StaticGraph abstract class is used as a base class for different types of static graphs.
 *
 */
abstract class StaticGraph extends BaseFactory
{
    public const GRAPH_TYPE_BASIC_LINE = "evolution";
    public const GRAPH_TYPE_VERTICAL_BAR = "verticalBar";
    public const GRAPH_TYPE_HORIZONTAL_BAR = "horizontalBar";
    public const GRAPH_TYPE_3D_PIE = "3dPie";
    public const GRAPH_TYPE_BASIC_PIE = "pie";

    private static $availableStaticGraphTypes = array(
        self::GRAPH_TYPE_BASIC_LINE     => 'Evolution',
        self::GRAPH_TYPE_VERTICAL_BAR   => 'VerticalBar',
        self::GRAPH_TYPE_HORIZONTAL_BAR => 'HorizontalBar',
        self::GRAPH_TYPE_BASIC_PIE      => 'Pie',
        self::GRAPH_TYPE_3D_PIE         => 'Pie3D',
    );

    public const ABSCISSA_SERIE_NAME = 'ABSCISSA';

    private $aliasedGraph;

    /**
     * @var Image
     */
    protected $pImage;
    /**
     * @var Data
     */
    protected $pData;
    protected $ordinateLabels;
    protected $showLegend;
    protected $abscissaSeries;
    protected $abscissaLogos;
    protected $ordinateSeries;
    protected $ordinateLogos;
    protected $colors;
    protected $font;
    protected $fontSize;
    protected $textColor = [];
    protected $backgroundColor = [];
    protected $gridColor = [];
    protected $legendFontSize;
    protected $width;
    protected $height;
    protected $forceSkippedLabels = false;

    abstract protected function getDefaultColors();

    abstract public function renderGraph();

    protected static function getClassNameFromClassId($graphType)
    {
        $className = self::$availableStaticGraphTypes[$graphType];
        $className = __NAMESPACE__ . "\\StaticGraph\\" . $className;
        return $className;
    }

    protected static function getInvalidClassIdExceptionMessage($graphType)
    {
        return Piwik::translate(
            'General_ExceptionInvalidStaticGraphType',
            array($graphType, implode(', ', self::getAvailableStaticGraphTypes()))
        );
    }

    public static function getAvailableStaticGraphTypes()
    {
        return array_keys(self::$availableStaticGraphTypes);
    }

    public static function fixWhitespaceNonUnifont($value)
    {
        if (!is_string($value)) {
            return $value;
        }

        return strtr(
            $value,
            [
                // thin space
                "\xE2\x80\x89" => ' ',
                // narrow non-break-space
                "\xE2\x80\xAF" => "\xC2\xA0",
            ]
        );
    }

    /**
     * Save rendering to disk
     *
     * @param string $filename without path
     * @return string path of file
     */
    public function sendToDisk($filename)
    {
        $filePath = self::getOutputPath($filename);
        $this->pImage->render($filePath);
        return $filePath;
    }

    /**
     * @return resource  rendered static graph
     */
    public function getRenderedImage()
    {
        return $this->pImage->Picture;
    }

    /**
     * Output rendering to browser
     */
    public function sendToBrowser()
    {
        $this->pImage->stroke();
    }

    public function setWidth($width)
    {
        $this->width = $width;
    }

    public function setHeight($height)
    {
        $this->height = $height;
    }

    public function setFontSize($fontSize)
    {
        if (!is_numeric($fontSize)) {
            $fontSize = API::DEFAULT_FONT_SIZE;
        }
        $this->fontSize = $fontSize;
    }

    public function setLegendFontSize($legendFontSize)
    {
        $this->legendFontSize = $legendFontSize;
    }

    public function setFont($font)
    {
        $this->font = $font;
    }

    public function setTextColor($textColor)
    {
        $this->textColor = self::hex2rgb($textColor);
    }

    public function setBackgroundColor($backgroundColor)
    {
        $this->backgroundColor = self::hex2rgb($backgroundColor);
    }

    public function setGridColor($gridColor)
    {
        $this->gridColor = self::hex2rgb($gridColor);
    }

    public function setOrdinateSeries($ordinateSeries)
    {
        $this->ordinateSeries = $ordinateSeries;
    }

    public function setOrdinateLogos($ordinateLogos)
    {
        $this->ordinateLogos = $ordinateLogos;
    }

    public function setAbscissaLogos($abscissaLogos)
    {
        $this->abscissaLogos = $abscissaLogos;
    }

    public function setAbscissaSeries($abscissaSeries)
    {
        $this->abscissaSeries = $abscissaSeries;
    }

    public function setShowLegend($showLegend)
    {
        $this->showLegend = $showLegend;
    }

    public function setForceSkippedLabels($forceSkippedLabels)
    {
        $this->forceSkippedLabels = $forceSkippedLabels;
    }

    public function setOrdinateLabels($ordinateLabels)
    {
        $this->ordinateLabels = $ordinateLabels;
    }

    public function setAliasedGraph($aliasedGraph)
    {
        $this->aliasedGraph = $aliasedGraph;
    }

    public function setColors($colors)
    {
        $i = 0;
        foreach ($this->getDefaultColors() as $colorKey => $defaultColor) {
            if (isset($colors[$i]) && $this->hex2rgb($colors[$i])) {
                $hexColor = $colors[$i];
            } else {
                $hexColor = $defaultColor;
            }

            $this->colors[$colorKey] = $this->hex2rgb($hexColor);
            $i++;
        }
    }

    /**
     * Return $filename with temp directory and delete file
     *
     * @static
     * @param  $filename
     * @return string path of file in temp directory
     */
    protected static function getOutputPath($filename)
    {
        $outputFilename = StaticContainer::get('path.tmp') . '/assets/' . $filename;

        @chmod($outputFilename, 0600);
        @unlink($outputFilename);
        return $outputFilename;
    }

    protected function initpData()
    {
        $this->pData = new Data();

        foreach ($this->ordinateSeries as $column => $data) {
            $this->pData->addPoints($data, $column);
            if (isset($this->ordinateLabels[$column])) {
                $this->pData->setSerieDescription($column, $this->ordinateLabels[$column]);
            }

            if (isset($this->ordinateLogos[$column])) {
                $ordinateLogo = $this->createResizedImageCopyIfNeeded($this->ordinateLogos[$column]);
                $this->pData->setSeriePicture($column, $ordinateLogo);
            }
        }

        // Fix whitespace if not using unifont
        $abscissaSeries = $this->abscissaSeries;
        $formatMethodName = 'formatYAxis';

        if (false === strpos($this->font, API::UNICODE_FONT)) {
            $abscissaSeries = array_map([$this, 'fixWhitespaceNonUnifont'], $abscissaSeries);
            $formatMethodName = 'formatYAxisNonUnifont';
        }

        $this->pData->setAxisDisplay(0, AXIS_FORMAT_CUSTOM, '\\Piwik\\Plugins\\ImageGraph\\' . $formatMethodName);
        $this->pData->addPoints($abscissaSeries, self::ABSCISSA_SERIE_NAME);
        $this->pData->setAbscissa(self::ABSCISSA_SERIE_NAME);
    }

    protected function createResizedImageCopyIfNeeded($image)
    {
        $size = getimagesize($image);

        if ($size[0] <= 16 && $size[1] <= 16) {
            return $image; // use original image if size fits
        }

        $ratio = $size[0] / $size[1];
        if ($ratio > 1) {
            $width  = 16;
            $height = 16 / $ratio;
        } else {
            $width  = 16 * $ratio;
            $height = 16;
        }

        $newImage = self::getOutputPath(md5($image) . '.png');

        $src = imagecreatefromstring(file_get_contents($image));
        $dst = imagecreatetruecolor($width, $height);
        imagesavealpha($dst, true);
        $color = imagecolorallocatealpha($dst, 0, 0, 0, 127);
        imagefill($dst, 0, 0, $color);
        imagecopyresampled($dst, $src, 0, 0, 0, 0, $width, $height, $size[0], $size[1]);
        imagedestroy($src);
        imagepng($dst, $newImage);
        imagedestroy($dst);

        return $newImage;
    }

    protected function initpImage()
    {
        $this->pImage = new Image($this->width, $this->height, $this->pData);
        $this->pImage->Antialias = $this->aliasedGraph;

        $this->pImage->setFontProperties(
            array_merge(
                array(
                     'FontName' => $this->font,
                     'FontSize' => $this->fontSize,
                ),
                $this->textColor
            )
        );
    }

    protected function getTextWidthHeight($text, $fontSize = false)
    {
        if (!$fontSize) {
            $fontSize = $this->fontSize;
        }

        if (!$this->pImage) {
            $this->initpImage();
        }

        // could not find a way to get pixel perfect width & height info using imageftbbox
        $textInfo = $this->pImage->drawText(
            0,
            0,
            $text,
            array(
                 'Alpha'    => 0,
                 'FontSize' => $fontSize,
                 'FontName' => $this->font,
            )
        );

        return array($textInfo[1]['X'] + 1, $textInfo[0]['Y'] - $textInfo[2]['Y']);
    }

    protected function getMaximumTextWidthHeight($values)
    {
        if (array_values($values) === $values) {
            $values = array('' => $values);
        }

        $maxWidth = 0;
        $maxHeight = 0;
        foreach ($values as $data) {
            foreach ($data as $value) {
                [$valueWidth, $valueHeight] = $this->getTextWidthHeight($value);

                if ($valueWidth > $maxWidth) {
                    $maxWidth = $valueWidth;
                }

                if ($valueHeight > $maxHeight) {
                    $maxHeight = $valueHeight;
                }
            }
        }

        return array($maxWidth, $maxHeight);
    }

    protected function drawBackground()
    {
        $this->pImage->drawFilledRectangle(
            0,
            0,
            $this->width,
            $this->height,
            array_merge(array('Alpha' => 100), $this->backgroundColor)
        );
    }

    private static function hex2rgb($hexColor)
    {
        if (preg_match('/([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/', $hexColor, $matches)) {
            return array(
                'R' => hexdec($matches[1]),
                'G' => hexdec($matches[2]),
                'B' => hexdec($matches[3]),
            );
        } else {
            return [];
        }
    }
}

/**
 * Global format method - unifont
 *
 * required to format y axis values using CpChart internal format callbacks
 * @param $value
 * @return mixed
 */
function formatYAxis($value)
{
    return NumberFormatter::getInstance()->format($value);
}

/**
 * Global format method - non-unifont
 *
 * required to format y axis values using CpChart internal format callbacks
 * @param $value
 * @return mixed
 */
function formatYAxisNonUnifont($value)
{
    return StaticGraph::fixWhitespaceNonUnifont(
        NumberFormatter::getInstance()->format($value)
    );
}
