// Shared utilities, image atoms, and theme tokens.
// Exposes via window: MediaImage, justifyRows, applyTheme, themeVars, fontStacks.

// ─── Real media tile (image or video) ──────────────────────────────────
// One canonical way to render any portfolio item. `crop` forces cover-fit
// to its container (used for the hero); the gallery never passes crop=true,
// so images keep their true aspect ratio. Videos render a poster frame
// with a play badge — clicking is handled upstream (opens the lightbox).
function MediaItem({ item, crop = false, style = {}, className = "" }) {
  const isVideo = item.kind === "video";

  // Video without a separate poster image (e.g. user upload) — render the
  // <video> element itself with preload=metadata so the browser shows the
  // first frame.
  if (isVideo && !item.poster) {
    return (
      <div className={"mi " + className} style={{ position:"relative", width:"100%", height: crop ? "100%" : "auto", ...style }}>
        <video
          src={item.src}
          muted
          playsInline
          preload="metadata"
          draggable={false}
          style={{
            display: "block",
            width: "100%",
            height: crop ? "100%" : "auto",
            objectFit: crop ? "cover" : "fill",
            background: "#1a1816",
            userSelect: "none",
          }}
        />
        <VideoBadge/>
      </div>
    );
  }

  const src = isVideo ? item.poster : item.src;
  return (
    <div
      className={"mi " + className}
      style={{
        position: "relative",
        width: "100%",
        height: crop ? "100%" : "auto",
        ...style,
      }}
    >
      <img
        src={src}
        alt=""
        loading="lazy"
        draggable={false}
        style={{
          display: "block",
          width: "100%",
          height: crop ? "100%" : "auto",
          objectFit: crop ? "cover" : "fill",
          userSelect: "none",
        }}
      />
      {isVideo && <VideoBadge/>}
    </div>
  );
}

// Subtle play badge overlaid on video posters in the gallery.
function VideoBadge({ size = 56 }) {
  return (
    <div style={{
      position:"absolute", inset:0,
      display:"flex", alignItems:"center", justifyContent:"center",
      pointerEvents:"none",
    }}>
      <div style={{
        width: size, height: size, borderRadius: "50%",
        background: "rgba(10,10,10,0.32)",
        border: "1px solid rgba(246,240,226,0.8)",
        backdropFilter: "blur(6px)",
        WebkitBackdropFilter: "blur(6px)",
        display: "flex", alignItems: "center", justifyContent: "center",
      }}>
        <svg width={size * 0.34} height={size * 0.34} viewBox="0 0 24 24" fill="#f6f0e2" aria-hidden="true">
          <path d="M8 5v14l11-7z"/>
        </svg>
      </div>
    </div>
  );
}

// Back-compat alias — old call sites still import MediaImage.
const MediaImage = MediaItem;

// ─── Justified gallery packer ──────────────────────────────────────────
// Row-based justified layout (Flickr/Google-Photos style):
//
//   1. Each row starts with a target height.
//   2. Items are added at that height (width = aspect * height) until the
//      summed width plus gutters exceeds the container width.
//   3. The whole row is then scaled so widths sum exactly to the container
//      width — every image keeps its true aspect ratio, no crops, no gaps.
//   4. The trailing partial row is left at the target height (its natural
//      width may be less than the container, so it's optionally left-aligned
//      or extended depending on caller preference).
//
// Inputs:
//   items           — [{ w, h, ... }] in source order
//   containerWidth  — px available for the row
//   targetHeight    — preferred row height before justification (px)
//   gap             — gutter between items in a row, AND between rows (px)
//   maxHeightRatio  — cap how much an under-filled row can grow (e.g. 1.5×)
//
// Output: [{ rowHeight, items: [{ ...item, w: scaledW, h: scaledH }] }]
function justifyRows(items, containerWidth, targetHeight, gap = 8, maxHeightRatio = 1.4) {
  // ── Pass 1: greedy pack item indices into rows ────────────────────
  const packs = []; // each: array of indices into `items`
  let cur = [];
  let curW = 0;
  for (let i = 0; i < items.length; i++) {
    const aspect = items[i].w / items[i].h;
    cur.push(i);
    curW += aspect * targetHeight;
    const projected = curW + gap * (cur.length - 1);
    if (projected >= containerWidth) { packs.push(cur); cur = []; curW = 0; }
  }
  if (cur.length) packs.push(cur);

  // ── Pass 2: rebalance a lonely trailing item ──────────────────────
  // If the last row has just 1 item, it would scale up to maxHeightRatio
  // and tower over the gallery (and leave a big void to its right). If the
  // previous row has at least 3 items, lend it one — the result is two
  // tidy rows of similar height.
  if (packs.length >= 2) {
    const last = packs[packs.length - 1];
    const prev = packs[packs.length - 2];
    if (last.length === 1 && prev.length >= 3) {
      last.unshift(prev.pop());
    }
  }

  // ── Pass 3: compute per-row scaling and emit ──────────────────────
  return packs.map((indices, ri) => {
    const isLast = ri === packs.length - 1;
    const rowItems = indices.map((idx) => items[idx]);
    const sumAtTarget = rowItems.reduce((s, it) => s + (it.w / it.h) * targetHeight, 0);
    const totalGap = gap * (rowItems.length - 1);
    const available = containerWidth - totalGap;
    let scale = available / sumAtTarget;
    if (isLast && scale > maxHeightRatio) scale = maxHeightRatio;
    const h = targetHeight * scale;
    return {
      rowHeight: h,
      items: rowItems.map((it) => ({
        ...it,
        _w: (it.w / it.h) * h,
        _h: h,
      })),
    };
  });
}

