/**
 * GraphView module
 * @author Matthias Brunner
 * @copyright 2019-2021 University of Stuttgart, Institute of Aircraft Systems, Matthias Brunner
 */

import Label from "../model/graph/Label.js";
import Edge from "../model/graph/Edge.js";
import Vertex from "../model/graph/Vertex.js";
import { GraphViewPort, GraphViewPortOutline } from "./GraphViewPort.js";

/** Class managing the graph view and its outline */
class GraphView {
  /**
   * Create a graph view and its outline
   * @param {DOMelement} editorDOM - The DOM element that should contain the graph visualization.
   * @param {DOMelement} outlineDOM - The DOM element that should contain the graph outline.
   */
  constructor(editorDOM, outlineDOM) {
    var self = this;
    this.autoRefresh = false; //enable auto refresh after items have been added
    this.eventBroker = $app.plugins.require("eventBroker"); // should be set by the constructor instead
    this.lastClickLocation = { x: 0, y: 0 };

    try {
      this.graph = this._initializeGraph(editorDOM, outlineDOM);
    } catch (e) {
      console.error("graph initialization failed for GraphView: " + e);
    }

    this.graphController = null;

    this.loadingIndication = $(document.createElement("div"))
      .addClass("editorLoader")
      .prependTo(this.graph.container)
      .hide();

    //Enable HTML Labels
    this.graph.setHtmlLabels(true);

    //Event for Zoom In/Zoom Out via mouse wheel
    mxEvent.addMouseWheelListener(
      mxUtils.bind(this, function (evt, up) {
        var x = mxEvent.getClientX(evt);
        var y = mxEvent.getClientY(evt);
        var offset = mxUtils.getOffset(self.graph.container);
        var target = { x: x - offset.x, y: y - offset.y };
        var origin = mxUtils.getScrollOrigin();
        var elt = document.elementFromPoint(mxEvent.getClientX(evt), mxEvent.getClientY(evt));
        while (elt != null && elt != self.graph.container) {
          elt = elt.parentNode;
        }

        // Checks if event is inside the bounds of the graph container
        let isContained =
          elt != null &&
          x >= offset.x - origin.x &&
          y >= offset.y - origin.y &&
          x <= offset.x - origin.x + self.graph.container.offsetWidth &&
          y <= offset.y - origin.y + self.graph.container.offsetHeight;
        if (isContained) {
          if (up) {
            self.viewPort.zoomTo(target, 1.1);
          } else {
            self.viewPort.zoomTo(target, 1 / 1.1);
          }
          mxEvent.consume(evt);
        }
      }),
    );

    //Paste-Handling for the Editor
    editorDOM.setAttribute("contenteditable", true);
    editorDOM.addEventListener("paste", async (event) => {
      var clipboard = $app.plugins.require("clipboard");
      await clipboard.evalEvent(event);
      if (
        await clipboard.hasContents(function (e) {
          return self.graphController.isPastable(e);
        })
      ) {
        let cnts = await clipboard.getContents(function (e) {
          return self.graphController.isPastable(e);
        });
        cnts.forEach(function (e) {
          self.graphController.paste(e, null, self.lastClickLocation);
        });
      }
      event.stopPropagation();
    });

    //Copy-handling for the editor
    editorDOM.addEventListener("copy", async (event) => {
      var clipboard = $app.plugins.require("clipboard");
      var eObjects = [];
      var selectedCells = self.graph.getSelectionCells();
      if (selectedCells.length) {
        for (let i in selectedCells) {
          eObjects.push(ecoreSync.utils.getObjectURL(selectedCells[i].value.getEObject()));
        }
      }
      if (eObjects.length) {
        await clipboard.evalEvent(event, eObjects);
      }
      event.stopPropagation();
    });

    //Cut-handling for the editor
    editorDOM.addEventListener("cut", async (event) => {
      var clipboard = $app.plugins.require("clipboard");
      var eObjects = [];
      var selectedCells = self.graph.getSelectionCells();
      if (selectedCells.length) {
        for (let i in selectedCells) {
          eObjects.push(ecoreSync.utils.getObjectURL(selectedCells[i].value.getEObject()));
        }
      }
      if (eObjects.length) {
        await clipboard.evalEvent(event, eObjects);
      }
      event.stopPropagation();
    });

    // XGEE key listener
    editorDOM.addEventListener("keydown", (event) => {
      //sure do not want any further propagation
      event.stopImmediatePropagation();
      event.stopPropagation();
      event.preventDefault();

      var target = {};
      target.editorId = self.graphController.model.editorId;
      target.DOM = null;
      target.graphObject = null;
      target.originalEvent = event;

      var selectedCells = self.graph.getSelectionCells();
      // TODO: support multiple selected cells

      // Single selected cell
      if (selectedCells.length == 1) {
        var cell = selectedCells[0];
        target.isCanvas = false;
        target.isGraphObject = true;
        target.graphObject = cell.value;
        target.isVertex = selectedCells[0].value instanceof Vertex;
        target.isEdge = selectedCells[0].value instanceof Edge;
        target.eObject = target.graphObject.getEObject();
        if (target.isVertex) {
          target.position = cell.value.getPosition();
        }
        if (target.isEdge) {
          target.edgeSource = cell.source.value;
          target.edgeTarget = cell.target.value;
        }
      }

      // No selected cell
      if (selectedCells.length == 0) {
        target.isCanvas = true;
        target.isGraphObject = false;
        target.isVertex = false;
        target.isEdge = false;
        target.eObject = self.graphController.eObject;
      }

      self.eventBroker.publish("XGEE/KEYPRESS", {
        target: target,
        eObject: null,
        DOM: null,
        key: event.key,
        shiftKey: event.shiftKey,
        ctrlKey: event.ctrlKey,
        altKey: event.altKey,
      });
    });

    //Context Menu on Right-Click
    var contextMenuArmed = false;
    var panStart = null;
    var panTranslate = null;
    this.graph.addMouseListener({
      mouseDown: function (sender, evt) {
        if (mxEvent.isRightMouseButton(evt.evt)) {
          //Panning Handler start
          panStart = { x: evt.evt.x, y: evt.evt.y };
          panTranslate = self.viewPort.getTranslate();
          contextMenuArmed = true;
          evt.consume();
        } else {
          contextMenuArmed = false;
        }
      },
      mouseMove: function (sender, evt) {
        contextMenuArmed = false;
        if (panStart != null && panTranslate != null) {
          let panCurrent = { x: evt.evt.x, y: evt.evt.y };
          let panVector = {
            x: panCurrent.x - panStart.x,
            y: panCurrent.y - panStart.y,
          };
          self.viewPort.panRelative(panTranslate, panVector);
        }
      },
      mouseUp: function (sender, evt) {
        if (panStart != null) {
          panStart = null;
          panTranslate = null;
        }

        if (mxEvent.isRightMouseButton(evt.evt)) {
          if (contextMenuArmed) {
            var cell = self.graph.getCellAt(evt.graphX, evt.graphY);
            var cellState = self.graph.view.getState(cell);
            var target = {};
            target.editorId = self.graphController.model.editorId;
            target.DOM = cellState ? cellState.shape.node : null;
            target.isGraphObject = false;
            target.isVertex = false;
            target.isEdge = false;
            target.graphObject = null;
            target.originalEvent = evt;

            if (cell) {
              target.isCanvas = false;
              target.graphObject = cell.value;
              target.isGraphObject = true;
              target.isVertex = cell.value instanceof Vertex;
              target.isEdge = cell.value instanceof Edge;
              target.eObject = target.graphObject.getEObject();
              if (target.isVertex) {
                target.position = cell.value.getPosition();
              }
              if (target.isEdge) {
                target.edgeSource = cell.source.value;
                target.edgeTarget = cell.target.value;
              }
            } else {
              target.DOM = null;
              target.isCanvas = true;
              target.eObject = self.graphController.eObject;
            }

            self.eventBroker.publish("XGEE/CONTEXTMENU", {
              event: evt.evt,
              target: target,
            }); //maybe we should get rid of this nested event later (anti-pattern?)
          }
          evt.consume();
        }
      },
    });

    //Event for handling new edge connections

    this.graph.connectionHandler.addListener(mxEvent.CONNECT, function (sender, evt) {
      var cell = evt.getProperty("cell");
      self.graph.removeCells([cell]);
      var source = self.graph.getModel().getTerminal(cell, true);
      var target = self.graph.getModel().getTerminal(cell, false);
      self.graphController.createEdge(source, target);
      return true;
    });

    this.graph.getEdgeValidationError = function (edge, source, target) {
      if (!self.graphController.canCreateEdge(source, target)) {
        return "Invalid Edge Defintion";
      }
      return mxGraph.prototype.getEdgeValidationError.apply(this, arguments);
    };

    this.graph.isCellSelectable = function (cell) {
      return !(cell.value instanceof Label);
    };

    //On Press of DEL Key, remove the cell from its parent. The action should also be model-defined in the future.
    var keyHandler = new mxKeyHandler(this.graph);
    keyHandler.bindKey(46, function (evt) {
      if (self.graph.isEnabled()) {
        var selectedCells = self.graph.getSelectionCells();

        for (let cell of selectedCells) {
          if (cell.value.isDeletable && cell.value.isDeletable()) {
            if (cell.value instanceof Edge) {
              cell.value.delete(cell.source.value, cell.target.value);
            } else {
              cell.value.delete();
            }
          }
        }
      }
      self.graph.getSelectionModel().clear();
    });

    //register graph validation function
    this.graph.isValidDropTarget = function (targetCell, dropCells, evt) {
      if (targetCell)
        if (targetCell.value) {
          var isDroppable = dropCells.map(function (dropCell) {
            return self.graphController.isDroppable(
              targetCell.value.eObject,
              null,
              dropCell.value.eObject,
            );
          });
          var res = true;
          isDroppable.forEach(function (droppable) {
            res = res && droppable;
          });
          return res;
        } else {
          return false;
        }
    };

    this.graph.graphHandler.moveCells = function (cells, dx, dy, clone, target, evt) {
      if (clone) {
        cells = this.graph.getCloneableCells(cells);
      }

      // Removes cells from parent
      if (
        target == null &&
        this.isRemoveCellsFromParent() &&
        this.shouldRemoveCellsFromParent(this.graph.getModel().getParent(this.cell), cells, evt)
      ) {
        target = this.graph.getDefaultParent();
      }

      if (target) {
        //capture the move if there is a new target specified
        var allDroppable = true;
        var cellsDroppable = cells.map(function (dropCell) {
          return self.graphController.isDroppable(
            target.value.eObject,
            null,
            dropCell.value.eObject,
          );
        });
        cellsDroppable.forEach(function (droppable) {
          allDroppable = allDroppable && droppable;
        });

        if (allDroppable) {
          cells.forEach(function (dropCell) {
            self.graphController.drop(target.value.eObject, null, dropCell.value.eObject);
          });
        }
      } else {
        //execute the move

        // Cloning into locked cells is not allowed
        clone = clone && !this.graph.isCellLocked(target || this.graph.getDefaultParent());

        // Passes all selected cells in order to correctly clone or move into
        // the target cell. The method checks for each cell if its movable.
        cells = this.graph.moveCells(
          cells,
          dx - this.graph.panDx / this.graph.view.scale,
          dy - this.graph.panDy / this.graph.view.scale,
          clone,
          target,
          evt,
        );

        if (this.isSelectEnabled() && this.scrollOnMove) {
          this.graph.scrollCellToVisible(cells[0]);
        }

        // Selects the new cells if cells have been cloned
        if (clone) {
          this.graph.setSelectionCells(cells);
        }
      }
    };

    //Register own graphView with the mxGraph
    this.graph.graphView = this;

    this.viewPort = new GraphViewPort(
      $(editorDOM).find("g").get(0),
      $(editorDOM).width(),
      $(editorDOM).height(),
    );

    this.outline = new GraphViewPortOutline(this.viewPort, outlineDOM);

    //Grab focus
    editorDOM.focus();
  }

