<?php

namespace idoit\Module\Pro\Export;

use DOMDocument;
use DOMElement;
use Exception;
use idoit\Component\Helper\ArrayHelper;
use isys_application;
use isys_cmdb_dao;
use isys_component_template_language_manager;
use isys_exception_general;
use isys_helper_color;
use isys_tenantsettings;
use isys_tree_node_explorer;
use isys_visualization_model;
use isys_visualization_profile_model;
use isys_visualization_tree_model;

/**
 * i-doit
 * Visualization export class for "GraphML".
 *
 * @package     modules
 * @subpackage  pro
 * @copyright   synetics GmbH
 * @license     http://www.i-doit.com/license
 * @since       i-doit 1.5.0
 */
class GraphML
{
    private isys_cmdb_dao $dao;

    private DOMDocument $dom;

    private isys_component_template_language_manager $language;

    private string $emptyValue;

    private array $exportedNodes;

    private array $visibleObjects;

    private array $visualizationModels;

    private bool $bottomUp = true;

    public function __construct(isys_cmdb_dao $dao, isys_component_template_language_manager $language, string $type, int $object, int $profile, int $serviceFilter, array $visibleObjects)
    {
        $this->dao = $dao;
        $this->dom = new DOMDocument('1.0', 'UTF-8');
        $this->language = $language;
        $this->emptyValue = isys_tenantsettings::get('gui.empty_value', '-');
        $this->exportedNodes = [];
        $this->visibleObjects = $visibleObjects;
        $this->visualizationModels = [];

        $visualizationModel = "isys_visualization_{$type}_model";

        if (!class_exists($visualizationModel)) {
            throw new isys_exception_general("Graph type '{$type}' does not exist!");
        }

        if (!is_a($visualizationModel, isys_visualization_model::class, true)) {
            throw new isys_exception_general("Graph type '{$type}' needs to extend 'isys_visualization_model'!");
        }

        $database = isys_application::instance()->container->get('database');

        // @see ID-8541 Load the profile to establish "bottom-up" or "top-down"
        $profileConfiguration = isys_visualization_profile_model::instance($database)->get_profile_config($profile);

        $this->bottomUp = isset($profileConfiguration['master_top']) && $profileConfiguration['master_top'];

        $this->visualizationModels[] = $visualizationModel::instance($database)->recursion_run(
            $object,
            $serviceFilter,
            $profile
        );

        if ($visualizationModel === 'isys_visualization_tree_model') {
            $this->visualizationModels[] = isys_visualization_tree_model::instance($database)->recursion_run(
                $object,
                $serviceFilter,
                $profile,
                false
            );
        }
    }

