// ClipMedia — renders a real <video> for each clip. Uses native <video>
// streaming against the configured VIDEO_CDN_URL (Cloudflare R2 with a
// custom domain). R2 sends proper Accept-Ranges + Content-Length, so
// Safari/Chrome stream-decode mp4 directly — no base64 / data-URL
// workaround needed.
//
// Bandwidth strategy:
//   • src is attached lazily via IntersectionObserver — clips well
//     below the fold don't download until you scroll near them.
//   • preload="metadata" by default (just enough for poster + duration);
//     bounce-enabled clips get preload="auto" because rAF-driven reverse
//     playback needs the buffer ahead of currentTime to be populated.
//   • Hero / non-hover clips kick off play() once metadata loads.
//
// If you ever see first-bounce stutter on a long clip, set
// preload="auto" on that specific entry in clips.js (or add a
// `forceFullPreload: true` flag and read it here).

const { useEffect, useRef, useState } = React;

// Native aspect-ratio cache, src -> width/height. Populated on
// loadedmetadata so layouts (e.g. IndexGridB hover expand) can size
// against each clip's real dimensions instead of assuming 16:9.
window.__clipAspect = window.__clipAspect || {};
window.__clipAspectListeners = window.__clipAspectListeners || new Set();
function notifyAspectListeners() {
  window.__clipAspectListeners.forEach(fn => { try { fn(); } catch (e) {} });
}

// Resolve "Foo.mp4" → "https://videos.spencer-russell.com/Foo.mp4".
// Falls back to identity if the host page hasn't defined the helper
// (e.g. someone opens this file directly from the filesystem).
function resolveSrc(src) {
  if (typeof window.resolveClipSrc === 'function') return window.resolveClipSrc(src);
  return src;
}