// ─── Theme + Type tokens ───────────────────────────────────────────────
const themeVars = {
  light: {
    "--bg":           "#f6f4ef",  // bone, not pure white — printed feel
    "--bg-deep":      "#fbfaf6",
    "--fg":           "#0a0a0a",
    "--fg-soft":      "rgba(10,10,10,0.62)",
    "--fg-faint":     "rgba(10,10,10,0.38)",
    "--rule":         "rgba(10,10,10,0.14)",
    "--rule-strong":  "rgba(10,10,10,0.30)",
    "--chip-bg":      "rgba(10,10,10,0.04)",
  },
  dark: {
    "--bg":           "#0c0c0c",
    "--bg-deep":      "#050505",
    "--fg":           "#f3efe9",
    "--fg-soft":      "rgba(243,239,233,0.62)",
    "--fg-faint":     "rgba(243,239,233,0.38)",
    "--rule":         "rgba(243,239,233,0.14)",
    "--rule-strong":  "rgba(243,239,233,0.30)",
    "--chip-bg":      "rgba(243,239,233,0.06)",
  },
  sepia: {
    "--bg":           "#efe7d8",
    "--bg-deep":      "#f6f0e2",
    "--fg":           "#2a221a",
    "--fg-soft":      "rgba(42,34,26,0.62)",
    "--fg-faint":     "rgba(42,34,26,0.38)",
    "--rule":         "rgba(42,34,26,0.18)",
    "--rule-strong":  "rgba(42,34,26,0.34)",
    "--chip-bg":      "rgba(42,34,26,0.06)",
  },
};

const fontStacks = {
  "serif-led": {
    "--font-display": "\"Cormorant Garamond\", \"Cormorant\", \"GFS Didot\", serif",
    "--font-body":    "\"DM Sans\", ui-sans-serif, system-ui, sans-serif",
    "--display-weight": "400",
    "--display-tracking": "-0.025em",
    "--display-leading": "0.92",
    "--name-tracking": "-0.04em",
  },
  "grotesque-led": {
    "--font-display": "\"DM Sans\", ui-sans-serif, system-ui, sans-serif",
    "--font-body":    "\"DM Sans\", ui-sans-serif, system-ui, sans-serif",
    "--display-weight": "600",
    "--display-tracking": "-0.06em",
    "--display-leading": "0.86",
    "--name-tracking": "-0.08em",
  },
};

function applyTheme(el, { theme = "light", type = "serif-led" } = {}) {
  if (!el) return;
  const vars = { ...themeVars[theme], ...fontStacks[type] };
  for (const k in vars) el.style.setProperty(k, vars[k]);
  el.style.background = "var(--bg)";
  el.style.color = "var(--fg)";
  el.style.fontFamily = "var(--font-body)";
}

// ─── Window measure hook ───────────────────────────────────────────────
// Returns a stable number that updates on resize, so the justified gallery
// can repack at the right container width on viewport change.
function useElementWidth(ref) {
  const [w, setW] = React.useState(0);
  React.useEffect(() => {
    if (!ref.current) return;
    const ro = new ResizeObserver((entries) => {
      for (const e of entries) setW(e.contentRect.width);
    });
    ro.observe(ref.current);
    return () => ro.disconnect();
  }, [ref]);
  return w;
}

// ─── Lightbox ──────────────────────────────────────────────────────────
// Modal viewer. Image/video is sized explicitly from the item's true aspect
// ratio + the viewport so it fills the available space (no tiny images on
// big screens), and the prev/next arrows sit immediately beside the media
// rather than pinned to the viewport edges.

function useViewportSize() {
  const [s, setS] = React.useState(() => ({
    w: typeof window !== "undefined" ? window.innerWidth  : 1440,
    h: typeof window !== "undefined" ? window.innerHeight : 900,
  }));
  React.useEffect(() => {
    const r = () => setS({ w: window.innerWidth, h: window.innerHeight });
    window.addEventListener("resize", r);
    return () => window.removeEventListener("resize", r);
  }, []);
  return s;
}

function useLightbox(items) {
  const [idx, setIdx] = React.useState(-1);
  const open  = React.useCallback((i) => setIdx(i), []);
  const close = React.useCallback(() => setIdx(-1), []);
  const next  = React.useCallback(() => setIdx((i) => (i + 1) % items.length), [items.length]);
  const prev  = React.useCallback(() => setIdx((i) => (i - 1 + items.length) % items.length), [items.length]);
  return { idx, open, close, next, prev, isOpen: idx >= 0 };
}

