/* Portfolio — LIGHTBOX (the full-screen picture view).
   · files are fetched and handed to <img> as blob: tokens, so the path/filename never shows
   · a small LRU of blobs keeps neighbours ready; ±1 ±2 ±3 are prefetched by priority
   · desktop: click to zoom (frame stays put, picture pans inside), drag sideways to change work
   · phone  : pinch to zoom, swipe sideways to change work, swipe up to close
   · prev/next wrap around the list handed in via the `list` prop */
const { HUIconButton, HUMuseumLabel } = window;
const { HU_ASSET: ASSET } = window;

/* ---------------- Lightbox ---------------- */
const LB_LOADED = new Set();
/* The picture is handed to the browser as an in-memory object, so the <img> carries an opaque
   one-time token (blob:…) instead of the file path. Files on disk keep their work-id names. */
const LB_BLOBS = new Map();
const LB_ORDER = [];
function lbToken(src) {
  if (LB_BLOBS.has(src)) return Promise.resolve(LB_BLOBS.get(src));
  const p = fetch(src).then((r) => r.blob()).then((b) => {
    const url = URL.createObjectURL(b);
    LB_BLOBS.set(src, url);
    LB_ORDER.push(src);
    while (LB_ORDER.length > 14) { const old = LB_ORDER.shift(); const u = LB_BLOBS.get(old); if (u) { URL.revokeObjectURL(u); LB_BLOBS.delete(old); LB_LOADED.delete(old); } }
    LB_LOADED.add(src);
    return url;
  }).catch(() => src);
  LB_BLOBS.set(src, p);
  return p;
}
// start fetching a full-size file before it is asked for (hover / press on a card)
window.HU_PREFETCH = (img) => { if (img) lbToken(`${ASSET}/works-full/${img}`); };
function Lightbox({ work, lang, list, onClose, onPrev, onNext }) {
  const goRef = React.useRef(null);
  // opening grace: the nav/close zones appear right under the cursor that clicked the card —
  // swallow stray extra clicks (double-click, chattering mouse) for the first beat
  const bornRef = React.useRef(null);
  if (bornRef.current === null) bornRef.current = performance.now();
  const settledClick = () => performance.now() - bornRef.current > 400;
  React.useEffect(() => {
    const h = (e) => { if (e.key === "Escape") onClose(); if (e.key === "ArrowLeft" && settledClick()) goRef.current(-1); if (e.key === "ArrowRight" && settledClick()) goRef.current(1); };
    window.addEventListener("keydown", h);
    return () => window.removeEventListener("keydown", h);
  }, [onClose]);
  // lock the page behind the lightbox so the works grid can't scroll underneath
  React.useEffect(() => {
    const prev = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => { document.body.style.overflow = prev; };
  }, []);
  if (!work) return null;
  // preload the neighbours (three each way) so switching is instant; the nearest ones go first
  React.useEffect(() => {
    const arr = (list && list.length ? list : window.HU_DATA.works);
    const i = arr.findIndex((w) => w.id === work.id);
    if (i < 0) return;
    let cancelled = false;
    const queue = [1, -1, 2, -2, 3, -3].map((off) => arr[(i + off + arr.length) % arr.length]);
    (async () => {
      for (const w of queue) {
        if (cancelled) return;
        await lbToken(`${ASSET}/works-full/${w.img}`);
      }
    })();
    return () => { cancelled = true; };
  }, [work.id, list]);
  const fullSrc = `${ASSET}/works-full/${work.img}`;
  const [token, setToken] = React.useState(() => { const v = LB_BLOBS.get(fullSrc); return typeof v === "string" ? v : null; });
  const [dx, setDx] = React.useState(0);
  const [zoom, setZoom] = React.useState(1);
  const [nat, setNat] = React.useState(null);
  const [ready, setReady] = React.useState(false);
  const [entered, setEntered] = React.useState(false);
  // reset synchronously on work change — an effect would run after the ref and blank a cached image
  const shown = React.useRef(work.id);
  const instant = React.useRef(LB_LOADED.has(fullSrc));
  if (shown.current !== work.id) { shown.current = work.id; instant.current = LB_LOADED.has(fullSrc); if (zoom !== 1) setZoom(1); if (nat) setNat(null); setReady(false); setEntered(false); const v = LB_BLOBS.get(fullSrc); setToken(typeof v === "string" ? v : null); }
  React.useEffect(() => {
    let live = true;
    Promise.resolve(lbToken(fullSrc)).then((url) => { if (live) setToken(url); });
    return () => { live = false; };
  }, [fullSrc]);
  const [dy, setDy] = React.useState(0);
  const [snap, setSnap] = React.useState(false);
  const [dir, setDir] = React.useState(0); // enter direction: 1 = from right (next), -1 = from left (prev)
  const touch = React.useRef(null);
  const mobile = () => window.matchMedia("(pointer: coarse)").matches &&
    (window.innerWidth <= 680 || window.innerHeight <= 500);
  const go = (nd) => { setDir(nd); setSnap(false); setDx(0); setDy(0); nd > 0 ? onNext() : onPrev(); };
  goRef.current = go;
  const onTouchStart = (e) => { if (!mobile()) return; const t = e.touches[0]; touch.current = { x: t.clientX, y: t.clientY, axis: null }; setSnap(false); };
  const onTouchMove = (e) => {
    if (!mobile() || !touch.current) return;
    const t = e.touches[0], ddx = t.clientX - touch.current.x, ddy = t.clientY - touch.current.y;
    if (!touch.current.axis && (Math.abs(ddx) > 14 || Math.abs(ddy) > 14)) touch.current.axis = Math.abs(ddx) > Math.abs(ddy) ? "x" : "y";
    if (touch.current.axis === "x") setDx(ddx);
    else if (touch.current.axis === "y") setDy(Math.min(0, ddy)); // follow upward only
  };
  const onTouchEnd = (e) => {
    if (!touch.current) return;
    const t = e.changedTouches[0], ddx = t.clientX - touch.current.x, ddy = t.clientY - touch.current.y, axis = touch.current.axis;
    touch.current = null;
    if (axis === "y" && ddy < -70) { onClose(); return; }        // swipe up to exit
    if (axis === "x" && Math.abs(ddx) > 80) { go(ddx < 0 ? 1 : -1); return; }
    setSnap(true); setDx(0); setDy(0);
  };
  // mobile: tap the left/right edge of the image to change work (no arrows)
  // desktop: click the image to zoom to 100% (scroll to pan), click again to fit
  const onImgClick = (e) => {
    e.stopPropagation();
    if (!mobile()) {
      holdPoint(e.clientX, e.clientY);
      setZoom((z) => (z > 1 ? 1 : 1.6));
      return;
    }
    const r = e.currentTarget.getBoundingClientRect();
    const x = e.clientX - r.left;
    if (x < r.width * 0.22) go(-1);
    else if (x > r.width * 0.78) go(1);
  };
  // desktop: drag the picture sideways to change work (wraps at both ends, like the arrows)
  const drag = React.useRef(null);
  const onDragStart = (e) => {
    if (mobile() || zoom > 1 || (e.button != null && e.button !== 0)) return;
    drag.current = { x: e.clientX, moved: 0 };
    setSnap(false);
  };
  const onDragMove = (e) => {
    const d = drag.current;
    if (!d) return;
    const ddx = e.clientX - d.x;
    d.moved = Math.abs(ddx);
    if (d.moved > 4) setDx(ddx);
  };
  const onDragEnd = (e) => {
    const d = drag.current;
    if (!d) return;
    drag.current = null;
    const ddx = e.clientX - d.x;
    if (Math.abs(ddx) > 120) { go(ddx < 0 ? 1 : -1); return; }
    if (d.moved > 4) { setSnap(true); setDx(0); }
  };
  // reserve the frame from the measured pixel ratio so the caption never shifts while the full-res file loads
  const ar = ((window.HU_RATIOS && window.HU_RATIOS[work.img]) || "").replace("/", " / ");
  const capH = mobile() ? "96svh" : "min(88vh, 100vh - 132px)";
  const capW = mobile() ? "96vw" : "92vw";
  // the frame grows with zoom — the caption sits under it and moves with it as one block
  const boxStyle = {
    aspectRatio: ar || (nat ? `${nat[0]} / ${nat[1]}` : "4 / 5"),
    height: `calc(${capH} * ${zoom})`,
    maxWidth: `calc(${capW} * ${zoom})`,
  };
  const boxRef = React.useRef(null);
  const wrapRef = React.useRef(null);
  const prevZoom = React.useRef(zoom);
  // grow around the point you are looking at, so it reads as zooming into the picture
  const zoomAnchor = React.useRef(null);
  // remember which point of the picture should stay under the same spot on screen
  const holdPoint = (clientX, clientY) => {
    const b = boxRef.current, el = wrapRef.current;
    if (!b || !el) return;
    const r = b.getBoundingClientRect();
    const er = el.getBoundingClientRect();
    const vx = clientX == null ? er.left + el.clientWidth / 2 : clientX;
    const vy = clientY == null ? er.top + el.clientHeight / 2 : clientY;
    const cl = (v) => Math.max(0, Math.min(1, v));
    zoomAnchor.current = { fx: cl((vx - r.left) / r.width), fy: cl((vy - r.top) / r.height), vx, vy };
  };
  React.useLayoutEffect(() => {
    const el = wrapRef.current, b = boxRef.current, a = zoomAnchor.current;
    prevZoom.current = zoom;
    zoomAnchor.current = null;
    if (!el || !b || !a) return;
    const r = b.getBoundingClientRect();
    el.scrollLeft += (r.left + a.fx * r.width) - a.vx;
    el.scrollTop += (r.top + a.fy * r.height) - a.vy;
  }, [zoom]);
  React.useEffect(() => {
    if (!ready || entered) return;
    const r = requestAnimationFrame(() => requestAnimationFrame(() => setEntered(true)));
    return () => cancelAnimationFrame(r);
  }, [ready, entered, work.id]);
  const enterDx = entered ? 0 : (dir > 0 ? 56 : dir < 0 ? -56 : 0);
  const slideStyle = {
    transform: `translate(${dx + enterDx}px, ${dy}px)`,
    opacity: entered ? (dy ? Math.max(0.3, 1 + dy / 420) : 1) : 0,
    transition: snap || !entered || dx === 0 ? "transform .34s var(--ease-out), opacity .34s var(--ease-out)" : "none",
  };
  return (
    <div ref={wrapRef} className={"hu-lb" + (zoom > 1 ? " is-zoom" : "")} onClick={() => { if (settledClick()) onClose(); }}>
      <div className="hu-lb__top">
        <span className="hu-lb__zoombtns">
          <HUIconButton label="Zoom out" onink onClick={(e) => { e.stopPropagation(); holdPoint(); setZoom((z) => Math.max(1, +(z - 0.4).toFixed(2))); }}><i data-lucide="zoom-out"></i></HUIconButton>
          <HUIconButton label="Zoom in" onink onClick={(e) => { e.stopPropagation(); holdPoint(); setZoom((z) => Math.min(4, +(z + 0.4).toFixed(2))); }}><i data-lucide="zoom-in"></i></HUIconButton>
        </span>
        <HUIconButton label="Close" onink onClick={onClose}><i data-lucide="x"></i></HUIconButton>
      </div>
      <button className="hu-lb__nav hu-lb__nav--prev" aria-label="Previous"
        onClick={(e) => { e.stopPropagation(); if (settledClick()) go(-1); }}><i data-lucide="arrow-left"></i></button>
      <figure className={"hu-lb__fig" + (zoom > 1 || mobile() ? "" : " is-draggable")} onClick={(e) => e.stopPropagation()}
        onTouchStart={onTouchStart} onTouchMove={onTouchMove} onTouchEnd={onTouchEnd}
        onPointerDown={onDragStart} onPointerMove={onDragMove} onPointerUp={onDragEnd} onPointerCancel={onDragEnd}>
        <span key={work.id} className="hu-lb__slide" style={slideStyle}>
          <span ref={boxRef} className={"hu-lb__box" + (ready ? " is-ready" : "") + (instant.current ? " is-instant" : "")} style={boxStyle} onClick={(e) => { if (drag.current || dx) { e.stopPropagation(); return; } onImgClick(e); }}>
            <span className="hu-lb__pan">
              <img className="hu-lb__full" src={token || undefined} alt={work.en} decoding="async"
                ref={(im) => { if (im && im.complete && im.naturalWidth) { if (!nat) setNat([im.naturalWidth, im.naturalHeight]); if (!ready) setReady(true); } }}
                onError={(e) => { const im = e.target; if (im.dataset.fb) return; im.dataset.fb = "1"; im.src = `${ASSET}/works/${work.img}`; }}
                onLoad={(e) => { const im = e.target; if (im.naturalWidth) setNat([im.naturalWidth, im.naturalHeight]); (im.decode ? im.decode().catch(() => {}) : Promise.resolve()).then(() => setReady(true)); }} />
            </span>
          </span>
        </span>
        <figcaption><HUMuseumLabel work={work} lang={lang} onink /></figcaption>
      </figure>
      <button className="hu-lb__nav hu-lb__nav--next" aria-label="Next"
        onClick={(e) => { e.stopPropagation(); if (settledClick()) go(1); }}><i data-lucide="arrow-right"></i></button>
    </div>
  );
}

Object.assign(window, { Lightbox });
