Draw.loadPlugin(function(ui) {

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

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

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

const dropVocabularyPrefix = str => str.substring(41);
const dropDescriptionPrefix = str => str.substring(30);
const dropArtifactPrefix = str => str.substring(41);
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#>

SELECT DISTINCT ?element ?type
WHERE {
  ${reverse ? `?element <${link_type}> <${iri}>` : `<${iri}> <${link_type}> ?element`}.
  ?element a ?type .
  ?type rdfs:subClassOf object_diagram:Object .
  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
}`,
};

// (Hardcoded) link types exposed to the user.
const typeToLinkType = new Map([
  ["object_diagram#Object", [
    {
      relation: "cs_as#parsedAs",
      description: (element, type) => `Is parsed as ${type}`,
    },
    {
      relation: "cs_as#renderedAs",
      description: (element, type) => `Is rendered as ${type}`,
    },
    // Outcommented, because it's not an interesting relation:
    {
      relation: "object_diagram#inModel",
      description: (element, type) => `Is part of model ${type}`,
    },
    {
      relation: "traceability_model#traceLinkTo",
      description: (element, type) => `Trace-link (outgoing) ${element}`,
    },
    {
      relation: "traceability_model#traceLinkFrom",
      description: (element, type) => `Trace-link (incoming) ${element}`,
    },
  ]],
  ["drawio#Model", [
    {
      relation: "drawio#hasRootCell",
      description: (element, type) => `Has root ${type}`,
    },
  ]],
  ["pm#Activity", [
    {
      relation: "pm#isTransformation",
      description: (element, type) => `Is typed by ${type}`,
    },
    {
      relation: "pm#hasPort",
      description: (element, type) => `Has port ${type}`,
    },
  ]],
  ["pm#element", [
    {
      relation: "processtraces#relatesTo",
      description: (element, type) => `Enacted by ${element}`,
      reverse: true,
    },
  ]],
  ["processtraces#element", [
    {
      relation: "processtraces#relatesTo",
      description: (element, type) => `Enactment of ${element}`,
    },
  ]],
  ["pm#Artifact", [
    {
      relation: "pm#hasType",
      description: (element, type) => `Is typed by ${type}`,
    },
  ]],
  ["ftg#Transformation", [
    {
      relation: "pm#occursAsActivity",
      description: (element, type) => `Occurs as activity ${element}`,
    }
  ]],
  ["ftg#Formalism", [
    {
      relation: "pm#occursAsArtifact",
      description: (element, type) => `Occurs as artifact ${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 executeSPARQL(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, "\n\nResult:", json.results.bindings);
    return json.results.bindings;
  })
}

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 executeSPARQL(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("ModelVerse", null, null);
    menu.addSeparator();
    oldFactoryMethod.apply(this, arguments);

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

    function addMenuItem(element, type, maxRecursion, parentMenuItem, description) {
      if (alreadyVisited.has(entry(element, type))) {
        return; // don't go in circles
      }
      const shortType = dropVocabularyPrefix(type);
      const menuItemText = description(dropArtifactPrefix(element), shortType);
      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 ((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);
      }
      alreadyVisited.add(entry(element, type));
      if (maxRecursion <= 0) {
        return;
      }
      addSubmenuItems(element, type, createdMenuItem, maxRecursion);
    }
    function addSubmenuItems(element, type, parentMenuItem, maxRecursion) {
      const queries = getQueries(type);
      for (const {relation, description, reverse} of queries) {
        executeSPARQL(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);
          }
        })
      }
    }

    // Populate ModelVerse submenu asynchronously:
    if (cell && cell.vertex) {
      executeSPARQL(QUERIES.getCellStuff(cell.id))
      .then(results => {
        for (const {element, type} of results) {
          alreadyVisited.add(entry(element.value, type.value));
          addSubmenuItems(element.value, type.value, modelverseMenu, 4);
        }
      })
    }
  }


  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 rebuildOML() {
    // only perform one rebuild at a time -> queue rebuilds.
    rebuildOMLPromise = rebuildOMLPromise.then(() => {
      omlStatusDiv.style.color = null;
      omlStatusDiv.innerText = "Rebuilding OML...";
      return fetch(BACKEND + "/rebuild_oml", {
        method: "PUT",
      }).then(res => {
        res.text().then(text => {
          if (text.length > 0) {
            errWndDiv.innerText = text;
          }
          else {
            errWndDiv.innerText = "No error."
          }
        });
        if (res.status >= 500) {
          omlStatusDiv.innerHTML = "✗ Failed to build OML ";
          omlStatusDiv.style.color = "red";
          omlStatusDiv.appendChild(getErrDetailsAnchor());
        } else {
          omlStatusDiv.innerText = "✓ Built OML";
          omlStatusDiv.style.color = "green";

          // WORKAROUND:
          // The 'superclasses'-mapping gets populated when the plugin is loaded.
          // If the plugin was loaded while Fuseki was empty, superclasses will be empty as well.
          // After a successful OML build, Fuseki won't be empty and we can load the superclass relations.
          // A better way to handle this, would be to guarantee that even if the backend is empty (no models, and hence, no OML descriptions), the vocabularies are always built, and the resulting triples loaded into Fuseki.
          // For now, however, the following is workable:
          if (superclasses.size === 0) {
            loadSuperclasses();
          }
        }
      });
    });
  }

  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();
        rebuildOML();
      }
      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) {
    executeSPARQL(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 omlForceButton = mxUtils.button("Force rebuild", function() {
    rebuildOML();
  });
    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.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);
            extraStuff(modelName)
            .then(stuff => stuff.forEach(el => div.appendChild(el)));
            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));
  }

  const extraButtonsIfPM = modelName => {
    if (modelName.endsWith(":pm")) {
      const enactButton = mxUtils.button("Start New", ()=>{});
      const pmModel = addArtifactPrefix(modelName.substr(0, modelName.length-3)+"_pm#model");
      const urlEncoded = encodeURIComponent(pmModel);
      return Promise.all([
        fetch(WEE+"/traces/active/"+urlEncoded)
        .then(response => response.json())
        .then(enactments => {
          return enactments.map((enactment, i) => mxUtils.button(`Continue ${i}`, ()=>{}))
        }),
        fetch(WEE+"/traces/finished/"+urlEncoded)
        .then(response => response.json())
        .then(enactments => {
          return enactments.map((enactment, i) => mxUtils.button(`Finished ${i}`, ()=>{}))
        }),
      ])
      .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([]);
  };
  const noExtraButtons = () => Promise.resolve([]);

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

  // Load a model and add it as a new page to the editor
  function loadPage(pageName) {
    return fetch(BACKEND + "/files/drawio/" + pageName)
    .then(res => res.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(page => page.node.getAttribute("name") != pageName);
      ui.pages.push(page);
      ui.currentPage = page;
      ui.editor.setGraphXml(node);
    });
  }

  // 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.
})