function Lightbox({ items, idx, onClose, onPrev, onNext }) {
  const vp = useViewportSize();

  React.useEffect(() => {
    if (idx < 0) return;
    const k = (e) => {
      if (e.key === "Escape")     onClose();
      if (e.key === "ArrowLeft")  onPrev();
      if (e.key === "ArrowRight") onNext();
    };
    document.addEventListener("keydown", k);
    const prev = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => {
      document.removeEventListener("keydown", k);
      document.body.style.overflow = prev;
    };
  }, [idx, onClose, onPrev, onNext]);

  if (idx < 0) return null;
  const item = items[idx];
  if (!item) return null;
  const isVideo = item.kind === "video";

  // Compute display size from item aspect + viewport. Reserve room above
  // for the top bar and on either side for the arrow buttons so the media
  // never collides with chrome.
  const aspect = (item.w || 1) / (item.h || 1);
  const reservedW = 168; // 2 × (52 arrow + 24 gap + 8 breathing)
  const reservedH = 96;  // top bar
  const maxW = Math.max(280, Math.min(1600, vp.w - reservedW));
  const maxH = Math.max(280, vp.h - reservedH);
  let dispH = maxH;
  let dispW = dispH * aspect;
  if (dispW > maxW) { dispW = maxW; dispH = dispW / aspect; }

  return ReactDOM.createPortal(
    <div
      onClick={onClose}
      style={{
        position:"fixed", inset:0, zIndex: 2147483640,
        background:"rgba(8,7,6,0.92)",
        backdropFilter:"blur(8px)", WebkitBackdropFilter:"blur(8px)",
        cursor:"zoom-out",
      }}
    >
      {/* Top bar */}
      <div style={{
        position:"absolute", top:0, left:0, right:0, padding:"20px 28px",
        display:"flex", justifyContent:"space-between", alignItems:"center",
        color:"rgba(246,240,226,0.82)",
        fontFamily:'"JetBrains Mono", ui-monospace, monospace',
        fontSize: 11, letterSpacing:".18em", textTransform:"uppercase",
        pointerEvents:"none", zIndex: 2,
      }}>
        <span style={{pointerEvents:"auto"}}>
          {String(item.i).padStart(2, "0")} / {String(items.length).padStart(2, "0")}
        </span>
        <button
          aria-label="Close"
          onClick={(e) => { e.stopPropagation(); onClose(); }}
          style={{
            pointerEvents:"auto", appearance:"none", border:0, background:"transparent",
            color:"rgba(246,240,226,0.85)",
            width:36, height:36, borderRadius:18, cursor:"pointer",
            display:"flex", alignItems:"center", justifyContent:"center",
            fontSize:20, lineHeight:1,
          }}>×</button>
      </div>

      {/* Media row — arrows flank the media at a tight gap */}
      <div style={{
        position:"absolute", inset:0,
        display:"flex", alignItems:"center", justifyContent:"center",
        gap: 24,
      }}>
        <LbArrow dir="left"  onClick={(e) => { e.stopPropagation(); onPrev(); }}/>
        <div
          onClick={(e) => e.stopPropagation()}
          style={{
            width: dispW, height: dispH,
            flex: "0 0 auto",
            background: "#0a0908",
            cursor: "auto",
          }}
        >
          {isVideo ? (
            <video
              key={item.src}
              src={item.src}
              poster={item.poster}
              controls
              autoPlay
              playsInline
              style={{ width:"100%", height:"100%", display:"block", objectFit:"contain", background:"#000" }}
            />
          ) : (
            <img
              src={item.src}
              alt=""
              draggable={false}
              style={{ width:"100%", height:"100%", display:"block", objectFit:"contain" }}
            />
          )}
        </div>
        <LbArrow dir="right" onClick={(e) => { e.stopPropagation(); onNext(); }}/>
      </div>
    </div>,
    document.body,
  );
}

function LbArrow({ dir, onClick }) {
  return (
    <button
      onClick={onClick}
      aria-label={dir === "left" ? "Previous" : "Next"}
      style={{
        flex: "0 0 auto",
        appearance:"none", border:0, cursor:"pointer",
        background:"rgba(246,240,226,0.10)",
        width:56, height:56, borderRadius:28,
        color:"#f6f0e2",
        display:"flex", alignItems:"center", justifyContent:"center",
        transition:"background .15s, transform .15s",
      }}
      onMouseEnter={(e) => { e.currentTarget.style.background = "rgba(246,240,226,0.22)"; e.currentTarget.style.transform = "scale(1.04)"; }}
      onMouseLeave={(e) => { e.currentTarget.style.background = "rgba(246,240,226,0.10)"; e.currentTarget.style.transform = "scale(1)"; }}
    >
      <svg width="22" height="22" viewBox="0 0 20 20" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round">
        <path d={dir === "left" ? "M12 4l-6 6 6 6" : "M8 4l6 6-6 6"}/>
      </svg>
    </button>
  );
}

Object.assign(window, {
  MediaItem, MediaImage, justifyRows, applyTheme, themeVars, fontStacks,
  useElementWidth, Lightbox, useLightbox, VideoBadge,
});