  init() {
    var self = this;

    //Listener for double click on cells
    this.graph.addListener(mxEvent.DOUBLE_CLICK, function (sender, evt) {
      var cell = evt.getProperty("cell");
      if (cell) {
        var cellState = self.graph.view.getState(cell);
        if (cellState) {
          self.eventBroker.publish("PROPERTIESVIEW/OPEN", {
            eObject: cell.value.eObject,
            DOM: cellState.shape.node,
          });
        }
      }
    });

    //Listener for the selection of cells for updating the selection provider
    this.graph.getSelectionModel().addListener(mxEvent.CHANGE, async function (sender, evt) {
      try {
        var nElements = sender.cells.length;
        if (nElements > 0) {
          var elements = [];
          for (var i = 0; i < nElements; i++) {
            if (sender.cells[i].value) {
              if (!(sender.cells[i].value instanceof Label))
                elements.push(sender.cells[i].value.getEObject());
            }
          }
          if (
            self.graphController.paletteController.activeTool &&
            self.graphController.paletteController.activeTool.isSelectionTool &&
            (await self.graphController.paletteController.activeTool.canUse(elements))
          ) {
            self.graphController.paletteController.activeTool.applyTo(elements);
          } else {
            var domElement = null;
            var cellState = self.graph.view.getState(sender.cells[0]);
            if (cellState) {
              var domElement = cellState.shape.node;
              let changeEvent = new eventBroker.SelectionChangedEvent(self, elements, domElement);
              self.eventBroker.publish("SELECTION/CHANGE", changeEvent);
            }
          }
        }
      } catch (e) {
        console.error("Selection failed: " + e);
      }
    });

    //Events for changing vertex positions and sizes
    this.graph.model.addListener(mxEvent.CHANGE, function (sender, evt) {
      var changes = evt.getProperty("edit").changes;
      changes.forEach(function (change) {
        if (change.cell) {
          if (change.cell.vertex && change.geometry) {
            if (change.geometry.relative) {
              let offsetX = 0;
              let offsetY = 0;
              if (change.geometry.offset) {
                offsetX = change.cell.value.parent.getRelativeX(change.geometry.offset.x);
                offsetY = change.cell.value.parent.getRelativeY(change.geometry.offset.y);
              }
              change.cell.value.moveTo(
                Math.min(1, Math.max(0, change.geometry.x + offsetX)),
                Math.min(1, Math.max(0, change.geometry.y + offsetY)),
              );
            } else {
              //Absolute positioning
              if (
                change.geometry.x != change.previous.x ||
                change.geometry.y != change.previous.y
              ) {
                // self.graphController.model.layout.setVertexPosition(change.cell.value,change.geometry.x,change.geometry.y)
                change.cell.value.moveTo(change.geometry.x, change.geometry.y);
              }
            }

            if (
              change.geometry.width != change.previous.width ||
              change.geometry.height != change.previous.height
            ) {
              //self.graphController.model.layout.setVertexPosition(change.cell.value,change.geometry.width,change.geometry.height)
              change.cell.value.resize(change.geometry.width, change.geometry.height);
            }
          }

          if (change.cell.edge) {
            if (change.cell.value) {
              change.cell.value.setSupportPoints(change.cell.geometry.points);
            }
          }
        }
      });
    });

    this.graph.addListener(mxEvent.CLICK, function (sender, evt) {
      var event = evt.getProperty("event");
      var location = self.viewPort.toOriginalCoordinate({
        x: event.layerX,
        y: event.layerY,
      });
      self.lastClickLocation = Object.assign({}, location);
      //Propagate click of labels to parent
      var cell = evt.getProperty("cell"); // cell may be null

      if (cell != null) {
        if (cell.value instanceof Label) {
          self.graph.selectCellForEvent(cell.parent, evt);
        }
      } else {
        var selectedCells = self.graph.getSelectionCells();
        if (selectedCells.length == 1 && selectedCells[0].value instanceof Edge) {
          let edge = selectedCells[0];
          let state = self.graph.view.getState(edge);

          //Remove edge support point
          if (event.altKey) {
            let geo = self.graph.model.getGeometry(edge);

            if (geo.points != null) {
              let dNearestPoint = 1000;
              let nearestPoint = null;
              for (let point of geo.points) {
                let d = Math.sqrt(
                  Math.pow(point.x - location.x, 2) + Math.pow(point.y - location.y, 2),
                );
                if (d < dNearestPoint) {
                  nearestPoint = point;
                  dNearestPoint = d;
                }
              }

              if (nearestPoint) {
                geo.points.splice(geo.points.indexOf(nearestPoint), 1);
                self.graph.model.setGeometry(edge, geo);
                edge.value.setSupportPoints(geo.points); //seems to be necessary because CHANGE event not fired by mxGraph
                self.graph.refresh(edge);
              }
            }
            evt.consume();
          }

          //Add edge support point
          if (event.ctrlKey) {
            let idx = mxUtils.findNearestSegment(state, location.x, location.y);
            let geo = self.graph.model.getGeometry(edge);

            geo = geo.clone();
            if (!geo.points) {
              geo.points = [new mxPoint(parseInt(location.x), parseInt(location.y))];
            } else {
              geo.points.splice(idx, 0, new mxPoint(parseInt(location.x), parseInt(location.y)));
            }

            self.graph.model.setGeometry(edge, geo);
            self.graph.refresh(edge);
            evt.consume();
          }
        }
      }
    });

    this.graph.addListener(mxEvent.DOUBLE_CLICK, function (sender, evt) {
      //Propagate click of labels to parent
      var cell = evt.getProperty("cell"); // cell may be null
      if (cell != null) {
        if (cell.value instanceof Label) {
          document
            .getElementById("uuid::" + cell.getParent().id)
            .dispatchEvent(new MouseEvent("dblclick"));
          evt.consume();
        }
      }
    });

    //Install viewport listeners
    this.viewPort.onZoom((scale) => {
      self.graphController.model.layout.setScale(scale);
    });

    this.viewPort.onPan((translate) => {
      self.graphController.model.layout.setTranslate(translate.x, translate.y);
    });
  }

