<?php

namespace dokuwiki\template\sprintdoc;

if(!defined('DOKU_INC')) define('DOKU_INC', dirname(__FILE__) . '/../../../');
require_once(DOKU_INC . 'inc/init.php');

/**
 * Custom XML node that allows prepending
 */
class SvgNode extends \SimpleXMLElement {
    /**
     * @param string $name Name of the new node
     * @param null|string $value
     * @return SvgNode
     */
    public function prependChild($name, $value = null) {
        $dom = dom_import_simplexml($this);

        $new = $dom->insertBefore(
            $dom->ownerDocument->createElement($name, $value),
            $dom->firstChild
        );

        return simplexml_import_dom($new, get_class($this));
    }

    /**
     * @param \SimpleXMLElement $node the node to be added
     * @return \SimpleXMLElement
     */
    public function appendNode(\SimpleXMLElement $node) {
        $dom = dom_import_simplexml($this);
        $domNode = dom_import_simplexml($node);

        $newNode = $dom->appendChild($domNode);
        return simplexml_import_dom($newNode, get_class($this));
    }

    /**
     * @param \SimpleXMLElement $node the child to remove
     * @return \SimpleXMLElement
     */
    public function removeChild(\SimpleXMLElement $node) {
        $dom = dom_import_simplexml($node);
        $dom->parentNode->removeChild($dom);
        return $node;
    }

    /**
     * Wraps all elements of $this in a `<g>` tag
     *
     * @return SvgNode
     */
    public function groupChildren() {
        $dom = dom_import_simplexml($this);

        $g = $dom->ownerDocument->createElement('g');
        while($dom->childNodes->length > 0) {
            $child = $dom->childNodes->item(0);
            $dom->removeChild($child);
            $g->appendChild($child);
        }
        $g = $dom->appendChild($g);

        return simplexml_import_dom($g, get_class($this));
    }

    /**
     * Add new style definitions to this element
     * @param string $style
     */
    public function addStyle($style) {
        $defs = $this->defs;
        if(!$defs) {
            $defs = $this->prependChild('defs');
        }
        $defs->addChild('style', $style);
    }
}

/**
 * Manage SVG recoloring
 */
class SVG {

    const IMGDIR = __DIR__ . '/img/';
    const BACKGROUNDCLASS = 'sprintdoc-background';
    const CDNBASE = 'https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/';

    protected $file;
    protected $replacements;

    /**
     * SVG constructor
     */
    public function __construct() {
        global $INPUT;

        $svg = cleanID($INPUT->str('svg'));
        if(blank($svg)) $this->abort(404);

        // try local file first
        $file = self::IMGDIR . $svg;
        if(!file_exists($file)) {
            // try media file
            $file = mediaFN($svg);
            if(file_exists($file)) {
                // media files are ACL protected
                if(auth_quickaclcheck($svg) < AUTH_READ) $this->abort(403);
            } else {
                // get it from material design icons
                $file = getCacheName($svg, '.svg');
                if (!file_exists($file)) {
                    io_download(self::CDNBASE . $svg, $file);
                }
            }

        }
        // check if media exists
        if(!file_exists($file)) $this->abort(404);

        $this->file = $file;
    }

    /**
     * Generate and output
     */
    public function out() {
        global $conf;
        $file = $this->file;
        $params = $this->getParameters();

        header('Content-Type: image/svg+xml');
        $cachekey = md5($file . serialize($params) . $conf['template'] . filemtime(__FILE__));
        $cache = new \dokuwiki\Cache\Cache($cachekey, '.svg');
        $cache->setEvent('SVG_CACHE');

        http_cached($cache->cache, $cache->useCache(array('files' => array($file, __FILE__))));
        if($params['e']) {
            $content = $this->embedSVG($file);
        } else {
            $content = $this->generateSVG($file, $params);
        }
        http_cached_finish($cache->cache, $content);
    }

    /**
     * Generate a new SVG based on the input file and the parameters
     *
     * @param string $file the SVG file to load
     * @param array $params the parameters as returned by getParameters()
     * @return string the new XML contents
     */
    protected function generateSVG($file, $params) {
        /** @var SvgNode $xml */
        $xml = simplexml_load_file($file, SvgNode::class);
        $xml->addStyle($this->makeStyle($params));
        $this->createBackground($xml);
        $xml->groupChildren();

        return $xml->asXML();
    }

    /**
     * Return the absolute minimum path definition for direct embedding
     *
     * No styles will be applied. They have to be done in CSS
     *
     * @param string $file the SVG file to load
     * @return string the new XML contents
     */
    protected function embedSVG($file) {
        /** @var SvgNode $xml */
        $xml = simplexml_load_file($file, SvgNode::class);

        $def = hsc((string) $xml->path['d']);
        $w = hsc($xml['width'] ?? '100%');
        $h = hsc($xml['height'] ?? '100%');
        $v = hsc($xml['viewBox']);

        // if viewbox is not defined, construct it from width and height, if available
        if (empty($v) && !empty($w) && !empty($h)) {
            $v = hsc("0 0 $w $h");
        }

        return "<svg viewBox=\"$v\"><path d=\"$def\" /></svg>";
    }

