// public/carousel.jsx — window.Carousel. Renders an ordered list of
// { image, video?, alt, width, height } items with prev/next, swipe, counter.
// Controlled: parent owns `index` and `onIndex`. Uses window.MOTION for slide.
(function () {
  const { useRef, useEffect, useState } = React;

  function Carousel({ items, index, onIndex, dark, controlsBelow, loop }) {
    const trackRef = useRef(null);
    const startX = useRef(null);
    const prevIndexRef = useRef(index);
    const count = items.length;
    const clamp = (i) => Math.max(0, Math.min(count - 1, i));

    // `shown` is the slide currently painted. It lags `index` only for as long
    // as the target image takes to decode — see the decode-gate effect below.
    const [shown, setShown] = useState(index);
    // `pending` drives the loading indicator: true only while a swap's target
    // image is still decoding AND that wait has crossed a perceptibility
    // threshold (see the decode-gate effect). Fast (cached) swaps never flip it,
    // so the cue only appears when the user genuinely out-runs the load.
    const [pending, setPending] = useState(false);

    // Preload immediate neighbors so the off-DOM decode is already warm by the
    // time you navigate. Each step seeds the next.
    useEffect(() => {
      [index - 1, index + 1].forEach((i) => {
        const it = items[i];
        if (it && !it.video && it.image) { const im = new Image(); im.src = it.image; }
      });
    }, [index, count]);

    // Decode-gate the swap: when `index` moves, fully decode the target image
    // off-DOM BEFORE updating `shown`. The previous slide stays painted the
    // whole time, so the on-DOM <img> src only ever points at an already-decoded
    // image — no blank "reloading" gap on the async decode of a fresh slide.
    useEffect(() => {
      if (index === shown) return;
      const target = items[index];
      if (!target || target.video || !target.image) { setShown(index); setPending(false); return; }
      let cancelled = false;
      // Only surface the loading cue if the decode is slow enough to notice —
      // a cached/instant swap clears before this fires, so it never flashes.
      const pendingTimer = setTimeout(() => { if (!cancelled) setPending(true); }, 200);
      // Clear the threshold timer on reveal — otherwise a fast (e.g. 30ms) decode
      // still lets the 200ms timer fire afterward and flips `pending` true with
      // no pending swap to clear it, leaving the bar stuck on until the next nav.
      const reveal = () => { if (!cancelled) { clearTimeout(pendingTimer); setShown(index); setPending(false); } };
      const im = new Image();
      im.src = target.image;
      if (im.decode) im.decode().then(reveal).catch(reveal);
      else { im.onload = reveal; im.onerror = reveal; }
      return () => { cancelled = true; clearTimeout(pendingTimer); };
    }, [index]);

    // Entrance motion fires only when the painted slide actually changes.
    useEffect(() => {
      const node = trackRef.current;
      if (!node) return;
      // Directional: new slide enters from the right when advancing, from the
      // left when going back (forward = left-ward motion).
      const dir = shown >= prevIndexRef.current ? 1 : -1;
      prevIndexRef.current = shown;
      window.MOTION.animate(
        node,
        { opacity: [0, 1], transform: ["translateX(" + (dir * 32) + "px)", "translateX(0px)"] },
        { duration: window.MOTION.DUR.base, ease: window.MOTION.EASE.out }
      );
    }, [shown]);

    // `loop` wraps past the ends (last→first, first→last); otherwise clamp.
    function go(delta) { onIndex(loop ? (index + delta + count) % count : clamp(index + delta)); }

    function onTouchStart(e) { startX.current = e.touches[0].clientX; }
    function onTouchEnd(e) {
      if (startX.current == null) return;
      const dx = e.changedTouches[0].clientX - startX.current;
      if (Math.abs(dx) > 40) go(dx < 0 ? 1 : -1);
      startX.current = null;
    }

    // Paint the decoded slide (`shown`), which lags `index` only during decode.
    const item = items[shown] || {};
    const ink = dark ? "#FAF7F3" : "#393433";

    return (
      <div className="r-carousel" onTouchStart={onTouchStart} onTouchEnd={onTouchEnd}>
        <div className="r-carousel-stage" ref={trackRef}>
          {pending && <div className="r-carousel-loading" role="progressbar" aria-label="Loading slide" />}
          {item.video ? (
            <video className="r-carousel-media" src={item.video} controls playsInline poster={item.image} />
          ) : (
            <img className="r-carousel-media" src={item.image} alt={item.alt || ""}
                 width={item.width || undefined} height={item.height || undefined} loading="eager" />
          )}
        </div>
        {count > 1 && (controlsBelow ? (
          // Footer controls: ‹ counter › in one row below the image (project
          // Photos). The default (deck viewer) keeps side-overlay arrows.
          <div className="r-carousel-controls">
            <button className="r-carousel-nav r-carousel-nav-inline" aria-label="Previous slide"
                    disabled={!loop && index === 0} onClick={() => go(-1)} style={{ color: ink }}>‹</button>
            <div className="r-carousel-counter" style={{ color: ink }}>
              <span style={{ fontVariantNumeric: "tabular-nums" }}>{shown + 1}</span> / {count}
            </div>
            <button className="r-carousel-nav r-carousel-nav-inline" aria-label="Next slide"
                    disabled={!loop && index === count - 1} onClick={() => go(1)} style={{ color: ink }}>›</button>
          </div>
        ) : (
          <React.Fragment>
            <button className="r-carousel-nav r-carousel-prev" aria-label="Previous slide"
                    disabled={!loop && index === 0} onClick={() => go(-1)} style={{ color: ink }}>‹</button>
            <button className="r-carousel-nav r-carousel-next" aria-label="Next slide"
                    disabled={!loop && index === count - 1} onClick={() => go(1)} style={{ color: ink }}>›</button>
            <div className="r-carousel-counter" style={{ color: ink }}>
              <span style={{ fontVariantNumeric: "tabular-nums" }}>{shown + 1}</span> / {count}
            </div>
          </React.Fragment>
        ))}
      </div>
    );
  }

  window.Carousel = Carousel;
})();