  _initializeGraph(editorDOM, outlineDOM) {
    var self = this;
    // Checks if the browser is supported
    if (!mxClient.isBrowserSupported()) {
      // Displays an error message if the browser is not supported.
      mxUtils.error("Browser is not supported!", 200, false);
    } else {
      // Enables guides
      mxGraphHandler.prototype.guidesEnabled = true;

      // Alt disables guides
      mxGuide.prototype.isEnabledForEvent = function (evt) {
        return mxEvent.isAltDown(evt);
      };

      // Enables snapping waypoints to terminals
      mxEdgeHandler.prototype.snapToTerminals = true;

      mxEvent.disableContextMenu(document.body);

      // Creates the graph inside the given container
      var model = new mxGraphModel();
      model.maintainEdgeParent = false;
      var graph = new mxGraph(editorDOM, model);

      graph.setPanning(false);
      graph.disconnectOnMove = false;
      graph.foldingEnabled = false;
      graph.cellsResizable = true;
      graph.cellsMovable = true;

      graph.extendParents = false;
      graph.setConnectable(true);
      graph.setAllowDanglingEdges(false);
      graph.graphHandler.setRemoveCellsFromParent(false);
      graph.setDropEnabled(true);

      graph.setConstrainChildren(true);
      graph.setConstrainRelativeChildren(true);

      var style = graph.getStylesheet().getDefaultEdgeStyle();
      style["strokeColor"] = "#000000";
      style["strokeWidth"] = 4;

      var parent = graph.getDefaultParent();

      //Disable auto-scroll, disable auto-extend
      graph.autoScroll = false;
      graph.autoExtend = false;
      graph.allowAutoPanning = false;

      //Prevents graph panning
      mxPanningHandler.prototype.panningEnabled = false;

      //Preserve Edge Style on auto-arrange (circle)
      mxCircleLayout.prototype.disableEdgeStyle = false;

      //Prevents the rubberband to be active on right mouse button
      mxRubberband.prototype.mouseDown = function (sender, me) {
        if (
          !me.isConsumed() &&
          this.isEnabled() &&
          this.graph.isEnabled() &&
          me.getState() == null &&
          !mxEvent.isMultiTouchEvent(me.getEvent()) &&
          !mxEvent.isRightMouseButton(me.getEvent())
        ) {
          var offset = mxUtils.getOffset(this.graph.container);
          var origin = mxUtils.getScrollOrigin(this.graph.container);
          origin.x -= offset.x;
          origin.y -= offset.y;
          this.start(me.getX() + origin.x, me.getY() + origin.y);

          // Does not prevent the default for this event so that the
          // event processing chain is still executed even if we start
          // rubberbanding. This is required eg. in ExtJs to hide the
          // current context menu. In mouseMove we'll make sure we're
          // not selecting anything while we're rubberbanding.
          me.consume(false);
        }
      };

      //Coordinate transformation for viewport
      mxRubberband.prototype.execute = function (evt) {
        if (this.graph.graphView) {
          let pt = this.graph.graphView.viewPort.toOriginalCoordinate({
            x: this.x,
            y: this.y,
          });
          this.x = pt.x;
          this.y = pt.y;
          this.width /= this.graph.graphView.viewPort.getScale();
          this.height /= this.graph.graphView.viewPort.getScale();
        }
        var rect = new mxRectangle(this.x, this.y, this.width, this.height);
        this.graph.selectRegion(rect, evt);
      };

      // Enables rubberband selection
      new mxRubberband(graph);

      //Disables Editing of cells labels
      graph.setCellsEditable(false);

      // Disables built-in DnD in IE (this is needed for cross-frame DnD, see below)
      if (mxClient.IS_IE) {
        mxEvent.addListener(img, "dragstart", function (evt) {
          evt.returnValue = false;
        });
      }

      //Initialize Outline
      //var outlineContainer = document.getElementById('outlineCanvas'+tab.name);
      //outline = new mxOutline(graph, outlineDOM);

      // Implements perimeter-less connection points as fixed points (computed before the edge style).
      graph.view.updateFixedTerminalPoint = function (edge, terminal, source, constraint) {
        mxGraphView.prototype.updateFixedTerminalPoint.apply(this, arguments);

        var pts = edge.absolutePoints;
        var pt = pts[source ? 0 : pts.length - 1];

        if (terminal != null && pt == null && this.getPerimeterFunction(terminal) == null) {
          edge.setAbsoluteTerminalPoint(
            new mxPoint(this.getRoutingCenterX(terminal), this.getRoutingCenterY(terminal)),
            source,
          );
        }
      };

      // Changes the default edge style
      graph.getStylesheet().getDefaultEdgeStyle()["edgeStyle"] = "orthogonalEdgeStyle";

      delete graph.getStylesheet().getDefaultEdgeStyle()["endArrow"];

      // Implements the connect preview
      graph.connectionHandler.createEdgeState = function (me) {
        var edge = graph.createEdge(null, null, null, null, null);

        return new mxCellState(graph.view, edge, graph.getCellStyle(edge));
      };

      // Overridden to define per-shape connection points
      mxGraph.prototype.getAllConnectionConstraints = function (terminal, source) {
        if (terminal != null && terminal.shape != null) {
          if (terminal.shape.stencil != null) {
            if (terminal.shape.stencil != null) {
              return terminal.shape.stencil.constraints;
            }
          } else if (terminal.shape.constraints != null) {
            return terminal.shape.constraints;
          }
        }

        return null;
      };

      /*
                            function(cell)

            {

                var geometry = this.model.getGeometry(cell);

                

                return this.isCellsLocked() || (geometry != null && this.model.isVertex(cell) && geometry.relative);

            }
            */

      graph.isCellLocked = function (cell) {
        return this.isCellsLocked();
      };

      //End of Mouse Interaction
    }
    return graph;
  }

