"use strict";

// Functions to load and parse .map/.gz files
async function quickLoad() {
  const blob = await ldb.get("lastMap");
  if (blob) loadMapPrompt(blob);
  else {
    tip("No map stored. Save map to browser storage first", true, "error", 2000);
    ERROR && console.error("No map stored");
  }
}

async function loadFromDropbox() {
  const mapPath = byId("loadFromDropboxSelect")?.value;

  console.info("Loading map from Dropbox:", mapPath);
  const blob = await Cloud.providers.dropbox.load(mapPath);
  uploadMap(blob);
}

async function createSharableDropboxLink() {
  const mapFile = document.querySelector("#loadFromDropbox select").value;
  const sharableLink = byId("sharableLink");
  const sharableLinkContainer = byId("sharableLinkContainer");

  try {
    const previewLink = await Cloud.providers.dropbox.getLink(mapFile);
    const directLink = previewLink.replace("www.dropbox.com", "dl.dropboxusercontent.com"); // DL allows CORS
    const finalLink = `${location.origin}${location.pathname}?maplink=${directLink}`;

    sharableLink.innerText = finalLink.slice(0, 45) + "...";
    sharableLink.setAttribute("href", finalLink);
    sharableLinkContainer.style.display = "block";
  } catch (error) {
    ERROR && console.error(error);
    return tip("Dropbox API error. Can not create link.", true, "error", 2000);
  }
}

function loadMapPrompt(blob) {
  const workingTime = (Date.now() - last(mapHistory).created) / 60000; // minutes
  if (workingTime < 5) {
    loadLastSavedMap();
    return;
  }

  alertMessage.innerHTML = /* html */ `Are you sure you want to load saved map?<br />
    All unsaved changes made to the current map will be lost`;
  $("#alert").dialog({
    resizable: false,
    title: "Load saved map",
    buttons: {
      Cancel: function () {
        $(this).dialog("close");
      },
      Load: function () {
        loadLastSavedMap();
        $(this).dialog("close");
      }
    }
  });

  function loadLastSavedMap() {
    WARN && console.warn("Load last saved map");
    try {
      uploadMap(blob);
    } catch (error) {
      ERROR && console.error(error);
      tip("Cannot load last saved map", true, "error", 2000);
    }
  }
}

function loadMapFromURL(maplink, random) {
  const URL = decodeURIComponent(maplink);

  fetch(URL, {method: "GET", mode: "cors"})
    .then(response => {
      if (response.ok) return response.blob();
      throw new Error("Cannot load map from URL");
    })
    .then(blob => uploadMap(blob))
    .catch(error => {
      showUploadErrorMessage(error.message, URL, random);
      if (random) generateMapOnLoad();
    });
}

function showUploadErrorMessage(error, URL, random) {
  ERROR && console.error(error);
  alertMessage.innerHTML = /* html */ `Cannot load map from the ${link(URL, "link provided")}. ${
    random ? `A new random map is generated. ` : ""
  } Please ensure the
  linked file is reachable and CORS is allowed on server side`;
  $("#alert").dialog({
    title: "Loading error",
    width: "32em",
    buttons: {
      "Clear cache": () => cleanupData(),
      OK: function () {
        $(this).dialog("close");
      }
    }
  });
}

function uploadMap(file, callback) {
  uploadMap.timeStart = performance.now();

  const fileReader = new FileReader();
  fileReader.onloadend = async function (fileLoadedEvent) {
    if (callback) callback();
    byId("coas").innerHTML = ""; // remove auto-generated emblems

    const result = fileLoadedEvent.target.result;
    const {mapData, mapVersion} = await parseLoadedResult(result);

    const isInvalid = !mapData || !isValidVersion(mapVersion) || mapData.length < 10 || !mapData[5];
    if (isInvalid) return showUploadMessage("invalid", mapData, mapVersion);

    const isUpdated = compareVersions(mapVersion, VERSION).isEqual;
    if (isUpdated) return showUploadMessage("updated", mapData, mapVersion);

    const isAncient = compareVersions(mapVersion, "0.70.0").isOlder;
    if (isAncient) return showUploadMessage("ancient", mapData, mapVersion);

    const isNewer = compareVersions(mapVersion, VERSION).isNewer;
    if (isNewer) return showUploadMessage("newer", mapData, mapVersion);

    const isOutdated = compareVersions(mapVersion, VERSION).isOlder;
    if (isOutdated) return showUploadMessage("outdated", mapData, mapVersion);
  };

  fileReader.readAsArrayBuffer(file);
}

