<?php

// SPDX-FileCopyrightText: 2018 Icinga GmbH <https://icinga.com>
// SPDX-License-Identifier: GPL-3.0-or-later

namespace Icinga\Chart;

use DOMElement;
use Icinga\Chart\Chart;
use Icinga\Chart\Axis;
use Icinga\Chart\Graph\BarGraph;
use Icinga\Chart\Graph\LineGraph;
use Icinga\Chart\Graph\StackedGraph;
use Icinga\Chart\Graph\Tooltip;
use Icinga\Chart\Primitive\Canvas;
use Icinga\Chart\Primitive\Rect;
use Icinga\Chart\Primitive\Path;
use Icinga\Chart\Render\LayoutBox;
use Icinga\Chart\Render\RenderContext;
use Icinga\Chart\Unit\AxisUnit;

/**
 * Base class for grid based charts.
 *
 * Allows drawing of Line and Barcharts. See the graphing documentation for further details.
 *
 * Example:
 * <pre>
 * <code>
 * $this->chart = new GridChart();
 * $this->chart->setAxisLabel("X axis label", "Y axis label");
 * $this->chart->setXAxis(Axis::CalendarUnit());
 * $this->chart->drawLines(
 * array(
 *      'data'  => array(
 *          array(time()-7200, 10),array(time()-3620, 30), array(time()-1800, 15), array(time(), 92))
 *      )
 * );
 * </code>
 * </pre>
 */
class GridChart extends Chart
{
    /**
     * Internal identifier for Line Chart elements
     */
    const TYPE_LINE = "LINE";

    /**
     * Internal identifier fo Bar Chart elements
     */
    const TYPE_BAR  = "BAR";

    /**
     * Internal array containing all elements to be drawn in the order they are drawn
     *
     * @var array
     */
    private $graphs = array();

    /**
     * An associative array containing all axis of this Chart in the  "name" => Axis() form.
     *
     * Currently only the 'default' axis is really supported
     *
     * @var array
     */
    private $axis = array();

    /**
     * An associative array containing all StackedGraph objects used for cumulative graphs
     *
     * The array key is the 'stack' value given in the graph definitions
     *
     * @var array
     */
    private $stacks = array();

    /**
     * An associative array containing all Tooltips used to render the titles
     *
     * Each tooltip represents the summary for all y-values of a certain x-value
     * in the grid chart
     *
     * @var Tooltip
     */
    private $tooltips = array();

    public function __construct()
    {
        $this->title = t('Grid Chart');
        $this->description = t('Contains data in a bar or line chart.');
        parent::__construct();
    }

    /**
     * Check if the current dataset has the proper structure for this chart.
     *
     * Needs to be overwritten by extending classes. The default implementation returns false.
     *
     * @return bool True when the dataset is valid, otherwise false
     */
    public function isValidDataFormat()
    {
        foreach ($this->graphs as $values) {
            foreach ($values as $value) {
                if (!isset($value['data']) || !is_array($value['data'])) {
                    return false;
                }
            }
        }
        return true;
    }

    /**
     * Calls Axis::addDataset for every graph added to this GridChart
     *
     * @see Axis::addDataset
     */
    private function configureAxisFromDatasets()
    {
        foreach ($this->graphs as $axis => &$graphs) {
            $axisObj = $this->axis[$axis];
            foreach ($graphs as &$graph) {
                $axisObj->addDataset($graph);
            }
        }
    }

    /**
     * Add an arbitrary number of lines to be drawn
     *
     * Refer to the graphs.md for a detailed list of allowed attributes
     *
     * @param   array $axis,... The line definitions to draw
     *
     * @return  $this            Fluid interface
     */
    public function drawLines(array $axis)
    {
        $this->draw(self::TYPE_LINE, func_get_args());
        return $this;
    }

    /**
     * Add arbitrary number of bars to be drawn
     *
     * Refer to the graphs.md for a detailed list of allowed attributes
     *
     * @param   array $axis
     * @return  $this
     */
    public function drawBars(array $axis)
    {
        $this->draw(self::TYPE_BAR, func_get_args());
        return $this;
    }