  /**
   * Returns the current graph selection as eObjects
   *  @return {Array} An array containing the selected eObjects.
   */
  getCurrentGraphSelection() {
    var selectionCells = this.graph.getSelectionCells();
    return selectionCells.map((c) => {
      return c.value.getEObject();
    });
  }

  /**
   * Clears the graph selection
   */
  clearSelection() {
    this.graph.clearSelection();
  }

  /**
   *   Sets the scale of the view. The default scale is 1.00.
   */
  setScale(scale = 1) {
    this.graphController.model.layout.setScale(scale);
  }

  /**
   *   Sets the translate of the view. The default translate is x=0, y=0
   */
  setTranslate(translate = { x: 0, y: 0 }) {
    this.graphController.model.layout.setTranslate(translate.x, translate.y);
  }

  /**
   *   Captures and saves the current layout
   */
  captureLayout() {
    var self = this;
    this.graphController.model.layout.setScale(this.viewPort.getScale());
    var translate = this.viewPort.getTranslate();
    this.graphController.model.layout.setTranslate(translate.x, translate.y);
    var allDisplayed = this.getDisplayedItems();
    allDisplayed.forEach(function (e) {
      let cell = self.graph.model.getCell(e.uuid);
      if (cell.vertex) {
        self.graphController.model.layout.setVertexPosition(e, cell.geometry.x, cell.geometry.y);
        self.graphController.model.layout.setVertexSize(
          e,
          cell.geometry.width,
          cell.geometry.height,
        );
      }
    });
  }

