Draw.loadPlugin(function(ui) {

  const model = ui.editor.graph.model;

  const defaultConfig = {
    // When making an activity wider, the x-position of ports will be scaled proportionally to the new width, and vice-versa with height and y-position. This may be annoying when simply resizing an activity to add an extra port.
    movePortsOnResizeActivity: false,
    // The new algorithm probably gives better results under all circumstances.
    newPortSnappingAlgorithm: true,

    // Edge styles.
    edgeStyleCtrlFlow: "edgeStyle=orthogonalEdgeStyle;endArrow=classic;rounded=0;html=1;strokeWidth=2;fontSize=14;strokeColor=#004C99;jumpStyle=gap;",
    edgeStyleDataFlow: "edgeStyle=orthogonalEdgeStyle;endArrow=open;rounded=0;html=1;strokeWidth=1;fontSize=14;fontColor=#000000;fillColor=#d5e8d4;strokeColor=#6D9656;endFill=0;jumpStyle=gap;",
    edgeStyleTypedBy: "edgeStyle=0;endArrow=blockThin;html=1;strokeWidth=1;fontSize=14;fontColor=#000000;dashed=1;endFill=1;endSize=6;strokeColor=#999999;",
    edgeStyleParentVersion: "edgeStyle=orthogonalEdgeStyle;endArrow=classic;rounded=0;html=1;fontSize=11;fontColor=#000000;fillColor=#fff2cc;strokeColor=#d6b656;rounded=0;jumpStyle=gap;",
    edgeStyleSSLink: "edgeStyle=0;endArrow=blockThin;html=1;fontSize=11;strokeColor=#694B2E;rounded=0;dashed=1;dashPattern=1 4;endFill=1;",
    edgeStyleComment: "edgeStyle=0;endArrow=none;dashed=1;html=1;",


    portLabelOffsetEdgeDirection: 22,
    portLabelOffsetPerpendicularToEdgeDirection: 24,
  };

  let currentConfig;

  const existingConfig = window.localStorage.getItem('ftgpmConfig');
  if (existingConfig === null) { // unlike Object and Map, localStorage returns 'null' when item doesn't exist...
    currentConfig = {};
  } else {
    console.log("Have existing ftgpm config...", existingConfig)
    currentConfig = JSON.parse(existingConfig);
  }

  // Configuration dialog window:
  function createFtgpmConfigWindow() { // only called once - this is only a function in order not to pollute namespace

    // Drawio has no decent abstraction for UI elements or even displaying a dialog window.
    // The following is based on code from src/main/webapp/js/diagramly/Dialogs.js
    const wndDiv = document.createElement('div');
      wndDiv.style.userSelect = 'none';
      wndDiv.style.overflow = 'hidden';
      wndDiv.style.padding = '10px';
      wndDiv.style.height = '100%';
    const wnd = new mxWindow("FTG+PM Plugin Configuration",
      wndDiv, 100, 100, 640, 570, true, true);
      wnd.destroyOnClose = false;
      wnd.setMaximizable(false);
      wnd.setResizable(false);
      wnd.setClosable(true);
      wnd.addListener('show', mxUtils.bind(this, function() {
        statusText.innerHTML = "";
      }));

    function createConfig(labelText, json, readonly) {
      const columnDiv = document.createElement('div');
      columnDiv.style.display = 'inline-block';

      const label = document.createElement('label');
      const textarea = document.createElement('textarea');
      label.setAttribute('for', textarea);
      label.innerHTML = labelText;
      textarea.setAttribute('wrap', 'off');
      textarea.setAttribute('spellcheck', 'false');
      textarea.setAttribute('autocorrect', 'off');
      textarea.setAttribute('autocomplete', 'off');
      textarea.setAttribute('autocapitalize', 'off');
      textarea.setAttribute('wrap', 'hard');
      if (readonly) {
        textarea.setAttribute('readonly', 'true');
      }
      textarea.value = JSON.stringify(json, null, 2);
      textarea.style.overflow = 'auto';
      textarea.style.resize = 'none';
      textarea.style.width = '300px';
      textarea.style.height = '360px';
      textarea.style.marginBottom = '16px';

      columnDiv.appendChild(label);
      mxUtils.br(columnDiv);
      columnDiv.appendChild(textarea);
      return [columnDiv, textarea];
    }

    const [defaultColumn] = createConfig("Default config (hardcoded & read-only):", defaultConfig, true);
    const [customColumn, customTextArea] = createConfig("Custom config (acts as an 'overlay' on top of default config):", currentConfig, false);
    wndDiv.appendChild(defaultColumn);
    wndDiv.appendChild(customColumn);

    mxUtils.br(wndDiv);

    const buttonsDiv = document.createElement('div');
    buttonsDiv.style.textAlign = 'right';
    const statusText = document.createElement('div');
    statusText.style.display = 'inline-block';

    // attempts to save user configuration and displays status text as a side effect
    // returns true if all went well
    function saveConfigurationAndDisplayStatus() {
      let parsed;
      try {
        parsed = JSON.parse(customTextArea.value); // may throw
      } catch (parseErr) {
        statusText.innerHTML = "Parse error: " + parseErr.toString();
        statusText.style.color = 'red';
        return false;
      }
      if (typeof parsed !== 'object' || Array.isArray(parsed) || parsed === null) {
        statusText.innerHTML = "JSON value is not an object";
        statusText.style.color = 'red';
        return false;
      }
      currentConfig = parsed;
      window.localStorage.setItem('ftgpmConfig', JSON.stringify(parsed));
      customTextArea.value = JSON.stringify(parsed, null, 2); // prettify
      statusText.innerHTML = "Configuration successfully set and saved (in window.localStorage)";
        statusText.style.color = 'green';
      return true;
    }

    const applyButton =  mxUtils.button("Apply", saveConfigurationAndDisplayStatus);
    applyButton.className = 'geBtn gePrimaryBtn';
    applyButton.style.float = 'none'; // override geBtn style

    const resetButton = mxUtils.button("Reset", function() {
      customTextArea.value = JSON.stringify(currentConfig, null, 2);
    });
    resetButton.className = 'geBtn';
    resetButton.style.float = 'none'; // override geBtn style

    buttonsDiv.appendChild(statusText);
    buttonsDiv.appendChild(applyButton);
    buttonsDiv.appendChild(resetButton);

    // hacks buttons:
    const forceEdgeStyleButton = mxUtils.button("Apply style rules to all edges", function() {
      if (saveConfigurationAndDisplayStatus()) {
        model.beginUpdate();
        for (const [cellId, cell] of Object.entries(model.cells)) {
          applyCellStyleFromConfig(cell);
        }
        model.endUpdate();
      }
    });
    const forcePortSnapButton = mxUtils.button("Re-snap ports to their activities and re-position port labels", function() {
      if (saveConfigurationAndDisplayStatus()) {
        model.beginUpdate();
        for (const [cellId, cell] of Object.entries(model.cells)) {
          if (cell.geometry) {
            // we'll force a geometry update:
            const newGeometry = cell.geometry.clone();
            snapToBorderIfCellIsPortAndParentIsActivity(cell, newGeometry);
            model.setGeometry(cell, newGeometry);
          }
        }
        model.endUpdate();
      }
    })
    forceEdgeStyleButton.className = 'geBtn';
    forceEdgeStyleButton.style.float = 'none'; // override geBtn style
    forcePortSnapButton.className = 'geBtn';
    forcePortSnapButton.style.float = 'none'; // override geBtn style

    hacksDiv = document.createElement('div');
    hacksDiv.appendChild(document.createTextNode("Hacks: (use undo if you fuck things up)"));
    mxUtils.br(hacksDiv);
    hacksDiv.appendChild(forceEdgeStyleButton);
    mxUtils.br(hacksDiv);
    hacksDiv.appendChild(forcePortSnapButton);

    wndDiv.appendChild(buttonsDiv);
    mxUtils.br(wndDiv);
    wndDiv.appendChild(hacksDiv);

    return wnd;
  }

  const ftgpmConfigWindow = createFtgpmConfigWindow();

  function getFromConfig(parameter) {
    if (currentConfig.hasOwnProperty(parameter)) {
      return currentConfig[parameter];
    } else {
      return defaultConfig[parameter];
    }
  }


  // // Force style window:
  // const forceStyleDiv = document.createElement('div');

  // const forceStyleWindow = new mxWindow("FTG+PM: Force Style Rules",
  //   forceStyleDiv, 200, 200, 400, 90, true, true);
  // forceStyleWindow.destroyOnClose = false;
  // forceStyleWindow.setMaximizable(false);
  // forceStyleWindow.setResizable(false);
  // forceStyleWindow.setClosable(true);

  // const forceEdgeStyleButton = mxUtils.button("Apply style rules to all edges", function() {
  //   for (const [cellId, cell] of Object.entries(model.cells)) {
  //     applyCellStyleFromConfig(cell);
  //   }
  // });
  // const forcePortSnapButton = mxUtils.button("Re-snap ports to their activities and re-position port labels", function() {
  //   for (const [cellId, cell] of Object.entries(model.cells)) {
  //     if (cell.geometry) {
  //       // we'll force a geometry update:
  //       const newGeometry = cell.geometry.clone();
  //       snapToBorderIfCellIsPortAndParentIsActivity(cell, newGeometry);
  //       model.setGeometry(cell, newGeometry);
  //     }
  //   }
  // })
  // forceEdgeStyleButton.className = 'geBtn';
  // forcePortSnapButton.className = 'geBtn';
  // forceStyleDiv.appendChild(forceEdgeStyleButton);
  // mxUtils.br(forceStyleDiv);
  // forceStyleDiv.appendChild(forcePortSnapButton);

  window.onkeyup = function(e) {
    // Shortcut to show FTG+PM config:
    if (e.ctrlKey && e.key === '.') {
      ftgpmConfigWindow.show();
    }
    // // Shortcut to apply style from config to all edges:
    // if (e.ctrlKey && e.key === 'Enter') {
    //   forceStyleWindow.show();
    // }
  }


  // For auto-updating port orientation
  // Note: this is only the 'shape', not the full style
  const encodedPortShapes = {
    in: {
      l: `stencil(pZNdD4IgFIZ/DbcNYa3rZvU/yE7JRHBAX/++Y0c3zXC2btzO+4wHfFEm81CqBpjgJZM7JoRc4wPHO42C06hCA0Wk7Ka8VkcDREL0roK7PsXOoG0JXseWyj3jW9Y6OJN54axFiXY2jMiAo0xpi2v5g2Td9s9uWnWna3CHGiJ4yjNKmTgsFmf/iGWOSep1ZH5URXXx7mpP01VJdnYevoAeN6rtdwJ6XLsbJFuba6cXGG0Hgg9Dtun7+UUydshFhsK4ADNXiXmiCWxQG0NfY+rKJh2/U/oD3sEL)`,
      r: `stencil(pZNdD4IgFIZ/DbcNYa3rZvU/yE7JQnBAVv++o0eXH9ls3bid9xkP8qJMpiFXJTDBcyZ3TAi5xgeOdxoFp1GFErJIWaW8VkcDREL07gp3fYqtQdscvI41lXvGt6x2cCbTzFmLEu1sGJAeR5nSFtfyB8na7Z/ttGrfrsQdCojgKU8oZeKwWJz8I5YpJnPHkelRZdeLdzd7mq6aZWfn4QPocKnqfiegw4Wr4H24cW3f6ukMRtueYShINl0/vzhGErlIkRkX4MtdYj5TBVaojaHPce7OJiU3Kf0CTfAC)`,
      t: `stencil(nZPRDoIgGIWfhtuGsNZ1s3oP0r9kIjggrbfv1183S23VDds5Z3zAAZhMQ6FqYIIXTB6YEILjgLIlKbckVaghi+Q1ymt1NkBJiN6V0Oo8DgRtC/A6dqk8Mr5nHZIzmWbOWoRoZ8NLMskRprTFufxOML4ZNvAYNKkaV6gggic3IZeJ09/g5BewTNFZO45Mzyorr97dbD6ftZpdnIeFYIxr1fU7C8a4cg1MDrfU2XI7I8BoOwHIxdZ/ISS7t4K/g2TGBfhwleivNIENamPoNa5d2azj3qUf0BtP)`,
      b: `stencil(nZNRD4IgFIV/Da8NYa3nZvU/EG/JQnBAWv++q1c3m9pWL27nnPHJPQCTeaxUA0zwiskTE0Jw/KDsSMo9SRUb0Im8VgWjCguUxBT8HTpTppFgXAXBpD6VZ8aPrEdyJnPtnUOI8S5+JLMcYco4XMufBOO7cQOvUZNq8A81JAjkZuQycfkbnP0Cljk6W+PIvFD6fgv+4crlqs3s6gOsBFPcqL7fRTDFtW9hNtzHaJNcr2ciWONmhOywXvwvDLn/Yxva+ghfzhL9jSqwQmMtXcetM1uUPLj0BAbjDQ==)`,
    }
  };
  encodedPortShapes.out = {
    r: encodedPortShapes.in.l,
    l: encodedPortShapes.in.r,
    t: encodedPortShapes.in.b,
    b: encodedPortShapes.in.t,
  }

  // Styles for our different edge types
  // Mapping from 'pmRole' attribute to key in config.
  const edgeStyles = {
    ctrl_flow: 'edgeStyleCtrlFlow',
    data_flow: 'edgeStyleDataFlow',
    typed_by: 'edgeStyleTypedBy',
    parent_version: 'edgeStyleParentVersion',
    comment_edge: 'edgeStyleComment',
  };

  function applyCellStyleFromConfig(cell) {
    const ftgpmType = cell.getAttribute(TYPE_ATTR);
    if (ftgpmType) {
      if (edgeStyles.hasOwnProperty(ftgpmType)) {
        setEdgeStyle(cell, ftgpmType);
      }
      // TODO: do the same on nodes.
    }
  }


  const TYPE_ATTR = "pmRole"; // cell attribute holding the ftgpm type

  function parseStyle(str) {
    const pairs = str.split(';').filter(s => s !== '');
    const map = new Map();
    pairs.map(pair => {
      const [key,value] = pair.split('=');
      map.set(key, value);
    });
    return map;
  }

  function unparseStyle(map) {
    let str = "";
    for (const [key, value] of map.entries()) {
      if (value === undefined) {
        str += key + ';';
      } else {
        str += key + '=' + value + ';';
      }
    }
    return str;
  }

  // copy selected keys (=array) from fromMap to toMap
  function mapAssign(toMap, fromMap, keys) {
    for (const key of keys) {
      const value = fromMap.get(key);
      if (value !== undefined) {
        toMap.set(key, value);
      }
    }
  }

  function reverseEdge(edge, sourceCell, targetCell) {
    // Reverse source and target
    ui.editor.graph.model.setTerminals(edge, targetCell, sourceCell);

    // Reverse 'waypoints'
    if (edge.geometry.points) {
      const newGeometry = edge.geometry.clone();
      newGeometry.points.reverse();
      ui.editor.graph.model.setGeometry(edge, newGeometry);
    }

    // Reverse entry and exit points
    const oldStyle = parseStyle(model.getStyle(edge));
    const newStyle = new Map(oldStyle);
    // entry becomes exit
    newStyle.set('entryX', oldStyle.get('exitX'));
    newStyle.set('entryY', oldStyle.get('exitY'));
    // exit becomes entry
    newStyle.set('exitX', oldStyle.get('entryX'));
    newStyle.set('exitY', oldStyle.get('entryY'));
    // entryD becomes exitD
    newStyle.set('entryDx', oldStyle.get('exitDx'));
    newStyle.set('entryDy', oldStyle.get('exitDy'));
    // exitD becomes entryD
    newStyle.set('exitDx', oldStyle.get('entryDx'));
    newStyle.set('exitDy', oldStyle.get('entryDy'));
    model.setStyle(edge, unparseStyle(newStyle));
  }

  // Simply sets the data attribute TYPE_ATTR to ftgpm_type.
  function setFtgpmType(cell, ftgpm_type) {
    // Set type attribute
    let value = model.getValue(cell);
    if (!value) {
      // Workaround: 'value' must be an XML element
      value = mxUtils.createXmlDocument().createElement('object');
      value.setAttribute('label', '');
    }
    value.setAttribute(TYPE_ATTR, ftgpm_type);
    model.setValue(cell, value);    
  }

  function setEdgeStyle(edge, style_type) {
    // Update style
  
    // Workaround: don't overwrite connection points
    const oldstyle = parseStyle(model.getStyle(edge));
    const newstyle = new Map(parseStyle(getFromConfig(edgeStyles[style_type])));
    mapAssign(newstyle, oldstyle, [
      // retain these properties from oldstyle:
      "entryX", "entryY", "entryDx", "entryDy",
      "exitX",  "exitY",  "exitDx",  "exitDy"
    ]);
    model.setStyle(edge, unparseStyle(newstyle));
  }

  function isArtifact(type) {
    return type === "artifact";
  }
  function isFormalism(type) {
    return type === "formalism";
  }
  function isTransformation(type) {
    return type === "auto_transformation" || type === "transformation" || type === "comp_transformation";
  }
  function isActivityNode(type) {
    return type === "autom_activity" || type === "activity" || type === "comp_activity";
  }
  function isControlFlowPort(type) {
    return type === "ctrl_in" || type === "ctrl_out";
  }
  function isDataPort(type) {
    return type === "data_in" || type === "data_out";
  }
  function isInport(type) {
    return type === "data_in" || type === "ctrl_in";
  }
  function isOutport(type) {
    return type === "data_out" || type === "ctrl_out";
  }
  function isPort(type) {
    return ["data_in", "data_out", "ctrl_in", "ctrl_out"].includes(type);
  }
  function isControlFlowNode(type) {
    return isControlFlowPort(type) || type === "initial" || type === "final" || type === "fork_join";
  }
  function isDataFlowNode(type) {
    return isDataPort(type) || isArtifact(type) || isFormalism(type);
  }
  function isTraceEvent(type) {
    return type === "traceevent_begin" || type === "traceevent_end";
  }
  function isArtifactVersion(type) {
    return type === "artifact_version";
  }
  function getFlowType(type) {
    if (isDataFlowNode(type)) {
      return "data";
    }
    if (isControlFlowNode(type)) {
      return "ctrl";
    }
    // throw new Error("unknown flow type");
    console.log("unknown flow type");
  }

  // [ condition, flow-type ]
  const edgeTypeRules = [
    // PM control flow
    [ (src, tgt) => isControlFlowNode(src) && isControlFlowNode(tgt), "ctrl_flow" ],

    // PM data flow
    [ (src, tgt) => isDataFlowNode(src) && isDataFlowNode(tgt), "data_flow" ],

    // FTG data flow
    [ (src, tgt) => isTransformation(src) && isFormalism(tgt), "data_flow" ],
    [ (src, tgt) => isFormalism(src) && isTransformation(tgt), "data_flow" ],

    [ (src,tgt) => isActivityNode(src) && isTransformation(tgt), "typed_by" ],
    [ (src,tgt) => isArtifact(src) && isFormalism(tgt), "typed_by" ],

    [ (src,tgt) => isTraceEvent(src) && isArtifactVersion(tgt), "data_flow"],
    [ (src,tgt) => isArtifactVersion(src) && isTraceEvent(tgt), "data_flow"],
    [ (src,tgt) => isTraceEvent(src) && isTraceEvent(tgt), "ctrl_flow"],

    [ (src,tgt) => src === "traceevent_begin" && tgt === "ctrl_in", "typed_by"],
    [ (src,tgt) => src === "traceevent_end" && tgt === "ctrl_out", "typed_by"],
    [ (src,tgt) => isArtifactVersion(src) && isArtifact(tgt), "typed_by"],

    [ (src,tgt) => isArtifactVersion(src) && isArtifactVersion(tgt), "parent_version"],

    [ (src,tgt) => isArtifactVersion(src) && tgt === "storage", "ss_link"],
    [ (src,tgt) => isArtifactVersion(src) && tgt === "real_object", "ss_link"],
    [ (src,tgt) => src === "traceevent_begin" && tgt === "service", "ss_link"],

    [ (src,tgt) => src === "comment" || tgt === "comment", "comment_edge"],
  ];

  // Very specific: This function is used to determine the correct direction of an arrow automatically
  // An activity or artifact at the highest level (= a direct child of a layer) has level 0
  // Every time something is nested in an activity, the level increases by 1.
  // Ports are special: they can only be a child of an activity, but they have the same level as the activity.
  function getNestedLevel(cell) {
    function getLvlRecursive(cell) {
      if (!cell.parent) {
        throw Error("getNestedLevel called with parentless cell (i.e. cell is root, or is deleted)");
      }
      if (model.isLayer(cell)) {
        return 0;
        // throw Error("getNestedLevel called with layer cell");
      }
      if (cell.parent === model.isLayer(cell.parent)) {
        return 0;
      }
      else if (isActivityNode(cell.parent.getAttribute(TYPE_ATTR))) {
        return getLvlRecursive(cell.parent) + 1;
      }
      else {
        return getLvlRecursive(cell.parent);
      }
    }
    const baseLevel = getLvlRecursive(cell);
    if (isPort(cell.getAttribute(TYPE_ATTR))) {
      return baseLevel - 1;
    } else {
      return baseLevel;
    }
  }

  function isDirectionless(type) {
    return isArtifact(type) || type === "fork_join";
  }

  ui.editor.graph.addListener(mxEvent.CELL_CONNECTED, (_, eventObj) => {
    // Happens whenever an edge is (dis)connected.

    const edge = eventObj.properties.edge;
    const sourceCell = edge.source;
    const targetCell = edge.getTerminal();

    // This will change the edge style WITHIN the transaction of the edit operation.
    // The terminal-change and style-change will be one edit operation from point of view of undo manager.

    const sourceType = sourceCell ? sourceCell.getAttribute(TYPE_ATTR) : null;
    const targetType = targetCell ? targetCell.getAttribute(TYPE_ATTR) : null;

    // Update style if necessary
    for (const [cond, linkType] of edgeTypeRules) {
      if (cond(sourceType, targetType)) {
        setFtgpmType(edge, linkType);
        setEdgeStyle(edge, linkType);
      }
    }

    if (sourceCell && targetCell) {
      // Auto-correct edge direction in certain cases.
      // We call 'reverseEdge' if we have an illegal situation, but reversing the arrow gives a legal situation.
      const sourceLvl = getNestedLevel(sourceCell);
      const targetLvl = getNestedLevel(targetCell);

      // We have 2 flow types: ctrl and data. They remain segregated.
      if (getFlowType(sourceType) === getFlowType(targetType)) {
        if (sourceLvl === targetLvl) {
          // Equally nested ...
          if (isInport(sourceType) && isOutport(targetType)
           || isInport(sourceType) && isDirectionless(targetType)
           || isDirectionless(sourceType) && isOutport(targetType)) {
            reverseEdge(edge, sourceCell, targetCell);
          }
        }
        else if (sourceLvl === targetLvl + 1) {
          // Source of arrow is nested deeper ...
          if (isInport(sourceType) && isInport(targetType)
           || isDirectionless(sourceType) && isInport(targetType)) {
            // Should flow from shallowly nested input port to deeply nested input port:
            reverseEdge(edge, sourceCell, targetCell);
          }
        }
        else if (sourceLvl + 1 === targetLvl) {
          // Target of arrow is nested deeper ...
          if (isOutport(sourceType) && isOutport(targetType)
           || isOutport(sourceType) && isDirectionless(targetType)) {
            // Should flow from deeply nested output port to shallowly nested output port
            reverseEdge(edge, sourceCell, targetCell);
          }
        }
      }
    }
  });

  function outsideOffset(wOrH) {
    return 0;
  }
  function centerOffset(wOrH) {
    return -wOrH / 2;
  }
  function insideOffset(wOrH) {
    return -wOrH;
  }

  // Change this to position ports outside, inside or centered at the edge.
  const borderOffset = centerOffset;

  function rightOrBottomBorder(wOrH, parentWorH) {
    return parentWorH + borderOffset(wOrH);
  }
  function leftOrTopBorder(wOrH) {
    return -wOrH - borderOffset(wOrH);
  }

  function cellCenter(cellGeometry) {
    // Coordinates are relative to topleft corner of parent shape
    const {x,y, width, height} = cellGeometry;
    // Center of cell
    return {
      x: x + width/2,
      y: y + height/2,
    };
  }

  // Deprecated snapping algorithm
  function closestBorder(cellGeometry, parentGeometry) {
    // Cell center is relative to parent: (0,0) is topleft
    const c = cellCenter(cellGeometry);

    // We draw two imaginary diagonals through the parent shape, to determine whether the child shape is more close to the top, right, bottom or left side.
    //
    //           2
    //          /
    //    +----+
    //    |\ t/|
    //    |l\/ |
    //    | /\r|
    //    |/b \|
    //    +----+
    //          \
    //           1
    const slope = parentGeometry.height / parentGeometry.width;
    const above1 = c.y < slope * c.x;
    const above2 = c.y < parentGeometry.height - slope * c.x;

    if (above1)
      if (above2)
        return 't';
      else
        return 'r';
    else
      if (above2)
        return 'l';
      else
        return 'b';
  }

  // Improved snapping algorithm
  function closestBorder2(cellGeometry, parentGeometry) {
    // Cell center is relative to parent: (0,0) is topleft
    const c = cellCenter(cellGeometry);

    const slope = 1; // fixed slope, 45 degrees

    const above1 = c.y < slope * c.x;
    const above3 = c.y < (-slope) * c.x + parentGeometry.height ;

    const above4 = c.y < slope * (c.x - parentGeometry.width) + parentGeometry.height;
    const above2 = c.y < (-slope) * (c.x - parentGeometry.width);

    if (parentGeometry.width >= parentGeometry.height) {
      // Parent's width > height
      //
      //  1          2
      //   \        /
      //    +------+      --> x
      //    |\ t  /|
      //    |l\__/ |__ 5
      //    | /  \r|
      //    |/ b  \|
      //    +------+
      //   /        \
      //  3          4
      //    |
      //    V
      //    y

      const above5 = c.y < parentGeometry.height / 2;

      if (!above1 && above3) {
        return 'l';
      }
      if (!above2 && above4) {
        return 'r';
      }
      if (above1 && above2 && above5) {
        return 't';
      }
      if (!above3 && !above4 && !above5) {
        return 'b';
      }
    }

    else {
      // Parent's height > width
      //  1        2
      //   \      /
      //    +----+
      //    |\ t/|
      //    | \/ |
      //    |l | |
      //    |  | |
      //    | /\r|
      //    |/b \|
      //    +----+
      //   /   |  \
      //  3    5   4

      const leftOf5 = c.x < parentGeometry.width / 2;

      if (above1 && above2) {
        return 't';
      }
      if (!above3 && !above4) {
        return 'b';
      }
      if (above3 && !above1 && leftOf5) {
        return 'l';
      }
      if (above4 && !above2 && !leftOf5) {
        return 'r';
      }
    }
  }

  // Update port shape (so it points in the right direction), move port (and port label) to border of activity.
  function snapPortToBorder(cell, cellGeometry, parentGeometry, border) {
    const style = parseStyle(model.getStyle(cell));
    const type = cell.getAttribute(TYPE_ATTR);
    const inOrOut = isInport(type) ? "in" : "out";
    style.set('shape', encodedPortShapes[inOrOut][border]);

    switch (border) {
      case 't':
        cellGeometry.width = 35;
        cellGeometry.height = 20;
        cellGeometry.y = leftOrTopBorder(cellGeometry.height);

        style.set('align', 'right');
        style.set('verticalAlign', 'bottom');

        cellGeometry.offset = new mxPoint(
          -getFromConfig('portLabelOffsetPerpendicularToEdgeDirection'),
          -getFromConfig('portLabelOffsetEdgeDirection'));
        break;
      case 'r':
        cellGeometry.width = 20;
        cellGeometry.height = 35;
        cellGeometry.x = rightOrBottomBorder(cellGeometry.width, parentGeometry.width);

        style.set('align', 'left');
        style.set('verticalAlign', 'bottom');

        cellGeometry.offset = new mxPoint(
          getFromConfig('portLabelOffsetEdgeDirection'),
          -getFromConfig('portLabelOffsetPerpendicularToEdgeDirection'));
        break;
      case 'l':
        cellGeometry.width = 20;
        cellGeometry.height = 35;
        cellGeometry.x = leftOrTopBorder(cellGeometry.width);

        style.set('align', 'right');
        style.set('verticalAlign', 'top');

        cellGeometry.offset = new mxPoint(
          -getFromConfig('portLabelOffsetEdgeDirection'),
          getFromConfig('portLabelOffsetPerpendicularToEdgeDirection'));
        break;
      case 'b':
        cellGeometry.width = 35;
        cellGeometry.height = 20;
        cellGeometry.y = rightOrBottomBorder(cellGeometry.height, parentGeometry.height);

        style.set('align', 'left');
        style.set('verticalAlign', 'top');

        cellGeometry.offset = new mxPoint(
          getFromConfig('portLabelOffsetPerpendicularToEdgeDirection'),
          getFromConfig('portLabelOffsetEdgeDirection'));
        break;
    }

    model.setStyle(cell, unparseStyle(style));
  }

  function snapToBorderIfCellIsPortAndParentIsActivity(cell, geometry) {
    const type = cell.getAttribute(TYPE_ATTR);
    if (isPort(type)) {
      // Port was moved
      if (cell.parent) {
        const parentType = cell.parent.getAttribute(TYPE_ATTR);
        if (isActivityNode(parentType) || isTransformation(parentType)) {
          // Snap port to activity border
          const portSnappingAlgorithm = getFromConfig('newPortSnappingAlgorithm') ? closestBorder2 : closestBorder;
          // border will be one of: 'l', 'r', 't', 'b'
          const border = portSnappingAlgorithm(geometry, cell.parent.geometry);
          snapPortToBorder(cell, geometry, cell.parent.geometry, border);
        }
      }
    }    
  }

  ui.editor.graph.addListener(mxEvent.MOVE_CELLS, (_, eventObj) => {
    // High-level event: Happens when the user releases dragged shape(s)
    for (const cell of eventObj.properties.cells) {
      snapToBorderIfCellIsPortAndParentIsActivity(cell, cell.geometry);
    }
  });

  ui.editor.graph.addListener(mxEvent.RESIZE_CELLS, (_, eventObj) => {
    // High-level event: Happens when the user resized cell(s)
    for (let i=0; i<eventObj.properties.cells.length; i++) {
      const cell = eventObj.properties.cells[i];
      const type = cell.getAttribute(TYPE_ATTR);
      if (isActivityNode(type) || isTransformation(type)) {
        // Activity was resized...

        // In drawio, 'arcSize' of a rounded rectangle is relative to size of shape.
        // Retain apparent arc size of activity:
        const style = parseStyle(model.getStyle(cell));
        const newArcSize = 1000 / Math.sqrt(cell.geometry.width * cell.geometry.height);
        style.set("arcSize", newArcSize);
        model.setStyle(cell, unparseStyle(style));          

        if (cell.children) {
          // Need this for moving contained ports:
          const prevGeometry = eventObj.properties.previous[i];
          const scaleW = cell.geometry.width / prevGeometry.width;
          const scaleH = cell.geometry.height / prevGeometry.height;

          for (const child of cell.children) {
            const childType = child.getAttribute(TYPE_ATTR);
            // Keep activity icon in place
            if (childType === "activityIcon") {
              newIconGeometry = child.geometry.clone();
              newIconGeometry.x = cell.geometry.width - 28;
              newIconGeometry.y = 4;
              model.setGeometry(child, newIconGeometry);
            }
            else if (isPort(childType)) {
              // Move contained ports
              const border = closestBorder2(child.geometry, prevGeometry); // < what was the border in the old geometry?
              const newGeometry = child.geometry.clone();
              snapPortToBorder(child, newGeometry, cell.geometry, border);
              if (getFromConfig('movePortsOnResizeActivity')) {
                // Scale position
                switch (border) {
                  case 't':
                    newGeometry.x = child.geometry.x * scaleW;
                    break;
                  case 'l':
                    newGeometry.y = child.geometry.y * scaleH;
                    break;
                  case 'b':
                    newGeometry.x = child.geometry.x * scaleW;
                    break;
                  case 'r':
                    newGeometry.y = child.geometry.y * scaleH;
                    break;
                }
              }
              model.setGeometry(child, newGeometry);
            }
          }            
        }
      }
    }
  });

  // Hardcoded primitive shape libraries.
  // To alter, make changes to the library in the drawio webapp, then 'save' (this downloads the library as XML), and overwrite the file(s) in drawiolibs.
  Promise.all([
    fetch("myPlugins/shape_libs/common.xml"),
    fetch("myPlugins/shape_libs/ftg.xml"),
    fetch("myPlugins/shape_libs/pm.xml"),
    fetch("myPlugins/shape_libs/pt.xml"),
    fetch("myPlugins/shape_libs/ss.xml"),
  ])
  .then(all => Promise.all(all.map(response => response.text())))
  .then(([common, ftg, pm, pt, ss]) => {
     ui.loadLibrary(new LocalLibrary(ui, ss, "FTG+PM - S/S"));
     ui.loadLibrary(new LocalLibrary(ui, pt, "FTG+PM - PT"));
     ui.loadLibrary(new LocalLibrary(ui, pm, "FTG+PM - PM"));
     ui.loadLibrary(new LocalLibrary(ui, ftg, "FTG+PM - FTG"));
     ui.loadLibrary(new LocalLibrary(ui, common, "FTG+PM - Common"));
  });

  // For debugging only
  window.ui = ui;
});
