<?php

/**
 * 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 isys_visualization_export_graphml implements isys_visualization_export
{
    /**
     * This is our DOM document, which all the nodes get appended to.
     *
     * @var  DOMDocument
     */
    protected $m_dom;

    /**
     * Cache for "already exported nodes" to not export them more than once.
     *
     * @var  array
     */
    protected $m_exported_nodes = [];

    /**
     * Options array.
     *
     * @var  array
     */
    protected $m_options = [];

    /**
     * This variable will hold the complete algorithm tree.
     *
     * @var  isys_tree[]
     */
    protected $m_tree = [];

    /**
     * @see ID-8541 Use this information to form the graph direction
     * @var bool
     */
    private bool $bottomUp = true;

    /**
     * Export method.
     *
     * @return  string
     */
    public function export()
    {
        try {
            $this->m_dom = new DOMDocument('1.0', 'UTF-8');

            $l_graphml = $this->m_dom->createElement('graphml');
            $l_graphml->setAttribute('xmlns', 'http://graphml.graphdrawing.org/xmlns');
            $l_graphml->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
            $l_graphml->setAttribute('xsi:schemaLocation', 'http://graphml.graphdrawing.org/xmlns http://www.yworks.com/xml/schema/graphml/1.1/ygraphml.xsd');
            $l_graphml->setAttribute('xmlns:y', 'http://www.yworks.com/xml/graphml');

            $l_key_0 = $this->m_dom->createElement('key');
            $l_key_0->setAttribute('id', 'd0');
            $l_key_0->setAttribute('for', 'node');
            $l_key_0->setAttribute('yfiles.type', 'nodegraphics');

            $l_key_1 = $this->m_dom->createElement('key');
            $l_key_1->setAttribute('id', 'd1');
            $l_key_1->setAttribute('for', 'edge');
            $l_key_1->setAttribute('yfiles.type', 'edgegraphics');

            $l_graph = $this->m_dom->createElement('graph');
            $l_graph->setAttribute('id', 'G');
            $l_graph->setAttribute('edgedefault', 'undirected');

            $i = 0;

            $edgeCache = [];

            // Because some visualizations will render multiple trees, we need this.
            foreach ($this->m_tree as $treeIndex => $l_tree) {
                // Now we iterate over our tree items to build the complete content.
                foreach ($l_tree->all_nodes() as $l_node) {
                    $l_node_data = $l_node->get_data();

                    if (!isset($this->m_exported_nodes[$l_node_data['data']['obj_id']])) {
                        $l_graph->appendChild($this->render_node([
                            'id'    => $l_node_data['data']['obj_id'],
                            'color' => isys_helper_color::unifyHexColor($l_node_data['data']['obj_type_color']),
                            'label' => $l_node_data['data']['obj_type_title'] . PHP_EOL . $l_node_data['data']['obj_title']
                        ]));

                        $this->m_exported_nodes[$l_node_data['data']['obj_id']] = true;
                    }

                    if ($l_node->has_children()) {
                        foreach ($l_node->get_childs() as $l_child_node) {
                            $l_child_node_data = $l_child_node->get_data();

                            $source = $treeIndex === 0 ? $l_node_data['data']['obj_id'] : $l_child_node_data['data']['obj_id'];
                            $target = $treeIndex === 0 ? $l_child_node_data['data']['obj_id'] : $l_node_data['data']['obj_id'];
                            $edgeCacheKey = $source . '>' . $target;

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

                            $edgeCache[$edgeCacheKey] = true;

                            // @see  ID-6972  The `$treeIndex` check is a bit messy but necessary since the second tree instance is "bottom-up".
                            $l_graph->appendChild($this->render_edge([
                                'id'     => $i,
                                'source' => $source,
                                'target' => $target
                            ]));

                            $i++;
                        }
                    }
                }
            }

            $l_graphml->appendChild($l_key_0);
            $l_graphml->appendChild($l_key_1);
            $l_graphml->appendChild($l_graph);

            $this->m_dom->appendChild($l_graphml);

            return $this->m_dom->saveXML();
        } catch (Exception $e) {
            return '<pre>' . var_export($e, true) . '</pre>';
        }
    }

    /**
     * Static factory method for.
     *
     * @return  isys_visualization_export_graphml
     */
    public static function factory()
    {
        return new self;
    }

    /**
     * Initialization method.
     *
     * @param array $p_options
     *
     * @return $this
     * @throws isys_exception_general
     */
    public function init(array $p_options = [])
    {
        $this->m_options = $p_options;

        $l_type_class = 'isys_visualization_' . $this->m_options['type'] . '_model';

        if (!class_exists($l_type_class)) {
            throw new isys_exception_general('Graph type "' . $this->m_options['type'] . '" does not exist!');
        }

        $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($this->m_options['profile-id']);

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

        /** @var $l_type_class isys_visualization_model */
        $this->m_tree[] = $l_type_class::instance($database)
            ->recursion_run($this->m_options['object-id'], $this->m_options['service-filter-id'], $this->m_options['profile-id']);

        if ($l_type_class === 'isys_visualization_tree_model') {
            $this->m_tree[] = isys_visualization_tree_model::instance($database)
                ->recursion_run($this->m_options['object-id'], $this->m_options['service-filter-id'], $this->m_options['profile-id'], false);
        }

        return $this;
    }

    /**
     * @param array $p_data
     *
     * @return DOMElement|false
     * @throws DOMException
     */
    protected function render_node(array $p_data)
    {
        // Prepare the node content.
        $l_node = $this->m_dom->createElement('node');
        $l_node_data = $this->m_dom->createElement('data');
        $l_shape_node = $this->m_dom->createElement('y:ShapeNode');
        $l_geometry = $this->m_dom->createElement('y:Geometry');
        $l_geometry->setAttribute('width', '120');
        $l_geometry->setAttribute('height', '40');
        $l_fill = $this->m_dom->createElement('y:Fill');
        $l_fill->setAttribute('color', $p_data['color']);
        $l_fill->setAttribute('transparent', 'false');
        $l_borderstyle = $this->m_dom->createElement('y:BorderStyle');
        $l_borderstyle->setAttribute('type', 'line');
        $l_borderstyle->setAttribute('width', '1.0');
        $l_borderstyle->setAttribute('color', '#000000');
        $l_nodelabel = $this->m_dom->createElement('y:NodeLabel');
        $l_nodelabel->appendChild($this->m_dom->createCDATASection($p_data['label'] ?: isys_tenantsettings::get('gui.empty_value', '-')));
        $l_shape = $this->m_dom->createElement('y:Shape');
        $l_shape->setAttribute('type', 'rectangle');

        // Add the child elements to the node.
        $l_shape_node->appendChild($l_geometry);
        $l_shape_node->appendChild($l_fill);
        $l_shape_node->appendChild($l_borderstyle);
        $l_shape_node->appendChild($l_nodelabel);
        $l_shape_node->appendChild($l_shape);

        $l_node_data->setAttribute('key', 'd0');
        $l_node_data->appendChild($l_shape_node);

        $l_node->setAttribute('id', 'n' . $p_data['id']);
        $l_node->appendChild($l_node_data);

        return $l_node;
    }

    /**
     * @param   array $p_data
     *
     * @return  DOMElement
     */
    protected function render_edge(array $p_data)
    {
        // Prepare the edge content.
        $l_edge = $this->m_dom->createElement('edge');
        $l_edge_data = $this->m_dom->createElement('data');
        $l_polyline = $this->m_dom->createElement('y:PolyLineEdge');
        $l_line_style = $this->m_dom->createElement('y:LineStyle');
        $l_line_style->setAttribute('type', 'line');
        $l_line_style->setAttribute('width', '1.0');
        $l_line_style->setAttribute('color', '#000000');
        $l_arrows = $this->m_dom->createElement('y:Arrows');
        $l_arrows->setAttribute('source', 'none');
        $l_arrows->setAttribute('target', 'standard');
        $l_edge_label = $this->m_dom->createElement('y:EdgeLabel', $p_data['label'] ?: null);
        $l_bend_style = $this->m_dom->createElement('y:BendStyle');
        $l_bend_style->setAttribute('smoothed', 'false');

        // Add the child elements to the edge.
        $l_polyline->appendChild($l_line_style);
        $l_polyline->appendChild($l_arrows);
        $l_polyline->appendChild($l_edge_label);
        $l_polyline->appendChild($l_bend_style);

        $l_edge_data->setAttribute('key', 'd1');
        $l_edge_data->appendChild($l_polyline);

        $source = $p_data['target'];
        $target = $p_data['source'];

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

        $l_edge->setAttribute('id', 'e' . $p_data['id']);
        $l_edge->setAttribute('source', 'n' . $source);
        $l_edge->setAttribute('target', 'n' . $target);
        $l_edge->appendChild($l_edge_data);

        return $l_edge;
    }
}