  /**
   *   Adds a container to the graph.
   *   @param {container} container - The container graph object.
   */
  addContainerToGraph(container) {
    let parentCell = null;
    var isRelative = false;

    if (container.parent && container.parent.uuid) {
      isRelative = true;
      parentCell = this.graph.model.getCell(container.parent.uuid + ".span(0)");
    } else {
      parentCell = this.graph.getDefaultParent();
    }

    if (parentCell) {
      let cell = this.graph.insertVertex(
        parentCell,
        container.uuid,
        container,
        container.position.x,
        container.position.y,
        container.size.x,
        container.size.y,
        container.type.getStyle(),
        isRelative,
      );
      cell.getGeometry().offset = new mxPoint(-container.size.x / 2, -container.size.y / 2);

      cell.setConnectable(false);

      if (container.isResizable()) {
        this.graph.setCellStyles(mxConstants.STYLE_RESIZABLE, "1", [cell]);
      } else {
        this.graph.setCellStyles(mxConstants.STYLE_RESIZABLE, "0", [cell]);
      }

      if (container.isMovable()) {
        this.graph.setCellStyles(mxConstants.STYLE_MOVABLE, "1", [cell]);
      } else {
        this.graph.setCellStyles(mxConstants.STYLE_MOVABLE, "0", [cell]);
      }
    } else {
      console.warn(`Container for Edge #${ecoreSync.rlookup(container.eObject)} of class ` +
        `${container.eObject.eClass.get("name")} cannot be displayed because the Edge itself ` +
        `could not be displayed. See warning above. 
        Container UUID: ${container.uuid}, Edge UUID: ${container.parent.uuid}`)
    }
  }

  /**
   *   Removes a container from the graph.
   *   @param {container} container - The container graph object.
   */
  removeContainerFromGraph(container) {
    let cell = graph.model.getCell(container.uuid);
    if (cell) {
      this.graph.removeCells([cell]);
    } else {
      throw "Container with UUID:" + container.uuid + " is not in graph";
    }
  }

