<?php
/* Icinga Web 2 | (c) 2013 Icinga Development Team | GPLv2+ */

namespace Icinga\Chart;

use DOMElement;
use Icinga\Chart\Chart;
use Icinga\Chart\Primitive\Canvas;
use Icinga\Chart\Primitive\PieSlice;
use Icinga\Chart\Primitive\RawElement;
use Icinga\Chart\Primitive\Rect;
use Icinga\Chart\Render\RenderContext;
use Icinga\Chart\Render\LayoutBox;

/**
 * Graphing component for rendering Pie Charts.
 *
 * See the graphs.md documentation for further information about how to use this component
 */
class PieChart extends Chart
{
    /**
     * Stack multiple pies
     */
    const STACKED = "stacked";

    /**
     * Draw multiple pies beneath each other
     */
    const ROW = "row";

    /**
     * The drawing stack containing all pie definitions in the order they will be drawn
     *
     * @var array
     */
    private $pies = array();

    /**
     * The composition type currently used
     *
     * @var string
     */
    private $type = PieChart::STACKED;

    /**
     * Disable drawing of captions when set true
     *
     * @var bool
     */
    private $noCaption = false;

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

    /**
     * Test if the given pies have the correct format
     *
     * @return bool True when the given pies are correct, otherwise false
     */
    public function isValidDataFormat()
    {
        foreach ($this->pies as $pie) {
            if (!isset($pie['data']) || !is_array($pie['data'])) {
                return false;
            }
        }
        return true;
    }

    /**
     * Create renderer and normalize the dataset to represent percentage information
     */
    protected function build()
    {
        $this->renderer = new SVGRenderer(($this->type === self::STACKED) ? 1 : count($this->pies), 1);
        foreach ($this->pies as &$pie) {
            $this->normalizeDataSet($pie);
        }
    }

    /**
     * Normalize the given dataset to represent percentage information instead of absolute valuess
     *
     * @param array $pie The pie definition given in the drawPie call
     */
    private function normalizeDataSet(&$pie)
    {
        $total = array_sum($pie['data']);
        if ($total === 100) {
            return;
        }
        if ($total == 0) {
            return;
        }
        foreach ($pie['data'] as &$slice) {
            $slice = $slice/$total * 100;
        }
    }

    /**
     * Draw an arbitrary number of pies in this chart
     *
     * @param   array $dataSet,...  The pie definition, see graphs.md for further details concerning the format
     *
     * @return  $this                Fluent interface
     */
    public function drawPie(array $dataSet)
    {
        $dataSets = func_get_args();
        $this->pies += $dataSets;
        foreach ($dataSets as $dataSet) {
            $this->legend->addDataset($dataSet);
        }
        return $this;
    }

    /**
     * Return the SVG representation of this graph
     *
     * @param RenderContext $ctx    The context to use for drawings
     *
     * @return DOMElement           The SVG representation of this graph
     */
    public function toSvg(RenderContext $ctx)
    {
        $labelBox = $ctx->getDocument()->createElement('g');
        if (!$this->noCaption) {
            // Scale SVG to make room for captions
            $outerBox = new Canvas('outerGraph', new LayoutBox(33, -5, 40, 40));
            $innerBox = new Canvas('graph', new LayoutBox(0, 0, 100, 100));
            $innerBox->getLayout()->setPadding(10, 10, 10, 10);
        } else {
            $outerBox = new Canvas('outerGraph', new LayoutBox(1.5, -10, 124, 124));
            $innerBox = new Canvas('graph', new LayoutBox(0, 0, 100, 100));
            $innerBox->getLayout()->setPadding(0, 0, 0, 0);
        }
        $this->createContentClipBox($innerBox);
        $this->renderPies($innerBox, $labelBox);
        $innerBox->addElement(new RawElement($labelBox));
        $outerBox->addElement($innerBox);

        return $outerBox->toSvg($ctx);
    }

    /**
     * Render the pies in the draw stack using the selected algorithm for composition
     *
     * @param Canvas $innerBox      The canvas to use for inserting the pies
     * @param DOMElement $labelBox  The DOM element to add the labels to (so they can't be overlapped by pie elements)
     */
    private function renderPies(Canvas $innerBox, DOMElement $labelBox)
    {
        if ($this->type === self::STACKED) {
            $this->renderStackedPie($innerBox, $labelBox);
        } else {
            $this->renderPieRow($innerBox, $labelBox);
        }
    }

