<?php
// {{{ICINGA_LICENSE_HEADER}}}
/**
 * This file is part of Icinga Web 2.
 *
 * Icinga Web 2 - Head for multiple monitoring backends.
 * Copyright (C) 2013 Icinga Development Team
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 *
 * @copyright  2013 Icinga Development Team <info@icinga.org>
 * @license    http://www.gnu.org/licenses/gpl-2.0.txt GPL, version 2
 * @author     Icinga Development Team <info@icinga.org>
 *
 */
// {{{ICINGA_LICENSE_HEADER}}}

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 futher 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;

    /**
     * Scaling level of the rendered svgs width in percent.
     *
     * @var float
     */
    private $width = 100;

    /**
     * Scaling level of the rendered svgs height in percent.
     *
     * @var int
     */
    private $height = 100;

    /**
     * Set the size of the rendered pie-chart svg.
     *
     * @param $width    int The width in percent.
     *
     * @return self  Fluent interface
     */
    public function setWidth($width)
    {
        $this->width = $width;
        return $this;
    }

    /**
     * Set the size of the rendered pie-chart svg.
     *
     * @param $height   int The height in percent.
     *
     * @return self  Fluent interface
     */
    public function setHeight($height)
    {
        $this->height = $height;
        return $this;
    }

    /**
     * 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 ? $this->width : count($this->pies) * $this->width,
            $this->width
        );
        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  self                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)
    {
        $outerBox = new Canvas('outerGraph', new LayoutBox(0, 0, 100, 100));
        $innerBox = new Canvas('graph', new LayoutBox(0, 0, 100, 100));
        $labelBox = $ctx->getDocument()->createElement('g');
        if (!$this->noCaption) {
            $innerBox->getLayout()->setPadding(10, 10, 10, 10);
        }
        $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 === 100) {
                    $dataset = 99.9;
                }
                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 self         Fluent interface
     */
    public function setType($type)
    {
        $this->type = $type;
        return $this;
    }

    /**
     * Hide the caption from this PieChart
     *
     * @return self         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);
    }
}
