<?php
/**
 * ---------------------------------------------------------------------
 * GLPI - Gestionnaire Libre de Parc Informatique
 * Copyright (C) 2015-2021 Teclib' and contributors.
 *
 * http://glpi-project.org
 *
 * based on GLPI - Gestionnaire Libre de Parc Informatique
 * Copyright (C) 2003-2014 by the INDEPNET Development Team.
 *
 * ---------------------------------------------------------------------
 *
 * LICENSE
 *
 * This file is part of GLPI.
 *
 * GLPI 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.
 *
 * GLPI 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 GLPI. If not, see <http://www.gnu.org/licenses/>.
 * ---------------------------------------------------------------------
 */

if (!defined('GLPI_ROOT')) {
   die("Sorry. You can't access this file directly");
}

/**
 * @since 9.5.0
 */
class Impact extends CommonGLPI {
   // Constants used to express the direction or "flow" of a graph
   // Theses constants can also be used to express if an edge is reachable
   // when exploring the graph forward, backward or both (0b11)
   const DIRECTION_FORWARD    = 0b01;
   const DIRECTION_BACKWARD   = 0b10;

   // Default colors used for the edges of the graph according to their flow
   const DEFAULT_COLOR            = 'black';   // The edge is not accessible from the starting point of the graph
   const IMPACT_COLOR             = '#ff3418'; // Forward
   const DEPENDS_COLOR            = '#1c76ff'; // Backward
   const IMPACT_AND_DEPENDS_COLOR = '#ca29ff'; // Forward and backward

   const NODE_ID_DELIMITER = "::";
   const EDGE_ID_DELIMITER = "->";

   // Consts for depth values
   const DEFAULT_DEPTH = 5;
   const MAX_DEPTH = 10;
   const NO_DEPTH_LIMIT = 10000;

   // Config values
   const CONF_ENABLED = 'impact_enabled_itemtypes';

   public static function getTypeName($nb = 0) {
      return __('Impact analysis');
   }