    /**
     * Return the color to be used for the given pie slice
     *
     * @param array $pie    The pie configuration as provided in the drawPie call
     * @param int $dataIdx  The index of the pie slice in the pie configuration
     *
     * @return string       The hex color string to use for the pie slice
     */
    private function getColorForPieSlice(array $pie, $dataIdx)
    {
        if (isset($pie['colors']) && is_array($pie['colors']) && isset($pie['colors'][$dataIdx])) {
            return $pie['colors'][$dataIdx];
        }
        $type = Palette::NEUTRAL;
        if (isset($pie['palette']) && is_array($pie['palette']) && isset($pie['palette'][$dataIdx])) {
            $type = $pie['palette'][$dataIdx];
        }
        return $this->palette->getNext($type);
    }

    /**
     * Render a row of pies
     *
     * @param Canvas $innerBox      The canvas to insert the pies to
     * @param DOMElement $labelBox  The DOMElement to use for adding label elements
     */
    private function renderPieRow(Canvas $innerBox, DOMElement $labelBox)
    {
        $radius = 50 / count($this->pies);
        $x = $radius;
        foreach ($this->pies as $pie) {
            $labelPos = 0;
            $lastRadius = 0;

            foreach ($pie['data'] as $idx => $dataset) {
                $slice = new PieSlice($radius, $dataset, $lastRadius);
                $slice->setX($x)
                    ->setStrokeColor('#000')
                    ->setStrokeWidth(1)
                    ->setY(50)
                    ->setFill($this->getColorForPieSlice($pie, $idx));
                $innerBox->addElement($slice);
                // add caption if not disabled
                if (!$this->noCaption && isset($pie['labels'])) {
                    $slice->setCaption($pie['labels'][$labelPos++])
                        ->setLabelGroup($labelBox);
                }
                $lastRadius += $dataset;
            }
            // shift right for next pie
            $x += $radius*2;
        }
    }

    /**
     * Render pies in a stacked way so one pie is nested in the previous pie
     *
     * @param Canvas $innerBox      The canvas to insert the pie to
     * @param DOMElement $labelBox  The DOMElement to use for adding label elements
     */
    private function renderStackedPie(Canvas $innerBox, DOMElement $labelBox)
    {
        $radius = 40;
        $minRadius = 20;
        if (count($this->pies) == 0) {
            return;
        }
        $shrinkStep = ($radius - $minRadius) / count($this->pies);
        $x = $radius;

        for ($i = 0; $i < count($this->pies); $i++) {
            $pie = $this->pies[$i];
            // the offset for the caption path, outer caption indicator shouldn't point
            // to the middle of the slice as there will be another pie
            $offset = isset($this->pies[$i+1]) ? $radius - $shrinkStep : 0;
            $labelPos = 0;
            $lastRadius = 0;
            foreach ($pie['data'] as $idx => $dataset) {
                $color = $this->getColorForPieSlice($pie, $idx);
                if ($dataset == 0) {
                    $labelPos++;
                    continue;
                }
                $slice = new PieSlice($radius, $dataset, $lastRadius);
                $slice->setY(50)
                    ->setX($x)
                    ->setStrokeColor('#000')
                    ->setStrokeWidth(1)
                    ->setFill($color)
                    ->setLabelGroup($labelBox);

                if (!$this->noCaption && isset($pie['labels'])) {
                    $slice->setCaption($pie['labels'][$labelPos++])
                        ->setCaptionOffset($offset)
                        ->setOuterCaptionBound(50);
                }
                $innerBox->addElement($slice);
                $lastRadius += $dataset;
            }
            // shrinken the next pie
            $radius -= $shrinkStep;
        }
    }

    /**
     * Set the composition type of this PieChart
     *
     * @param string $type  Either self::STACKED or self::ROW
     *
     * @return $this         Fluent interface
     */
    public function setType($type)
    {
        $this->type = $type;
        return $this;
    }

    /**
     * Hide the caption from this PieChart
     *
     * @return $this         Fluent interface
     */
    public function disableLegend()
    {
        $this->noCaption = true;
        return $this;
    }

    /**
     * Create the content for this PieChart
     *
     * @param Canvas $innerBox      The innerbox to add the clip mask 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);
    }
}