  /**
   *   Adds a vertex to the graph.
   *   @param {vertex} vertex - The vertex graph object.
   */
  addVertexToGraph(vertex) {
    var self = this;
    let parentCell = null;
    var isRelative = false;

    if (vertex.parent && vertex.parent.uuid) {
      isRelative = true;
      parentCell = this.graph.model.getCell(vertex.parent.uuid);
    } else {
      parentCell = this.graph.getDefaultParent();
    }

    if (parentCell) {
      try {
        var cell = this.graph.insertVertex(
          parentCell,
          vertex.uuid,
          vertex,
          vertex.position.x,
          vertex.position.y,
          vertex.size.x,
          vertex.size.y,
          vertex.type.getStyle(),
          isRelative,
        );
      } catch (e) {
        throw "displaying vertex failed. reason:" + e;
      }
      //cell.getGeometry().offset=new mxPoint(0,0);

      cell.setConnectable(false);

      if (vertex.isResizable()) {
        this.graph.setCellStyles(mxConstants.STYLE_RESIZABLE, "1", [cell]);
      } else {
        this.graph.setCellStyles(mxConstants.STYLE_RESIZABLE, "0", [cell]);
      }

      if (vertex.isMovable()) {
        this.graph.setCellStyles(mxConstants.STYLE_MOVABLE, "1", [cell]);
      } else {
        this.graph.setCellStyles(mxConstants.STYLE_MOVABLE, "0", [cell]);
      }

      //Update view upon graph model changes
      var timeout = null;
      var timeoutInterval = 100;
      var refreshTimer = function () {
        if (timeout) {
          clearTimeout(timeout);
        }
        timeout = window.setTimeout(function () {
          self.refreshVertex(vertex);
        }, timeoutInterval);
      };
      vertex.on("MOVE", function () {
        refreshTimer();
      });

      vertex.on("RESIZE", function () {
        refreshTimer();
      });

      //Track remote position
      this.graphController.model.layout.observeVertexPosition(vertex, function (pos) {
        vertex.moveTo(pos.x, pos.y);
      });

      //Track remote size
      this.graphController.model.layout.observeVertexSize(vertex, function (size) {
        vertex.resize(size.x, size.y);
      });
    } else {
      throw (
        "Vertex with UUID:" +
        vertex.uuid +
        " could not be displayed, because the parent cell uuid:" +
        vertex.parent.uuid +
        " is invalid"
      );
    }
  }

  /**
   *   Removes a vertex from the graph.
   *   @param {vertex} vertex - The vertex graph object.
   */
  removeVertexFromGraph(vertex) {
    let parent = null;
    if (vertex.parent != this.graph.model) {
      parent = vertex.parent;
    }

    let cell = this.graph.model.getCell(vertex.uuid);
    if (cell) {
      this.graph.removeCells([cell]);
    } else {
      throw "Vertex with UUID:" + vertex.uuid + " is not in graph";
    }

    if (parent) this.refreshVertex(parent);
  }

  /**
   *   Sets a vertex connectable for tools.
   *   @param {vertex} vertex - The vertex graph object.
   */
  setVertexConnectable(vertex) {
    let cell = this.graph.model.getCell(vertex.uuid);
    if (cell) {
      cell.setConnectable(true);
    } else {
      throw "Vertex with UUID:" + vertex.uuid + " is not in graph";
    }
  }

  /**
   *   Sets a vertex not connectable for tools.
   *   @param {vertex} vertex - The vertex graph object.
   */
  setVertexNotConnectable(vertex) {
    let cell = this.graph.model.getCell(vertex.uuid);
    if (cell) {
      cell.setConnectable(false);
    } else {
      throw "Vertex with UUID:" + vertex.uuid + " is not in graph";
    }
  }

  /**
   *   Adds a label to the graph.
   *   @param {label} label - The label graph object.
   */
  addLabelToGraph(label) {
    let parentCell = null;
    var isRelative = false;

    if (label.parent && label.parent.uuid) {
      isRelative = true;
      parentCell = this.graph.model.getCell(label.parent.uuid);
    } else {
      parentCell = this.graph.getDefaultParent();
    }

    if (parentCell) {
      var labelPosition = label.getPosition();
      let cell = this.graph.insertVertex(
        parentCell,
        label.uuid,
        label,
        labelPosition.x,
        labelPosition.y,
        0,
        0,
        label.getStyle(),
        true,
      );
      cell.setConnectable(false);

      if (label.isResizable()) {
        this.graph.setCellStyles(mxConstants.STYLE_RESIZABLE, "1", [cell]);
      } else {
        this.graph.setCellStyles(mxConstants.STYLE_RESIZABLE, "0", [cell]);
      }

      if (label.isMovable()) {
        this.graph.setCellStyles(mxConstants.STYLE_MOVABLE, "1", [cell]);
      } else {
        this.graph.setCellStyles(mxConstants.STYLE_MOVABLE, "0", [cell]);
      }
    } else {
      throw (
        "Label with UUID:" +
        label.uuid +
        " could not be displayed, because the parent cell uuid:" +
        label.parent.uuid +
        " is invalid"
      );
    }
  }

  /**
   *   Removes a label from the graph.
   *   @param {label} label - The label graph object.
   */
  removeLabelFromGraph(label) {
    let cell = graph.model.getCell(label.uuid);
    if (cell) {
      this.graph.removeCells([cell]);
    } else {
      throw "Label with UUID:" + label.uuid + " is not in graph";
    }
  }

