// FMG helper functions
"use strict";

// add boundary points to pseudo-clip voronoi cells
function getBoundaryPoints(width, height, spacing) {
  const offset = rn(-1 * spacing);
  const bSpacing = spacing * 2;
  const w = width - offset * 2;
  const h = height - offset * 2;
  const numberX = Math.ceil(w / bSpacing) - 1;
  const numberY = Math.ceil(h / bSpacing) - 1;
  let points = [];
  for (let i = 0.5; i < numberX; i++) {
    let x = Math.ceil((w * i) / numberX + offset);
    points.push([x, offset], [x, h + offset]);
  }
  for (let i = 0.5; i < numberY; i++) {
    let y = Math.ceil((h * i) / numberY + offset);
    points.push([offset, y], [w + offset, y]);
  }
  return points;
}

// get points on a regular square grid and jitter them a bit
function getJitteredGrid(width, height, spacing) {
  const radius = spacing / 2; // square radius
  const jittering = radius * 0.9; // max deviation
  const jitter = () => Math.random() * 2 * jittering - jittering;

  let points = [];
  for (let y = radius; y < height; y += spacing) {
    for (let x = radius; x < width; x += spacing) {
      const xj = Math.min(rn(x + jitter(), 2), width);
      const yj = Math.min(rn(y + jitter(), 2), height);
      points.push([xj, yj]);
    }
  }
  return points;
}

// return cell index on a regular square grid
function findGridCell(x, y) {
  return Math.floor(Math.min(y / grid.spacing, grid.cellsY - 1)) * grid.cellsX + Math.floor(Math.min(x / grid.spacing, grid.cellsX - 1));
}

// return array of cell indexes in radius on a regular square grid
function findGridAll(x, y, radius) {
  const c = grid.cells.c;
  let r = Math.floor(radius / grid.spacing);
  let found = [findGridCell(x, y)];
  if (!r || radius === 1) return found;
  if (r > 0) found = found.concat(c[found[0]]);
  if (r > 1) {
    let frontier = c[found[0]];
    while (r > 1) {
      let cycle = frontier.slice();
      frontier = [];
      cycle.forEach(function (s) {
        c[s].forEach(function (e) {
          if (found.indexOf(e) !== -1) return;
          found.push(e);
          frontier.push(e);
        });
      });
      r--;
    }
  }

  return found;
}

// return closest pack points quadtree datum
function find(x, y, radius = Infinity) {
  return pack.cells.q.find(x, y, radius);
}

// return closest cell index
function findCell(x, y, radius = Infinity) {
  const found = pack.cells.q.find(x, y, radius);
  return found ? found[2] : undefined;
}

// return array of cell indexes in radius
function findAll(x, y, radius) {
  const found = pack.cells.q.findAll(x, y, radius);
  return found.map(r => r[2]);
}

// get polygon points for packed cells knowing cell id
function getPackPolygon(i) {
  return pack.cells.v[i].map(v => pack.vertices.p[v]);
}

// get polygon points for initial cells knowing cell id
function getGridPolygon(i) {
  return grid.cells.v[i].map(v => grid.vertices.p[v]);
}

// mbostock's poissonDiscSampler
function* poissonDiscSampler(x0, y0, x1, y1, r, k = 3) {
  if (!(x1 >= x0) || !(y1 >= y0) || !(r > 0)) throw new Error();

  const width = x1 - x0;
  const height = y1 - y0;
  const r2 = r * r;
  const r2_3 = 3 * r2;
  const cellSize = r * Math.SQRT1_2;
  const gridWidth = Math.ceil(width / cellSize);
  const gridHeight = Math.ceil(height / cellSize);
  const grid = new Array(gridWidth * gridHeight);
  const queue = [];

  function far(x, y) {
    const i = (x / cellSize) | 0;
    const j = (y / cellSize) | 0;
    const i0 = Math.max(i - 2, 0);
    const j0 = Math.max(j - 2, 0);
    const i1 = Math.min(i + 3, gridWidth);
    const j1 = Math.min(j + 3, gridHeight);
    for (let j = j0; j < j1; ++j) {
      const o = j * gridWidth;
      for (let i = i0; i < i1; ++i) {
        const s = grid[o + i];
        if (s) {
          const dx = s[0] - x;
          const dy = s[1] - y;
          if (dx * dx + dy * dy < r2) return false;
        }
      }
    }
    return true;
  }

  function sample(x, y) {
    queue.push((grid[gridWidth * ((y / cellSize) | 0) + ((x / cellSize) | 0)] = [x, y]));
    return [x + x0, y + y0];
  }

  yield sample(width / 2, height / 2);

  pick: while (queue.length) {
    const i = (Math.random() * queue.length) | 0;
    const parent = queue[i];

    for (let j = 0; j < k; ++j) {
      const a = 2 * Math.PI * Math.random();
      const r = Math.sqrt(Math.random() * r2_3 + r2);
      const x = parent[0] + r * Math.cos(a);
      const y = parent[1] + r * Math.sin(a);
      if (0 <= x && x < width && 0 <= y && y < height && far(x, y)) {
        yield sample(x, y);
        continue pick;
      }
    }

    const r = queue.pop();
    if (i < queue.length) queue[i] = r;
  }
}

