// pumlv viewer — pan, zoom (including cursor-centred wheel), minimap, SSE reload
(function () {
  "use strict";

  // ── State ──────────────────────────────────────────────────────────────────

  let state = { scale: 1, tx: 0, ty: 0 };
  let baseSize = { width: 0, height: 0 };

  let panToolActive = false;
  let spacePressed  = false;
  let isDragging    = false;
  let dragStart     = { x: 0, y: 0, tx: 0, ty: 0 };

  let minimapVisible  = false;
  let sidebarVisible  = true;
  let activeFile      = null;
  let initialFileLoaded = false;

  // ── DOM getters ────────────────────────────────────────────────────────────

  const getCanvas  = () => document.getElementById("canvas");
  const getStage   = () => document.getElementById("svg-stage");
  const getSvg     = () => getStage()?.querySelector("svg");

  // ── Stage wrapping ─────────────────────────────────────────────────────────
  // The SVG returned by /api/render is placed directly inside #canvas.
  // We wrap it in a #svg-stage div so we can apply a CSS transform to it
  // without the SVG's own coordinate system interfering.

  function ensureStageWrapped() {
    if (getStage()) return;
    const canvas = getCanvas();
    if (!canvas) return;
    const svg = canvas.querySelector("svg");
    if (!svg) return;
    const stage = document.createElement("div");
    stage.id = "svg-stage";
    canvas.innerHTML = "";
    stage.appendChild(svg);
    canvas.appendChild(stage);
  }

  // ── Base size ──────────────────────────────────────────────────────────────

  function computeBaseSize() {
    const svg = getSvg();
    if (!svg) return;
    const vb = svg.viewBox?.baseVal;
    if (vb?.width && vb?.height) {
      baseSize.width  = vb.width;
      baseSize.height = vb.height;
      return;
    }
    try {
      const bb = svg.getBBox();
      baseSize.width  = bb.width  || svg.clientWidth  || 800;
      baseSize.height = bb.height || svg.clientHeight || 600;
    } catch {
      baseSize.width  = svg.clientWidth  || 800;
      baseSize.height = svg.clientHeight || 600;
    }
  }

  // ── Center offsets ─────────────────────────────────────────────────────────
  // The stage is centred in the canvas as the "zero" pan position.
  // User pan is stored in state.tx / state.ty as an offset from that centre.

  function centerOffsetForScale(scale) {
    const canvas = getCanvas();
    if (!canvas || !baseSize.width || !baseSize.height) return { cx: 0, cy: 0 };
    return {
      cx: (canvas.clientWidth  - baseSize.width  * scale) / 2,
      cy: (canvas.clientHeight - baseSize.height * scale) / 2,
    };
  }

  // ── Transform ──────────────────────────────────────────────────────────────

  function applyTransform() {
    ensureStageWrapped();
    const stage = getStage();
    if (!stage) return;
    const { cx, cy } = centerOffsetForScale(state.scale);
    stage.style.transform =
      `translate(${cx + state.tx}px, ${cy + state.ty}px) scale(${state.scale})`;
    stage.classList.toggle("pan-enabled",  panToolActive || spacePressed);
    stage.classList.toggle("pan-dragging", isDragging);
    updateMinimap();
  }

  // ── Zoom ───────────────────────────────────────────────────────────────────

  /**
   * @param {number} factor  Multiply current scale by this value.
   * @param {number} [mx]    Zoom origin X relative to the canvas element.
   * @param {number} [my]    Zoom origin Y relative to the canvas element.
   *
   * When mx/my are supplied (wheel events) the diagram point under the cursor
   * stays fixed.  Without them (button clicks) the centre stays fixed.
   */
  function zoom(factor, mx, my) {
    const prevScale = state.scale;
    state.scale = Math.max(0.05, Math.min(50, prevScale * factor));

    if (mx !== undefined && my !== undefined) {
      // Compute diagram-space coord of the cursor at the previous scale.
      const { cx: prevCx, cy: prevCy } = centerOffsetForScale(prevScale);
      const diagX = (mx - (prevCx + state.tx)) / prevScale;
      const diagY = (my - (prevCy + state.ty)) / prevScale;

      // Adjust tx/ty so that same diagram coord maps back to (mx, my).
      const { cx: newCx, cy: newCy } = centerOffsetForScale(state.scale);
      state.tx = mx - newCx - diagX * state.scale;
      state.ty = my - newCy - diagY * state.scale;
    }

    applyTransform();
  }

  function fitToWindow() {
    const canvas = getCanvas();
    if (!canvas || !baseSize.width || !baseSize.height) return;
    const sx = canvas.clientWidth  / baseSize.width;
    const sy = canvas.clientHeight / baseSize.height;
    state.scale = Math.min(sx, sy) * 0.95;
    state.tx = 0;
    state.ty = 0;
    applyTransform();
  }

  // ── Mouse drag pan ─────────────────────────────────────────────────────────

  function onStageMouseDown(e) {
    if (e.button !== 0 || !(panToolActive || spacePressed)) return;
    isDragging = true;
    dragStart  = { x: e.clientX, y: e.clientY, tx: state.tx, ty: state.ty };
    e.preventDefault();
    applyTransform();
    window.addEventListener("mousemove", onMouseMove);
    window.addEventListener("mouseup",   onMouseUp);
  }

  function onMouseMove(e) {
    if (!isDragging) return;
    state.tx = dragStart.tx + (e.clientX - dragStart.x);
    state.ty = dragStart.ty + (e.clientY - dragStart.y);
    applyTransform();
  }

  function onMouseUp() {
    if (!isDragging) return;
    isDragging = false;
    applyTransform();
    window.removeEventListener("mousemove", onMouseMove);
    window.removeEventListener("mouseup",   onMouseUp);
  }

  // ── Wheel zoom ─────────────────────────────────────────────────────────────

  function onCanvasWheel(e) {
    e.preventDefault();
    const rect = getCanvas().getBoundingClientRect();
    const mx = e.clientX - rect.left;
    const my = e.clientY - rect.top;
    zoom(e.deltaY < 0 ? 1.1 : 1 / 1.1, mx, my);
  }

  // ── Minimap ────────────────────────────────────────────────────────────────

  const MM_W = 168, MM_H = 112; // content area in px

  function initMinimap() {
    const svg = getSvg();
    const mmContent = document.getElementById("minimap-content");
    if (!svg || !mmContent || !baseSize.width || !baseSize.height) return;

    const s = Math.min(MM_W / baseSize.width, MM_H / baseSize.height);
    mmContent.innerHTML = "";
    const g = document.createElementNS("http://www.w3.org/2000/svg", "g");
    g.setAttribute("transform", `scale(${s})`);
    g.innerHTML = svg.innerHTML;
    // Remove pointer-events so the minimap is not interactive
    g.style.pointerEvents = "none";
    mmContent.appendChild(g);
  }

  function updateMinimap() {
    if (!minimapVisible) return;
    const canvas = getCanvas();
    const mmVp   = document.getElementById("minimap-vp");
    if (!canvas || !mmVp || !baseSize.width || !baseSize.height) return;

    const mmScale = Math.min(MM_W / baseSize.width, MM_H / baseSize.height);
    const { cx, cy } = centerOffsetForScale(state.scale);

    // Visible rectangle in diagram coordinates
    const visX = -(cx + state.tx) / state.scale;
    const visY = -(cy + state.ty) / state.scale;
    const visW =  canvas.clientWidth  / state.scale;
    const visH =  canvas.clientHeight / state.scale;

    mmVp.setAttribute("x",      String(visX * mmScale));
    mmVp.setAttribute("y",      String(visY * mmScale));
    mmVp.setAttribute("width",  String(visW * mmScale));
    mmVp.setAttribute("height", String(visH * mmScale));
  }

  // ── SVG export ─────────────────────────────────────────────────────────────

  function exportSvg() {
    const svg = getSvg();
    if (!svg) return;
    const clone = svg.cloneNode(true);
    clone.setAttribute("xmlns",       "http://www.w3.org/2000/svg");
    clone.setAttribute("xmlns:xlink", "http://www.w3.org/1999/xlink");
    if (!clone.viewBox?.baseVal?.width) {
      clone.setAttribute("viewBox", `0 0 ${baseSize.width} ${baseSize.height}`);
    }
    const xml  = new XMLSerializer().serializeToString(clone);
    const blob = new Blob([xml], { type: "image/svg+xml;charset=utf-8" });
    const url  = URL.createObjectURL(blob);
    const a    = document.createElement("a");
    a.href     = url;
    a.download = (activeFile?.split("/").pop() ?? "diagram").replace(/\.puml$/, ".svg");
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
    URL.revokeObjectURL(url);
  }

  // ── File loading ───────────────────────────────────────────────────────────

  // resetView: true  → fit-to-window after load (initial / manual click)
  // resetView: false → keep current zoom + pan (auto-reload via SSE)
  async function loadFile(relPath, { resetView = true } = {}) {
    if (!relPath) return;
    activeFile = relPath;

    const label = document.getElementById("active-file-label");
    if (label) label.textContent = relPath;
    updateSidebarActive();

    const canvas = getCanvas();
    const placeholder = document.getElementById("canvas-placeholder");
    if (placeholder) placeholder.style.display = "none";

    try {
      const resp = await fetch(
        `/api/render?f=${encodeURIComponent(relPath)}`,
        { cache: "no-store" },
      );
      if (!resp.ok) {
        const msg = await resp.text();
        canvas.innerHTML =
          `<div style="padding:16px;color:#c00;font-family:monospace;white-space:pre-wrap">${escapeHtml(msg)}</div>`;
        return;
      }
      const svg = await resp.text();
      canvas.innerHTML = svg;
      ensureStageWrapped();
      computeBaseSize();
      if (resetView) {
        state = { scale: 1, tx: 0, ty: 0 };
        fitToWindow();
      } else {
        applyTransform(); // re-apply existing zoom/pan to the new stage element
      }
      attachStageHandlers();
      if (minimapVisible) { initMinimap(); updateMinimap(); }
    } catch (err) {
      canvas.innerHTML =
        `<div style="padding:16px;color:#c00">Failed to load ${escapeHtml(relPath)}: ${escapeHtml(String(err))}</div>`;
    }
  }

  function attachStageHandlers() {
    const stage = getStage();
    if (!stage || stage._pumlvBound) return;
    stage._pumlvBound = true;
    stage.addEventListener("mousedown", onStageMouseDown);
  }

  // ── Sidebar path display ──────────────────────────────────────────────────
  // Show the filename plus the nearest parent folder if it fits in the
  // available pixel width.  Anything dropped from the left gets a "…/" prefix.

  const _mc   = document.createElement("canvas");
  const _mctx = _mc.getContext("2d");

  function _displayPath(path, availWidth) {
    const parts = path.split("/");
    if (parts.length === 1) return path;            // root-level file, nothing to shorten

    const filename = parts[parts.length - 1];
    const parent   = parts[parts.length - 2];
    const dropped  = parts.length > 2;             // there are grandparent dirs above

    // Preferred: (…/)parent/filename
    const withParent = (dropped ? "…/" : "") + parent + "/" + filename;
    if (_mctx.measureText(withParent).width <= availWidth) return withParent;

    // Fallback: …/filename — always show … because we're hiding the parent now
    return "…/" + filename;
  }

  function shortenSidebarPaths() {
    const items = document.querySelectorAll("#file-list .file-item");
    if (!items.length) return;

    // Read the font once from a real element so canvas measurement matches rendering.
    _mctx.font = getComputedStyle(items[0]).font || "13px system-ui, sans-serif";

    const cs    = getComputedStyle(items[0]);
    const avail = items[0].clientWidth
                  - parseFloat(cs.paddingLeft)
                  - parseFloat(cs.paddingRight);

    items.forEach((el) => {
      const path = el.dataset.path || "";
      if (path.includes("/")) el.textContent = _displayPath(path, avail);
    });
  }

  function updateSidebarActive() {
    document.querySelectorAll(".file-item").forEach((el) => {
      el.classList.toggle("active", el.dataset.path === activeFile);
    });
  }

  // ── File filter ─────────────────────────────────────────────────────────────

  function setupFileFilter() {
    document.getElementById("file-filter")?.addEventListener("input", function () {
      const q = this.value.toLowerCase();
      document.querySelectorAll(".file-item").forEach((el) => {
        const path = el.dataset.path ?? "";
        el.style.display = path.toLowerCase().includes(q) ? "" : "none";
      });
    });
  }

  // ── HTMX hook: file list swapped ────────────────────────────────────────────

  document.body.addEventListener("htmx:afterSettle", (e) => {
    if (e.target?.id !== "file-list") return;

    // Delegated clicks wired once on the container.
    if (!e.target._pumlvDelegated) {
      e.target._pumlvDelegated = true;
      e.target.addEventListener("click", (ev) => {
        const item = ev.target.closest(".file-item");
        if (item) {
          ev.preventDefault();
          loadFile(item.dataset.path);
        }
      });
    }

    updateSidebarActive();
    shortenSidebarPaths();

    // Load the initial file from the <meta> tag on first settle.
    if (!initialFileLoaded) {
      const meta = document.querySelector('meta[name="pumlv-initial-file"]');
      const f    = meta?.getAttribute("content") || "";
      if (f) loadFile(f);
      initialFileLoaded = true; // don't reload on subsequent list refreshes
    }
  });

  // ── Toolbar ────────────────────────────────────────────────────────────────

  function setupToolbar() {
    document.getElementById("btn-zoom-in") ?.addEventListener("click", () => zoom(1.2));
    document.getElementById("btn-zoom-out")?.addEventListener("click", () => zoom(1 / 1.2));
    document.getElementById("btn-fit")     ?.addEventListener("click", fitToWindow);
    document.getElementById("btn-export")  ?.addEventListener("click", exportSvg);

    document.getElementById("btn-pan")?.addEventListener("click", () => {
      panToolActive = !panToolActive;
      document.getElementById("btn-pan").classList.toggle("active", panToolActive);
      applyTransform();
    });

    document.getElementById("btn-minimap")?.addEventListener("click", () => {
      minimapVisible = !minimapVisible;
      const mm = document.getElementById("minimap");
      mm.style.display = minimapVisible ? "flex" : "none";
      document.getElementById("btn-minimap").classList.toggle("active", minimapVisible);
      if (minimapVisible) { initMinimap(); updateMinimap(); }
    });

    document.getElementById("btn-sidebar")?.addEventListener("click", () => {
      sidebarVisible = !sidebarVisible;
      document.body.classList.toggle("sidebar-hidden", !sidebarVisible);
      document.getElementById("btn-sidebar").classList.toggle("active", sidebarVisible);
      // Refit after layout shift
      requestAnimationFrame(() => { if (getSvg()) applyTransform(); });
    });
  }

  // ── Keyboard ───────────────────────────────────────────────────────────────

  function setupKeyboard() {
    document.addEventListener("keydown", (e) => {
      if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return;
      switch (e.key) {
        case " ":
          e.preventDefault();
          spacePressed = true;
          applyTransform();
          break;
        case "+": case "=": zoom(1.2); break;
        case "-":           zoom(1 / 1.2); break;
        case "f": case "F": fitToWindow(); break;
        case "p": case "P":
          panToolActive = !panToolActive;
          document.getElementById("btn-pan")?.classList.toggle("active", panToolActive);
          applyTransform();
          break;
      }
    });

    document.addEventListener("keyup", (e) => {
      if (e.key === " ") { spacePressed = false; applyTransform(); }
    });
  }

  // ── SSE ─────────────────────────────────────────────────────────────────────

  function setupSSE() {
    const dot = document.getElementById("sse-dot");
    const src = new EventSource("/api/events");

    src.addEventListener("change", (e) => {
      const changed = e.data;
      const autoReload = document.getElementById("chk-auto-reload")?.checked ?? true;
      if (changed === activeFile && autoReload) loadFile(activeFile, { resetView: false });
      // Always refresh the file list (handles create / remove)
      htmx.ajax("GET", "/api/files", { target: "#file-list", swap: "innerHTML" });
    });

    src.onopen  = () => dot?.classList.add("connected");
    src.onerror = () => dot?.classList.remove("connected");
  }

  // ── Window resize ──────────────────────────────────────────────────────────

  window.addEventListener("resize", () => {
    if (getSvg()) applyTransform();
  });

  // ── Wheel listener ─────────────────────────────────────────────────────────

  getCanvas()?.addEventListener("wheel", onCanvasWheel, { passive: false });

  // ── Helpers ────────────────────────────────────────────────────────────────

  function escapeHtml(s) {
    return String(s)
      .replace(/&/g, "&amp;")
      .replace(/</g, "&lt;")
      .replace(/>/g, "&gt;");
  }

  // ── Init ───────────────────────────────────────────────────────────────────

  setupToolbar();
  setupKeyboard();
  setupFileFilter();
  setupSSE();
})();