function ClipMedia({ clip, style, className, hover = false, bounce = false }) {
  const ref = useRef(null);
  const wrapRef = useRef(null);
  const [errored, setErrored] = useState(false);
  // "near" gates whether we even attach a src to the <video>. Toggled by
  // an IntersectionObserver against the wrapping element.
  const [near, setNear] = useState(false);
  const [isHover, setIsHover] = useState(false);
  // Becomes true once the video has decoded a frame and is actually
  // visible — used to fade out the static poster underneath.
  const [videoReady, setVideoReady] = useState(false);

  const resolvedSrc = clip.src ? resolveSrc(clip.src) : null;

  // Intersection-driven src attachment. Margin is generous so clips
  // start loading slightly before they enter the viewport — feels
  // instant when scrolling at normal speed without front-loading the
  // whole catalog on first paint.
  useEffect(() => {
    const el = wrapRef.current;
    if (!el) return;
    if (!('IntersectionObserver' in window)) {
      // Old browser fallback — just attach immediately.
      setNear(true);
      return;
    }
    const io = new IntersectionObserver((entries) => {
      for (const entry of entries) {
        if (entry.isIntersecting) {
          setNear(true);
          // Once we've decided to load, no need to keep watching — the
          // <video> manages its own lifecycle from here.
          io.disconnect();
          return;
        }
      }
    }, {
      // Preload when within ~1 viewport in any direction.
      rootMargin: '100% 0% 100% 0%',
      threshold: 0.01,
    });
    io.observe(el);
    return () => io.disconnect();
  }, [resolvedSrc]);

  // Reset transient state when the clip identity changes.
  useEffect(() => {
    setErrored(false);
    setVideoReady(false);
  }, [resolvedSrc]);

  // Seed the start position once metadata loads. For non-hover clips
  // (the hero), kick off autoplay. For hover clips, leave paused on its
  // poster frame until the user hovers.
  useEffect(() => {
    const v = ref.current;
    if (!v || !near || !resolvedSrc) return;
    const onErr = () => setErrored(true);
    const onReady = () => setVideoReady(true);
    const onMeta = () => {
      const dur = v.duration || 0;
      if (dur > 0) {
        const seed = parseInt(String(clip.id).replace(/\D/g, ''), 10) || 0;
        v.currentTime = (seed * 0.31) % dur;
      }
      // Cache the native aspect ratio so layouts can read it.
      if (clip.src && v.videoWidth && v.videoHeight) {
        const ratio = v.videoWidth / v.videoHeight;
        if (window.__clipAspect[clip.src] !== ratio) {
          window.__clipAspect[clip.src] = ratio;
          notifyAspectListeners();
        }
      }
      if (!hover) v.play().catch(() => {});
    };
    v.addEventListener('error', onErr);
    v.addEventListener('loadedmetadata', onMeta);
    v.addEventListener('loadeddata', onReady);
    return () => {
      v.removeEventListener('error', onErr);
      v.removeEventListener('loadedmetadata', onMeta);
      v.removeEventListener('loadeddata', onReady);
    };
  }, [clip.id, clip.src, near, resolvedSrc, hover]);

  // Bounce playback: when the clip would loop, flip playbackRate so it
  // plays forward → reverse → forward instead. Browsers don't natively
  // support negative playbackRate reliably across codecs, so we drive
  // currentTime manually via rAF when in the reverse leg. Note: this
  // depends on the buffer ahead of currentTime being populated — see
  // README on the preload="metadata" → preload="auto" override per clip.
  useEffect(() => {
    const v = ref.current;
    if (!v || !near || !resolvedSrc || !bounce) return;
    let dir = 1; // 1 = forward, -1 = reverse
    let raf = 0;
    let last = 0;
    // Disable native loop while bouncing — we'll flip direction at the ends.
    v.loop = false;
    const tick = (ts) => {
      raf = requestAnimationFrame(tick);
      if (!last) { last = ts; return; }
      const dt = (ts - last) / 1000;
      last = ts;
      const dur = v.duration || 0;
      if (!dur) return;
      if (dir === -1) {
        // Manually rewind. Pause native playback so it doesn't fight us.
        if (!v.paused) v.pause();
        v.currentTime = Math.max(0, v.currentTime - dt);
        if (v.currentTime <= 0.02) {
          dir = 1;
          v.play().catch(() => {});
        }
      } else {
        // Forward leg — let native playback handle it; flip when we hit the end.
        if (v.currentTime >= dur - 0.05) {
          dir = -1;
        }
      }
    };
    raf = requestAnimationFrame(tick);
    return () => {
      cancelAnimationFrame(raf);
      v.loop = true;
    };
  }, [near, resolvedSrc, bounce]);

  // Hover-driven play/pause. The initial seed (set on loadedmetadata above)
  // gives each clip a unique "poster" frame the first time you see it.
  // After that, hovering away just pauses — the playhead stays put so the
  // next hover resumes from where you left off.
  useEffect(() => {
    const v = ref.current;
    if (!v || !near || !resolvedSrc || !hover) return;
    if (isHover) {
      v.play().catch(() => {});
    } else {
      v.pause();
    }
  }, [isHover, hover, near, resolvedSrc]);

  const wrapHandlers = hover ? {
    onMouseEnter: () => setIsHover(true),
    onMouseLeave: () => setIsHover(false),
  } : {};

  // Bounce + hover both want a populated forward buffer; everything else
  // can get away with metadata-only. Per-clip override: clip.preload.
  const preload = clip.preload || (bounce ? 'auto' : 'metadata');

  // Resolve the poster against the same CDN base, so a future
  // posters/foo.jpg path on R2 works the same way as the video src.
  const resolvedPoster = clip.poster ? resolveSrc(clip.poster) : null;

  return (
    <div
      ref={wrapRef}
      className={className}
      style={{ position: 'relative', overflow: 'hidden', background: clip.bg, ...style }}
      {...wrapHandlers}
    >
      {/* Static poster — extracted frame, loads instantly. Sits underneath
          the video and fades out once a real frame has decoded. Also
          remains as the silent fallback if the video errors. */}
      {resolvedPoster && !errored && (
        <img
          src={resolvedPoster}
          alt=""
          aria-hidden="true"
          draggable={false}
          style={{
            position: 'absolute', inset: 0, width: '100%', height: '100%',
            objectFit: 'cover', display: 'block',
            opacity: videoReady ? 0 : 1,
            transition: 'opacity 320ms ease',
            pointerEvents: 'none',
          }}
        />
      )}
      {/* Subtle scan-shimmer while we're waiting on the video. Disappears
          the instant videoReady flips. Also covers the no-poster case. */}
      {!errored && !videoReady && (
        <div
          aria-hidden="true"
          style={{
            position: 'absolute', inset: 0,
            background: 'linear-gradient(105deg, transparent 40%, rgba(255,255,255,0.06) 50%, transparent 60%)',
            backgroundSize: '220% 100%',
            animation: 'clipShimmer 1.6s linear infinite',
            pointerEvents: 'none',
            mixBlendMode: 'screen',
          }}
        />
      )}
      {near && resolvedSrc && !errored && (
        <video
          ref={ref}
          src={resolvedSrc}
          autoPlay={!hover}
          muted
          loop
          playsInline
          preload={preload}
          // crossOrigin lets us draw frames to a canvas (used by
          // dir-b-mobile's poster extraction). Requires the bucket to
          // send Access-Control-Allow-Origin: * (or a matching origin).
          crossOrigin="anonymous"
          style={{
            position: 'relative',
            width: '100%', height: '100%', objectFit: 'cover', display: 'block',
            opacity: videoReady ? 1 : 0,
            transition: 'opacity 320ms ease',
          }}
        />
      )}
      {errored && (
        <div style={{
          position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column',
          alignItems: 'center', justifyContent: 'center', gap: 6,
          color: 'rgba(255,255,255,0.55)',
          fontFamily: 'ui-monospace, "SF Mono", Menlo, monospace',
          fontSize: 10, letterSpacing: 0.4, textAlign: 'center', padding: 16,
          background: `repeating-linear-gradient(135deg, ${clip.bg || '#0a0a0a'} 0 8px, rgba(255,255,255,0.025) 8px 16px)`,
        }}>
          <div style={{ fontSize: 9, opacity: 0.6, textTransform: 'uppercase' }}>Re-encode required</div>
          <div style={{ opacity: 0.85, maxWidth: 220 }}>{(clip.src || '').split('/').pop()}</div>
        </div>
      )}
    </div>
  );
}

window.ClipCanvas = ClipMedia;
window.ClipMedia = ClipMedia;