    /**
     * Generic method for adding elements to the drawing stack
     *
     * @param string $type The type of the element to draw (see TYPE_ constants in this class)
     * @param array $data The data given to the draw call
     */
    private function draw($type, $data)
    {
        $axisName = 'default';
        if (is_string($data[0])) {
            $axisName =  $data[0];
            array_shift($data);
        }
        foreach ($data as &$graph) {
            $graph['graphType'] = $type;
            if (isset($graph['stack'])) {
                if (!isset($this->stacks[$graph['stack']])) {
                    $this->stacks[$graph['stack']] = new StackedGraph();
                }
                $this->stacks[$graph['stack']]->addGraph($graph);
                $graph['stack'] = $this->stacks[$graph['stack']];
            }

            if (!isset($graph['color'])) {
                $colorType = isset($graph['palette']) ?  $graph['palette'] : Palette::NEUTRAL;
                $graph['color'] = $this->palette->getNext($colorType);
            }
            $this->graphs[$axisName][] = $graph;
            if ($this->legend) {
                $this->legend->addDataset($graph);
            }
        }
        $this->initTooltips($data);
    }


    private function initTooltips($data)
    {
        foreach ($data as &$graph) {
            foreach ($graph['data'] as $x => $point) {
                if (!array_key_exists($x, $this->tooltips)) {
                    $this->tooltips[$x] = new Tooltip(
                        array(
                            'color' => $graph['color'],

                        )
                    );
                }
                $this->tooltips[$x]->addDataPoint($point);
            }
        }
    }

    /**
     * Set the label for the x and y axis
     *
     * @param   string $xAxisLabel  The label to use for the x axis
     * @param   string $yAxisLabel  The label to use for the y axis
     * @param   string $axisName    The name of the axis, for now 'default'
     *
     * @return  $this                Fluid interface
     */
    public function setAxisLabel($xAxisLabel, $yAxisLabel, $axisName = 'default')
    {
        $this->axis[$axisName]->setXLabel($xAxisLabel)->setYLabel($yAxisLabel);
        return $this;
    }

    /**
     * Set the AxisUnit to use for calculating the values of the x axis
     *
     * @param   AxisUnit    $unit       The unit for the x axis
     * @param   string      $axisName   The name of the axis to set the label for, currently only 'default'
     *
     * @return  $this                    Fluid interface
     */
    public function setXAxis(AxisUnit $unit, $axisName = 'default')
    {
        $this->axis[$axisName]->setUnitForXAxis($unit);
        return $this;
    }

    /**
     * Set the AxisUnit to use for calculating the values of the y axis
     *
     * @param   AxisUnit    $unit       The unit for the y axis
     * @param   string      $axisName   The name of the axis to set the label for, currently only 'default'
     *
     * @return  $this                    Fluid interface
     */
    public function setYAxis(AxisUnit $unit, $axisName = 'default')
    {
        $this->axis[$axisName]->setUnitForYAxis($unit);
        return $this;
    }

    /**
     * Pre-render setup of the axis
     *
     * @see Chart::build
     */
    protected function build()
    {
        $this->configureAxisFromDatasets();
    }

    /**
     * Initialize the renderer and overwrite it with an 2:1 ration renderer
     */
    protected function init()
    {
        $this->renderer = new SVGRenderer(100, 100);
        $this->setAxis(Axis::createLinearAxis());
    }

    /**
     * Overwrite the axis to use
     *
     * @param   Axis    $axis The new axis to use
     * @param   string  $name The name of the axis, currently only 'default'
     *
     * @return  $this          Fluid interface
     */
    public function setAxis(Axis $axis, $name = 'default')
    {
        $this->axis = array($name => $axis);
        return $this;
    }

    /**
     * Add an axis to this graph (not really supported right now)
     *
     * @param   Axis    $axis The axis object to add
     * @param   string  $name The name of the axis
     *
     * @return  $this          Fluid interface
     */
    public function addAxis(Axis $axis, $name)
    {
        $this->axis[$name] = $axis;
        return $this;
    }

    /**
     * Set minimum values for the x and y axis.
     *
     * Setting null to an axis means this will use a value determined by the dataset
     *
     * @param   int     $xMin       The minimum value for the x axis or null to use a dynamic value
     * @param   int     $yMin       The minimum value for the y axis or null to use a dynamic value
     * @param   string  $axisName   The name of the axis to set the minimum, currently only 'default'
     *
     * @return  $this                Fluid interface
     */
    public function setAxisMin($xMin = null, $yMin = null, $axisName = 'default')
    {
        $this->axis[$axisName]->setXMin($xMin)->setYMin($yMin);
        return $this;
    }