// filter land cells
function isLand(i) {
  return pack.cells.h[i] >= 20;
}

// filter water cells
function isWater(i) {
  return pack.cells.h[i] < 20;
}

// convert RGB color string to HEX without #
function toHEX(rgb) {
  if (rgb.charAt(0) === "#") {
    return rgb;
  }
  rgb = rgb.match(/^rgba?[\s+]?\([\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?,[\s+]?(\d+)[\s+]?/i);
  return rgb && rgb.length === 4 ? "#" + ("0" + parseInt(rgb[1], 10).toString(16)).slice(-2) + ("0" + parseInt(rgb[2], 10).toString(16)).slice(-2) + ("0" + parseInt(rgb[3], 10).toString(16)).slice(-2) : "";
}

// return array of standard shuffled colors
function getColors(number) {
  const c12 = ["#dababf", "#fb8072", "#80b1d3", "#fdb462", "#b3de69", "#fccde5", "#c6b9c1", "#bc80bd", "#ccebc5", "#ffed6f", "#8dd3c7", "#eb8de7"];
  const cRB = d3.scaleSequential(d3.interpolateRainbow);
  const colors = d3.shuffle(d3.range(number).map(i => (i < 12 ? c12[i] : d3.color(cRB((i - 12) / (number - 12))).hex())));
  return colors;
}

function getRandomColor() {
  return d3.color(d3.scaleSequential(d3.interpolateRainbow)(Math.random())).hex();
}

// mix a color with a random color
function getMixedColor(color, mix = 0.2, bright = 0.3) {
  const c = color && color[0] === "#" ? color : getRandomColor(); // if provided color is not hex (e.g. harching), generate random one
  return d3.color(d3.interpolate(c, getRandomColor())(mix)).brighter(bright).hex();
}

// conver temperature from °C to other scales
function convertTemperature(c) {
  switch (temperatureScale.value) {
    case "°C":
      return c + "°C";
    case "°F":
      return rn((c * 9) / 5 + 32) + "°F";
    case "K":
      return rn(c + 273.15) + "K";
    case "°R":
      return rn(((c + 273.15) * 9) / 5) + "°R";
    case "°De":
      return rn(((100 - c) * 3) / 2) + "°De";
    case "°N":
      return rn((c * 33) / 100) + "°N";
    case "°Ré":
      return rn((c * 4) / 5) + "°Ré";
    case "°Rø":
      return rn((c * 21) / 40 + 7.5) + "°Rø";
    default:
      return c + "°C";
  }
}

// random number in a range
function rand(min, max) {
  if (min === undefined && max === undefined) return Math.random();
  if (max === undefined) {
    max = min;
    min = 0;
  }
  return Math.floor(Math.random() * (max - min + 1)) + min;
}

// probability shorthand
function P(probability) {
  if (probability >= 1) return true;
  if (probability <= 0) return false;
  return Math.random() < probability;
}

function each(n) {
  return i => i % n === 0;
}

// random number (normal or gaussian distribution)
function gauss(expected = 100, deviation = 30, min = 0, max = 300, round = 0) {
  return rn(Math.max(Math.min(d3.randomNormal(expected, deviation)(), max), min), round);
}

// probability shorthand for floats
function Pint(float) {
  return ~~float + +P(float % 1);
}

// round value to d decimals
function rn(v, d = 0) {
  const m = Math.pow(10, d);
  return Math.round(v * m) / m;
}

// round string to d decimals
function round(s, d = 1) {
  return s.replace(/[\d\.-][\d\.e-]*/g, function (n) {
    return rn(n, d);
  });
}

// corvent number to short string with SI postfix
function si(n) {
  if (n >= 1e9) return rn(n / 1e9, 1) + "B";
  if (n >= 1e8) return rn(n / 1e6) + "M";
  if (n >= 1e6) return rn(n / 1e6, 1) + "M";
  if (n >= 1e4) return rn(n / 1e3) + "K";
  if (n >= 1e3) return rn(n / 1e3, 1) + "K";
  return rn(n);
}

// getInteger number from user input data
function getInteger(value) {
  const metric = value.slice(-1);
  if (metric === "K") return parseInt(value.slice(0, -1) * 1e3);
  if (metric === "M") return parseInt(value.slice(0, -1) * 1e6);
  if (metric === "B") return parseInt(value.slice(0, -1) * 1e9);
  return parseInt(value);
}

// remove parent element (usually if child is clicked)
function removeParent() {
  this.parentNode.parentNode.removeChild(this.parentNode);
}

// return string with 1st char capitalized
function capitalize(string) {
  return string.charAt(0).toUpperCase() + string.slice(1);
}

// transform string to array [translateX,translateY,rotateDeg,rotateX,rotateY,scale]
function parseTransform(string) {
  if (!string) {
    return [0, 0, 0, 0, 0, 1];
  }
  const a = string
    .replace(/[a-z()]/g, "")
    .replace(/[ ]/g, ",")
    .split(",");
  return [a[0] || 0, a[1] || 0, a[2] || 0, a[3] || 0, a[4] || 0, a[5] || 1];
}

// findAll d3.quandtree search from https://bl.ocks.org/lwthatcher/b41479725e0ff2277c7ac90df2de2b5e
void (function addFindAll() {
  const Quad = function (node, x0, y0, x1, y1) {
    this.node = node;
    this.x0 = x0;
    this.y0 = y0;
    this.x1 = x1;
    this.y1 = y1;
  };

  const tree_filter = function (x, y, radius) {
    var t = {x, y, x0: this._x0, y0: this._y0, x3: this._x1, y3: this._y1, quads: [], node: this._root};
    if (t.node) {
      t.quads.push(new Quad(t.node, t.x0, t.y0, t.x3, t.y3));
    }
    radiusSearchInit(t, radius);

    var i = 0;
    while ((t.q = t.quads.pop())) {
      i++;

      // Stop searching if this quadrant can’t contain a closer node.
      if (!(t.node = t.q.node) || (t.x1 = t.q.x0) > t.x3 || (t.y1 = t.q.y0) > t.y3 || (t.x2 = t.q.x1) < t.x0 || (t.y2 = t.q.y1) < t.y0) continue;

      // Bisect the current quadrant.
      if (t.node.length) {
        t.node.explored = true;
        var xm = (t.x1 + t.x2) / 2,
          ym = (t.y1 + t.y2) / 2;

        t.quads.push(new Quad(t.node[3], xm, ym, t.x2, t.y2), new Quad(t.node[2], t.x1, ym, xm, t.y2), new Quad(t.node[1], xm, t.y1, t.x2, ym), new Quad(t.node[0], t.x1, t.y1, xm, ym));

        // Visit the closest quadrant first.
        if ((t.i = ((y >= ym) << 1) | (x >= xm))) {
          t.q = t.quads[t.quads.length - 1];
          t.quads[t.quads.length - 1] = t.quads[t.quads.length - 1 - t.i];
          t.quads[t.quads.length - 1 - t.i] = t.q;
        }
      }

      // Visit this point. (Visiting coincident points isn’t necessary!)
      else {
        var dx = x - +this._x.call(null, t.node.data),
          dy = y - +this._y.call(null, t.node.data),
          d2 = dx * dx + dy * dy;
        radiusSearchVisit(t, d2);
      }
    }
    return t.result;
  };
  d3.quadtree.prototype.findAll = tree_filter;

  var radiusSearchInit = function (t, radius) {
    t.result = [];
    (t.x0 = t.x - radius), (t.y0 = t.y - radius);
    (t.x3 = t.x + radius), (t.y3 = t.y + radius);
    t.radius = radius * radius;
  };

  var radiusSearchVisit = function (t, d2) {
    t.node.data.scanned = true;
    if (d2 < t.radius) {
      do {
        t.result.push(t.node.data);
        t.node.data.selected = true;
      } while ((t.node = t.node.next));
    }
  };
})();

// get segment of any point on polyline
function getSegmentId(points, point, step = 10) {
  if (points.length === 2) return 1;
  const d2 = (p1, p2) => (p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2;

  let minSegment = 1;
  let minDist = Infinity;

  for (let i = 0; i < points.length - 1; i++) {
    const p1 = points[i];
    const p2 = points[i + 1];

    const length = Math.sqrt(d2(p1, p2));
    const segments = Math.ceil(length / step);
    const dx = (p2[0] - p1[0]) / segments;
    const dy = (p2[1] - p1[1]) / segments;

    for (let s = 0; s < segments; s++) {
      const x = p1[0] + s * dx;
      const y = p1[1] + s * dy;
      const dist2 = d2(point, [x, y]);

      if (dist2 >= minDist) continue;
      minDist = dist2;
      minSegment = i + 1;
    }
  }

  return minSegment;
}

// normalization function
function normalize(val, min, max) {
  return Math.min(Math.max((val - min) / (max - min), 0), 1);
}

// return a random integer from min to max biased towards one end based on exponent distribution (the bigger ex the higher bias towards min)
// from https://gamedev.stackexchange.com/a/116875
function biased(min, max, ex) {
  return Math.round(min + (max - min) * Math.pow(Math.random(), ex));
}

// return array of values common for both array a and array b
function common(a, b) {
  const setB = new Set(b);
  return [...new Set(a)].filter(a => setB.has(a));
}

// clip polygon by graph bbox
function clipPoly(points, secure = 0) {
  return polygonclip(points, [0, 0, graphWidth, graphHeight], secure);
}

// check if char is vowel or can serve as vowel
function vowel(c) {
  return `aeiouyɑ'əøɛœæɶɒɨɪɔɐʊɤɯаоиеёэыуюяàèìòùỳẁȁȅȉȍȕáéíóúýẃőűâêîôûŷŵäëïöüÿẅãẽĩõũỹąęįǫųāēīōūȳăĕĭŏŭǎěǐǒǔȧėȯẏẇạẹịọụỵẉḛḭṵṳ`.includes(c);
}

// remove vowels from the end of the string
function trimVowels(string) {
  while (string.length > 3 && vowel(last(string))) {
    string = string.slice(0, -1);
  }
  return string;
}

// get adjective form from noun
function getAdjective(string) {
  // special cases for some suffixes
  if (string.length > 8 && string.slice(-6) === "orszag") return string.slice(0, -6);
  if (string.length > 6 && string.slice(-4) === "stan") return string.slice(0, -4);
  if (P(0.5) && string.slice(-4) === "land") return string + "ic";
  if (string.slice(-4) === " Guo") string = string.slice(0, -4);

  // don't change is name ends on suffix
  if (string.slice(-2) === "an") return string;
  if (string.slice(-3) === "ese") return string;
  if (string.slice(-1) === "i") return string;

  const end = string.slice(-1); // last letter of string
  if (end === "a") return (string += "n");
  if (end === "o") return (string = trimVowels(string) + "an");
  if (vowel(end) || end === "c") return (string += "an"); // ceiuy
  if (end === "m" || end === "n") return (string += "ese");
  if (end === "q") return (string += "i");
  return trimVowels(string) + "ian";
}

// get ordinal out of integer: 1 => 1st
const nth = n => n + (["st", "nd", "rd"][((((n + 90) % 100) - 10) % 10) - 1] || "th");

// get two-letters code (abbreviation) from string
function abbreviate(name, restricted = []) {
  const parsed = name.replace("Old ", "O ").replace(/[()]/g, ""); // remove Old prefix and parentheses
  const words = parsed.split(" ");
  const letters = words.join("");

  let code = words.length === 2 ? words[0][0] + words[1][0] : letters.slice(0, 2);
  for (let i = 1; i < letters.length - 1 && restricted.includes(code); i++) {
    code = letters[0] + letters[i].toUpperCase();
  }
  return code;
}

// conjunct array: [A,B,C] => "A, B and C"
function list(array) {
  if (!Intl.ListFormat) return array.join(", ");
  const conjunction = new Intl.ListFormat(window.lang || "en", {style: "long", type: "conjunction"});
  return conjunction.format(array);
}

// split string into 2 almost equal parts not breaking words
function splitInTwo(str) {
  const half = str.length / 2;
  const ar = str.split(" ");
  if (ar.length < 2) return ar; // only one word
  let first = "",
    last = "",
    middle = "",
    rest = "";

  ar.forEach((w, d) => {
    if (d + 1 !== ar.length) w += " ";
    rest += w;
    if (!first || rest.length < half) first += w;
    else if (!middle) middle = w;
    else last += w;
  });

  if (!last) return [first, middle];
  if (first.length < last.length) return [first + middle, last];
  return [first, middle + last];
}

// return the last element of array
function last(array) {
  return array[array.length - 1];
}

// return random value from the array
function ra(array) {
  return array[Math.floor(Math.random() * array.length)];
}

// return random value from weighted array {"key1":weight1, "key2":weight2}
function rw(object) {
  const array = [];
  for (const key in object) {
    for (let i = 0; i < object[key]; i++) {
      array.push(key);
    }
  }
  return array[Math.floor(Math.random() * array.length)];
}

// return value in range [0, 100] (height range)
function lim(v) {
  return Math.max(Math.min(v, 100), 0);
}

// get number from string in format "1-3" or "2" or "0.5"
function getNumberInRange(r) {
  if (typeof r !== "string") {
    ERROR && console.error("The value should be a string", r);
    return 0;
  }
  if (!isNaN(+r)) return ~~r + +P(r - ~~r);
  const sign = r[0] === "-" ? -1 : 1;
  if (isNaN(+r[0])) r = r.slice(1);
  const range = r.includes("-") ? r.split("-") : null;
  if (!range) {
    ERROR && console.error("Cannot parse the number. Check the format", r);
    return 0;
  }
  const count = rand(range[0] * sign, +range[1]);
  if (isNaN(count) || count < 0) {
    ERROR && console.error("Cannot parse number. Check the format", r);
    return 0;
  }
  return count;
}

// return center point of common edge of 2 pack cells
function getMiddlePoint(cell1, cell2) {
  const {cells, vertices} = pack;

  const commonVertices = cells.v[cell1].filter(vertex => vertices.c[vertex].some(cell => cell === cell2));
  const [x1, y1] = vertices.p[commonVertices[0]];
  const [x2, y2] = vertices.p[commonVertices[1]];

  const x = (x1 + x2) / 2;
  const y = (y1 + y2) / 2;

  return [x, y];
}

// helper function non-used for the generation
function drawCellsValue(data) {
  debug.selectAll("text").remove();
  debug
    .selectAll("text")
    .data(data)
    .enter()
    .append("text")
    .attr("x", (d, i) => pack.cells.p[i][0])
    .attr("y", (d, i) => pack.cells.p[i][1])
    .text(d => d);
}

// helper function non-used for the generation
function drawPolygons(data) {
  const max = d3.max(data),
    min = d3.min(data),
    scheme = getColorScheme();
  data = data.map(d => 1 - normalize(d, min, max));

  debug.selectAll("polygon").remove();
  debug
    .selectAll("polygon")
    .data(data)
    .enter()
    .append("polygon")
    .attr("points", (d, i) => getPackPolygon(i))
    .attr("fill", d => scheme(d))
    .attr("stroke", d => scheme(d));
}

// polyfill for composedPath
function getComposedPath(node) {
  let parent;
  if (node.parentNode) parent = node.parentNode;
  else if (node.host) parent = node.host;
  else if (node.defaultView) parent = node.defaultView;
  if (parent !== undefined) return [node].concat(getComposedPath(parent));
  return [node];
}

// polyfill for replaceAll
if (!String.prototype.replaceAll) {
  String.prototype.replaceAll = function (str, newStr) {
    if (Object.prototype.toString.call(str).toLowerCase() === "[object regexp]") return this.replace(str, newStr);
    return this.replace(new RegExp(str, "g"), newStr);
  };
}

// get next unused id
function getNextId(core, i = 1) {
  while (document.getElementById(core + i)) i++;
  return core + i;
}

function debounce(func, ms) {
  let isCooldown = false;

  return function () {
    if (isCooldown) return;
    func.apply(this, arguments);
    isCooldown = true;
    setTimeout(() => (isCooldown = false), ms);
  };
}

function throttle(func, ms) {
  let isThrottled = false;
  let savedArgs;
  let savedThis;

  function wrapper() {
    if (isThrottled) {
      savedArgs = arguments;
      savedThis = this;
      return;
    }

    func.apply(this, arguments);
    isThrottled = true;

    setTimeout(function () {
      isThrottled = false;
      if (savedArgs) {
        wrapper.apply(savedThis, savedArgs);
        savedArgs = savedThis = null;
      }
    }, ms);
  }

  return wrapper;
}

// parse error to get the readable string in Chrome and Firefox
function parseError(error) {
  const isFirefox = navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
  const errorString = isFirefox ? error.toString() + " " + error.stack : error.stack;
  const regex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/gi;
  const errorNoURL = errorString.replace(regex, url => "<i>" + last(url.split("/")) + "</i>");
  const errorParsed = errorNoURL.replace(/at /gi, "<br>&nbsp;&nbsp;at ");
  return errorParsed;
}

// polyfills
if (Array.prototype.flat === undefined) {
  Array.prototype.flat = function () {
    return this.reduce((acc, val) => (Array.isArray(val) ? acc.concat(val.flat()) : acc.concat(val)), []);
  };
}

// check if string is a valid for JSON parse
JSON.isValid = str => {
  try {
    JSON.parse(str);
  } catch (e) {
    return false;
  }
  return true;
};

function getBase64(url, callback) {
  const xhr = new XMLHttpRequest();
  xhr.onload = function () {
    const reader = new FileReader();
    reader.onloadend = function () {
      callback(reader.result);
    };
    reader.readAsDataURL(xhr.response);
  };
  xhr.open("GET", url);
  xhr.responseType = "blob";
  xhr.send();
}

function getDefaultTexture() {
  return "data:image/jpeg;base64,[encoded data, 120824 characters]";
}

function getAbsolutePath(href) {
  if (!href) return "";
  const link = document.createElement("a");
  link.href = href;
  return link.href;
}

// open URL in a new tab or window
function openURL(url) {
  window.open(url, "_blank");
}

// open project wiki-page
function wiki(page) {
  window.open("https://github.com/Azgaar/Fantasy-Map-Generator/wiki/" + page, "_blank");
}

// wrap URL into html a element
function link(URL, description) {
  return `<a href="${URL}" rel="noopener" target="_blank">${description}</a>`;
}

function isCtrlClick(event) {
  // meta key is cmd key on MacOs
  return event.ctrlKey || event.metaKey;
}

function generateDate(from = 100, to = 1000) {
  return new Date(rand(from, to), rand(12), rand(31)).toLocaleDateString("en", {year: "numeric", month: "long", day: "numeric"});
}

function getQGIScoordinates(x, y) {
  const cx = mapCoordinates.lonW + (x / graphWidth) * mapCoordinates.lonT;
  const cy = mapCoordinates.latN - (y / graphHeight) * mapCoordinates.latT; // this is inverted in QGIS otherwise
  return [cx, cy];
}

// prompt replacer (prompt does not work in Electron)
void (function () {
  const prompt = document.getElementById("prompt");
  const form = prompt.querySelector("#promptForm");

  window.prompt = function (promptText = "Please provide an input", options = {default: 1, step: 0.01, min: 0, max: 100}, callback) {
    if (options.default === undefined) {
      ERROR && console.error("Prompt: options object does not have default value defined");
      return;
    }
    const input = prompt.querySelector("#promptInput");
    prompt.querySelector("#promptText").innerHTML = promptText;
    const type = typeof options.default === "number" ? "number" : "text";
    input.type = type;
    if (options.step !== undefined) input.step = options.step;
    if (options.min !== undefined) input.min = options.min;
    if (options.max !== undefined) input.max = options.max;
    input.placeholder = "type a " + type;
    input.value = options.default;
    prompt.style.display = "block";

    form.addEventListener(
      "submit",
      event => {
        prompt.style.display = "none";
        const v = type === "number" ? +input.value : input.value;
        event.preventDefault();
        if (callback) callback(v);
      },
      {once: true}
    );
  };

  const cancel = prompt.querySelector("#promptCancel");
  cancel.addEventListener("click", () => (prompt.style.display = "none"));
})();

// indexedDB; ldb object
!(function () {
  function e(t, o) {
    return n
      ? void (n.transaction("s").objectStore("s").get(t).onsuccess = function (e) {
          var t = (e.target.result && e.target.result.v) || null;
          o(t);
        })
      : void setTimeout(function () {
          e(t, o);
        }, 100);
  }
  var t = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
  if (!t) return void ERROR && console.error("indexedDB not supported");
  var n,
    o = {k: "", v: ""},
    r = t.open("d2", 1);
  (r.onsuccess = function (e) {
    n = this.result;
  }),
    (r.onerror = function (e) {
      ERROR && console.error("indexedDB request error"), INFO && console.log(e);
    }),
    (r.onupgradeneeded = function (e) {
      n = null;
      var t = e.target.result.createObjectStore("s", {keyPath: "k"});
      t.transaction.oncomplete = function (e) {
        n = e.target.db;
      };
    }),
    (window.ldb = {
      get: e,
      set: function (e, t) {
        (o.k = e), (o.v = t), n.transaction("s", "readwrite").objectStore("s").put(o);
      }
    });
})();