async function uncompress(compressedData) {
  try {
    const uncompressedStream = new Blob([compressedData]).stream().pipeThrough(new DecompressionStream("gzip"));

    let uncompressedData = [];
    for await (const chunk of uncompressedStream) {
      uncompressedData = uncompressedData.concat(Array.from(chunk));
    }

    return new Uint8Array(uncompressedData);
  } catch (error) {
    ERROR && console.error(error);
    return null;
  }
}

async function parseLoadedResult(result) {
  try {
    const resultAsString = new TextDecoder().decode(result);

    // data can be in FMG internal format or base64 encoded
    const isDelimited = resultAsString.substring(0, 10).includes("|");
    let content = isDelimited ? resultAsString : decodeURIComponent(atob(resultAsString));

    // fix if svg part has CRLF line endings instead of LF
    const svgMatch = content.match(/<svg[^>]*id="map"[\s\S]*?<\/svg>/);
    const svgContent = svgMatch[0];
    const hasCrlfEndings = svgContent.includes("\r\n");
    if (hasCrlfEndings) {
      const correctedSvgContent = svgContent.replace(/\r\n/g, "\n");
      content = content.replace(svgContent, correctedSvgContent);
    }

    const mapData = content.split("\r\n"); // split by CRLF
    const mapVersion = parseMapVersion(mapData[0].split("|")[0] || mapData[0] || "");

    return {mapData, mapVersion};
  } catch (error) {
    const uncompressedData = await uncompress(result); // file can be gzip compressed
    if (uncompressedData) return parseLoadedResult(uncompressedData);

    ERROR && console.error(error);
    return {mapData: null, mapVersion: null};
  }
}

function showUploadMessage(type, mapData, mapVersion) {
  let message, title;

  if (type === "invalid") {
    message = "The file does not look like a valid save file.<br>Please check the data format";
    title = "Invalid file";
  } else if (type === "updated") {
    parseLoadedData(mapData, mapVersion);
    return;
  } else if (type === "ancient") {
    const archive = link("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/Changelog", "archived version");
    message = `The map version you are trying to load (${mapVersion}) is too old and cannot be updated to the current version.<br>Please keep using an ${archive}`;
    title = "Ancient file";
  } else if (type === "newer") {
    message = `The map version you are trying to load (${mapVersion}) is newer than the current version.<br>Please load the file in the appropriate version`;
    title = "Newer file";
  } else if (type === "outdated") {
    INFO && console.info(`Loading map. Auto-updating from ${mapVersion} to ${VERSION}`);
    parseLoadedData(mapData, mapVersion);
    return;
  }

  alertMessage.innerHTML = message;
  $("#alert").dialog({
    title,
    buttons: {
      "Clear cache": () => cleanupData(),
      OK: function () {
        $(this).dialog("close");
      }
    }
  });
}