    /**
     * Get the supported parameters from request
     *
     * @return array
     */
    protected function getParameters() {
        global $INPUT;

        $params = array(
            'e' => $INPUT->bool('e', false),
            's' => $this->fixColor($INPUT->str('s')),
            'f' => $this->fixColor($INPUT->str('f')),
            'b' => $this->fixColor($INPUT->str('b')),
            'sh' => $this->fixColor($INPUT->str('sh')),
            'fh' => $this->fixColor($INPUT->str('fh')),
            'bh' => $this->fixColor($INPUT->str('bh')),
        );

        return $params;
    }

    /**
     * Generate a style setting from the input variables
     *
     * @param array $params associative array with the given parameters
     * @return string
     */
    protected function makeStyle($params) {
        $element = 'path'; // FIXME configurable?

        if(empty($params['b'])) {
            $params['b'] = $this->fixColor('00000000');
        }

        $style = 'g rect.' . self::BACKGROUNDCLASS . '{fill:' . $params['b'] . ';}';

        if($params['bh']) {
            $style .= 'g:hover rect.' . self::BACKGROUNDCLASS . '{fill:' . $params['bh'] . ';}';
        }

        if($params['s'] || $params['f']) {
            $style .= 'g ' . $element . '{';
            if($params['s']) $style .= 'stroke:' . $params['s'] . ';';
            if($params['f']) $style .= 'fill:' . $params['f'] . ';';
            $style .= '}';
        }

        if($params['sh'] || $params['fh']) {
            $style .= 'g:hover ' . $element . '{';
            if($params['sh']) $style .= 'stroke:' . $params['sh'] . ';';
            if($params['fh']) $style .= 'fill:' . $params['fh'] . ';';
            $style .= '}';
        }

        return $style;
    }

    /**
     * Takes a hexadecimal color string in the following forms:
     *
     * RGB
     * RRGGBB
     * RRGGBBAA
     *
     * Converts it to rgba() form.
     *
     * Alternatively takes a replacement name from the current template's style.ini
     *
     * @param string $color
     * @return string
     */
    protected function fixColor($color) {
        if($color === '') return '';
        if(preg_match('/^([0-9a-f])([0-9a-f])([0-9a-f])$/i', $color, $m)) {
            $r = hexdec($m[1] . $m[1]);
            $g = hexdec($m[2] . $m[2]);
            $b = hexdec($m[3] . $m[3]);
            $a = hexdec('ff');
        } elseif(preg_match('/^([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})?$/i', $color, $m)) {
            $r = hexdec($m[1]);
            $g = hexdec($m[2]);
            $b = hexdec($m[3]);
            if(isset($m[4])) {
                $a = hexdec($m[4]);
            } else {
                $a = hexdec('ff');
            }
        } else {
            if(is_null($this->replacements)) $this->initReplacements();
            if(isset($this->replacements[$color])) {
                return $this->replacements[$color];
            }
            if(isset($this->replacements['__' . $color . '__'])) {
                return $this->replacements['__' . $color . '__'];
            }
            return '';
        }

        return "rgba($r,$g,$b,$a)";
    }

    /**
     * sets a rectangular background of the size of the svg/this itself
     *
     * @param SvgNode $g
     * @return SvgNode
     */
    protected function createBackground(SvgNode $g) {
        $rect = $g->prependChild('rect');
        $rect->addAttribute('class', self::BACKGROUNDCLASS);

        $rect->addAttribute('x', '0');
        $rect->addAttribute('y', '0');
        $rect->addAttribute('height', '100%');
        $rect->addAttribute('width', '100%');
        return $rect;
    }

    /**
     * Abort processing with given status code
     *
     * @param int $status
     */
    protected function abort($status) {
        http_status($status);
        exit;
    }

    /**
     * Initialize the available replacement patterns
     *
     * Loads the style.ini from the template (and various local locations)
     * via a core function only available through some hack.
     */
    protected function initReplacements() {
        global $conf;
        if (!class_exists('\dokuwiki\StyleUtils')) {
            // Pre-Greebo Compatibility

            define('SIMPLE_TEST', 1); // hacky shit
            include DOKU_INC . 'lib/exe/css.php';
            $ini = css_styleini($conf['template']);
            $this->replacements = $ini['replacements'];
            return;
        }

        $stuleUtils = new \dokuwiki\StyleUtils();
        $ini = $stuleUtils->cssStyleini('sprintdoc');
        $this->replacements = $ini['replacements'];
    }
}

// main
$svg = new SVG();
$svg->out();