    public function export(): string
    {
        $xmlTemplateString = <<<XML
<?xml version="1.0" encoding="UTF-8"?>
<graphml xmlns="http://graphml.graphdrawing.org/xmlns" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd"
         xmlns:y="http://www.yworks.com/xml/graphml">
    <key id="d0" for="node" yfiles.type="nodegraphics"/>
    <key id="d1" for="edge" yfiles.type="edgegraphics"/>
    <graph id="G" edgedefault="undirected">
    </graph>
</graphml>
XML;

        $this->dom->loadXML($xmlTemplateString);
        $graph = $this->dom->getElementsByTagName('graph')->item(0);

        $i = 0;

        $edgeCache = [];
        $additionalObjects = [];

        // Because some visualizations will render multiple trees, we need this.
        foreach ($this->visualizationModels as $index => $model) {
            // Now we iterate over our tree items to build the complete content.
            foreach ($model->all_nodes() as $node) {
                /** @var isys_tree_node_explorer $node */
                $nodeData = $node->get_data()->toArray();
                $nodeParent = $node->get_parent()?->get_data()->toArray();

                $additionalObjects[] = ($nodeParent['data']['obj_id'] ?? 0) . '>' . $nodeData['data']['obj_id'];

                $this->createNode(
                    $graph,
                    (int)$nodeData['data']['obj_id'],
                    isys_helper_color::unifyHexColor($nodeData['data']['obj_type_color']),
                    $nodeData['data']['obj_type_title'] . PHP_EOL . $nodeData['data']['obj_title']
                );

                if (!$node->has_children()) {
                    continue;
                }

                foreach ($node->get_childs() as $child) {
                    $childData = $child->get_data()->toArray();

                    $source = $index === 0 ? $nodeData['data']['obj_id'] : $childData['data']['obj_id'];
                    $target = $index === 0 ? $childData['data']['obj_id'] : $nodeData['data']['obj_id'];
                    $edgeCacheKey = $source . '>' . $target;

                    if (isset($edgeCache[$edgeCacheKey])) {
                        continue;
                    }

                    $edgeCache[$edgeCacheKey] = true;

                    // @see  ID-6972  The `$index` check is a bit messy but necessary since the second tree instance is "bottom-up".
                    $this->createEdge($graph, $i, $source, $target);

                    $i++;
                }
            }
        }

        foreach (ArrayHelper::difference($this->visibleObjects, $additionalObjects) as $additionalObject) {
            [$parent, $object] = explode('>', $additionalObject);

            if (!isset($this->exportedNodes[$parent])) {
                $parentData = $this->dao->get_object($parent)->get_row();

                $this->createNode(
                    $graph,
                    (int)$parent,
                    isys_helper_color::unifyHexColor($parentData['isys_obj_type__color']),
                    $this->language->get_in_text($parentData['isys_obj_type__title'] . PHP_EOL . $parentData['isys_obj__title'])
                );
            }

            if (!isset($this->exportedNodes[$object])) {
                $objectData = $this->dao->get_object($object)->get_row();

                $this->createNode(
                    $graph,
                    (int)$object,
                    isys_helper_color::unifyHexColor($objectData['isys_obj_type__color']),
                    $this->language->get_in_text($objectData['isys_obj_type__title'] . PHP_EOL . $objectData['isys_obj__title'])
                );
            }

            $edgeCacheKey = $parent . '>' . $object;

            if (isset($edgeCache[$edgeCacheKey])) {
                continue;
            }

            $edgeCache[$edgeCacheKey] = true;

            $this->createEdge($graph, $i, $parent, $object);

            $i++;
        }

        $xmlString = $this->dom->saveXML();

        if (!is_string($xmlString)) {
            throw new Exception('The GraphML export could not be created.');
        }

        return $xmlString;
    }

    private function createNode(DOMElement $graph, int $id, string $color, string $label): void
    {
        if (isset($this->exportedNodes[$id])) {
            return;
        }

        if ($this->visuallySkip($id)) {
            return;
        }

        $this->exportedNodes[$id] = true;

        $label = $label ?: $this->emptyValue;

        $nodeTemplateString = <<<XML
<node id="n{$id}">
    <data key="d0">
        <y:ShapeNode>
            <y:Geometry width="120" height="40"/>
            <y:Fill color="{$color}" transparent="false"/>
            <y:BorderStyle type="line" width="1.0" color="#000000"/>
            <y:NodeLabel><![CDATA[{$label}]]></y:NodeLabel>
            <y:Shape type="rectangle"/>
        </y:ShapeNode>
    </data>
</node>
XML;

        $edgeDom = new DOMDocument('1.0', 'UTF-8');
        $edgeDom->loadXML($nodeTemplateString);

        // Import document to prevent "Wrong Document Error".
        $importedEdge = $this->dom->importNode($edgeDom->documentElement, true);

        $graph->appendChild($importedEdge);
    }

    private function createEdge(DOMElement $graph, int $id, int $source, int $target): void
    {
        if ($this->visuallySkip($source)) {
            return;
        }

        if ($this->visuallySkip($target)) {
            return;
        }

        if (!$this->bottomUp) {
            [$source, $target] = [$target, $source];
        }

        $edgeTemplateString = <<<XML
<edge id="e{$id}" source="n{$source}" target="n{$target}">
    <data key="d1">
        <y:PolyLineEdge>
            <y:LineStyle type="line" width="1.0" color="#000000"/>
            <y:Arrows source="none" target="standard"/>
            <y:EdgeLabel/>
            <y:BendStyle smoothed="false"/>
        </y:PolyLineEdge>
    </data>
</edge>
XML;

        $edgeDom = new DOMDocument('1.0', 'UTF-8');
        $edgeDom->loadXML($edgeTemplateString);

        // Import document to prevent "Wrong Document Error".
        $importedEdge = $this->dom->importNode($edgeDom->documentElement, true);

        $graph->appendChild($importedEdge);
    }

    private function visuallySkip(int $objectId): bool
    {
        foreach ($this->visibleObjects as $visibleObjects) {
            [$parent, $child] = array_map('intval', explode('>', $visibleObjects));

            if ($parent === $objectId || $child === $objectId) {
                return false;
            }
        }

        return true;
    }
}