   public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) {
      global $DB;

      // Class of the current item
      $class = get_class($item);

      // Only enabled for CommonDBTM
      if (!is_a($item, "CommonDBTM", true)) {
         throw new InvalidArgumentException(
            "Argument \$item ($class) must be a CommonDBTM."
         );
      }

      $is_enabled_asset = self::isEnabled($class);
      $is_itil_object = is_a($item, "CommonITILObject", true);

      // Check if itemtype is valid
      if (!$is_enabled_asset && !$is_itil_object) {
         throw new InvalidArgumentException(
            "Argument \$item ($class) is not a valid target for impact analysis."
         );
      }

      if (!$_SESSION['glpishow_count_on_tabs']
         || !isset($item->fields['id'])
         || $is_itil_object
      ) {
         // Count is disabled in config OR no item loaded OR ITIL object -> no count
         $total = 0;
      } else if ($is_enabled_asset) {
         // If on an asset, get the number of its direct dependencies
         $total = count($DB->request([
            'FROM'  => ImpactRelation::getTable(),
            'WHERE' => [
               'OR' => [
                  [
                     // Source item is our item
                     'itemtype_source' => get_class($item),
                     'items_id_source' => $item->fields['id'],
                  ],
                  [
                     // Impacted item is our item AND source item is enabled
                     'itemtype_impacted' => get_class($item),
                     'items_id_impacted' => $item->fields['id'],
                     'itemtype_source'   => self::getEnabledItemtypes()
                  ]
               ]
            ]
         ]));
      }

      return self::createTabEntry(__("Impact analysis"), $total);
   }

   public static function displayTabContentForItem(
      CommonGLPI $item,
      $tabnum = 1,
      $withtemplate = 0
   ) {
      // Impact analysis should not be available outside of central
      if (Session::getCurrentInterface() !== "central") {
         return false;
      }

      $class = get_class($item);

      // Only enabled for CommonDBTM
      if (!is_a($item, "CommonDBTM")) {
         throw new InvalidArgumentException(
            "Argument \$item ($class) must be a CommonDBTM)."
         );
      }

      $ID = $item->fields['id'];

      // Don't show the impact analysis on new object
      if ($item->isNewID($ID)) {
         return false;
      }

      // Check READ rights
      $itemtype = $item->getType();
      if (!$itemtype::canView()) {
         return false;
      }

      // For an ITIL object, load the first linked element by default
      if (is_a($item, "CommonITILObject")) {
         $linked_items = $item->getLinkedItems();

         // Search for a valid linked item of this ITILObject
         $items_data = [];
         foreach ($linked_items as $itemtype => $linked_item_ids) {
            $class = $itemtype;
            if (self::isEnabled($class)) {
               $item = new $class;
               foreach ($linked_item_ids as $linked_item_id) {
                  if (!$item->getFromDB($linked_item_id)) {
                     continue;
                  }
                  $items_data[] = [
                     'itemtype' => $itemtype,
                     'items_id' => $linked_item_id,
                     'name'     => $item->getNameID(),
                  ];
               }
            }
         }

         // No valid linked item were found, tab shouldn't be visible
         if (empty($items_data)) {
            return false;
         }

         self::printAssetSelectionForm($items_data);
      }

      // Check is the impact analysis is enabled for $class
      if (!self::isEnabled($class)) {
         return false;
      }

      // Build graph and params
      $graph = self::buildGraph($item);
      $params = self::prepareParams($item);
      $readonly = !$item->can($item->fields['id'], UPDATE);

      // Print header
      self::printHeader(self::makeDataForCytoscape($graph), $params, $readonly);

      // Displays views
      self::displayGraphView($item);
      self::displayListView($item, $graph, true);

      // Select view
      echo Html::scriptBlock("
         // Select default view
         $(document).ready(function() {
            if (location.hash == '#list') {
               showListView();
            } else {
               showGraphView();
            }
         });
      ");

      return true;
   }

   /**
    * Display the impact analysis as an interactive graph
    *
    * @param CommonDBTM $item    starting point of the graph
    */
   public static function displayGraphView(
      CommonDBTM $item
   ) {
      self::loadLibs();

      echo '<div id="impact_graph_view">';
      self::prepareImpactNetwork($item);
      echo '</div>';
   }

   /**
    * Display the impact analysis as a list
    *
    * @param CommonDBTM $item   starting point of the graph
    * @param string     $graph  array containing the graph nodes and egdes
    */
   public static function displayListView(
      CommonDBTM $item,
      array $graph,
      bool $scripts = false
   ) {
      global $CFG_GLPI;

      $impact_item = ImpactItem::findForItem($item);
      $impact_context = ImpactContext::findForImpactItem($impact_item);

      if (!$impact_context) {
         $max_depth = self::DEFAULT_DEPTH;
      } else {
         $max_depth = $impact_context->fields['max_depth'];
      }

      echo '<div id="impact_list_view">';
      echo '<div class="impact-list-container">';

      // One table will be printed for each direction
      $lists = [
         __("Impact")      => self::DIRECTION_FORWARD,
         __("Impacted by") => self::DIRECTION_BACKWARD,
      ];
      $has_impact = false;

      foreach ($lists as $label => $direction) {
         $start_node_id = self::getNodeID($item);
         $data = self::buildListData($graph, $direction, $item, $max_depth);

         if (!count($data)) {
            continue;
         }

         $has_impact = true;
         echo '<table class="tab_cadre_fixehov impact-list-group">';

         // Header
         echo '<thead>';
         echo '<tr class="noHover">';
         echo '<th class="impact-list-header" colspan="6" width="90%"><h3>' . $label . '';
         echo '<i class="fas fa-2x fa-caret-down impact-toggle-subitems-master impact-pointer"></i></h3></th>';
         echo '</tr>';
         echo '<tr class="noHover">';
         echo '<th>'._n('Item', 'Items', 1).'</th>';
         echo '<th>'.__('Relation').'</th>';
         echo '<th>'.Ticket::getTypeName(Session::getPluralNumber()).'</th>';
         echo '<th>'.Problem::getTypeName(Session::getPluralNumber()).'</th>';
         echo '<th>'.Change::getTypeName(Session::getPluralNumber()).'</th>';
         echo '<th width="50px"></th>';
         echo '</tr>';
         echo '</thead>';

         foreach ($data as $itemtype => $items) {
            echo '<tbody>';

            // Subheader
            echo '<tr class="tab_bg_1">';
            echo '<td class="left subheader impact-left" colspan="6">';
            $total = count($items);
            echo '<a>' . $itemtype::getTypeName() . '</a>' . ' (' . $total . ')';
            echo '<i class="fas fa-2x fa-caret-down impact-toggle-subitems impact-pointer"></i>';
            echo '</td>';
            echo '</tr>';

            foreach ($items as $itemtype_item) {
               // Content: one row per item
               echo '<tr class=tab_bg_1><div></div>';
               echo '<td class="impact-left" width="15%">';
               echo '<div><a target="_blank" href="' .
                  $itemtype_item['stored']->getLinkURL() . '">' .
                  $itemtype_item['stored']->getFriendlyName() . '</a></div>';
               echo '</td>';
               echo '<td width="40%"><div>';

               $path = [];
               foreach ($itemtype_item['node']['path'] as $node) {
                  if ($node['id'] == $start_node_id) {
                     $path[] = '<b>' . $node['label'] . '</b>';
                  } else {
                     $path[] = $node['label'];
                  }
               }
               $separator = '<i class="fas fa-angle-right"></i>';
               echo implode(" $separator ", $path);

               echo '</div></td>';

               self::displayListNumber(
                  $itemtype_item['node']['ITILObjects']['incidents'],
                  Ticket::class,
                  $itemtype_item['node']['id']
               );
               self::displayListNumber(
                  $itemtype_item['node']['ITILObjects']['problems'],
                  Problem::class,
                  $itemtype_item['node']['id']
               );
               self::displayListNumber(
                  $itemtype_item['node']['ITILObjects']['changes'],
                  Change::class,
                  $itemtype_item['node']['id']
               );

               echo '<td class="center"><div></div></td>';
               echo '</tr>';
            }

            echo '</tbody>';
         }

         echo '</table>';
      }

      if (!$has_impact) {
         echo '<p>' . __("This asset doesn't have any dependencies.") . '</p>';
      }

      echo '</div>';

      $can_update = $item->can($item->fields['id'], UPDATE);

      // Toolbar
      echo '<div class="impact-list-toolbar">';
      if ($has_impact) {
         echo '<a target="_blank" href="'.$CFG_GLPI['root_doc'].'/front/impactcsv.php?itemtype=' . $impact_item->fields['itemtype'] . '&items_id=' . $impact_item->fields['items_id'] .'">';
         echo '<i class="fas fa-download impact-pointer impact-list-tools" title="' . __('Export to csv') .'"></i>';
         echo '</a>';
      }
      if ($can_update && $impact_context) {
         echo '<i id="impact-list-settings" class="fas fa-cog impact-pointer impact-list-tools" title="' . __('Settings') .'"></i>';
      }
      echo '</div>';

      // Settings dialog
      if ($can_update && $impact_context) {
         $rand = mt_rand();

         echo '<div id="list_depth_dialog" class="impact-dialog" title=' . __("Settings") . '>';
         echo '<form action="'.$CFG_GLPI['root_doc'].'/front/impactitem.form.php" method="POST">';
         echo '<table class="tab_cadre_fixe">';
         echo '<tr>';
         echo '<td><label for="impact_max_depth_' . $rand . '">' . __("Max depth") . '</label></td>';
         echo '<td>' . Html::input("max_depth", [
            'id'    => "impact_max_depth_$rand",
            'value' => $max_depth >= self::MAX_DEPTH ? '' : $max_depth,
         ]) . '</td>';
         echo '</tr>';
         echo '<tr>';
         echo '<td><label for="check_no_limit_' . $rand . '">' . __("No limit") . '</label></td>';
         echo '<td>' . Html::getCheckbox([
            'name'    => 'no_limit',
            'id'      => "check_no_limit_$rand",
            'checked' => $max_depth >= self::MAX_DEPTH,
         ]) . '</td>';
         echo '</tr>';
         echo '</table>';
         echo Html::input('id', [
            'type'  => "hidden",
            'value' => $impact_context->fields['id'],
         ]);
         echo Html::input('update', [
            'type'  => "hidden",
            'value' => "1",
         ]);
         Html::closeForm();
         echo '</div>';
      }

      echo '</div>';

      // Stop here if we do not need to generate scripts
      if (!$scripts) {
         return;
      }

      // Hide / show handler
      echo Html::scriptBlock('
         // jQuery doesn\'t allow slide animation on table elements, we need
         // to apply the animation to each cells content and then remove the
         // padding to get the desired "slide" animation

         function impactListUp(target) {
            target.removeClass("fa-caret-down");
            target.addClass("fa-caret-up");
            target.closest("tbody").find(\'tr:gt(0) td\').animate({padding: \'0px\'}, {duration: 400});
            target.closest("tbody").find(\'tr:gt(0) div\').slideUp("400");
         }

         function impactListDown(target) {
            target.addClass("fa-caret-down");
            target.removeClass("fa-caret-up");
            target.closest("tbody").find(\'tr:gt(0) td\').animate({padding: \'8px 5px\'}, {duration: 400});
            target.closest("tbody").find(\'tr:gt(0) div\').slideDown("400");
         }

         $(document).on("click", ".impact-toggle-subitems", function(e) {
            if ($(e.target).hasClass("fa-caret-up")) {
               impactListDown($(e.target));
            } else {
               impactListUp($(e.target));
            }
         });

         $(document).on("click", ".impact-toggle-subitems-master", function(e) {
            $(e.target).closest("table").find(".impact-toggle-subitems").each(function(i, elem) {
               if ($(e.target).hasClass("fa-caret-up")) {
                  impactListDown($(elem));
               } else {
                  impactListUp($(elem));
               }
            });

            $(e.target).toggleClass("fa-caret-up");
            $(e.target).toggleClass("fa-caret-down");
         });

         $(document).on("impactUpdated", function() {
            $.ajax({
               type: "GET",
               url: "' . $CFG_GLPI['root_doc'] . '/ajax/impact.php",
               data: {
                  itemtype: "' . get_class($item) . '",
                  items_id: "' . $item->fields['id'] . '",
                  action  : "load",
                  view    : "list",
               },
               success: function(data){
                  $("#impact_list_view").replaceWith(data);
                  showGraphView();
               },
            });
         });
      ');

      if ($can_update) {
         // Handle settings actions
         echo Html::scriptBlock('
            $("#impact-list-settings").click(function() {
               $("#list_depth_dialog").dialog({
                  modal: true,
                  buttons: {
                     ' . __("Save") . ': function() {
                        if ($("input[name=\'no_limit\']:checked").length > 0) {
                           $("input[name=\'max_depth\']").val(' . self::NO_DEPTH_LIMIT . ');
                        }

                        $(this).find("form").submit();
                     },
                     ' . __("Cancel") . ': function() {
                        $(this).dialog( "close" );
                     }
                  },
               });
            });
         ');
      }
   }

   /**
    * Display "number" cell in list view
    * The cell is empty if no itilobjets are found, else it contains the
    * number of iitilobjets found, use the highest priority as it's background
    * color and is a link to matching search result
    *
    * @param array   $itil_objects
    * @param string  $type
    * @param string  $node_id
    */
   private static function displayListNumber($itil_objects, $type, $node_id) {
      $user = new User();
      $user->getFromDB(Session::getLoginUserID());
      $user->computePreferences();

      $count = count($itil_objects) ?: "";
      $extra = "";
      $node_details = explode(self::NODE_ID_DELIMITER, $node_id);

      if ($count) {
         $priority = 1;
         $id = "impact_list_itilcount_" . mt_rand();
         $link = "";

         switch ($type) {
            case Ticket::class:
               $link = Ticket::getSearchURL();
               $link .= "?is_deleted=0&as_map=0&search=Search&itemtype=Ticket";
               $link .= "&criteria[0][link]=AND&criteria[0][field]=13&criteria[0][searchtype]=contains&criteria[0][value]=" . $node_details[1];
               $link .= "&criteria[1][link]=AND&criteria[1][field]=131&criteria[1][searchtype]=equals&criteria[1][value]=" . $node_details[0];
               $link .= "&criteria[2][link]=AND&criteria[2][field]=14&criteria[2][searchtype]=equals&criteria[2][value]=1";
               $link .= "&criteria[3][link]=AND&criteria[3][field]=12&criteria[3][searchtype]=equals&criteria[3][value]=notold";
               break;

            case Problem::class:
               $link = Problem::getSearchURL();
               $link .= "?is_deleted=0&as_map=0&search=Search&itemtype=Problem";
               $link .= "&criteria[0][link]=AND&criteria[0][field]=13&criteria[0][searchtype]=contains&criteria[0][value]=" . $node_details[1];
               $link .= "&criteria[1][link]=AND&criteria[1][field]=131&criteria[1][searchtype]=equals&criteria[1][value]=" . $node_details[0];
               $link .= "&criteria[3][link]=AND&criteria[3][field]=12&criteria[3][searchtype]=equals&criteria[3][value]=notold";
               break;

            case Change::class:
               $link = Change::getSearchURL();
               $link .= "?is_deleted=0&as_map=0&search=Search&itemtype=Change";
               $link .= "&criteria[0][link]=AND&criteria[0][field]=13&criteria[0][searchtype]=contains&criteria[0][value]=" . $node_details[1];
               $link .= "&criteria[1][link]=AND&criteria[1][field]=131&criteria[1][searchtype]=equals&criteria[1][value]=" . $node_details[0];
               $link .= "&criteria[3][link]=AND&criteria[3][field]=12&criteria[3][searchtype]=equals&criteria[3][value]=notold";
               break;
         }

         // Compute max priority
         foreach ($itil_objects as $itil_object) {
            if ($priority < $itil_object['priority']) {
               $priority = $itil_object['priority'];
            }
         }
         $extra = 'id="' . $id . '" style="background-color:' .  $user->fields["priority_$priority"] .'; cursor:pointer;"';

         echo Html::scriptBlock('
            $(document).on("click", "#' . $id . '", function(e) {
               window.open("' . $link . '");
            });
         ');
      }

      echo '<td class="center" ' . $extra . '><div>' . $count . '</div></td>';
   }

   /**
    * Build the data used to represent the impact graph as a semi-flat list
    *
    * @param array      $graph        array containing the graph nodes and egdes
    * @param int        $direction    should the list be build for item that are
    *                                 impacted by $item or that impact $item ?
    * @param CommonDBTM $item         starting point of the graph
    * @param int        $max_depth    max depth from context
    *
    * @return array
    */
   public static function buildListData(
      array $graph,
      int $direction,
      CommonDBTM $item,
      int $max_depth
   ) {
      $data = [];

      // Filter tree
      $sub_graph = self::filterGraph($graph, $direction);

      // Empty graph, no need to go further
      if (!count($sub_graph['nodes'])) {
         return $data;
      }

      // Evaluate path to each assets from the starting node
      $start_node_id = self::getNodeID($item);
      $start_node = $sub_graph['nodes'][$start_node_id];

      foreach ($sub_graph['nodes'] as $key => $vertex) {
         if ($key !== $start_node_id) {
            // Set path for target node using BFS
            $path = self::bfs(
               $sub_graph,
               $start_node,
               $vertex,
               $direction
            );

            // Add if path is not longer than the allowed value
            if (count($path) - 1 <= $max_depth) {
               $sub_graph['nodes'][$key]['path'] = $path;
            }
         }
      }

      // Split the items by type
      foreach ($sub_graph['nodes'] as $node) {
         $details = explode(self::NODE_ID_DELIMITER, $node['id']);
         $itemtype = $details[0];
         $items_id = $details[1];

         // Skip start node or empty path
         if ($node['id'] == $start_node_id || !isset($node['path'])) {
            continue;
         }

         // Init itemtype if empty
         if (!isset($data[$itemtype])) {
            $data[$itemtype] = [];
         }

         // Add to itemtype
         $itemtype_item = new $itemtype;
         $itemtype_item->getFromDB($items_id);
         $data[$itemtype][] = [
            'stored' => $itemtype_item,
            'node'   => $node,
         ];
      }

      return $data;
   }

   /**
    * Return a subgraph matching the given direction
    *
    * @param array $graph      array containing the graph nodes and egdes
    * @param int   $direction  direction to match
    *
    * @return array
    */
   public static function filterGraph(array $graph, int $direction) {
      $new_graph = [
         'edges' => [],
         'nodes' => [],
      ];

      // For each edge in the graph
      foreach ($graph['edges'] as $edge) {
         // Filter on direction
         if ($edge['flag'] & $direction) {
            // Add the edge and its two connected nodes
            $source = $edge['source'];
            $target = $edge['target'];

            $new_graph['edges'][] = $edge;
            $new_graph['nodes'][$source] = $graph['nodes'][$source];
            $new_graph['nodes'][$target] = $graph['nodes'][$target];
         }
      }

      return $new_graph;
   }

   /**
    * Evaluate the path from one node to another using BFS algorithm
    *
    * @param array  $graph          array containing the graph nodes and egdes
    * @param array  $a              a node of the graph
    * @param array  $b              a node of the graph
    * @param int    $direction      direction used to travel the graph
    */
   public static function bfs(array $graph, array $a, array $b, int $direction) {
      switch ($direction) {
         case self::DIRECTION_FORWARD:
            $start = $a;
            $target = $b;
            break;

         case self::DIRECTION_BACKWARD:
            $start = $b;
            $target = $a;
            break;

         default:
            throw new \InvalidArgumentException("Invalid direction : $direction");
      }

      // Insert start node in the queue
      $queue = [];
      $queue[] = $start;
      $discovered = [$start['id'] => true];

      // Label start as discovered
      $start['discovered'] = true;

      // For each other nodes
      while (count($queue) > 0) {
         $node = array_shift($queue);

         if ($node['id'] == $target['id']) {
            // target found, build path to node
            $path = [$target];

            while (isset($node['dfs_parent'])) {
               $node = $node['dfs_parent'];
               array_unshift($path, $node);
            }

            return $path;
         }

         foreach ($graph['edges'] as $edge) {
            // Skip edge if not connected to the current node
            if ($edge['source'] !== $node['id']) {
               continue;
            }

            $nextNode = $graph['nodes'][$edge['target']];

            // Skip already discovered node
            if (isset($discovered[$nextNode['id']])) {
               continue;
            }

            $nextNode['dfs_parent'] = $node;
            $discovered[$nextNode['id']] = true;

            $queue[] = $nextNode;
         }
      }
   }

   /**
    * Print the title and view switch
    *
    * @param string  $graph      The network graph (json)
    * @param string  $params     Params of the graph (json)
    * @param bool    $readonly   Is the graph editable ?
    */
   public static function printHeader(
      string $graph,
      string $params,
      bool $readonly
   ) {
      echo '<div class="impact-header">';
      echo "<h2>" . __("Impact analysis") . "</h2>";
      echo "<div id='switchview'>";
      echo "<a id='sviewlist' href='#list'><i class='pointer fa fa-list-alt' title='".__('View as list')."'></i></a>";
      echo "<a id='sviewgraph' href='#graph'><i class='pointer fa fa-bezier-curve' title='".__('View graphical representation')."'></i></a>";
      echo "</div>";
      echo "</div>";

      // View selection
      echo Html::scriptBlock("
         function showGraphView() {
            $('#impact_list_view').hide();
            $('#impact_graph_view').show();
            $('#sviewlist i').removeClass('selected');
            $('#sviewgraph i').addClass('selected');

            if (window.GLPIImpact !== undefined && GLPIImpact.cy === null) {
               GLPIImpact.buildNetwork($graph, $params, $readonly);
            }
         }

         function showListView() {
            $('#impact_graph_view').hide();
            $('#impact_list_view').show();
            $('#sviewgraph i').removeClass('selected');
            $('#sviewlist i').addClass('selected');
            $('#save_impact').removeClass('clean');
         }

         $('#sviewgraph').click(function() {
            showGraphView();
         });

         $('#sviewlist').click(function() {
            showListView();
         });
      ");
   }

   /**
    * Load the cytoscape library
    *
    * @since 9.5
    */
   public static function loadLibs() {
      echo Html::css('public/lib/cytoscape.css');
      echo Html::script("public/lib/cytoscape.js");
   }

   /**
    * Print the asset selection form used in the impact tab of ITIL objects
    *
    * @param array $items
    *    Each array should contains "itemtype", "items_id" and "name".
    *
    * @since 9.5
    */
   public static function printAssetSelectionForm(array $items) {
      global $CFG_GLPI;

      // Dropdown values
      $values = [];

      // Add a value in the dropdown for each items, grouped by type
      foreach ($items as $item) {
         if (self::isEnabled($item['itemtype'])) {
            // Add itemtype group if it doesn't exist in the dropdown yet
            $itemtype_label =  $item['itemtype']::getTypeName();
            if (!isset($values[$itemtype_label])) {
               $values[$itemtype_label] = [];
            }

            $key = $item['itemtype'] . "::" . $item['items_id'];
            $values[$itemtype_label][$key] = $item['name'];
         }
      }

      Dropdown::showFromArray("impact_assets_selection_dropdown", $values);
      echo '<div class="impact-mb-2"></div>';

      // Form interaction: load a new graph on value change
      echo Html::scriptBlock('
         $(function() {
            var selector = "select[name=impact_assets_selection_dropdown]";

            $(selector).change(function(){
               var values = $(selector + " option:selected").val().split("::");

               $.ajax({
                  type: "GET",
                  url: "'. $CFG_GLPI['root_doc'] . '/ajax/impact.php",
                  data: {
                     itemtype: values[0],
                     items_id: values[1],
                     action  : "load",
                  },
                  success: function(data, textStatus, jqXHR) {
                     GLPIImpact.buildNetwork(
                        JSON.parse(data.graph),
                        JSON.parse(data.params),
                        data.readonly
                     );
                  }
               });
            });
         });
      ');
   }

   /**
    * Search asset by itemtype and name
    *
    * @param string  $itemtype   type
    * @param array   $used       ids to exlude from the search
    * @param string  $filter     filter on name
    * @param int     $page       page offset
    */
   public static function searchAsset(
      string $itemtype,
      array $used,
      string $filter,
      int $page = 0
   ) {
      global $DB;

      // Check if this type is enabled in config
      if (!self::isEnabled($itemtype)) {
         throw new \InvalidArgumentException(
            "itemtype ($itemtype) must be enabled in config"
         );
      }

      // Check class exist and is a child of CommonDBTM
      if (!is_subclass_of($itemtype, "CommonDBTM", true)) {
         throw new \InvalidArgumentException(
            "itemtype ($itemtype) must be a valid child of CommonDBTM"
         );
      }

      // Return empty result if the user doesn't have READ rights
      if (!Session::haveRight($itemtype::$rightname, READ)) {
         return [
            "items" => [],
            "total" => 0
         ];
      }

      // This array can't be empty since we will use it in the NOT IN part of the reqeust
      if (!count($used)) {
         $used[] = -1;
      }

      // Search for items
      $table = $itemtype::getTable();
      $base_request = [
         'FROM'   => $table,
         'WHERE'  => [
            'NOT' => [
               "$table.id" => $used
            ],
         ],
      ];

      // Add friendly name search criteria
      $base_request['WHERE'] = array_merge(
         $base_request['WHERE'],
         $itemtype::getFriendlyNameSearchCriteria($filter)
      );

      if (is_subclass_of($itemtype, "ExtraVisibilityCriteria", true)) {
         $base_request = array_merge_recursive(
            $base_request,
            $itemtype::getVisibilityCriteria()
         );
      }

      $item = new $itemtype();
      if ($item->isEntityAssign()) {
         $base_request['WHERE'] = array_merge_recursive(
            $base_request['WHERE'],
            getEntitiesRestrictCriteria($itemtype::getTable())
         );
      }

      if ($item->mayBeDeleted()) {
         $base_request['WHERE']["$table.is_deleted"] = 0;
      }

      if ($item->mayBeTemplate()) {
         $base_request['WHERE']["$table.is_template"] = 0;
      }

      $select = [
         'SELECT' => ["$table.id", $itemtype::getFriendlyNameFields()],
      ];
      $limit = [
         'START' => $page * 20,
         'LIMIT' => "20",
      ];
      $count = [
         'COUNT' => "total",
      ];

      // Get items
      $rows = $DB->request($base_request + $select + $limit);

      // Get total
      $total = $DB->request($base_request + $count);

      return [
         "items" => iterator_to_array($rows, false),
         "total" => iterator_to_array($total, false)[0]['total'],
      ];
   }

   /**
    * Load the impact network container
    *
    * @since 9.5
    */
   public static function printImpactNetworkContainer() {
      global $CFG_GLPI;

      $action = $CFG_GLPI['root_doc'] . '/ajax/impact.php';
      $formName = "form_impact_network";

      echo "<form name=\"$formName\" action=\"$action\" method=\"post\" class='no-track'>";
      echo "<table class='tab_cadre_fixe network-table'>";
      echo '<tr><td class="network-parent">';
      echo '<span id="help_text"></span>';

      echo '<div id="network_container"></div>';
      echo '<img class="impact-drop-preview">';

      echo '<div class="impact-side">';

      echo '<div class="impact-side-panel">';

      echo '<div class="impact-side-add-node">';
      echo '<h3>' . __('Add assets') . '</h3>';
      echo '<div class="impact-side-select-itemtype">';

      echo Html::input("impact-side-filter-itemtypes", [
         'id' => 'impact-side-filter-itemtypes',
         'placeholder' => __('Filter itemtypes...'),
      ]);

      echo '<div class="impact-side-filter-itemtypes-items">';
      $itemtypes = $CFG_GLPI["impact_asset_types"];
      // Sort by translated itemtypes
      uksort($itemtypes, function($a, $b) {
         return strcasecmp($a::getTypeName(), $b::getTypeName());
      });
      foreach ($itemtypes as $itemtype => $icon) {
         // Do not display this itemtype if the user doesn't have READ rights
         if (!Session::haveRight($itemtype::$rightname, READ)) {
            continue;
         }

         // Skip if not enabled
         if (!self::isEnabled($itemtype)) {
            continue;
         }

         $icon = self::checkIcon($icon);

         echo '<div class="impact-side-filter-itemtypes-item">';
         echo '<h4><img class="impact-side-icon" src="' . $CFG_GLPI['root_doc'] . '/' . $icon . '" title="' . $itemtype::getTypeName() . '" data-itemtype="' . $itemtype . '">';
         echo "<span>" . $itemtype::getTypeName() . "</span></h4>";
         echo '</div>'; // impact-side-filter-itemtypes-item
      }
      echo '</div>'; // impact-side-filter-itemtypes-items
      echo '</div>'; // <div class="impact-side-select-itemtype">

      echo '<div class="impact-side-search">';
      echo '<h4><i class="fas fa-chevron-left"></i><img><span></span></h4>';
      echo Html::input("impact-side-filter-assets", [
         'id' => 'impact-side-filter-assets',
         'placeholder' => __('Filter assets...'),
      ]);

      echo '<div class="impact-side-search-panel">';
      echo '<div class="impact-side-search-results"></div>';

      echo '<div class="impact-side-search-more">';
      echo '<h4><i class="fas fa-chevron-down"></i>' . __("More...") . '</h4>';
      echo '</div>'; // <div class="impact-side-search-more">

      echo '<div class="impact-side-search-no-results">';
      echo '<p>'. __("No results") . '</p>';
      echo '</div>'; // <div class="impact-side-search-no-results">

      echo '<div class="impact-side-search-spinner">';
      echo '<i class="fas fa-spinner fa-2x fa-spin"></i>';
      echo '</div>'; // <div class="impact-side-search-spinner">

      echo '</div>'; // <div class="impact-side-search-panel">

      echo '</div>'; // <div class="impact-side-search">

      echo '</div>'; // div class="impact-side-add-node">

      echo '<div class="impact-side-settings">';
      echo '<h3>' . __('Settings') . '</h3>';

      echo '<h4>' . __('Visibility') . '</h4>';
      echo '<div class="impact-side-settings-item">';
      echo Html::getCheckbox([
         'id'      => "toggle_impact",
         'name'    => "toggle_impact",
         'checked' => "true",
      ]);
      echo '<span class="impact-checkbox-label">' . __("Show impact") . '</span>';
      echo '</div>';

      echo '<div class="impact-side-settings-item">';
      echo Html::getCheckbox([
         'id'      => "toggle_depends",
         'name'    => "toggle_depends",
         'checked' => "true",
      ]);
      echo '<span class="impact-checkbox-label">' . __("Show depends") . '</span>';
      echo '</div>';

      echo '<h4>' . __('Colors') . '</h4>';
      echo '<div class="impact-side-settings-item">';
      Html::showColorField("depends_color", []);
      echo '<span class="impact-checkbox-label">' . __("Depends") . '</span>';
      echo '</div>';

      echo '<div class="impact-side-settings-item">';
      Html::showColorField("impact_color", []);
      echo '<span class="impact-checkbox-label">' . __("Impact") . '</span>';
      echo '</div>';

      echo '<div class="impact-side-settings-item">';
      Html::showColorField("impact_and_depends_color", []);
      echo '<span class="impact-checkbox-label">' . __("Impact and depends") . '</span>';
      echo '</div>';

      echo '<h4>' . __('Max depth') . '</h4>';
      echo '<div class="impact-side-settings-item">';
      echo '<input id="max_depth" type="range" class="impact-range" min="1" max ="10" step="1" value="5"><span id="max_depth_view" class="impact-checkbox-label"></span>';
      echo '</div>';

      echo '</div>'; // div class="impact-side-settings">

      echo '<div class="impact-side-search-footer"></div>';
      echo '</div>'; // div class="impact-side-panel">

      echo '<ul>';
      echo '<li id="save_impact" title="' . __("Save") .'"><i class="fas fa-fw fa-save"></i></li>';
      echo '<li id="impact_undo" class="impact-disabled" title="' . __("Undo") .'"><i class="fas fa-fw fa-undo"></i></li>';
      echo '<li id="impact_redo" class="impact-disabled" title="' . __("Redo") .'"><i class="fas fa-fw fa-redo"></i></li>';
      echo '<li class="impact-separator"></li>';
      echo '<li id="add_node" title="' . __("Add asset") .'"><i class="fas fa-fw fa-plus"></i></li>';
      echo '<li id="add_edge" title="' . __("Add relation") .'"><i class="fas fa-fw fa-slash"></i></li>';
      echo '<li id="add_compound" title="' . __("Add group") .'"><i class="far fa-fw fa-object-group"></i></li>';
      echo '<li id="delete_element" title="' . __("Delete element") .'"><i class="fas fa-fw fa-trash"></i></li>';
      echo '<li class="impact-separator"></li>';
      echo '<li id="export_graph" title="' . __("Download") .'"><i class="fas fa-fw fa-download"></i></li>';
      echo '<li id="toggle_fullscreen" title="' . __("Fullscreen") .'"><i class="fas fa-fw fa-expand"></i></li>';
      echo '<li id="impact_settings" title="' . __("Settings") .'"><i class="fas fa-fw fa-cog"></i></li>';
      echo '</ul>';
      echo '<span class="impact-side-toggle"><i class="fas fa-2x fa-chevron-left"></i></span>';
      echo '</div>'; // <div class="impact-side impact-side-expanded">
      echo "</td></tr>";
      echo "</table>";
      Html::closeForm();
   }

   /**
    * Build the impact graph starting from a node
    *
    * @since 9.5
    *
    * @param CommonDBTM $item Current item
    *
    * @return array Array containing edges and nodes
    */
   public static function buildGraph(CommonDBTM $item) {
      $nodes = [];
      $edges = [];

      // Explore the graph forward
      self::buildGraphFromNode($nodes, $edges, $item, self::DIRECTION_FORWARD);

      // Explore the graph backward
      self::buildGraphFromNode($nodes, $edges, $item, self::DIRECTION_BACKWARD);

      // Add current node to the graph if no impact relations were found
      if (count($nodes) == 0) {
         self::addNode($nodes, $item);
      }

      // Add special flag to start node
      $nodes[self::getNodeID($item)]['start'] = 1;

      return [
         'nodes' => $nodes,
         'edges' => $edges
      ];
   }

   /**
    * Explore dependencies of the current item, subfunction of buildGraph()
    *
    * @since 9.5
    *
    * @param array      $edges          Edges of the graph
    * @param array      $nodes          Nodes of the graph
    * @param CommonDBTM $node           Current node
    * @param int        $direction      The direction in which the graph
    *                                   is being explored : DIRECTION_FORWARD
    *                                   or DIRECTION_BACKWARD
    * @param array      $explored_nodes List of nodes that have already been
    *                                   explored
    *
    * @throws InvalidArgumentException
    */
   private static function buildGraphFromNode(
      array &$nodes,
      array &$edges,
      CommonDBTM $node,
      int $direction,
      array $explored_nodes = []
   ) {
      global $DB;

      // Source and target are determined by the direction in which we are
      // exploring the graph
      switch ($direction) {
         case self::DIRECTION_BACKWARD:
            $source = "source";
            $target = "impacted";
            break;
         case self::DIRECTION_FORWARD:
            $source = "impacted";
            $target = "source";
            break;
         default:
            throw new InvalidArgumentException(
               "Invalid value for argument \$direction ($direction)."
            );
      }

      // Get relations of the current node
      $relations = $DB->request([
         'FROM'   => ImpactRelation::getTable(),
         'WHERE'  => [
            'itemtype_' . $target => get_class($node),
            'items_id_' . $target => $node->fields['id']
         ]
      ]);

      // Add current code to the graph if we found at least one impact relation
      if (count($relations)) {
         self::addNode($nodes, $node);
      }
      // Iterate on each relations found
      foreach ($relations as $related_item) {
         // Do not explore disabled itemtypes
         if (!self::isEnabled($related_item['itemtype_' . $source])) {
            continue;
         }

         // Add the related node
         if (!($related_node = getItemForItemtype($related_item['itemtype_' . $source]))) {
            continue;
         }
         $related_node->getFromDB($related_item['items_id_' . $source]);
         self::addNode($nodes, $related_node);

         // Add or update the relation on the graph
         $edgeID = self::getEdgeID($node, $related_node, $direction);
         self::addEdge($edges, $edgeID, $node, $related_node, $direction);

         // Keep exploring from this node unless we already went through it
         $related_node_id = self::getNodeID($related_node);
         if (!isset($explored_nodes[$related_node_id])) {
            $explored_nodes[$related_node_id] = true;
            self::buildGraphFromNode(
               $nodes,
               $edges,
               $related_node,
               $direction,
               $explored_nodes
            );
         }
      }
   }

   /**
    * Check if the icon path is valid, if not return a fallback path
    *
    * @param string $icon_path
    * @return string
    */
   private static function checkIcon(string $icon_path): string {
      // Special case for images returned dynamicly
      if (strpos($icon_path, ".php") !== false) {
         return $icon_path;
      }

      // Check if icon exist on the filesystem
      $file_path = GLPI_ROOT . "/$icon_path";
      if (file_exists($file_path) && is_file($file_path)) {
         return $icon_path;
      }

      // Fallback "default" icon
      return "pics/impact/default.png";
   }

   /**
    * Add a node to the node list if missing
    *
    * @param array      $nodes  Nodes of the graph
    * @param CommonDBTM $item   Node to add
    *
    * @since 9.5
    *
    * @return bool true if the node was missing, else false
    */
   private static function addNode(array &$nodes, CommonDBTM $item) {
      global $CFG_GLPI;

      // Check if the node already exist
      $key = self::getNodeID($item);
      if (isset($nodes[$key])) {
         return false;
      }

      // Get web path to the image matching the itemtype from config
      $image_name = $CFG_GLPI["impact_asset_types"][get_class($item)] ?? "";
      $image_name = self::checkIcon($image_name);

      // Define basic data of the new node
      $new_node = [
         'id'          => $key,
         'label'       => $item->getFriendlyName(),
         'image'       => $CFG_GLPI['root_doc'] . "/$image_name",
         'ITILObjects' => $item->getITILTickets(true),
      ];

      // Only set GOTO link if the user have READ rights
      if ($item::canView()) {
         $new_node['link'] = $item->getLinkURL();
      }

      // Set incident badge if needed
      if (count($new_node['ITILObjects']['incidents'])) {
         $priority = 0;
         foreach ($new_node['ITILObjects']['incidents'] as $incident) {
            if ($priority < $incident['priority']) {
               $priority = $incident['priority'];
            }
         }

         $user = new User();
         $user->getFromDB(Session::getLoginUserID());
         $user->computePreferences();
         $new_node['badge'] = [
            'color'  => $user->fields["priority_$priority"],
            'count'  => count($new_node['ITILObjects']['incidents']),
            'target' => Ticket::getSearchURL(),
         ];
      }

      // Alter the label if we found some linked ITILObjects
      $itil_tickets_count = $new_node['ITILObjects']['count'];
      if ($itil_tickets_count > 0) {
         $new_node['label'] .= " ($itil_tickets_count)";
         $new_node['hasITILObjects'] = 1;
      }

      // Load or create a new ImpactItem object
      $impact_item = ImpactItem::findForItem($item);

      // Load node position and parent
      $new_node['impactitem_id'] = $impact_item->fields['id'];
      $new_node['parent']        = $impact_item->fields['parent_id'];

      // If the node has a parent, add it to the node list aswell
      if (!empty($new_node['parent'])) {
         $compound = new ImpactCompound();
         $compound->getFromDB($new_node['parent']);

         if (!isset($nodes[$new_node['parent']])) {
            $nodes[$new_node['parent']] = [
               'id'    => $compound->fields['id'],
               'label' => $compound->fields['name'],
               'color' => $compound->fields['color'],
            ];
         }
      }

      // Insert the node
      $nodes[$key] = $new_node;
      return true;
   }

   /**
    * Add an edge to the edge list if missing, else update it's direction
    *
    * @param array      $edges      Edges of the graph
    * @param string     $key        ID of the new edge
    * @param CommonDBTM $itemA      One of the node connected to this edge
    * @param CommonDBTM $itemB      The other node connected to this edge
    * @param int        $direction  Direction of the edge : A to B or B to A ?
    *
    * @since 9.5
    *
    * @return bool true if the node was missing, else false
    *
    * @throws InvalidArgumentException
    */
   private static function addEdge(
      array &$edges,
      string $key,
      CommonDBTM $itemA,
      CommonDBTM $itemB,
      int $direction
   ) {
      // Just update the flag if the edge already exist
      if (isset($edges[$key])) {
         $edges[$key]['flag'] = $edges[$key]['flag'] | $direction;
         return;
      }

      // Assign 'from' and 'to' according to the direction
      switch ($direction) {
         case self::DIRECTION_FORWARD:
            $from = self::getNodeID($itemA);
            $to = self::getNodeID($itemB);
            break;
         case self::DIRECTION_BACKWARD:
            $from = self::getNodeID($itemB);
            $to = self::getNodeID($itemA);
            break;
         default:
            throw new InvalidArgumentException(
               "Invalid value for argument \$direction ($direction)."
            );
      }

      // Add the new edge
      $edges[$key] = [
         'id'     => $key,
         'source' => $from,
         'target' => $to,
         'flag'   => $direction
      ];
   }

   /**
    * Build the graph and the cytoscape object
    *
    * @since 9.5
    *
    * @param string  $graph      The network graph (json)
    * @param string  $params     Params of the graph (json)
    * @param bool    $readonly   Is the graph editable ?
    */
   public static function buildNetwork(
      string $graph,
      string $params,
      bool $readonly
   ) {
      echo Html::scriptBlock("
         $(function() {
            GLPIImpact.buildNetwork($graph, $params, $readonly);
         });
      ");
   }

   /**
    * Get saved graph params for the current item
    *
    * @param CommonDBTM $item
    *
    * @return string $item
    */
   public static function prepareParams(CommonDBTM $item) {
      $impact_item = ImpactItem::findForItem($item);

      $params = array_intersect_key($impact_item->fields, [
         'parent_id'         => 1,
         'impactcontexts_id' => 1,
         'is_slave'          => 1,
      ]);

      // Load context if exist
      if ($params['impactcontexts_id']) {
         $impact_context = ImpactContext::findForImpactItem($impact_item);

         if ($impact_context) {
            $params = $params + array_intersect_key(
               $impact_context->fields,
               [
                  'positions'                => 1,
                  'zoom'                     => 1,
                  'pan_x'                    => 1,
                  'pan_y'                    => 1,
                  'impact_color'             => 1,
                  'depends_color'            => 1,
                  'impact_and_depends_color' => 1,
                  'show_depends'             => 1,
                  'show_impact'              => 1,
                  'max_depth'                => 1,
               ]
            );
         }
      }

      return json_encode($params);
   }

   /**
    * Convert the php array reprensenting the graph into the format required by
    * the Cytoscape library
    *
    * @param array $graph
    *
    * @return string json data
    */
   public static function makeDataForCytoscape(array $graph) {
      $data = [];

      foreach ($graph['nodes'] as $node) {
         $data[] = [
            'group'    => 'nodes',
            'data'     => $node,
         ];
      }

      foreach ($graph['edges'] as $edge) {
         $data[] = [
            'group' => 'edges',
            'data'  => $edge,
         ];
      }

      return json_encode($data);
   }

   /**
    * Load the "show ongoing tickets" dialog
    *
    * @since 9.5
    */
   public static function printShowOngoingDialog() {
      // This dialog will be built dynamically by the front end
      echo '<div id="ongoing_dialog"></div>';
   }

   /**
    * Load the "edit compound" dialog
    *
    * @since 9.5
    */
   public static function printEditCompoundDialog() {
      echo '<div id="edit_compound_dialog"  class="impact-dialog">';
      echo "<table class='tab_cadre_fixe'>";

      // First row: name field
      echo "<tr>";
      echo "<td>";
      echo "<label>&nbsp;" . __("Name") . "</label>";
      echo "</td>";
      echo "<td>";
      echo Html::input("compound_name", []);
      echo "</td>";
      echo "</tr>";

      // Second row: color field
      echo "<tr>";
      echo "<td>";
      echo "<label>&nbsp;" . __("Color") . "</label>";
      echo "</td>";
      echo "<td>";
      Html::showColorField("compound_color", [
         'value' => '#d2d2d2'
      ]);
      echo "</td>";
      echo "</tr>";

      echo "</table>";
      echo "</div>";
   }

   /**
    * Prepare the impact network
    *
    * @since 9.5
    *
    * @param CommonDBTM $item The specified item
    */
   public static function prepareImpactNetwork(CommonDBTM $item) {
      // Load requirements
      self::printImpactNetworkContainer();
      self::printShowOngoingDialog();
      self::printEditCompoundDialog();
      echo Html::script("js/impact.js");

      // Load backend values
      $default   = self::DEFAULT_COLOR;
      $forward   = self::IMPACT_COLOR;
      $backward  = self::DEPENDS_COLOR;
      $both      = self::IMPACT_AND_DEPENDS_COLOR;
      $start_node = self::getNodeID($item);

      // Bind the backend values to the client and start the network
      echo  Html::scriptBlock("
         $(function() {
            GLPIImpact.prepareNetwork(
               $(\"#network_container\"),
               {
                  default : '$default',
                  forward : '$forward',
                  backward: '$backward',
                  both    : '$both',
               },
               '$start_node'
            )
         });
      ");
   }

   /**
    * Check that a given asset exist in the DB
    *
    * @param string $itemtype Class of the asset
    * @param string $items_id id of the asset
    */
   public static function assetExist(string $itemtype, string $items_id) {
      try {
         // Check this asset type is enabled
         if (!self::isEnabled($itemtype)) {
            return false;
         }

         // Try to create an object matching the given item type
         $reflection_class = new ReflectionClass($itemtype);
         if (!$reflection_class->isInstantiable()) {
            return false;
         }

         // Look for a matching asset in the DB
         $asset = new $itemtype();
         return $asset->getFromDB($items_id);
      } catch (ReflectionException $e) {
         // Class does not exist
         return false;
      }
   }

   /**
    * Create an ID for a node (itemtype::items_id)
    *
    * @param CommonDBTM  $item Name of the node
    *
    * @return string
    */
   public static function getNodeID(CommonDBTM $item) {
      return get_class($item) . self::NODE_ID_DELIMITER . $item->fields['id'];
   }

   /**
    * Create an ID for an edge (NodeID->NodeID)
    *
    * @param CommonDBTM  $itemA     First node of the edge
    * @param CommonDBTM  $itemB     Second node of the edge
    * @param int         $direction Direction of the edge : A to B or B to A ?
    *
    * @return string|null
    *
    * @throws InvalidArgumentException
    */
   public static function getEdgeID(
      CommonDBTM $itemA,
      CommonDBTM $itemB,
      int $direction
   ) {
      switch ($direction) {
         case self::DIRECTION_FORWARD:
            return self::getNodeID($itemA) . self::EDGE_ID_DELIMITER . self::getNodeID($itemB);

         case self::DIRECTION_BACKWARD:
            return self::getNodeID($itemB) . self::EDGE_ID_DELIMITER . self::getNodeID($itemA);

         default:
            throw new InvalidArgumentException(
               "Invalid value for argument \$direction ($direction)."
            );
      }
   }


   /**
    * Clean impact records for a given item that has been purged form the db
    *
    * @param CommonDBTM $item The item being purged
    */
   public static function clean(\CommonDBTM $item) {
      global $DB;

      // Skip if not a valid impact type
      if (!self::isEnabled($item::getType())) {
         return;
      }

      // Remove each relations
      $DB->delete(\ImpactRelation::getTable(), [
         'OR' => [
            [
               'itemtype_source' => get_class($item),
               'items_id_source' => $item->fields['id']
            ],
            [
               'itemtype_impacted' => get_class($item),
               'items_id_impacted' => $item->fields['id']
            ],
         ]
      ]);

      // Remove associated ImpactItem
      $impact_item = ImpactItem::findForItem($item, false);
      if (!$impact_item) {
         // Stop here if no impactitem, nothing more to delete
         return;
      }

      $impact_item->delete($impact_item->fields);

      // Remove impact context if defined and not a slave, update others
      // contexts if they are slave to us
      if ($impact_item->fields['impactcontexts_id'] != 0
         && $impact_item->fields['is_slave'] != 0) {
         $DB->update(ImpactItem::getTable(), [
               'impactcontexts_id' => 0,
            ], [
               'impactcontexts_id' => $impact_item->fields['impactcontexts_id'],
            ]
         );

         $DB->delete(ImpactContext::getTable(), [
            'id' => $impact_item->fields['impactcontexts_id']
         ]);
      }

      // Delete group if less than two children remaining
      if ($impact_item->fields['parent_id'] != 0) {
         $count = countElementsInTable(ImpactItem::getTable(), [
            'parent_id' => $impact_item->fields['parent_id']
         ]);

         if ($count < 2) {
            $DB->update(ImpactItem::getTable(), [
                  'parent_id' => 0,
               ], [
                  'parent_id' => $impact_item->fields['parent_id']
               ]
            );

            $DB->delete(ImpactCompound::getTable(), [
               'id' => $impact_item->fields['parent_id']
            ]);
         }
      }
   }

   /**
    * Check if the given itemtype is enabled in impact config
    *
    * @param string $itemtype
    * @return bool
    */
   public static function isEnabled(string $itemtype): bool {
      return in_array($itemtype, self::getEnabledItemtypes());
   }

   /**
    * Return enabled itemtypes
    *
    * @return array
    */
   public static function getEnabledItemtypes(): array {
      // Get configured values
      $conf = Config::getConfigurationValues('core');

      if (!isset($conf[self::CONF_ENABLED])) {
         return [];
      }

      $enabled = importArrayFromDB($conf[self::CONF_ENABLED]);

      // Remove any forbidden values
      return array_filter($enabled, function($itemtype) {
         global $CFG_GLPI;

         return isset($CFG_GLPI['impact_asset_types'][$itemtype]);
      });
   }

   /**
    * Return default itemtypes
    *
    * @return array
    */
   public static function getDefaultItemtypes() {
      global $CFG_GLPI;

      $values = $CFG_GLPI["default_impact_asset_types"];
      return array_keys($values);
   }

   /**
    * Print the impact config tab
    */
   public static function showConfigForm() {
      global $CFG_GLPI;

      // Form head
      $action = Toolbox::getItemTypeFormURL(Config::getType());
      echo "<form name='form' action='$action' method='post'>";

      // Table head
      echo '<table class="tab_cadre_fixe">';
      echo '<tr><th colspan="2">' . __('Impact analysis configuration') . '</th></tr>';

      // First row: enabled itemtypes
      $input_name = self::CONF_ENABLED;
      $values = $CFG_GLPI["impact_asset_types"];
      foreach ($values as $itemtype => $icon) {
         $values[$itemtype]= $itemtype::getTypeName();
      }
      echo '<tr class="tab_bg_2">';

      echo '<td width="40%">';
      echo "<label for='$input_name'>";
      echo __('Enabled itemtypes');
      echo '</label>';
      echo '</td>';

      $core_config = Config::getConfigurationValues("core");
      $db_values = importArrayFromDB($core_config[self::CONF_ENABLED]);
      echo '<td>';
      Dropdown::showFromArray($input_name, $values, [
         'multiple' => true,
         'values'   => $db_values
      ]);
      echo "</td>";

      echo "</tr>";

      echo '</table>';

      // Submit button
      echo '<div style="text-align:center">';
      echo Html::submit(__('Save'), ['name' => 'update']);
      echo '</div>';

      Html::closeForm();
   }
}