async function parseLoadedData(data, mapVersion) {
  try {
    // exit customization
    if (window.closeDialogs) closeDialogs();
    customization = 0;
    if (customizationMenu.offsetParent) styleTab.click();

    {
      const params = data[0].split("|");
      if (params[3]) {
        seed = params[3];
        optionsSeed.value = seed;
        INFO && console.group("Loaded Map " + seed);
      } else INFO && console.group("Loaded Map");
      if (params[4]) graphWidth = +params[4];
      if (params[5]) graphHeight = +params[5];
      mapId = params[6] ? +params[6] : Date.now();
    }

    {
      const settings = data[1].split("|");
      if (settings[0]) applyOption(distanceUnitInput, settings[0]);
      if (settings[1]) distanceScale = distanceScaleInput.value = settings[1];
      if (settings[2]) areaUnit.value = settings[2];
      if (settings[3]) applyOption(heightUnit, settings[3]);
      if (settings[4]) heightExponentInput.value = settings[4];
      if (settings[5]) temperatureScale.value = settings[5];
      // setting 6-11 (scaleBar) are part of style now, kept as "" in newer versions for compatibility
      if (settings[12]) populationRate = populationRateInput.value = settings[12];
      if (settings[13]) urbanization = urbanizationInput.value = settings[13];
      if (settings[14]) mapSizeInput.value = mapSizeOutput.value = minmax(settings[14], 1, 100);
      if (settings[15]) latitudeInput.value = latitudeOutput.value = minmax(settings[15], 0, 100);
      if (settings[18]) precInput.value = precOutput.value = settings[18];
      if (settings[19]) options = JSON.parse(settings[19]);
      // setting 16 and 17 (temperature) are part of options now, kept as "" in newer versions for compatibility
      if (settings[16]) options.temperatureEquator = +settings[16];
      if (settings[17]) options.temperatureNorthPole = options.temperatureSouthPole = +settings[17];
      if (settings[20]) mapName.value = settings[20];
      if (settings[21]) hideLabels.checked = +settings[21];
      if (settings[22]) stylePreset.value = settings[22];
      if (settings[23]) rescaleLabels.checked = +settings[23];
      if (settings[24]) urbanDensity = urbanDensityInput.value = +settings[24];
      if (settings[25]) longitudeInput.value = longitudeOutput.value = minmax(settings[25] || 50, 0, 100);
      if (settings[26]) growthRate.value = settings[26];
    }

    {
      stateLabelsModeInput.value = options.stateLabelsMode;
      yearInput.value = options.year;
      eraInput.value = options.era;
      shapeRendering.value = viewbox.attr("shape-rendering") || "geometricPrecision";
    }

    {
      if (data[2]) mapCoordinates = JSON.parse(data[2]);
      if (data[4]) notes = JSON.parse(data[4]);
      if (data[33]) rulers.fromString(data[33]);
      if (data[34]) {
        const usedFonts = JSON.parse(data[34]);
        usedFonts.forEach(usedFont => {
          const {family: usedFamily, unicodeRange: usedRange, variant: usedVariant} = usedFont;
          const defaultFont = fonts.find(
            ({family, unicodeRange, variant}) =>
              family === usedFamily && unicodeRange === usedRange && variant === usedVariant
          );
          if (!defaultFont) fonts.push(usedFont);
          declareFont(usedFont);
        });
      }
    }

    {
      const biomes = data[3].split("|");
      biomesData = Biomes.getDefault();
      biomesData.color = biomes[0].split(",");
      biomesData.habitability = biomes[1].split(",").map(h => +h);
      biomesData.name = biomes[2].split(",");
      // push custom biomes if any
      for (let i = biomesData.i.length; i < biomesData.name.length; i++) {
        biomesData.i.push(biomesData.i.length);
        biomesData.iconsDensity.push(0);
        biomesData.icons.push([]);
        biomesData.cost.push(50);
      }
    }

    {
      svg.remove();
      document.body.insertAdjacentHTML("afterbegin", data[5]);
    }

    {
      svg = d3.select("#map");
      defs = svg.select("#deftemp");
      viewbox = svg.select("#viewbox");
      scaleBar = svg.select("#scaleBar");
      legend = svg.select("#legend");
      ocean = viewbox.select("#ocean");
      oceanLayers = ocean.select("#oceanLayers");
      oceanPattern = ocean.select("#oceanPattern");
      lakes = viewbox.select("#lakes");
      landmass = viewbox.select("#landmass");
      texture = viewbox.select("#texture");
      terrs = viewbox.select("#terrs");
      biomes = viewbox.select("#biomes");
      ice = viewbox.select("#ice");
      cells = viewbox.select("#cells");
      gridOverlay = viewbox.select("#gridOverlay");
      coordinates = viewbox.select("#coordinates");
      compass = viewbox.select("#compass");
      rivers = viewbox.select("#rivers");
      terrain = viewbox.select("#terrain");
      relig = viewbox.select("#relig");
      cults = viewbox.select("#cults");
      regions = viewbox.select("#regions");
      statesBody = regions.select("#statesBody");
      statesHalo = regions.select("#statesHalo");
      provs = viewbox.select("#provs");
      zones = viewbox.select("#zones");
      borders = viewbox.select("#borders");
      stateBorders = borders.select("#stateBorders");
      provinceBorders = borders.select("#provinceBorders");
      routes = viewbox.select("#routes");
      roads = routes.select("#roads");
      trails = routes.select("#trails");
      searoutes = routes.select("#searoutes");
      temperature = viewbox.select("#temperature");
      coastline = viewbox.select("#coastline");
      prec = viewbox.select("#prec");
      population = viewbox.select("#population");
      emblems = viewbox.select("#emblems");
      labels = viewbox.select("#labels");
      icons = viewbox.select("#icons");
      burgIcons = icons.select("#burgIcons");
      anchors = icons.select("#anchors");
      armies = viewbox.select("#armies");
      markers = viewbox.select("#markers");
      ruler = viewbox.select("#ruler");
      fogging = viewbox.select("#fogging");
      debug = viewbox.select("#debug");
      burgLabels = labels.select("#burgLabels");

      if (!texture.size()) {
        texture = viewbox
          .insert("g", "#landmass")
          .attr("id", "texture")
          .attr("data-href", "./images/textures/plaster.jpg");
      }
      if (!emblems.size()) {
        emblems = viewbox.insert("g", "#labels").attr("id", "emblems").style("display", "none");
      }
    }

    {
      grid = JSON.parse(data[6]);
      const {cells, vertices} = calculateVoronoi(grid.points, grid.boundary);
      grid.cells = cells;
      grid.vertices = vertices;
      grid.cells.h = Uint8Array.from(data[7].split(","));
      grid.cells.prec = Uint8Array.from(data[8].split(","));
      grid.cells.f = Uint16Array.from(data[9].split(","));
      grid.cells.t = Int8Array.from(data[10].split(","));
      grid.cells.temp = Int8Array.from(data[11].split(","));
    }

    {
      reGraph();
      Features.markupPack();
      pack.features = JSON.parse(data[12]);
      pack.cultures = JSON.parse(data[13]);
      pack.states = JSON.parse(data[14]);
      pack.burgs = JSON.parse(data[15]);
      pack.religions = data[29] ? JSON.parse(data[29]) : [{i: 0, name: "No religion"}];
      pack.provinces = data[30] ? JSON.parse(data[30]) : [0];
      pack.rivers = data[32] ? JSON.parse(data[32]) : [];
      pack.markers = data[35] ? JSON.parse(data[35]) : [];
      pack.routes = data[37] ? JSON.parse(data[37]) : [];
      pack.zones = data[38] ? JSON.parse(data[38]) : [];
      pack.cells.biome = Uint8Array.from(data[16].split(","));
      pack.cells.burg = Uint16Array.from(data[17].split(","));
      pack.cells.conf = Uint8Array.from(data[18].split(","));
      pack.cells.culture = Uint16Array.from(data[19].split(","));
      pack.cells.fl = Uint16Array.from(data[20].split(","));
      pack.cells.pop = Float32Array.from(data[21].split(","));
      pack.cells.r = Uint16Array.from(data[22].split(","));
      // data[23] had deprecated cells.road
      pack.cells.s = Uint16Array.from(data[24].split(","));
      pack.cells.state = Uint16Array.from(data[25].split(","));
      pack.cells.religion = data[26] ? Uint16Array.from(data[26].split(",")) : new Uint16Array(pack.cells.i.length);
      pack.cells.province = data[27] ? Uint16Array.from(data[27].split(",")) : new Uint16Array(pack.cells.i.length);
      // data[28] had deprecated cells.crossroad
      pack.cells.routes = data[36] ? JSON.parse(data[36]) : {};
      pack.ice = data[39] ? JSON.parse(data[39]) : [];

      if (data[31]) {
        const namesDL = data[31].split("/");
        namesDL.forEach((d, i) => {
          const e = d.split("|");
          if (!e.length) return;
          const b = e[5].split(",").length > 2 || !nameBases[i] ? e[5] : nameBases[i].b;
          nameBases[i] = {name: e[0], min: e[1], max: e[2], d: e[3], m: e[4], b};
        });
      }
    }

    {
      const isVisible = selection => selection.node() && selection.style("display") !== "none";
      const isVisibleNode = node => node && node.style.display !== "none";
      const hasChildren = selection => selection.node()?.hasChildNodes();
      const hasChild = (selection, selector) => selection.node()?.querySelector(selector);
      const turnOn = el => byId(el).classList.remove("buttonoff");

      // turn all layers off
      byId("mapLayers")
        .querySelectorAll("li")
        .forEach(el => el.classList.add("buttonoff"));

      // turn on active layers
      if (hasChild(texture, "image")) turnOn("toggleTexture");
      if (hasChildren(terrs.select("#landHeights"))) turnOn("toggleHeight");
      if (hasChildren(biomes)) turnOn("toggleBiomes");
      if (hasChildren(cells)) turnOn("toggleCells");
      if (hasChildren(gridOverlay)) turnOn("toggleGrid");
      if (hasChildren(coordinates)) turnOn("toggleCoordinates");
      if (isVisible(compass) && hasChild(compass, "use")) turnOn("toggleCompass");
      if (hasChildren(rivers)) turnOn("toggleRivers");
      if (isVisible(terrain) && hasChildren(terrain)) turnOn("toggleRelief");
      if (hasChildren(relig)) turnOn("toggleReligions");
      if (hasChildren(cults)) turnOn("toggleCultures");
      if (hasChildren(statesBody)) turnOn("toggleStates");
      if (hasChildren(provs)) turnOn("toggleProvinces");
      if (hasChildren(zones) && isVisible(zones)) turnOn("toggleZones");
      if (isVisible(borders) && hasChild(borders, "path")) turnOn("toggleBorders");
      if (isVisible(routes) && hasChild(routes, "path")) turnOn("toggleRoutes");
      if (hasChildren(temperature)) turnOn("toggleTemperature");
      if (hasChild(population, "line")) turnOn("togglePopulation");
      if (isVisible(ice)) turnOn("toggleIce");
      if (hasChild(prec, "circle")) turnOn("togglePrecipitation");
      if (isVisible(emblems) && hasChild(emblems, "use")) turnOn("toggleEmblems");
      if (isVisible(labels)) turnOn("toggleLabels");
      if (isVisible(icons)) turnOn("toggleBurgIcons");
      if (hasChildren(armies) && isVisible(armies)) turnOn("toggleMilitary");
      if (hasChildren(markers)) turnOn("toggleMarkers");
      if (isVisible(ruler)) turnOn("toggleRulers");
      if (isVisible(scaleBar)) turnOn("toggleScaleBar");
      if (isVisibleNode(byId("vignette"))) turnOn("toggleVignette");

      getCurrentPreset();
    }

    {
      scaleBar.on("mousemove", () => tip("Click to open Units Editor")).on("click", () => editUnits());
      legend
        .on("mousemove", () => tip("Drag to change the position. Click to hide the legend"))
        .on("click", () => clearLegend());
    }

    {
      // dynamically import and run auto-update script
      const {resolveVersionConflicts} = await import("../dynamic/auto-update.js?v=1.113.0");
      resolveVersionConflicts(mapVersion);
    }

    // add custom heightmap color scheme if any
    if (heightmapColorSchemes) {
      const oceanScheme = byId("oceanHeights")?.getAttribute("scheme");
      if (oceanScheme && !(oceanScheme in heightmapColorSchemes)) addCustomColorScheme(oceanScheme);
      const landScheme = byId("#landHeights")?.getAttribute("scheme");
      if (landScheme && !(landScheme in heightmapColorSchemes)) addCustomColorScheme(landScheme);
    }

    {
      // add custom texture if any
      const textureHref = texture.attr("data-href");
      if (textureHref) updateTextureSelectValue(textureHref);
    }

    // data integrity checks
    {
      const {cells, vertices} = pack;

      const cellsMismatch = cells.i.length !== cells.state.length;
      const featureVerticesMismatch = pack.features.some(f => f?.vertices?.some(vertex => !vertices.p[vertex]));

      if (cellsMismatch || featureVerticesMismatch) {
        const message = "[Data integrity] Striping issue detected. To fix try to edit the heightmap in ERASE mode";
        throw new Error(message);
      }

      const invalidStates = [...new Set(cells.state)].filter(s => !pack.states[s] || pack.states[s].removed);
      invalidStates.forEach(s => {
        const invalidCells = cells.i.filter(i => cells.state[i] === s);
        invalidCells.forEach(i => (cells.state[i] = 0));
        ERROR && console.error("[Data integrity] Invalid state", s, "is assigned to cells", invalidCells);
      });

      const invalidProvinces = [...new Set(cells.province)].filter(
        p => p && (!pack.provinces[p] || pack.provinces[p].removed)
      );
      invalidProvinces.forEach(p => {
        const invalidCells = cells.i.filter(i => cells.province[i] === p);
        invalidCells.forEach(i => (cells.province[i] = 0));
        ERROR && console.error("[Data integrity] Invalid province", p, "is assigned to cells", invalidCells);
      });

      const invalidCultures = [...new Set(cells.culture)].filter(c => !pack.cultures[c] || pack.cultures[c].removed);
      invalidCultures.forEach(c => {
        const invalidCells = cells.i.filter(i => cells.culture[i] === c);
        invalidCells.forEach(i => (cells.province[i] = 0));
        ERROR && console.error("[Data integrity] Invalid culture", c, "is assigned to cells", invalidCells);
      });

      const invalidReligions = [...new Set(cells.religion)].filter(
        r => !pack.religions[r] || pack.religions[r].removed
      );
      invalidReligions.forEach(r => {
        const invalidCells = cells.i.filter(i => cells.religion[i] === r);
        invalidCells.forEach(i => (cells.religion[i] = 0));
        ERROR && console.error("[Data integrity] Invalid religion", r, "is assigned to cells", invalidCells);
      });

      const invalidFeatures = [...new Set(cells.f)].filter(f => f && !pack.features[f]);
      invalidFeatures.forEach(f => {
        const invalidCells = cells.i.filter(i => cells.f[i] === f);
        // No fix as for now
        ERROR && console.error("[Data integrity] Invalid feature", f, "is assigned to cells", invalidCells);
      });

      const invalidBurgs = [...new Set(cells.burg)].filter(
        burgId => burgId && (!pack.burgs[burgId] || pack.burgs[burgId].removed)
      );
      invalidBurgs.forEach(burgId => {
        const invalidCells = cells.i.filter(i => cells.burg[i] === burgId);
        invalidCells.forEach(i => (cells.burg[i] = 0));
        ERROR && console.error("[Data integrity] Invalid burg", burgId, "is assigned to cells", invalidCells);
      });

      const invalidRivers = [...new Set(cells.r)].filter(r => r && !pack.rivers.find(river => river.i === r));
      invalidRivers.forEach(r => {
        const invalidCells = cells.i.filter(i => cells.r[i] === r);
        invalidCells.forEach(i => (cells.r[i] = 0));
        rivers.select("river" + r).remove();
        ERROR && console.error("[Data integrity] Invalid river", r, "is assigned to cells", invalidCells);
      });

      pack.burgs.forEach(burg => {
        if (typeof burg.capital === "boolean") burg.capital = Number(burg.capital);

        if (!burg.i && burg.lock) {
          ERROR && console.error(`[Data integrity] Burg 0 is marked as locked, removing the status`);
          delete burg.lock;
          return;
        }

        if (burg.removed && burg.lock) {
          ERROR && console.error(`[Data integrity] Removed burg ${burg.i} is marked as locked. Unlocking the burg`);
          delete burg.lock;
          return;
        }

        if (!burg.i || burg.removed) return;

        if (burg.cell === undefined || burg.x === undefined || burg.y === undefined) {
          ERROR &&
            console.error(`[Data integrity] Burg ${burg.i} is missing cell info or coordinates. Removing the burg`);
          burg.removed = true;
        }

        if (burg.port < 0) {
          ERROR && console.error("[Data integrity] Burg", burg.i, "has invalid port value", burg.port);
          burg.port = 0;
        }

        if (burg.cell >= cells.i.length) {
          ERROR && console.error("[Data integrity] Burg", burg.i, "is linked to invalid cell", burg.cell);
          burg.cell = findCell(burg.x, burg.y);
          cells.i.filter(i => cells.burg[i] === burg.i).forEach(i => (cells.burg[i] = 0));
          cells.burg[burg.cell] = burg.i;
        }

        if (burg.state && !pack.states[burg.state]) {
          ERROR && console.error("[Data integrity] Burg", burg.i, "is linked to invalid state", burg.state);
          burg.state = 0;
        }

        if (burg.state && pack.states[burg.state].removed) {
          ERROR && console.error("[Data integrity] Burg", burg.i, "is linked to removed state", burg.state);
          burg.state = 0;
        }

        if (burg.state === undefined) {
          ERROR && console.error("[Data integrity] Burg", burg.i, "has no state data");
          burg.state = 0;
        }
      });

      pack.states.forEach(state => {
        if (state.removed) return;

        const stateBurgs = pack.burgs.filter(b => b.state === state.i && !b.removed);
        const capitalBurgs = stateBurgs.filter(b => b.capital);

        if (!state.i && capitalBurgs.length) {
          ERROR &&
            console.error(
              `[Data integrity] Neutral burgs (${capitalBurgs.map(b => b.i).join(", ")}) marked as capitals`
            );

          capitalBurgs.forEach(burg => {
            burg.capital = 0;
            Burgs.changeGroup(burg);
          });

          return;
        }

        if (capitalBurgs.length > 1) {
          const message = `[Data integrity] State ${state.i} has multiple capitals (${capitalBurgs
            .map(b => b.i)
            .join(", ")}) assigned. Keeping the first as capital and moving others`;
          ERROR && console.error(message);

          capitalBurgs.forEach((burg, i) => {
            if (!i) return;
            burg.capital = 0;
            Burgs.changeGroup(burg);
          });

          return;
        }

        if (state.i && stateBurgs.length && !capitalBurgs.length) {
          ERROR && console.error(`[Data integrity] State ${state.i} has no capital. Making the first burg capital`);
          const capital = stateBurgs[0];
          capital.capital = 1;
          Burgs.changeGroup(capital);
        }
      });

      pack.provinces.forEach(p => {
        if (!p.i || p.removed) return;
        if (pack.states[p.state] && !pack.states[p.state].removed) return;
        ERROR &&
          console.error(
            `[Data integrity] Province ${p.i} is linked to removed state ${p.state}. Removing the province`
          );
        p.removed = true;
      });

      pack.routes.forEach(route => {
        if (!route.points || route.points.length < 2) {
          ERROR && console.error(`[Data integrity] Route ${route.i} has less than 2 points. Removing the route`);
          Routes.remove(route);
        }
      });

      for (const from in pack.cells.routes) {
        const value = pack.cells.routes[from];
        if (!value) continue;

        if (Object.keys(value).length === 0) {
          // remove empty object
          delete pack.cells.routes[from];
          continue;
        }

        for (const to in value) {
          const routeId = value[to];
          const route = pack.routes.find(r => r.i === routeId);
          if (!route) {
            ERROR &&
              console.error(`[Data integrity] Route ${routeId} from ${from} to ${to} is missing. Removing the route`);
            delete pack.cells.routes[from][to];
          }
        }
      }

      {
        const markerIds = [];
        let nextId = last(pack.markers)?.i + 1 || 0;

        pack.markers.forEach(marker => {
          if (markerIds[marker.i]) {
            ERROR && console.error("[Data integrity] Marker", marker.i, "has non-unique id. Changing to", nextId);

            const domElements = document.querySelectorAll("#marker" + marker.i);
            if (domElements[1]) domElements[1].id = "marker" + nextId; // rename 2nd dom element

            const noteElements = notes.filter(note => note.id === "marker" + marker.i);
            if (noteElements[1]) noteElements[1].id = "marker" + nextId; // rename 2nd note

            marker.i = nextId;
            nextId += 1;
          } else {
            markerIds[marker.i] = true;
          }
        });

        // sort markers by index
        pack.markers.sort((a, b) => a.i - b.i);
      }
    }

    {
      // remove href from emblems, to trigger rendering on load
      emblems.selectAll("use").attr("href", null);
    }

    {
      // draw data layers (not kept in svg)
      if (rulers && layerIsOn("toggleRulers")) rulers.draw();
      if (layerIsOn("toggleGrid")) drawGrid();
    }

    {
      if (window.restoreDefaultEvents) restoreDefaultEvents();
      focusOn(); // based on searchParams focus on point, cell or burg
      invokeActiveZooming();
      fitMapToScreen();
    }

    WARN && console.warn(`TOTAL: ${rn((performance.now() - uploadMap.timeStart) / 1000, 2)}s`);
    showStatistics();
    INFO && console.groupEnd("Loaded Map " + seed);
    tip("Map is successfully loaded", true, "success", 7000);
  } catch (error) {
    ERROR && console.error(error);
    clearMainTip();

    alertMessage.innerHTML = /* html */ `An error is occured on map loading. Select a different file to load, <br>generate a new random map or cancel the loading.<br>Map version: ${mapVersion}. Generator version: ${VERSION}.
      <p id="errorBox">${parseError(error)}</p>`;

    $("#alert").dialog({
      resizable: false,
      title: "Loading error",
      maxWidth: "40em",
      buttons: {
        "Clear cache": () => cleanupData(),
        "Select file": function () {
          $(this).dialog("close");
          mapToLoad.click();
        },
        "New map": function () {
          $(this).dialog("close");
          regenerateMap("loading error");
        },
        Cancel: function () {
          $(this).dialog("close");
        }
      },
      position: {my: "center", at: "center", of: "svg"}
    });
  }
}
