/* world.jsx — the navigable galaxy canvas.
 *
 * Departures from the internal deck shell (per brief):
 *   • LEFT-drag pans (over empty space OR locked panels) — because panels
 *     are locked by default, a left-drag on one pans rather than moves it.
 *   • NO right-click behaviors at all.
 *   • Double-click empty space = fit-to-window.
 *   • Touch: one finger pans, two fingers pinch-zoom.
 *   • Auto-fit on load + resize so it never feels cramped/tiny.
 *
 * Publishes window.__nbaZoom = { scale, tx, ty } so Panel drag/resize math
 * stays 1:1 at any zoom.
 */
const { useState: useWState, useEffect: useWEffect, useRef: useWRef, useCallback: useWCb } = React;

const ZMIN = 0.3, ZMAX = 2.5;
const clampZ = (s) => Math.max(ZMIN, Math.min(ZMAX, s));
const INTERACTIVE = "button, a, input, textarea, select, label, [contenteditable='true'], .panel-resize, .panel-ctl, .nopan";

function GalaxyWorld({ children, fitSignal }) {
  const [zoom, setZoom] = useWState({ scale: 1, tx: 0, ty: 0 });
  const [panning, setPanning] = useWState(false);
  const clipRef = useWRef(null);
  const worldRef = useWRef(null);
  const fitRef = useWRef({ scale: 1, tx: 0, ty: 0 });

  useWEffect(() => { window.__nbaZoom = zoom; }, [zoom]);

  /* ---- fit-to-window: union all panel rects, scale to fit with margin ---- */
  const computeFit = useWCb(() => {
    const clip = clipRef.current, world = worldRef.current;
    if (!clip || !world) return null;
    const panels = world.querySelectorAll(".dpanel[data-panel-id]");
    if (!panels.length) return null;
    let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
    panels.forEach((p) => {
      const x = p.offsetLeft, y = p.offsetTop, w = p.offsetWidth, h = p.offsetHeight;
      minX = Math.min(minX, x); minY = Math.min(minY, y);
      maxX = Math.max(maxX, x + w); maxY = Math.max(maxY, y + h);
    });
    if (!isFinite(minX)) return null;
    const r = clip.getBoundingClientRect();
    const small = r.width < 760;
    const margin = small ? 16 : 40;
    const availW = r.width - margin * 2;
    const availH = r.height - margin * 2;
    const cw = maxX - minX, ch = maxY - minY;
    // On phones we deliberately allow a closer zoom so visitors land on
    // something readable, not a tiny far-away grid.
    const maxFit = small ? 1.15 : 1.0;
    const scale = clampZ(Math.min(availW / cw, availH / ch, maxFit));
    const tx = margin + (availW - cw * scale) / 2 - minX * scale;
    const ty = margin + (availH - ch * scale) / 2 - minY * scale;
    return { scale, tx, ty };
  }, []);

  const fit = useWCb(() => { const f = computeFit(); if (f) { fitRef.current = f; setZoom(f); } }, [computeFit]);

  useWEffect(() => {
    let r1, r2;
    r1 = requestAnimationFrame(() => { r2 = requestAnimationFrame(fit); });
    // Web fonts reflow panel sizes after first paint — refit when they land,
    // plus a couple of backstops in case layout settles late.
    const t1 = setTimeout(fit, 180);
    const t2 = setTimeout(fit, 500);
    if (document.fonts && document.fonts.ready) document.fonts.ready.then(() => fit());
    const onResize = () => fit();
    window.addEventListener("resize", onResize);
    return () => { cancelAnimationFrame(r1); cancelAnimationFrame(r2); clearTimeout(t1); clearTimeout(t2); window.removeEventListener("resize", onResize); };
  }, [fit]);

  // External "fit" requests (e.g. coming back from simple view).
  useWEffect(() => { if (fitSignal !== undefined) { const id = requestAnimationFrame(fit); return () => cancelAnimationFrame(id); } }, [fitSignal, fit]);

  /* ---- wheel zoom toward cursor ---- */
  useWEffect(() => {
    const el = clipRef.current; if (!el) return;
    const onWheel = (e) => {
      if (e.target.closest(".allow-scroll, input, textarea, select")) return;
      e.preventDefault();
      const r = el.getBoundingClientRect();
      const cx = e.clientX - r.left, cy = e.clientY - r.top;
      setZoom((z) => {
        const factor = e.deltaY < 0 ? 1.12 : 1 / 1.12;
        const ns = clampZ(z.scale * factor);
        if (ns === z.scale) return z;
        const wx = (cx - z.tx) / z.scale, wy = (cy - z.ty) / z.scale;
        return { scale: ns, tx: cx - wx * ns, ty: cy - wy * ns };
      });
    };
    el.addEventListener("wheel", onWheel, { passive: false });
    return () => el.removeEventListener("wheel", onWheel);
  }, []);

  /* ---- LEFT-drag pan (skip interactive + unlocked panels) ---- */
  const beginPan = (e) => {
    if (e.button !== 0) return;
    if (e.target.closest(INTERACTIVE)) return;            // let controls work
    if (e.target.closest(".dpanel.is-unlocked")) return;  // panel will move itself
    const sx = e.clientX, sy = e.clientY;
    const start = zoom;
    let moved = false;
    const onMove = (ev) => {
      const dx = ev.clientX - sx, dy = ev.clientY - sy;
      if (!moved && Math.hypot(dx, dy) < 4) return;
      if (!moved) { moved = true; setPanning(true); }
      setZoom({ scale: start.scale, tx: start.tx + dx, ty: start.ty + dy });
    };
    const onUp = () => {
      setPanning(false);
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("mouseup", onUp);
    };
    window.addEventListener("mousemove", onMove);
    window.addEventListener("mouseup", onUp);
  };

  /* ---- touch: 1 finger pan, 2 finger pinch ---- */
  useWEffect(() => {
    const el = clipRef.current; if (!el) return;
    let mode = null; // 'pan' | 'pinch'
    let sx = 0, sy = 0, startZoom = null;
    let pinchStartDist = 0, pinchMid = { x: 0, y: 0 }, pinchStartScale = 1, pinchAnchor = { x: 0, y: 0 };
    const dist = (t) => Math.hypot(t[0].clientX - t[1].clientX, t[0].clientY - t[1].clientY);
    const mid = (t) => ({ x: (t[0].clientX + t[1].clientX) / 2, y: (t[0].clientY + t[1].clientY) / 2 });

    const onStart = (e) => {
      if (e.touches.length === 1) {
        if (e.target.closest(INTERACTIVE) || e.target.closest(".allow-scroll")) { mode = null; return; }
        if (e.target.closest(".dpanel.is-unlocked")) { mode = null; return; }
        mode = "pan"; sx = e.touches[0].clientX; sy = e.touches[0].clientY; startZoom = zoom;
      } else if (e.touches.length === 2) {
        mode = "pinch";
        pinchStartDist = dist(e.touches);
        const r = el.getBoundingClientRect();
        const m = mid(e.touches);
        pinchMid = { x: m.x - r.left, y: m.y - r.top };
        setZoom((z) => {
          pinchStartScale = z.scale;
          pinchAnchor = { x: (pinchMid.x - z.tx) / z.scale, y: (pinchMid.y - z.ty) / z.scale };
          return z;
        });
      }
    };
    const onMove = (e) => {
      if (mode === "pan" && e.touches.length === 1 && startZoom) {
        e.preventDefault();
        const dx = e.touches[0].clientX - sx, dy = e.touches[0].clientY - sy;
        setZoom({ scale: startZoom.scale, tx: startZoom.tx + dx, ty: startZoom.ty + dy });
      } else if (mode === "pinch" && e.touches.length === 2) {
        e.preventDefault();
        const ns = clampZ(pinchStartScale * (dist(e.touches) / (pinchStartDist || 1)));
        setZoom({ scale: ns, tx: pinchMid.x - pinchAnchor.x * ns, ty: pinchMid.y - pinchAnchor.y * ns });
      }
    };
    const onEnd = (e) => { if (e.touches.length === 0) mode = null; else if (e.touches.length === 1) { mode = "pan"; sx = e.touches[0].clientX; sy = e.touches[0].clientY; startZoom = zoom; } };
    el.addEventListener("touchstart", onStart, { passive: false });
    el.addEventListener("touchmove", onMove, { passive: false });
    el.addEventListener("touchend", onEnd);
    return () => { el.removeEventListener("touchstart", onStart); el.removeEventListener("touchmove", onMove); el.removeEventListener("touchend", onEnd); };
  }, [zoom]);

  const resetActual = () => setZoom((z) => {
    const r = clipRef.current.getBoundingClientRect();
    const cx = r.width / 2, cy = r.height / 2;
    const wx = (cx - z.tx) / z.scale, wy = (cy - z.ty) / z.scale;
    return { scale: 1, tx: cx - wx, ty: cy - wy };
  });

  return (
    <>
      <div ref={clipRef}
           className={"world-clip" + (panning ? " is-panning" : "")}
           onMouseDown={beginPan}
           onContextMenu={(e) => e.preventDefault()}
           onDoubleClick={(e) => { if (!e.target.closest(".dpanel")) fit(); }}>
        <div ref={worldRef} className="world"
             style={{ transform: `translate(${Math.round(zoom.tx)}px, ${Math.round(zoom.ty)}px) scale(${zoom.scale})`, transformOrigin: "0 0" }}>
          {children}
        </div>
      </div>
      <ZoomControl zoom={zoom} onFit={fit} onActual={resetActual}/>
    </>
  );
}

function ZoomControl({ zoom, onFit, onActual }) {
  const pct = Math.round(zoom.scale * 100);
  return (
    <div className="zoomctl" title="Wheel to zoom · drag to pan · double-click empty space to fit">
      <button className="zoomctl__btn" onClick={onFit} title="Fit to window" aria-label="Fit to window">
        <svg width="13" height="13" viewBox="0 0 12 12" fill="none"><path d="M1 4V1h3M11 4V1H8M1 8v3h3M11 8v3H8" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round"/></svg>
      </button>
      <span className="zoomctl__pct">{pct}%</span>
      <button className="zoomctl__btn" onClick={onActual} title="Actual size (100%)" aria-label="Actual size">100</button>
    </div>
  );
}

window.GalaxyWorld = GalaxyWorld;