  /**
   *   Adds an edge to the graph.
   *   @param {edge} edge - The edge graph object.
   */
  addEdgeToGraph(edge) {
    let parentCell = null;
    if (edge.parent && edge.parent.uuid) {
      parentCell = this.graph.model.getCell(edge.parent.uuid);
    } else {
      parentCell = this.graph.getDefaultParent();
    }

    let edgeSpans = edge.getEdgeSpans();
    let spanCount = edgeSpans.length;
    let edgeSpanId = 0;
    for (let edgeSpan of edgeSpans) {
      let sourceAnchor = edgeSpan.source;
      let targetAnchor = edgeSpan.target;

      var source = null;
      var target = null;

      if (sourceAnchor) {
        source = this.graph.model.getCell(sourceAnchor.getUUID());
      }

      if (targetAnchor) {
        target = this.graph.model.getCell(targetAnchor.getUUID());
      }

      if (source && target) {
        let cell = this.graph.insertEdge(
          parentCell,
          edge.uuid + ".span(" + edgeSpanId + ")",
          edge,
          source,
          target,
          edge.getStyle(sourceAnchor, targetAnchor),
        );
        let supportPoints = edge.getSupportPoints();
        let geo = cell.geometry.clone();
        geo.points = [];
        for (let supportPoint of supportPoints) {
          geo.points.push(new mxPoint(supportPoint.x, supportPoint.y)); //INTEGER?
        }
        this.graph.model.setGeometry(cell, geo);
      } else {
        if (!source) {
          console.warn(
            `Edge #${ecoreSync.rlookup(edge.eObject)} of class ${edge.eObject.eClass.get("name")} cannot be displayed because source #${ecoreSync.rlookup(sourceAnchor.eObject)} is not present in current view. Total anchor count=${edge.anchors.length}`,
          );
        }

        if (!target) {
          console.warn(
            `Edge #${ecoreSync.rlookup(edge.eObject)} of class ${edge.eObject.eClass.get("name")} cannot be displayed because target #${ecoreSync.rlookup(targetAnchor.eObject)} is not present in current view. Total anchor count=${edge.anchors.length}`,
          );
        }
      }
      edgeSpanId += 1;
    }
  }

  /**
   *   Removes an edge from the graph.
   *   @param {edge} edge - The edge graph object.
   */
  removeEdgeFromGraph(edge) {
    let cellUUIDs = Object.keys(this.graph.model.cells).filter((cellUUID) => {
      return cellUUID.includes(edge.uuid);
    });
    let cells = cellUUIDs
      .map((uuid) => {
        return this.graph.model.getCell(uuid);
      })
      .filter((cell) => {
        return cell ? true : false;
      });
    this.graph.removeCells(cells);
  }

  /**
   *   Refreshes a vertex and its subvertices.
   *   @param {vertex} vertex - The vertex graph object.
   */
  refreshVertex(vertex) {
    var self = this;
    if (this.isDisplayed(vertex.uuid)) {
      //Refresh vertex display state
      let cell = this.graph.model.getCell(vertex.uuid);

      var geometry = cell.getGeometry().clone();

      if (geometry.relative) {
        geometry.x = vertex.position.x;
        geometry.y = vertex.position.y;
        geometry.offset = new mxPoint(0, 0);
      } else {
        if (geometry.offset) {
          geometry.offset.x = vertex.position.x;
          geometry.offset.y = vertex.position.y;
        }
      }
      geometry.width = vertex.size.x;
      geometry.height = vertex.size.y;
      self.graph.model.setGeometry(cell, geometry);

      //Update subvertex positions
      vertex.vertices.forEach(function (v) {
        let cell = self.graph.model.getCell(v.uuid);
        if (cell) {
          var geometry = cell.getGeometry().clone();
          geometry.x = v.position.x;
          geometry.y = v.position.y;
          self.graph.model.setGeometry(cell, geometry);
        }
      });

      self.graph.refresh();
    }
  }

  /**
   *   Refreshes multiple vertices and their subvertices.
   *   @param {Array} vertices - The vertex graph objects.
   */
  refreshVertices(vertices) {
    var self = this;
    this.graph.model.beginUpdate();
    vertices.forEach((vertex) => {
      if (this.isDisplayed(vertex.uuid)) {
        //Refresh vertex display state
        let cell = this.graph.model.getCell(vertex.uuid);
        var geometry = cell.getGeometry().clone();
        geometry.x = vertex.position.x;
        geometry.y = vertex.position.y;
        geometry.width = vertex.size.x;
        geometry.height = vertex.size.y;
        self.graph.model.setGeometry(cell, geometry);

        //Update subvertex positions
        vertex.vertices.forEach(function (v) {
          let cell = self.graph.model.getCell(v.uuid);
          if (cell) {
            var geometry = cell.getGeometry().clone();
            geometry.x = v.position.x;
            geometry.y = v.position.y;
            self.graph.model.setGeometry(cell, geometry);
          }
        });
      }
    });
    this.graph.model.endUpdate();
  }

  /**
   *   Waits asynchronously for a graph object with the given UUID to be dispayed.
   *   @async
   *   @param {UUID} uuid - The uuid of a graph object.
   *   @return {Promise} Promise object representing a boolean, that indicates wether the graph object identified by the uuid is displayed.
   */
  async isAsyncDisplayed(uuid) {
    var self = this;
    if (this.graph.getCell(uuid)) {
      return true;
    } else {
      await new Promise(function (resolve, reject) {
        var callback = function () {
          if (self.graph.getCell(uuid)) {
            self.graph.removeListener(mxEvent.CELLS_ADDED, this);
            resolve(true);
          }
        };
        self.graph.addListener(mxEvent.CELLS_ADDED, callback);
      });
    }
  }

  /**
   *   Checks wether a graph object with the given UUID is dispayed.
   *   @param {UUID} uuid - The uuid of a graph object.
   *   @return {boolean} A boolean, that indicates wether the graph object identified by the uuid is displayed.
   */
  isDisplayed(uuid) {
    //TODO: Does not work for edges anymore!
    if (this.graph.model.getCell(uuid)) {
      return true;
    } else {
      return false;
    }
  }