    /**
     * Set maximum values for the x and y axis.
     *
     * Setting null to an axis means this will use a value determined by the dataset
     *
     * @param   int     $xMax       The maximum value for the x axis or null to use a dynamic value
     * @param   int     $yMax       The maximum value for the y axis or null to use a dynamic value
     * @param   string  $axisName   The name of the axis to set the maximum, currently only 'default'
     *
     * @return  $this                Fluid interface
     */
    public function setAxisMax($xMax = null, $yMax = null, $axisName = 'default')
    {
        $this->axis[$axisName]->setXMax($xMax)->setYMax($yMax);
        return $this;
    }

    /**
     * Render this GridChart to SVG
     *
     * @param   RenderContext $ctx The context to use for rendering
     *
     * @return  DOMElement
     */
    public function toSvg(RenderContext $ctx)
    {
        $outerBox = new Canvas('outerGraph', new LayoutBox(0, 0, 100, 100));
        $innerBox = new Canvas('graph', new LayoutBox(0, 0, 95, 90));

        $maxPadding = array(0,0,0,0);
        foreach ($this->axis as $axis) {
            $padding = $axis->getRequiredPadding();
            for ($i=0; $i < count($padding); $i++) {
                $maxPadding[$i] = max($maxPadding[$i], $padding[$i]);
            }
            $innerBox->addElement($axis);
        }
        $this->renderGraphContent($innerBox);

        $innerBox->getLayout()->setPadding($maxPadding[0], $maxPadding[1], $maxPadding[2], $maxPadding[3]);
        $this->createContentClipBox($innerBox);

        $outerBox->addElement($innerBox);
        if ($this->legend) {
            $outerBox->addElement($this->legend);
        }
        return $outerBox->toSvg($ctx);
    }

    /**
     * Create a clip box that defines which area of the graph is drawable and adds it to the graph.
     *
     * The clipbox has the id '#clip' and can be used in the clip-mask element
     *
     * @param Canvas $innerBox The inner canvas of the graph to add the clip box to
     */
    private function createContentClipBox(Canvas $innerBox)
    {
        $clipBox = new Canvas('clip', new LayoutBox(0, 0, 100, 100));
        $clipBox->toClipPath();
        $innerBox->addElement($clipBox);
        $rect = new Rect(0.1, 0, 100, 99.9);
        $clipBox->addElement($rect);
    }

    /**
     * Render the content of the graph, i.e. the draw stack
     *
     * @param Canvas $innerBox The inner canvas of the graph to add the content to
     */
    private function renderGraphContent(Canvas $innerBox)
    {
        foreach ($this->graphs as $axisName => $graphs) {
            $axis = $this->axis[$axisName];
            $graphObj = null;
            foreach ($graphs as $dataset => $graph) {
                // determine the type and create a graph object for it
                switch ($graph['graphType']) {
                    case self::TYPE_BAR:
                        $graphObj = new BarGraph(
                            $axis->transform($graph['data']),
                            $graphs,
                            $dataset,
                            $this->tooltips
                        );
                        break;
                    case self::TYPE_LINE:
                        $graphObj = new LineGraph(
                            $axis->transform($graph['data']),
                            $graphs,
                            $dataset,
                            $this->tooltips
                        );
                        break;
                    default:
                        continue 2;
                }
                $el = $this->setupGraph($graphObj, $graph);
                if ($el) {
                    $innerBox->addElement($el);
                }
            }
        }
    }

    /**
     * Setup the provided Graph type
     *
     * @param   mixed $graphObject  The graph class, needs the setStyleFromConfig method
     * @param   array $graphConfig  The configration array of the graph
     *
     * @return  mixed               Either the graph to be added or null if the graph is not directly added
     *                              to the document (e.g. stacked graphs are added by
     *                              the StackedGraph Composite object)
     */
    private function setupGraph($graphObject, array $graphConfig)
    {
        $graphObject->setStyleFromConfig($graphConfig);
        // When in a stack return the StackedGraph object instead of the graphObject
        if (isset($graphConfig['stack'])) {
            $graphConfig['stack']->addToStack($graphObject);
            if (!$graphConfig['stack']->stackEmpty()) {
                return $graphConfig['stack'];
            }
            // return no object when the graph should not be rendered
            return null;
        }
        return $graphObject;
    }
}
