Draw.loadPlugin(function(ui) {

const WEE = "http://localhost:8081";

const BACKEND = "http://localhost:5000";
const EXPECTED_BACKEND_VERSION = 6; // expected backend version

const SPARQL_SERVER   = "http://localhost:3030"
const SPARQL_ENDPOINT = "/SystemDesignOntology2Layers/"

const dropVocabularyPrefix = str => str.substring(41);
const dropDescriptionPrefix = str => str.substring(30);
const dropArtifactPrefix = str => str.substring(41);
const addFormalismsPrefix = str => "http://ua.be/sdo2l/vocabulary/formalisms/" + str;
const addArtifactPrefix = str => "http://ua.be/sdo2l/description/artifacts/" + str;

const QUERIES = {
  // Query that navigates the given link from the given source element, and returns the IRI and most-concrete-type of the target element.
  // If `reverse` is true, then incoming links are returned instead.
  getOutgoingLink: (iri, link_type, reverse=false) => `\
PREFIX object_diagram: <http://ua.be/sdo2l/vocabulary/formalisms/object_diagram#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX pt: <http://ua.be/sdo2l/vocabulary/formalisms/processtraces#>

SELECT DISTINCT ?element ?type
WHERE {
  ${reverse ? `?element <${link_type}> <${iri}>` : `<${iri}> <${link_type}> ?element`}.
  ?element a ?type .
  {
    ?type rdfs:subClassOf object_diagram:Object .
  } UNION {
    ?type rdfs:subClassOf pt:Event .
  } UNION {
    ?type rdfs:subClassOf pt:Artifact .
  }
  FILTER(NOT EXISTS {
    ?more_concrete_type rdfs:subClassOf ?type .
    ?element a ?more_concrete_type .
    FILTER(?more_concrete_type != ?type)
  })
  FILTER(NOT EXISTS {
    ?more_concrete_link_type rdfs:subPropertyOf <${link_type}> .
    ${reverse ? `?element ?more_concrete_link_type <${iri}>` : `<${iri}> ?more_concrete_link_type ?element`} . 
    FILTER(?more_concrete_link_type != <${link_type}>)
  })
}`,

  // Query that gets the IRI and most-concrete-type of a given drawio cell.
  getCellStuff: (cellId) => `\
PREFIX drawio: <http://ua.be/sdo2l/vocabulary/formalisms/drawio#>
PREFIX object_diagram: <http://ua.be/sdo2l/vocabulary/formalisms/object_diagram#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

SELECT DISTINCT ?element ?type
WHERE {
  ?element drawio:hasDrawioId "${cellId}" .
  ?element a ?type .
  ?type rdfs:subClassOf object_diagram:Object .
  NOT EXISTS {
    ?more_concrete_type rdfs:subClassOf ?type .
    ?element a ?more_concrete_type .
    FILTER(?more_concrete_type != ?type)
  }
}`,

  // Query that, for a given cell IRI, gets the name of the diagram containing the cell, and the ID of the cell.
  getDiagramAndCellId: cellIri => `\
PREFIX drawio: <http://ua.be/sdo2l/vocabulary/formalisms/drawio#>
PREFIX object_diagram: <http://ua.be/sdo2l/vocabulary/formalisms/object_diagram#>

SELECT DISTINCT ?diagramName ?cellId
WHERE {
  <${cellIri}> drawio:hasDrawioId ?cellId .
  <${cellIri}> object_diagram:inModel ?model .
  ?model object_diagram:hasName ?diagramName .
}`,

  // Query that gets ALL subclass relations.
  getSubClassRelations: `
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>

SELECT DISTINCT ?subclass ?superclass
WHERE {
  ?subclass rdfs:subClassOf ?superclass
}`,

  getAllGraphs: `
SELECT ?g (COUNT(*) as ?count) {GRAPH ?g {?s ?p ?o}} GROUP BY ?g
`,

  getArtifactFilename: artifactIri => `\
PREFIX pt: <http://ua.be/sdo2l/vocabulary/formalisms/processtraces#>
PREFIX pm: <http://ua.be/sdo2l/vocabulary/formalisms/pm#>
PREFIX base: <http://ua.be/sdo2l/vocabulary/base/base#>

SELECT DISTINCT ?filename ?formalismName
WHERE {
  <${artifactIri}> pt:hasLocation ?filename .
  <${artifactIri}> pt:relatesTo ?pmArtifact .
  ?pmArtifact pm:hasType ?ftgFormalism .
  ?ftgFormalism base:hasGUID ?formalismName .
}`,

  getModels: `\
PREFIX object_diagram: <http://ua.be/sdo2l/vocabulary/formalisms/object_diagram#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX cs_as: <http://ua.be/sdo2l/vocabulary/formalisms/cs_as#>

SELECT DISTINCT ?model ?type
WHERE {
  ?model a object_diagram:Model .
  ?model a ?type .
  ?type rdfs:subClassOf object_diagram:Model .
  FILTER(NOT EXISTS {
    ?model a cs_as:CorrespondenceModel .
  })
  FILTER(NOT EXISTS {
    ?more_concrete_type rdfs:subClassOf ?type .
    ?model a ?more_concrete_type .
    FILTER(?more_concrete_type != ?type)
  })
}
`,

  getProperty: (iri, propertyIri) => `\
SELECT DISTINCT ?value
WHERE {
  <${iri}> <${propertyIri}> ?value .
}
`,

  insertTraceLink: (fromIri, toIri) => `\
PREFIX trace: <http://ua.be/sdo2l/vocabulary/formalisms/traceability_model#>
INSERT DATA {
  GRAPH <http://ua.be/sdo2l/description/traces> {
    <${fromIri}> trace:traceLinkTo   <${toIri}> .
    <${fromIri}> trace:traceLinkFrom <${toIri}> .
  }
}
`,
};



// When rebuilding OML, all graphs will be deleted from Fuseki, except for the following (which are not sourced from OML, but inserted directly as RDF triples into Fuseki)
GRAPHS_NOT_TO_DELETE = [
  "http://ua.be/sdo2l/description/traces",
];

const typeToDescription = new Map([
  ["ftg#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `ftg "${modelName}"`)],
  ["xopp#Text", async (element, type, getProperty) => getProperty("xopp#hasText").then(text => `text "${text}"`)],
  ["xopp#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `xournal++ model "${modelName}"`)],
  ["drawio#Cell", async (element, type, getProperty) => getProperty("drawio#hasDrawioId").then(id => `cell "${id}"`)],
  ["drawio#Vertex", async (element, type, getProperty) => getProperty("drawio#hasDrawioId").then(id => `vertex "${id}"`)],
  ["drawio#Edge", async (element, type, getProperty) => getProperty("drawio#hasDrawioId").then(id => `edge "${id}"`)],
  ["drawio#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `drawio model "${modelName}"`)],
  ["pm#Model", async (element, type, getProperty) => getProperty("object_diagram#hasName").then(modelName => `process model "${modelName}"`)],
  ["pm#Activity", async (element, type, getProperty) => element],
  ["pm#Artifact", async (element, type, getProperty) => element],
  ["pm#CtrlInputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `ctrl inport "${portname}"`)],
  ["pm#CtrlOutputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `ctrl outport "${portname}"`)],
  ["pm#DataInputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `data inport "${portname}"`)],
  ["pm#DataOutputPort", async (element, type, getProperty) => getProperty("pm#hasName").then(portname => `data outport "${portname}"`)],
])

// (Hardcoded) link types exposed to the user.
const typeToLinkType = new Map([
  ["object_diagram#Object", [
    {
      relation: "cs_as#parsedAs",
      description: `Is parsed as`,
    },
    {
      relation: "cs_as#renderedAs",
      description: `Is rendered as`,
    },
    // Outcommented, because it's not an interesting relation:
    {
      relation: "object_diagram#inModel",
      description: `Is part of`,
    },
    {
      relation: "traceability_model#traceLinkTo",
      description: `Trace-link (outgoing)`,
    },
    {
      relation: "traceability_model#traceLinkFrom",
      description: `Trace-link (incoming)`,
    },
  ]],
  ["drawio#Model", [
    {
      relation: "drawio#hasRootCell",
      description: `Has root`,
    },
  ]],
  // Outcommented, blows up:
  // ["drawio#Cell", [
  //   {
  //     relation: "drawio#hasChild",
  //     description: `Has child`,
  //   },
  // ]],
  ["pm#Activity", [
    {
      relation: "pm#isTransformation",
      description: `Is typed by`,
    },
    {
      relation: "pm#hasPort",
      description: `Has`,
    },
  ]],
  ["pm#element", [
    {
      relation: "processtraces#relatesTo",
      description: `Enacted by`,
      reverse: true,
    },
  ]],
  ["processtraces#element", [
    {
      relation: "processtraces#relatesTo",
      description: `Enactment of`,
    },
    // pt-element is not a sub-type of 'Object', so we must repeat this:
    {
      relation: "cs_as#renderedAs",
      description: `Is rendered as`,
    },
  ]],
  ["pm#Artifact", [
    {
      relation: "pm#hasType",
      description: `Is typed by`,
    },
  ]],
  ["ftg#Transformation", [
    {
      relation: "pm#occursAsActivity",
      description: `Occurs as`,
    }
  ]],
  ["ftg#Formalism", [
    {
      relation: "pm#occursAsArtifact",
      description: `Occurs as`,
    },
  ]],
  ["xopp#Model", [
    {
      relation: "xopp#hasPage",
      description: `Has page`,
    },
  ]],
  ["xopp#Page", [
    {
      relation: "xopp#hasLayer",
      description: `Has layer`,
    },
  ]],  
  ["xopp#Layer", [
    {
      relation: "xopp#hasElement",
      description: `Has element`,
    },
  ]],
]);

// mapping from class-IRI to array of superclass-IRIs
const superclasses = new Map(); // map is populated as soon as the plugin is loaded

function getQueries(type) {
  const result = [].concat(
    typeToLinkType.get(dropVocabularyPrefix(type)) || [],
    ... (superclasses.get(type) || []).map(supertype => typeToLinkType.get(dropVocabularyPrefix(supertype)) || []));
  console.log("getQueries,type=",type,"superclasses=",superclasses.get(type),"result=",result);
  return result;
}

function querySPARQL(query) {
  const body = new URLSearchParams();
  body.append("query", query);
  return fetch(SPARQL_SERVER+SPARQL_ENDPOINT, {
    headers: new Headers({"Content-Type": "application/x-www-form-urlencoded"}),
    method: "POST",
    body,
  })
  .then(res => res.json())
  .then(json => {
    console.log("Query:\n"+query+"\Result:",json.results.bindings);
    return json.results.bindings;
  })  
}

function updateSPARQL(update) {
  const body = new URLSearchParams();
  body.append("update", update);
  return fetch(SPARQL_SERVER+SPARQL_ENDPOINT, {
    headers: new Headers({"Content-Type": "application/x-www-form-urlencoded"}),
    method: "POST",
    body,
  })
  .then(res => {
    console.log("Update:\n"+update+"\Result:",res);
  })
}


const defaultSettings = {
  dialogPosX: 100,
  dialogPosY: 100,
  dialogWidth: 240,
  dialogHeight: 400,
};

fetch(BACKEND+"/version")
.then(response => response.json())
.catch(() => 0) // parsing failed - probably backend doesn't even have a version
.then(version => {

  if (version !== EXPECTED_BACKEND_VERSION) {
    alert("Incorrect DTDesign Python backend version.\nExpected: " + EXPECTED_BACKEND_VERSION + ", got: " + version + ".\nRefusing to load plugin. Please upgrade :)");
    return;
  }
  else {
    console.log("Backend version is ", version);
    console.log("All good.")
  }

  // Loads all 'rdfs:subClassOf' relations and stores them in a mapping.
  function loadSuperclasses() {
    return querySPARQL(QUERIES.getSubClassRelations)
    .then(results => {
      for (const {subclass, superclass} of results) {
        const superclasslist = superclasses.get(subclass.value) || (() => {
          const list = [];
          superclasses.set(subclass.value, list);
          return list;
        })();
        superclasslist.push(superclass.value);
      }
    });    
  }

  loadSuperclasses();

  // Override context menu when right-clicking somewhere in the diagram:
  const oldFactoryMethod = ui.editor.graph.popupMenuHandler.factoryMethod;
  ui.editor.graph.popupMenuHandler.factoryMethod = function(menu, cell, evt) {
    const modelverseMenu = menu.addItem("Knowledge Graph...", null, null);
    const createLinkMenu = menu.addItem("Create traceability link to...", null, null);
    menu.addSeparator();
    oldFactoryMethod.apply(this, arguments);

    const entry = (element, type) => element+':'+type;

    function addMenuItem(element, type, maxRecursion, parentMenuItem, description, alreadyVisited, onClick) {
      if (alreadyVisited.has(entry(element, type))) {
        return; // don't go in circles
      }
      const shortType = dropVocabularyPrefix(type);
      const itemDescriptionGen = typeToDescription.get(shortType) || (async () => shortType);
      itemDescriptionGen(dropArtifactPrefix(element), shortType, property => {
        return querySPARQL(QUERIES.getProperty(element, addFormalismsPrefix(property)))
        .then(results => {
          if (results.length > 0) return results[0].value.value;
          else return "";
        });
      })
      .then(itemDescription => {
        const menuItemText = description + ' ' + itemDescription;
        let createdMenuItem;
        // If the menu item is a (subtype of a) drawio cell, then clicking on it will take us to that cell in drawio.
        if (onClick) {
          createdMenuItem = menu.addItem(menuItemText, null, () => onClick(element), parentMenuItem);
        }
        else if ((superclasses.get(type) || []).some(t => t.endsWith("drawio#Cell"))) {
          createdMenuItem = menu.addItem(menuItemText, null, () => goto(element), parentMenuItem);
        }
        else {
          createdMenuItem = menu.addItem(menuItemText, null, null, parentMenuItem);
        }
        if ((superclasses.get(type) || []).some(t => t.endsWith("processtraces#Artifact"))) {
          querySPARQL(QUERIES.getArtifactFilename(element))
          .then(results => {
            for (const {filename, formalismName} of results) {
              menu.addItem(`File "${filename.value}"`, null, () => (createDownloadHandler(formalismName.value))(filename.value), createdMenuItem);
            }
          })
        }
        alreadyVisited.add(entry(element, type));
        if (maxRecursion <= 0) {
          return;
        }
        addSubmenuItems(element, type, createdMenuItem, maxRecursion, alreadyVisited, onClick);        
      })
    }
    function addSubmenuItems(element, type, parentMenuItem, maxRecursion, alreadyVisited, onClick) {
      const queries = getQueries(type);
      for (const {relation, description, reverse} of queries) {
        querySPARQL(QUERIES.getOutgoingLink(element, "http://ua.be/sdo2l/vocabulary/formalisms/"+relation, reverse))
        .then(results => {
          for (const {element, type} of results) {
            addMenuItem(element.value, type.value, maxRecursion-1, parentMenuItem, description, alreadyVisited, onClick);
          }
        })
      }
    }

    // Populate ModelVerse submenu asynchronously:
    if (cell && cell.vertex) {
      querySPARQL(QUERIES.getCellStuff(cell.id))
      .then(results => {
        for (const {element, type} of results) {
          const alreadyVisited = new Set([[element.value, type.value]]);
          addSubmenuItems(element.value, type.value, modelverseMenu, 3, alreadyVisited);

          querySPARQL(QUERIES.getModels)
          .then(results => {
            const alreadyVisited2 = new Set([[element.value, type.value]]);
            for (const {model, type} of results) {
              addMenuItem(model.value, type.value, 3, createLinkMenu, "Model",
                alreadyVisited2,
                targetIri => updateSPARQL(QUERIES.insertTraceLink(element.value, targetIri)));
            }
          });
        }
      })
    }
  }


  function getSetting(prop) {
    const result = localStorage.getItem("dtdesign-"+prop);
    if (result !== null) {
      return JSON.parse(result);
    }
    return defaultSettings[prop];
  }
  function setSetting(prop, value) {
    localStorage.setItem("dtdesign-"+prop, JSON.stringify(value));
  }

  const errWndDiv = document.createElement('div');
    // wndDiv.style.color = "red";
    errWndDiv.style.overflow = "auto";
    errWndDiv.style.height = "100%";
    errWndDiv.style.padding = '12px 14px 8px 14px';
  const errWnd = new mxWindow("Error Details",
    errWndDiv, 300, 300, 300, 360, true, true);
    errWnd.destroyOnClose = false;
    errWnd.setMaximizable(false);
    errWnd.setResizable(true);
    errWnd.setClosable(true);

  function getErrDetailsAnchor() {
    const anchor = document.createElement('a');
      anchor.innerText = "Details";
      anchor.href = "#";
      anchor.onclick = function() {
        errWnd.show();
      };
    return anchor;
  }

  let rebuildOMLPromise = Promise.resolve();

  function rejectIfStatusAbove400(httpResponse) {
    if (httpResponse.status >= 400) {
      return httpResponse.text().then(text => {
        return Promise.reject(text);
      });
    }
    return Promise.resolve();
  }

  function rebuildOMLForced() {
    return querySPARQL(QUERIES.getAllGraphs)
    .then(results => {
      const graphsToDelete = results
        .map(({g,count}) => g.value)
        .filter(graphIri => !GRAPHS_NOT_TO_DELETE.includes(graphIri));
      // Delete all graphs first
      return Promise.all(graphsToDelete.map(graphToDelete =>
        fetch(SPARQL_SERVER+SPARQL_ENDPOINT+"?graph="+graphToDelete, {
          method: "DELETE",
        })
        .then(rejectIfStatusAbove400)
      ))
      .then(() => {
        // Build OML and load the resulting .owl files:
        return fetch(BACKEND+"/owl_load", {
          method: "PUT",
        })
        .then(rejectIfStatusAbove400)
      });
    });
  }

  function rebuildOMLIncremental() {
    // Backend takes care of all the heavy work:
    return fetch(BACKEND+"/owl_reload_incremental", { method: "PUT", })
    .then(rejectIfStatusAbove400);
  }

  function showRebuildOMLResult(rebuildAsyncCallback) {
    // only perform one rebuild at a time -> queue rebuilds.
    rebuildOMLPromise = rebuildOMLPromise.then(() => {
      omlStatusDiv.style.color = null;
      omlStatusDiv.innerText = "Rebuilding OML...";
      return rebuildAsyncCallback()
      .then(() => {
        omlStatusDiv.innerText = "✓ Built OML";
        omlStatusDiv.style.color = "green";
      })
      .catch(errText => {
        errWndDiv.innerText = errText;
        omlStatusDiv.innerHTML = "✗ Failed to build OML ";
        omlStatusDiv.style.color = "red";
        omlStatusDiv.appendChild(getErrDetailsAnchor());        
      });
    });
  }

  function uploadResponseHandler(statusDiv, refreshListCallback) {
    statusDiv.innerHTML = "Saving ...";
    statusDiv.style.color = null;
    omlStatusDiv.innerHTML = "";

    return res => {
      if (res.status >= 200 && res.status < 300) {
        errWndDiv.innerText = "No error.";
        res.json().then(parsedAs => {
          statusDiv.innerHTML = "✓ Generated OML (" + parsedAs.join(", ") + ")";
          statusDiv.style.color = "green";
        });
        refreshListCallback();
        showRebuildOMLResult(rebuildOMLIncremental);
      }
      else {
        statusDiv.innerHTML = "✗ Failed to save/parse. ";
        statusDiv.appendChild(getErrDetailsAnchor());
        res.text().then(text => {
          errWndDiv.innerText = text;
        });
        statusDiv.style.color = "red";
      }
    };
  }

  let highlight;

  // Given a cell IRI, open the diagram that contains the cell
  function goto(cellIri) {
    querySPARQL(QUERIES.getDiagramAndCellId(cellIri))
    .then(([{diagramName, cellId}]) => {
      // drop the "_drawio" at the end:
      const actualDiagramName = diagramName.value.substring(0, diagramName.value.length-7);
      loadPage(actualDiagramName)
      .then(() => {
        if (highlight) {
          highlight.destroy();
        }
        const [cell] = ui.editor.graph.getCellsById([cellId.value]);
        highlight = new mxCellHighlight(ui.editor.graph, "#eb34e8", 6);
        highlight.highlight(ui.editor.graph.view.getState(cell));        
      })
    })
  }

  const wndDiv = document.createElement('div');
    wndDiv.style.userSelect = 'none';
    wndDiv.style.height = '100%';
    wndDiv.style.color = "rgb(112, 112, 112)";
    wndDiv.style.overflow = 'auto';
    wndDiv.classList.add("geFormatContainer");

  const omlDiv = document.createElement('div');
    omlDiv.classList.add('geFormatSection')
    omlDiv.style.padding = '12px 14px 8px 14px';
    wndDiv.appendChild(omlDiv);

  const omlLabel = document.createElement('div');
    omlLabel.innerText = "OML Status";
    omlLabel.style.fontWeight = 'bold';
    omlDiv.appendChild(omlLabel);

  const omlStatusDiv = document.createElement('div');
    omlStatusDiv.innerText = "Ready";
    omlDiv.appendChild(omlStatusDiv);

  const omlIncButton = mxUtils.button("Incremental rebuild (faster)", function() {
    showRebuildOMLResult(rebuildOMLIncremental);
  });
    omlDiv.appendChild(omlIncButton);
  const omlForceButton = mxUtils.button("Forced rebuild (slow)", function() {
    showRebuildOMLResult(rebuildOMLForced);
  });
    omlDiv.appendChild(omlForceButton);

  function createSavePageDiv(refreshModelsCallback) {
    const savePageDiv = document.createElement('div');
    const saveButton = mxUtils.button("Save Current Page", function() {
      const responseHandler = uploadResponseHandler(saveStatusDiv, refreshModelsCallback);
      const headers = new Headers({
        "Content-Type": "application/xml",
      });
      const serializer = new XMLSerializer();
      const xmlnode = ui.getXmlFileData(
        null,  // ignore selection
        true,  // only current page
        true); // uncompressed
      const diagram = xmlnode.children[0]
      const diagramName = diagram.getAttribute("name");
      const xml = serializer.serializeToString(diagram);
      return fetch(BACKEND + "/files/drawio/"+diagramName, {
        method: "PUT",
        headers,
        body: xml,
      })
      .then(responseHandler);
    });
      saveButton.style.width = "100%";
      saveButton.style.marginTop = "4px";
      saveButton.style.marginBottom = "2px";
      savePageDiv.appendChild(saveButton);
    const saveStatusDiv = document.createElement('div');
      savePageDiv.appendChild(saveStatusDiv);
    return savePageDiv;
  }
  function createUploadDiv(modelType, mimeType) {
    return function(refreshModelsCallback) {
      const uploadDiv = document.createElement('div');
      const uploadLabel = document.createElement('label');
        uploadLabel.innerText = "Upload "+modelType+":";
        uploadLabel.for = "uploadButton";
        uploadDiv.appendChild(uploadLabel);
      const uploadButton = document.createElement('input');
        uploadButton.type = "file";
        uploadButton.id = "uploadButton";
        uploadButton.style.width = "100%";
        uploadButton.style.marginTop = "4px";
        uploadButton.style.marginBottom = "2px";
        uploadButton.accept = mimeType;
        // uploadButton.multiple = true;
        uploadButton.onchange = e => {
          const responseHandler = uploadResponseHandler(statusDiv, refreshModelsCallback);
          Array.from(e.target.files).map(file => {
            fetch(BACKEND+"/files/"+modelType+"/"+file.name, {
              method: "PUT",
              body: file,
            })
            .then(responseHandler);
          });
        };
        uploadDiv.appendChild(uploadButton);
      const statusDiv = document.createElement('div');
        uploadDiv.appendChild(statusDiv);
      return uploadDiv;
    };
  }
  function createDownloadHandler(modelType) {
    return modelName => {
      const a = document.createElement('a');
      document.body.appendChild(a);
      a.setAttribute('href', BACKEND + "/files/" + modelType + "/" + modelName);
      a.setAttribute('download', modelName);
      a.setAttribute('target', "_blank"); // for browsers that don't support the 'download' attribute, tell them to open the link in a new tab
      a.click();
      document.body.removeChild(a);
    };
  }
  function createModelList(modelType, createUploadDiv, downloadButtonLabel, onDownloadClick, extraStuff) {
    const containerDiv = document.createElement('div');
      containerDiv.classList.add('geFormatSection')
      containerDiv.style.padding = '12px 14px 8px 14px';
      wndDiv.appendChild(containerDiv);
    const labelDiv = document.createElement('div');
      labelDiv.innerText = "Models — " + modelType;
      labelDiv.style.fontWeight = 'bold';
      containerDiv.appendChild(labelDiv);
    const modelListDiv = document.createElement('div');
      containerDiv.appendChild(modelListDiv);

    // Refreshes the list of models shown in the ModelVerse window
    function refreshList() {
      fetch(BACKEND + "/files/" + modelType, {
        method: "GET",
      })
      .then(res => res.json())
      .then(models => {
        if (models.length > 0) {
          modelListDiv.replaceChildren(...models.sort().map(modelName => {
            const div = document.createElement('div');
              div.style.padding = '3px 0px';
            const loadButton = mxUtils.button(downloadButtonLabel, () => onDownloadClick(modelName));
              loadButton.style.marginLeft = "12px";
            div.appendChild(document.createTextNode(modelName));
            div.appendChild(loadButton);
            const extraStuffWrapper = document.createElement('div');
            div.appendChild(extraStuffWrapper);
            function refreshExtraStuff() {
              extraStuff(modelName)
              .then(extraDiv =>  {
                if (extraDiv) {
                  extraStuffWrapper.replaceChildren(extraDiv);
                  extraDiv.appendChild(mxUtils.button("⟳", refreshExtraStuff));
                }
              });
            }
            refreshExtraStuff();
            return div;
          }));
        }
        else {
          const div = document.createElement('div');
              div.style.padding = '3px 0px';
          div.appendChild(document.createTextNode("No "+modelType+" models."));
          modelListDiv.replaceChildren(div);
        }
      });
    }
    refreshList();

    containerDiv.appendChild(createUploadDiv(refreshList));

    return refreshList;
  }

  function createPTrenderCallback(startTraceIri) {
    return () => {
      return fetch(BACKEND+"/render_pt/"+encodeURIComponent(startTraceIri))
      .then(treatResponseAsPageXml)
      .then(() => {
        refreshDrawioModelList();
        showRebuildOMLResult(rebuildOMLIncremental);
      });
    }
  }

  const extraPMstuff = modelName => {
    if (modelName.endsWith(":pm")) {
      const pmModel = addArtifactPrefix(modelName.substr(0, modelName.length-3)+"_pm#model");
      const urlEncoded = encodeURIComponent(pmModel);
      const enactButton = mxUtils.button("Start New...", ()=>{
        // open WEE in new tab:
        window.open(WEE+"/gettraces?iri="+urlEncoded, "_blank");
      });
      const shortTraceName = traceIri => dropArtifactPrefix(traceIri).split('#').findLast(str => str.length > 0);
      return Promise.all([
        fetch(WEE+"/traces/active/"+urlEncoded)
        .then(response => response.json())
        .then(enactments => {
          return enactments.map((enactment, i) => mxUtils.button(`Open "${shortTraceName(enactment.iri)}" (ongoing)`, createPTrenderCallback(enactment.iri)))
        }),
        fetch(WEE+"/traces/finished/"+urlEncoded)
        .then(response => response.json())
        .then(enactments => {
          return enactments.map((enactment, i) => mxUtils.button(`Open "${shortTraceName(enactment.iri)}" (finished)`, createPTrenderCallback(enactment.iri)))
        }),
      ])
      .then(([ongoing, finished]) => {
        const pmDiv = document.createElement('div');
        pmDiv.style.marginLeft = '8px';
        pmDiv.style.borderWidth = '1px';
        pmDiv.style.borderStyle = 'solid';
        pmDiv.appendChild(document.createTextNode("PM Enactment: "));
        pmDiv.appendChild(enactButton);
        if (ongoing.length > 0) {
          ongoing.forEach(o => pmDiv.appendChild(o));
        }
        if (finished.length > 0) {
          finished.forEach(f => pmDiv.appendChild(f));
        }
        return pmDiv;
      });
    }
    return Promise.resolve(null);
  };
  const noExtraStuff = () => Promise.resolve(null);

  const refreshDrawioModelList = createModelList("drawio", createSavePageDiv, "Open", modelName => loadPage(modelName), extraPMstuff);
  createModelList("xopp", createUploadDiv("xopp", "application/x-xopp,.xopp"), "Download", createDownloadHandler("xopp"), noExtraStuff);
  createModelList("csv", createUploadDiv("csv", "text/csv,.csv"), "Download", createDownloadHandler("csv"), noExtraStuff);
  createModelList("file", createUploadDiv("file", ""), "Download", createDownloadHandler("file"), noExtraStuff);

  // Load a model and add it as a new page to the editor
  function treatResponseAsPageXml(response) {
    return response.text()
    .then(xmltext => {
      // console.log(xmltext);
      const parser = new DOMParser();
      const doc = parser.parseFromString(xmltext, "application/xml");
      const node = doc.documentElement;
      const page = new DiagramPage(node);
      // if page with same name already exists, erase it:
      ui.pages = ui.pages.filter(pg => pg.node.getAttribute("name") != page.node.getAttribute("name"));
      ui.pages.push(page);
      ui.currentPage = page;
      ui.editor.setGraphXml(node);
    });
  }

  function loadPage(pageName) {
    return fetch(BACKEND + "/files/drawio/" + pageName)
    .then(treatResponseAsPageXml);
  }

  // make sure the plugin popup window is always within the bounds of the browser window:
  const dialogPosX = x => Math.max(0, Math.min(x, window.innerWidth - getSetting("dialogWidth")));
  const dialogPosY = y => Math.max(0, Math.min(y, window.innerHeight - getSetting("dialogHeight")));

  const wnd = new mxWindow("ModelVerse", wndDiv,
    dialogPosX(getSetting("dialogPosX")),
    dialogPosY(getSetting("dialogPosY")),
    getSetting("dialogWidth"),
    getSetting("dialogHeight"),
    true, true);
    wnd.destroyOnClose = false;
    wnd.setMaximizable(false);
    wnd.setResizable(true);
    wnd.setClosable(false);
    // remember window geometry in localstorage:
    wnd.addListener('resize', function(wnd) {
      // couldn't find a better way to get the new window size but the following:
      const parsepx = px => parseInt(px.substring(0, px.length-2));
      setSetting("dialogWidth", parsepx(wnd.div.style.width));
      setSetting("dialogHeight", parsepx(wnd.div.style.height));
    })
    wnd.addListener('move', function() {
      setSetting("dialogPosX", wnd.getX());
      setSetting("dialogPosY", wnd.getY());
    })

  wnd.show();

  window.onresize = event => {
    // make sure popup window remains within browser window bounds:
    const newX = dialogPosX(getSetting("dialogPosX"));
    const newY = dialogPosY(getSetting("dialogPosY"));
    wnd.div.style.left = newX + "px";
    wnd.div.style.top = newY + "px";
    // setSetting("dialogPosX", newX);
    // setSetting("dialogPosY", newY);
  }

}) // end of promise that checks the backend version.
})