  /**
   *   Checks wether an edge is displayed.
   *   @param {edge} edge - The edge graph object.
   *   @return {boolean} A boolean, that indicates wether edge is displayed.
   */
  isEdgeDisplayed(edge) {
    let res = Object.keys(this.graph.model.cells).filter((cellUUID) => {
      return cellUUID.includes(edge.uuid);
    }).length
      ? true
      : false;
    return res;
  }

  /**
   *   Returns all displayed graph objects.
   *   @return {Array} An array containg the displayed graph objects.
   */
  getDisplayedItems() {
    var self = this;
    var modelIndex = self.graphController.model.getIndex();
    var allDisplayed = [];
    for (const [uuid, item] of modelIndex) {
      if (self.isDisplayed(uuid)) {
        allDisplayed.push(item);
      }
    }
    return allDisplayed;
  }

  /**
   *   Returns the graph object, that has been added last to the view
   *   @return {GraphObject} The last displayed graph object.
   */
  getLastDisplayedItem() {
    var res = null;
    var self = this;
    var modelIndex = self.graphController.model.getIndex();
    var allItems = Array.from(modelIndex).reverse();
    for (let i = 0; i <= allItems.length; i++) {
      if (self.isDisplayed(allItems[i][0])) {
        res = allItems[i][1];
        break;
      }
    }
    return res;
  }

  /**
   *   Returns the DOMelement of a graph object identified by its UUID
   *   @return {DOMelement} The DOMelement of the graph object.
   */
  getItemDOM(uuid) {
    var res = null;
    res = document.getElementById("uuid::" + uuid);
    return res;
  }

  /**
   *   Returns all undisplayed graph objects.
   *   @return {Array} An array containg the undisplayed graph objects.
   */
  getUndisplayedItems() {
    var self = this;
    var modelIndex = self.graphController.model.getIndex();
    var allNotDisplayed = [];
    modelIndex.forEach((item) => {
      if (!self.isDisplayed(item.uuid)) {
        allNotDisplayed.push(item);
      }
    });
    return allNotDisplayed;
  }

  /**
   *   Returns all undisplayed edges.
   *   @return {Array} An array containg the undisplayed edges.
   */
  getUndisplayedEdges() {
    var self = this;
    var modelIndex = self.graphController.model.getIndex();
    var allNotDisplayedEdges = [];
    modelIndex.forEach((item) => {
      if (!self.isDisplayed(item.uuid + ".span(0)")) {
        if (item instanceof Edge) {
          allNotDisplayedEdges.push(item);
        }
      }
    });
    return allNotDisplayedEdges;
  }

  /**
   *   Enables automatic refreshes of all graph objects, when graph objects are added/removed to/from the graph. Auto refresh is disabled by default.
   */
  enableAutoRefresh() {
    this.autoRefresh = true;
  }

  /**
   *   Disables automatic refreshes of all graph objects, when graph objects are added/removed to/from the graph. Auto refresh is disabled by default.
   */
  disableAutoRefresh() {
    this.autoRefresh = false;
  }

  /**
   *   Resets the view's viewport to the scale/translate currently stored in the model's layout data
   *   @async
   */
  async reset() {
    this.viewPort.setScale(await this.graphController.model.layout.getScale());
    this.viewPort.setTranslate(await this.graphController.model.layout.getTranslate());
  }

  /**
   *   Refreshes a graph object identified by its UUID. If the graph object does not exist, this does nothing.
   */
  refresh(uuid) {
    let cell = this.graph.model.getCell(uuid);
    if (cell) {
      this.graph.refresh(cell);
    }
  }

  /**
   *   Refreshes the display and attempts to display previously undisplayed edges. Usually called when new vertices are added to the graph.
   */
  refreshDisplay() {
    var self = this;
    var undisplayedEdges = this.getUndisplayedEdges();
    undisplayedEdges.forEach(function (edge) {
      self.addEdgeToGraph(edge);
    });
  }

  /**
   *   Highlights graph objects with a certain color. The default color is red (#FF00000).
   *   @param {Array} graphObjects - An array containg the graph objects to be highlighted.
   *   @param {string} color - The hex notation of the color as a string including the leading '#'
   *   @returns {function} A callback function that removes the highlight from the graph.
   */
  highlight(graphObjects, color = "#FF0000") {
    let highlights = [];
    let destroyHighlight = () => {
      highlights.forEach((highlight) => highlight.destroy());
    };

    graphObjects.forEach((graphObject) => {
      if (graphObject.uuid != null) {
        let cell = this.graph.model.getCell(graphObject.getUUID());
        var highlight = new mxCellHighlight(this.graph, color, 2);
        highlight.highlight(this.graph.view.getState(cell));
        highlights.push(highlight);
      }
    });

    return destroyHighlight;
  }
}

/** GraphView integration into mxGraph
 *  Replaces mxGraph functions for XGEE viewport. Integrates XGEE viewport coordinate transformation.
 */

mxUtils.convertPoint = function (container, x, y) {
  let activeViewport = $app.viewManager.activeView.XGEEInstance.graphView.viewPort;
  var origin = mxUtils.getScrollOrigin(container, false);
  var offset = mxUtils.getOffset(container);
  offset.x -= origin.x;
  offset.y -= origin.y;
  let pt = activeViewport.toOriginalCoordinate({
    x: x - offset.x,
    y: y - offset.y,
  });
  return new mxPoint(pt.x, pt.y);
};

mxGraph.prototype.scrollRectToVisible = function (rect) {
  let activeViewport = $app.viewManager.activeView.XGEEInstance.graphView.viewPort;
  activeViewport.adjustViewToRegion(rect.x, rect.y, rect.width, rect.height);
};

export default GraphView;
