(function () {
  const svgNamespace = "http://www.w3.org/2000/svg";
  const previewScaleFactor = 4;
  const exportDpi = 300;
  const zoomStep = 1.08;
  const selectedGlowColor = "#e47b1f";
  const hoverGlowColor = "#2b87ff";
  const defaultSettings = {
    noOfPanels: 5,
    dynamic: 5,
    vanishing: 5,
    verticalRatio: 5,
    goldenRatio: 5,
    bordering: 5,
    pageWidth: 148,
    pageHeight: 210,
    innerWidth: 125,
    innerHeight: 180,
    bleed: 5,
    horizontalMargin: 1,
    verticalMargin: 2.6,
    readingOrderLeft: true,
    isLeftPage: true,
  };

  let currentContext = null;
  let panels = [];
  let selectedPanelId = null;
  let hoverPanelId = null;
  let dragState = null;
  let contextMenuState = null;
  let nextPanelId = 1;
  let nextLayerId = 1;

  const pageSvg = document.getElementById("pageSvg");
  const sheet = document.getElementById("sheet");
  const printStyles = document.getElementById("printStyles");
  const panelContextMenu = document.getElementById("panelContextMenu");
  const resetLayerButton = document.getElementById("resetLayerButton");
  const clearPanelButton = document.getElementById("clearPanelButton");

  class Point {
    constructor(x, y) {
      this.x = x;
      this.y = y;
    }

    clone() {
      return new Point(this.x, this.y);
    }
  }

  class Panel {
    constructor(id, points, context) {
      this.id = id;
      this.points = points;
      this.context = context;
      this.visible = true;
      this.layers = [];
      this.activeLayerId = null;
      this.allowsOverlay = false;
    }

    split(startRatio, endRatio, isHorizontal) {
      if (isHorizontal) {
        const x2 = this.points[2].x;
        const y2 = this.points[2].y;
        const x3 = this.points[3].x;
        const y3 = this.points[3].y;

        this.points[2].x = lerp(this.points[1].x, this.points[2].x, endRatio);
        this.points[2].y = lerp(this.points[1].y, this.points[2].y, endRatio);
        this.points[3].x = lerp(this.points[0].x, this.points[3].x, startRatio);
        this.points[3].y = lerp(this.points[0].y, this.points[3].y, startRatio);

        const newPanel = new Panel(nextPanelId, [
          new Point(this.points[3].x, this.points[3].y),
          new Point(this.points[2].x, this.points[2].y),
          new Point(x2, y2),
          new Point(x3, y3),
        ], this.context);
        nextPanelId += 1;

        const index = panels.indexOf(this);
        panels.splice(index + 1, 0, newPanel);
        return newPanel;
      }

      const x1 = this.points[1].x;
      const y1 = this.points[1].y;
      const x2 = this.points[2].x;
      const y2 = this.points[2].y;

      this.points[1].x = lerp(this.points[0].x, this.points[1].x, endRatio);
      this.points[1].y = lerp(this.points[0].y, this.points[1].y, endRatio);
      this.points[2].x = lerp(this.points[3].x, this.points[2].x, startRatio);
      this.points[2].y = lerp(this.points[3].y, this.points[2].y, startRatio);

      const newPanel = new Panel(nextPanelId, [
        new Point(this.points[1].x, this.points[1].y),
        new Point(x1, y1),
        new Point(x2, y2),
        new Point(this.points[2].x, this.points[2].y),
      ], this.context);
      nextPanelId += 1;

      const index = panels.indexOf(this);
      if (this.context.readingOrderLeft) {
        panels.splice(index, 0, newPanel);
      } else {
        panels.splice(index + 1, 0, newPanel);
      }

      return newPanel;
    }

    setVisible(visible) {
      this.visible = visible;
    }

    setBordersDynamic(left, leftRandom, up, upRandom, right, rightRandom, down, downRandom) {
      const context = this.context;

      if (!approximatelyEquals(this.points[0].x, context.borderMinX) || !approximatelyEquals(this.points[3].x, context.borderMinX)) {
        left = false;
      }

      if (!approximatelyEquals(this.points[1].x, context.borderMaxX) || !approximatelyEquals(this.points[2].x, context.borderMaxX)) {
        right = false;
      }

      if (!approximatelyEquals(this.points[0].y, context.borderMinY) || !approximatelyEquals(this.points[1].y, context.borderMinY)) {
        up = false;
      }

      if (!approximatelyEquals(this.points[2].y, context.borderMaxY) || !approximatelyEquals(this.points[3].y, context.borderMaxY)) {
        down = false;
      }

      if (context.isLeftPage) {
        right = false;
      } else {
        left = false;
      }

      if (left) {
        if (!leftRandom || Math.random() > 0.5) {
          this.points[0].y += (this.points[1].y - this.points[0].y) * (this.points[0].x / (this.points[0].x - this.points[1].x));
          this.points[0].x = 0;
        }

        if (!leftRandom || Math.random() > 0.5) {
          this.points[3].y += (this.points[3].y - this.points[2].y) * (this.points[3].x / (this.points[2].x - this.points[3].x));
          this.points[3].x = 0;
        }
      }

      if (up) {
        if (!upRandom || Math.random() > 0.5) {
          this.points[0].x += (this.points[3].x - this.points[0].x) * (this.points[0].y / (this.points[0].y - this.points[3].y));
          this.points[0].y = 0;
        }

        if (!upRandom || Math.random() > 0.5) {
          this.points[1].x += (this.points[1].x - this.points[2].x) * (this.points[1].y / (this.points[2].y - this.points[1].y));
          this.points[1].y = 0;
        }
      }

      if (right) {
        if (!rightRandom || Math.random() > 0.5) {
          this.points[1].y += (this.points[0].y - this.points[1].y) * ((context.canvasWidth - this.points[1].x) / (this.points[0].x - this.points[1].x));
          this.points[1].x = context.canvasWidth;
        }

        if (!rightRandom || Math.random() > 0.5) {
          this.points[2].y += (this.points[3].y - this.points[2].y) * ((context.canvasWidth - this.points[2].x) / (this.points[3].x - this.points[2].x));
          this.points[2].x = context.canvasWidth;
        }
      }

      if (down) {
        if (!downRandom || Math.random() > 0.5) {
          this.points[2].x += (this.points[1].x - this.points[2].x) * ((context.canvasHeight - this.points[2].y) / (this.points[1].y - this.points[2].y));
          this.points[2].y = context.canvasHeight;
        }

        if (!downRandom || Math.random() > 0.5) {
          this.points[3].x += (this.points[0].x - this.points[3].x) * ((context.canvasHeight - this.points[3].y) / (this.points[0].y - this.points[3].y));
          this.points[3].y = context.canvasHeight;
        }
      }
    }

    getArea() {
      const lengths = [];
      let semiPerimeter = 0;

      for (let index = 0; index < this.points.length; index += 1) {
        const nextIndex = (index + 1) % this.points.length;
        const dx = (this.points[index].x - this.points[nextIndex].x) / this.context.canvasWidth;
        const dy = (this.points[index].y - this.points[nextIndex].y) / this.context.canvasHeight;
        const distance = Math.sqrt((dx * dx) + (dy * dy));
        lengths.push(distance);
        semiPerimeter += distance / 2;
      }

      let value = 1;
      for (const length of lengths) {
        value *= (semiPerimeter - length);
      }

      return Math.sqrt(value);
    }

    getRenderPoints() {
      const points = this.points.map((point) => point.clone());
      const context = this.context;

      if (!getEqualsWithEpsilon(this.points[0].x, 0, context.borderMinX, context.epsilon)) {
        points[0].x = this.points[0].x + context.margin.x;
      }

      if (!getEqualsWithEpsilon(this.points[0].y, 0, context.borderMinY, context.epsilon)) {
        points[0].y = this.points[0].y + context.margin.y;
      }

      if (!getEqualsWithEpsilon(this.points[1].x, context.canvasWidth, context.borderMaxX, context.epsilon)) {
        points[1].x = this.points[1].x - context.margin.x;
      }

      if (!getEqualsWithEpsilon(this.points[1].y, 0, context.borderMinY, context.epsilon)) {
        points[1].y = this.points[1].y + context.margin.y;
      }

      if (!getEqualsWithEpsilon(this.points[2].x, context.canvasWidth, context.borderMaxX, context.epsilon)) {
        points[2].x = this.points[2].x - context.margin.x;
      }

      if (!getEqualsWithEpsilon(this.points[2].y, context.canvasHeight, context.borderMaxY, context.epsilon)) {
        points[2].y = this.points[2].y - context.margin.y;
      }

      if (!getEqualsWithEpsilon(this.points[3].x, 0, context.borderMinX, context.epsilon)) {
        points[3].x = this.points[3].x + context.margin.x;
      }

      if (!getEqualsWithEpsilon(this.points[3].y, context.canvasHeight, context.borderMaxY, context.epsilon)) {
        points[3].y = this.points[3].y - context.margin.y;
      }

      return points;
    }

    getBounds() {
      const points = this.getRenderPoints();
      let minX = Number.POSITIVE_INFINITY;
      let minY = Number.POSITIVE_INFINITY;
      let maxX = Number.NEGATIVE_INFINITY;
      let maxY = Number.NEGATIVE_INFINITY;

      for (const point of points) {
        minX = Math.min(minX, point.x);
        minY = Math.min(minY, point.y);
        maxX = Math.max(maxX, point.x);
        maxY = Math.max(maxY, point.y);
      }

      return {
        x: minX,
        y: minY,
        width: maxX - minX,
        height: maxY - minY,
      };
    }

    getActiveLayer() {
      return this.layers.find((layer) => layer.id === this.activeLayerId) ?? null;
    }
  }

  initializeInteractions();

  function initializeInteractions() {
    pageSvg.addEventListener("pointerdown", handlePointerDown);
    pageSvg.addEventListener("contextmenu", handleContextMenu);
    window.addEventListener("pointermove", handlePointerMove);
    window.addEventListener("pointerup", handlePointerUp);
    window.addEventListener("pointerdown", handleWindowPointerDown, true);
    window.addEventListener("wheel", handleWheel, { passive: false });
    window.addEventListener("keydown", handleKeyDown);
    window.addEventListener("blur", hideContextMenu);

    document.addEventListener("dragstart", suppressDefaultDrag);
    document.addEventListener("dragenter", handleDocumentDragEnter);
    document.addEventListener("dragover", handleDocumentDragOver);
    document.addEventListener("dragleave", suppressDefaultDrag);
    document.addEventListener("drop", handleDocumentDrop);

    resetLayerButton.addEventListener("click", handleResetLayerClick);
    clearPanelButton.addEventListener("click", handleClearPanelClick);

    if (window.chrome && window.chrome.webview) {
      window.chrome.webview.addEventListener("message", handleHostMessage);
    }
  }

  function generate(settingsFromHost) {
    currentContext = createContext(settingsFromHost);
    panels = [];
    selectedPanelId = null;
    hoverPanelId = null;
    dragState = null;
    contextMenuState = null;
    nextPanelId = 1;
    nextLayerId = 1;
    hideContextMenu();

    panels.push(new Panel(nextPanelId, [
      new Point(currentContext.borderMinX, currentContext.borderMinY),
      new Point(currentContext.borderMaxX, currentContext.borderMinY),
      new Point(currentContext.borderMaxX, currentContext.borderMaxY),
      new Point(currentContext.borderMinX, currentContext.borderMaxY),
    ], currentContext));
    nextPanelId += 1;

    generationAlgorithm1(currentContext);
    render();
  }

  function createContext(settingsFromHost) {
    const trimWidth = normalizeNumber(settingsFromHost.pageWidth, 148);
    const trimHeight = normalizeNumber(settingsFromHost.pageHeight, 210);
    const bleed = normalizeNumber(settingsFromHost.bleed, 5);
    const totalWidthMillimeters = trimWidth + (bleed * 2);
    const totalHeightMillimeters = trimHeight + (bleed * 2);
    const canvasWidth = totalWidthMillimeters * previewScaleFactor;
    const canvasHeight = totalHeightMillimeters * previewScaleFactor;
    const insideWidth = normalizeNumber(settingsFromHost.innerWidth, 125) * previewScaleFactor;
    const insideHeight = normalizeNumber(settingsFromHost.innerHeight, 180) * previewScaleFactor;
    const horizontalMargin = normalizeNumber(settingsFromHost.horizontalMargin, 1) * previewScaleFactor;
    const verticalMargin = normalizeNumber(settingsFromHost.verticalMargin, 2.6) * previewScaleFactor;
    const borderMinX = (canvasWidth - insideWidth) / 2;
    const borderMinY = (canvasHeight - insideHeight) / 2;

    return {
      noOfPanels: clamp(normalizeInteger(settingsFromHost.noOfPanels, 5), 1, 10),
      dynamic: clamp(normalizeInteger(settingsFromHost.dynamic, 5), 0, 10) / 10,
      vanishing: clamp(normalizeInteger(settingsFromHost.vanishing, 5), 0, 10) / 10,
      verticalRatio: clamp(normalizeInteger(settingsFromHost.verticalRatio, 5), 0, 10) / 10,
      goldenRatio: clamp(normalizeInteger(settingsFromHost.goldenRatio, 5), 0, 10) / 10,
      bordering: clamp(normalizeInteger(settingsFromHost.bordering, 5), 0, 10) / 10,
      canvasWidth,
      canvasHeight,
      totalWidthMillimeters,
      totalHeightMillimeters,
      insideWidth,
      insideHeight,
      bleedSize: bleed * previewScaleFactor,
      borderMinX,
      borderMaxX: canvasWidth - borderMinX,
      borderMinY,
      borderMaxY: canvasHeight - borderMinY,
      margin: {
        x: horizontalMargin,
        y: verticalMargin,
      },
      readingOrderLeft: settingsFromHost.readingOrderLeft !== false,
      isLeftPage: settingsFromHost.isLeftPage !== false,
      epsilon: 1,
      strokeWidth: 2,
    };
  }

  function generationAlgorithm1(context) {
    const randomRatio = function () {
      if (Math.random() > context.goldenRatio) {
        return randomRange(0.5 - (context.dynamic * 0.45), 0.5 + (context.dynamic * 0.45));
      }

      return selectRandom([0.618, 0.5, 1 - 0.618]);
    };

    let tries = 0;
    while (panels.length < context.noOfPanels && tries < 1000) {
      tries += 1;
      const panelHeuristics = panels.map((panel, index) => ({
        n: index,
        w: panel.getArea(),
      }));

      const panel = panels[weightedRandom(panelHeuristics)];
      const isHorizontal = Math.random() > context.verticalRatio;
      let startRatio = randomRatio();
      const endRatio = randomRatio();

      if (Math.random() > context.dynamic) {
        startRatio = endRatio;
      }

      panel.split(startRatio, endRatio, isHorizontal);
    }

    for (const panel of panels) {
      panel.setVisible(Math.pow(Math.random(), 0.375) > context.vanishing);
      panel.setBordersDynamic(
        Math.random() < context.bordering,
        Math.pow(Math.random() * context.dynamic, 2) > 0.5,
        Math.random() < context.bordering,
        Math.pow(Math.random() * context.dynamic, 2) > 0.5,
        Math.random() < context.bordering,
        Math.pow(Math.random() * context.dynamic, 2) > 0.5,
        Math.random() < context.bordering,
        Math.pow(Math.random() * context.dynamic, 2) > 0.5,
      );
    }
  }

  function render() {
    pageSvg.replaceChildren();
    pageSvg.setAttribute("viewBox", `0 0 ${currentContext.canvasWidth} ${currentContext.canvasHeight}`);
    pageSvg.setAttribute("width", currentContext.canvasWidth);
    pageSvg.setAttribute("height", currentContext.canvasHeight);

    document.documentElement.style.setProperty("--page-ratio", String(currentContext.canvasWidth / currentContext.canvasHeight));
    sheet.style.aspectRatio = `${currentContext.canvasWidth} / ${currentContext.canvasHeight}`;

    const defs = createSvgElement("defs");
    const background = createSvgElement("rect");
    background.setAttribute("x", "0");
    background.setAttribute("y", "0");
    background.setAttribute("width", String(currentContext.canvasWidth));
    background.setAttribute("height", String(currentContext.canvasHeight));
    background.setAttribute("fill", "#ffffff");

    pageSvg.appendChild(defs);
    pageSvg.appendChild(background);

    for (const panel of getVisiblePanels()) {
      const renderPoints = panel.getRenderPoints();
      const clipPath = createSvgElement("clipPath");
      clipPath.setAttribute("id", `panel-clip-${panel.id}`);

      const clipPolygon = createSvgElement("polygon");
      clipPolygon.setAttribute("points", renderPoints.map((point) => `${point.x},${point.y}`).join(" "));
      clipPath.appendChild(clipPolygon);
      defs.appendChild(clipPath);

      const group = createSvgElement("g");
      pageSvg.appendChild(group);

      const fillPolygon = createSvgElement("polygon");
      fillPolygon.setAttribute("points", renderPoints.map((point) => `${point.x},${point.y}`).join(" "));
      fillPolygon.setAttribute("fill", "#ffffff");
      fillPolygon.setAttribute("stroke", "none");
      group.appendChild(fillPolygon);

      const imageGroup = createSvgElement("g");
      imageGroup.setAttribute("clip-path", `url(#panel-clip-${panel.id})`);
      group.appendChild(imageGroup);

      for (const layer of panel.layers) {
        const image = createSvgElement("image");
        image.setAttribute("href", layer.src);
        image.setAttribute("x", String(layer.x));
        image.setAttribute("y", String(layer.y));
        image.setAttribute("width", String(layer.width * layer.scale));
        image.setAttribute("height", String(layer.height * layer.scale));
        image.setAttribute("draggable", "false");
        image.setAttribute("pointer-events", "none");
        imageGroup.appendChild(image);
      }

      if (panel.id === selectedPanelId) {
        const glowPolygon = createSvgElement("polygon");
        glowPolygon.setAttribute("points", renderPoints.map((point) => `${point.x},${point.y}`).join(" "));
        glowPolygon.setAttribute("fill", "none");
        glowPolygon.setAttribute("stroke", selectedGlowColor);
        glowPolygon.setAttribute("stroke-width", "6");
        glowPolygon.setAttribute("stroke-linejoin", "miter");
        glowPolygon.setAttribute("stroke-opacity", "0.45");
        group.appendChild(glowPolygon);
      } else if (panel.id === hoverPanelId) {
        const hoverPolygon = createSvgElement("polygon");
        hoverPolygon.setAttribute("points", renderPoints.map((point) => `${point.x},${point.y}`).join(" "));
        hoverPolygon.setAttribute("fill", "none");
        hoverPolygon.setAttribute("stroke", hoverGlowColor);
        hoverPolygon.setAttribute("stroke-width", "4");
        hoverPolygon.setAttribute("stroke-linejoin", "miter");
        hoverPolygon.setAttribute("stroke-opacity", "0.35");
        group.appendChild(hoverPolygon);
      }

      const outlinePolygon = createSvgElement("polygon");
      outlinePolygon.setAttribute("points", renderPoints.map((point) => `${point.x},${point.y}`).join(" "));
      outlinePolygon.setAttribute("fill", "none");
      outlinePolygon.setAttribute("stroke", "#000000");
      outlinePolygon.setAttribute("stroke-width", String(currentContext.strokeWidth));
      outlinePolygon.setAttribute("stroke-linejoin", "miter");
      group.appendChild(outlinePolygon);
    }

    printStyles.textContent = `
      @page {
        size: ${currentContext.totalWidthMillimeters}mm ${currentContext.totalHeightMillimeters}mm;
        margin: 0;
      }

      @media print {
        html,
        body {
          width: ${currentContext.totalWidthMillimeters}mm;
          height: ${currentContext.totalHeightMillimeters}mm;
          margin: 0;
        }

        .sheet {
          width: ${currentContext.totalWidthMillimeters}mm;
          height: ${currentContext.totalHeightMillimeters}mm;
        }

        #pageSvg {
          width: ${currentContext.totalWidthMillimeters}mm;
          height: ${currentContext.totalHeightMillimeters}mm;
        }
      }
    `;
  }

  function exportImage(format) {
    if (!currentContext) {
      return "";
    }

    const exportWidth = Math.round((currentContext.totalWidthMillimeters / 25.4) * exportDpi);
    const exportHeight = Math.round((currentContext.totalHeightMillimeters / 25.4) * exportDpi);
    const canvas = renderToCanvas(exportWidth, exportHeight);
    return format === "jpeg"
      ? canvas.toDataURL("image/jpeg", 0.92)
      : canvas.toDataURL("image/png");
  }

  function renderToCanvas(width, height) {
    const canvas = document.createElement("canvas");
    canvas.width = width;
    canvas.height = height;

    const context = canvas.getContext("2d");
    context.fillStyle = "#ffffff";
    context.fillRect(0, 0, width, height);
    context.scale(width / currentContext.canvasWidth, height / currentContext.canvasHeight);

    for (const panel of getVisiblePanels()) {
      const renderPoints = panel.getRenderPoints();

      context.save();
      drawPolygonPath(context, renderPoints);
      context.clip();

      for (const layer of panel.layers) {
        context.drawImage(
          layer.image,
          layer.x,
          layer.y,
          layer.width * layer.scale,
          layer.height * layer.scale,
        );
      }

      context.restore();

      context.save();
      drawPolygonPath(context, renderPoints);
      context.fillStyle = "rgba(255,255,255,0)";
      context.strokeStyle = "#000000";
      context.lineWidth = currentContext.strokeWidth;
      context.lineJoin = "miter";
      context.fill();
      context.stroke();
      context.restore();
    }

    return canvas;
  }

  function drawPolygonPath(context, points) {
    context.beginPath();
    context.moveTo(points[0].x, points[0].y);
    for (let index = 1; index < points.length; index += 1) {
      context.lineTo(points[index].x, points[index].y);
    }

    context.closePath();
  }

  async function handleHostMessage(event) {
    if (!event.data || event.data.type !== "host-drop") {
      return;
    }

    hideContextMenu();

    const hit = getPanelHitFromViewport(event.data.x, event.data.y);
    if (!hit) {
      notifyHost("warning", "StatusDropOnPanelOnly");
      return;
    }

    selectedPanelId = hit.panel.id;
    hoverPanelId = hit.panel.id;

    for (const file of event.data.files) {
      await dropAssetOnPanel(hit.panel, file);
    }

    render();
  }

  function handleDocumentDragEnter(event) {
    if (shouldHandleBrowserDrop(event)) {
      suppressDefaultDrag(event);
    }
  }

  function handleDocumentDragOver(event) {
    if (shouldHandleBrowserDrop(event)) {
      suppressDefaultDrag(event);
    }
  }

  async function handleDocumentDrop(event) {
    if (!shouldHandleBrowserDrop(event)) {
      return;
    }

    suppressDefaultDrag(event);
    hideContextMenu();

    const hit = getPanelHitFromViewport(event.clientX, event.clientY);
    if (!hit) {
      notifyHost("warning", "StatusDropOnPanelOnly");
      return;
    }

    const files = await readBrowserDroppedFiles(event.dataTransfer);
    if (files.length === 0) {
      notifyHost("warning", "StatusDropUnsupported");
      return;
    }

    selectedPanelId = hit.panel.id;
    hoverPanelId = hit.panel.id;

    for (const file of files) {
      await dropAssetOnPanel(hit.panel, file);
    }

    render();
  }

  async function dropAssetOnPanel(panel, file) {
    if (!file || typeof file.dataUrl !== "string" || typeof file.contentType !== "string") {
      notifyHost("warning", "StatusDropUnsupported");
      return;
    }

    if (!file.contentType.startsWith("image/")) {
      notifyHost("warning", "StatusDropUnsupported");
      return;
    }

    const asset = await createAssetFromDrop(file);

    if (panel.layers.length === 0) {
      panel.layers.push(createLayerForPanel(panel, asset, asset.hasAlpha ? "fit-height" : "cover"));
      panel.activeLayerId = panel.layers[panel.layers.length - 1].id;
      panel.allowsOverlay = asset.hasAlpha;
      return;
    }

    if (!panel.allowsOverlay) {
      notifyHost("warning", "StatusPanelLocked");
      return;
    }

    if (!stringEqualsIgnoreCase(asset.contentType, "image/png") || !asset.hasAlpha) {
      notifyHost("warning", "StatusOverlayAlphaOnly");
      return;
    }

    panel.layers.push(createLayerForPanel(panel, asset, "fit-height"));
    panel.activeLayerId = panel.layers[panel.layers.length - 1].id;
  }

  async function createAssetFromDrop(file) {
    const image = await loadImage(file.dataUrl);
    const alphaCanvas = createAlphaCanvas(image);

    return {
      name: file.name,
      contentType: file.contentType,
      src: file.dataUrl,
      image,
      width: image.naturalWidth,
      height: image.naturalHeight,
      hasAlpha: detectAlpha(alphaCanvas, file.contentType),
      alphaCanvas,
    };
  }

  function createLayerForPanel(panel, asset, placement) {
    const bounds = panel.getBounds();
    let scale = placement === "fit-height"
      ? bounds.height / asset.height
      : Math.max(bounds.width / asset.width, bounds.height / asset.height);

    if (!Number.isFinite(scale) || scale <= 0) {
      scale = 1;
    }

    const x = bounds.x + ((bounds.width - (asset.width * scale)) / 2);
    const y = bounds.y + ((bounds.height - (asset.height * scale)) / 2);

    return {
      id: nextLayerId++,
      src: asset.src,
      image: asset.image,
      width: asset.width,
      height: asset.height,
      contentType: asset.contentType,
      hasAlpha: asset.hasAlpha,
      alphaCanvas: asset.alphaCanvas,
      scale,
      initialScale: scale,
      x,
      y,
      initialX: x,
      initialY: y,
    };
  }

  function handlePointerDown(event) {
    if (event.button !== 0) {
      return;
    }

    hideContextMenu();
    const hit = getPanelHitFromViewport(event.clientX, event.clientY);
    if (!hit) {
      selectedPanelId = null;
      hoverPanelId = null;
      dragState = null;
      render();
      return;
    }

    const panel = hit.panel;
    const hitLayer = findTopLayerAtPoint(panel, hit.pageX, hit.pageY);
    if (hitLayer) {
      panel.activeLayerId = hitLayer.id;
    }

    if (selectedPanelId !== panel.id) {
      selectedPanelId = panel.id;
      hoverPanelId = panel.id;
      dragState = null;
      render();
      event.preventDefault();
      return;
    }

    const activeLayer = hitLayer || panel.getActiveLayer();
    if (!activeLayer) {
      render();
      return;
    }

    panel.activeLayerId = activeLayer.id;
    dragState = {
      panelId: panel.id,
      layerId: activeLayer.id,
      startX: hit.pageX,
      startY: hit.pageY,
      originX: activeLayer.x,
      originY: activeLayer.y,
    };

    render();
    event.preventDefault();
  }

  function handlePointerMove(event) {
    const hit = getPanelHitFromViewport(event.clientX, event.clientY);
    const nextHoverPanelId = hit ? hit.panel.id : null;

    if (!dragState) {
      if (hoverPanelId !== nextHoverPanelId) {
        hoverPanelId = nextHoverPanelId;
        render();
      }

      return;
    }

    if (!currentContext) {
      return;
    }

    const pointer = viewportToPageCoordinates(event.clientX, event.clientY);
    if (!pointer) {
      return;
    }

    const panel = panels.find((candidate) => candidate.id === dragState.panelId);
    const layer = panel ? panel.layers.find((candidate) => candidate.id === dragState.layerId) : null;
    if (!panel || !layer) {
      dragState = null;
      return;
    }

    layer.x = dragState.originX + (pointer.x - dragState.startX);
    layer.y = dragState.originY + (pointer.y - dragState.startY);
    hoverPanelId = panel.id;
    render();
    event.preventDefault();
  }

  function handlePointerUp() {
    dragState = null;
  }

  function handleContextMenu(event) {
    const hit = getPanelHitFromViewport(event.clientX, event.clientY);
    if (!hit) {
      hideContextMenu();
      return;
    }

    const panel = hit.panel;
    if (panel.layers.length === 0) {
      selectedPanelId = panel.id;
      hoverPanelId = panel.id;
      hideContextMenu();
      render();
      event.preventDefault();
      return;
    }

    const hitLayer = findTopLayerAtPoint(panel, hit.pageX, hit.pageY);
    const activeLayer = hitLayer || panel.getActiveLayer() || panel.layers[panel.layers.length - 1];
    if (activeLayer) {
      panel.activeLayerId = activeLayer.id;
    }

    selectedPanelId = panel.id;
    hoverPanelId = panel.id;
    showContextMenu(panel.id, event.clientX, event.clientY);
    render();
    event.preventDefault();
  }

  function handleWindowPointerDown(event) {
    if (event.button === 2) {
      return;
    }

    if (panelContextMenu.hidden) {
      return;
    }

    if (panelContextMenu.contains(event.target)) {
      return;
    }

    hideContextMenu();
  }

  function handleKeyDown(event) {
    if (event.key === "Escape") {
      hideContextMenu();
    }
  }

  function handleWheel(event) {
    hideContextMenu();
    const hit = getPanelHitFromViewport(event.clientX, event.clientY);
    if (!hit || hit.panel.id !== selectedPanelId) {
      return;
    }

    const panel = hit.panel;
    const layer = findTopLayerAtPoint(panel, hit.pageX, hit.pageY) || panel.getActiveLayer();
    if (!layer) {
      return;
    }

    panel.activeLayerId = layer.id;
    const factor = event.deltaY < 0 ? zoomStep : (1 / zoomStep);
    zoomLayerAtPoint(layer, factor, hit.pageX, hit.pageY);
    render();
    event.preventDefault();
  }

  function zoomLayerAtPoint(layer, factor, focusX, focusY) {
    const originalScale = layer.scale;
    const nextScale = clamp(layer.scale * factor, 0.05, 40);
    if (approximatelyEquals(originalScale, nextScale)) {
      return;
    }

    const localX = (focusX - layer.x) / originalScale;
    const localY = (focusY - layer.y) / originalScale;
    layer.scale = nextScale;
    layer.x = focusX - (localX * nextScale);
    layer.y = focusY - (localY * nextScale);
  }

  function showContextMenu(panelId, clientX, clientY) {
    contextMenuState = { panelId };
    panelContextMenu.hidden = false;
    panelContextMenu.style.left = "0px";
    panelContextMenu.style.top = "0px";

    const menuWidth = panelContextMenu.offsetWidth;
    const menuHeight = panelContextMenu.offsetHeight;
    const padding = 12;
    const clampedX = clamp(clientX, padding, window.innerWidth - menuWidth - padding);
    const clampedY = clamp(clientY, padding, window.innerHeight - menuHeight - padding);

    panelContextMenu.style.left = `${clampedX}px`;
    panelContextMenu.style.top = `${clampedY}px`;
  }

  function hideContextMenu() {
    contextMenuState = null;
    panelContextMenu.hidden = true;
  }

  function handleResetLayerClick() {
    const panel = getContextMenuPanel();
    if (!panel) {
      hideContextMenu();
      return;
    }

    const layer = panel.getActiveLayer() || panel.layers[panel.layers.length - 1];
    if (!layer) {
      hideContextMenu();
      return;
    }

    layer.x = layer.initialX;
    layer.y = layer.initialY;
    layer.scale = layer.initialScale;
    panel.activeLayerId = layer.id;
    hideContextMenu();
    render();
  }

  function handleClearPanelClick() {
    const panel = getContextMenuPanel();
    if (!panel) {
      hideContextMenu();
      return;
    }

    panel.layers = [];
    panel.activeLayerId = null;
    panel.allowsOverlay = false;
    hideContextMenu();
    render();
  }

  function getContextMenuPanel() {
    return contextMenuState
      ? panels.find((candidate) => candidate.id === contextMenuState.panelId) ?? null
      : null;
  }

  function suppressDefaultDrag(event) {
    if (event.dataTransfer) {
      event.dataTransfer.dropEffect = "copy";
    }

    event.preventDefault();
  }

  function shouldHandleBrowserDrop(event) {
    if (window.chrome && window.chrome.webview) {
      return false;
    }

    if (!event.dataTransfer) {
      return false;
    }

    return event.dataTransfer.files.length > 0;
  }

  async function readBrowserDroppedFiles(dataTransfer) {
    if (!dataTransfer || dataTransfer.files.length === 0) {
      return [];
    }

    const results = [];
    for (const file of Array.from(dataTransfer.files)) {
      if (!file.type.startsWith("image/")) {
        continue;
      }

      const dataUrl = await readFileAsDataUrl(file);
      results.push({
        name: file.name,
        contentType: file.type,
        dataUrl,
      });
    }

    return results;
  }

  function readFileAsDataUrl(file) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = () => {
        if (typeof reader.result === "string") {
          resolve(reader.result);
          return;
        }

        reject(new Error("The dropped file could not be read."));
      };
      reader.onerror = () => {
        reject(reader.error ?? new Error("The dropped file could not be read."));
      };
      reader.readAsDataURL(file);
    });
  }

  function getPanelHitFromViewport(clientX, clientY) {
    const pagePoint = viewportToPageCoordinates(clientX, clientY);
    if (!pagePoint) {
      return null;
    }

    for (const panel of getVisiblePanels()) {
      if (pointInPolygon(pagePoint, panel.getRenderPoints())) {
        return {
          panel,
          pageX: pagePoint.x,
          pageY: pagePoint.y,
        };
      }
    }

    return null;
  }

  function viewportToPageCoordinates(clientX, clientY) {
    const rect = pageSvg.getBoundingClientRect();
    if (clientX < rect.left || clientX > rect.right || clientY < rect.top || clientY > rect.bottom) {
      return null;
    }

    return {
      x: ((clientX - rect.left) / rect.width) * currentContext.canvasWidth,
      y: ((clientY - rect.top) / rect.height) * currentContext.canvasHeight,
    };
  }

  function findTopLayerAtPoint(panel, pageX, pageY) {
    for (let index = panel.layers.length - 1; index >= 0; index -= 1) {
      const layer = panel.layers[index];
      const localX = (pageX - layer.x) / layer.scale;
      const localY = (pageY - layer.y) / layer.scale;

      if (localX < 0 || localY < 0 || localX >= layer.width || localY >= layer.height) {
        continue;
      }

      if (!layer.hasAlpha) {
        return layer;
      }

      const alpha = readAlphaAt(layer.alphaCanvas, Math.floor(localX), Math.floor(localY));
      if (alpha > 16) {
        return layer;
      }
    }

    return null;
  }

  function getVisiblePanels() {
    return panels.filter((panel) => panel.visible);
  }

  function createSvgElement(name) {
    return document.createElementNS(svgNamespace, name);
  }

  function createAlphaCanvas(image) {
    const canvas = document.createElement("canvas");
    canvas.width = image.naturalWidth;
    canvas.height = image.naturalHeight;

    const context = canvas.getContext("2d");
    context.drawImage(image, 0, 0);
    return canvas;
  }

  function detectAlpha(canvas, contentType) {
    if (!stringEqualsIgnoreCase(contentType, "image/png")) {
      return false;
    }

    const context = canvas.getContext("2d");
    const { data } = context.getImageData(0, 0, canvas.width, canvas.height);
    for (let index = 3; index < data.length; index += 4) {
      if (data[index] < 250) {
        return true;
      }
    }

    return false;
  }

  function readAlphaAt(canvas, x, y) {
    const context = canvas.getContext("2d");
    return context.getImageData(x, y, 1, 1).data[3];
  }

  function loadImage(source) {
    return new Promise((resolve, reject) => {
      const image = new Image();
      image.onload = function () {
        resolve(image);
      };
      image.onerror = reject;
      image.src = source;
    });
  }

  function notifyHost(severity, messageKey) {
    window.dispatchEvent(new CustomEvent("panelappstatus", {
      detail: {
        severity,
        messageKey,
      },
    }));

    if (!window.chrome || !window.chrome.webview) {
      return;
    }

    window.chrome.webview.postMessage({
      type: "status",
      severity,
      messageKey,
    });
  }

  function pointInPolygon(point, polygon) {
    let isInside = false;
    for (let currentIndex = 0, previousIndex = polygon.length - 1; currentIndex < polygon.length; previousIndex = currentIndex, currentIndex += 1) {
      const currentPoint = polygon[currentIndex];
      const previousPoint = polygon[previousIndex];

      const intersects = ((currentPoint.y > point.y) !== (previousPoint.y > point.y))
        && (point.x < (((previousPoint.x - currentPoint.x) * (point.y - currentPoint.y)) / (previousPoint.y - currentPoint.y)) + currentPoint.x);

      if (intersects) {
        isInside = !isInside;
      }
    }

    return isInside;
  }

  function normalizeNumber(value, fallback) {
    const parsed = Number.parseFloat(value);
    return Number.isFinite(parsed) ? parsed : fallback;
  }

  function normalizeInteger(value, fallback) {
    return Math.trunc(normalizeNumber(value, fallback));
  }

  function randomRange(minimum, maximum) {
    return (Math.random() * (maximum - minimum)) + minimum;
  }

  function selectRandom(values) {
    return values[Math.floor(Math.random() * values.length)];
  }

  function weightedRandom(values) {
    let total = 0;
    for (const value of values) {
      total += value.w;
    }

    let number = randomRange(0, total);
    for (const value of values) {
      number -= value.w;
      if (number <= 0) {
        return value.n;
      }
    }

    return values[values.length - 1].n;
  }

  function lerp(start, end, amount) {
    return ((1 - amount) * start) + (amount * end);
  }

  function getEqualsWithEpsilon(value, firstCandidate, secondCandidate, epsilon) {
    return Math.abs(value - firstCandidate) < epsilon || Math.abs(value - secondCandidate) < epsilon;
  }

  function approximatelyEquals(first, second) {
    return Math.abs(first - second) < 0.0001;
  }

  function clamp(value, minimum, maximum) {
    return Math.min(Math.max(value, minimum), maximum);
  }

  function stringEqualsIgnoreCase(left, right) {
    return String(left).toLowerCase() === String(right).toLowerCase();
  }

  window.panelApp = {
    exportImage,
    generate,
    getDefaultSettings: () => ({ ...defaultSettings }),
  };

  generate(defaultSettings);
}());
