// Direction B — Mobile
// One-clip-at-a-time vertical scroll designed for modern iPhones.
// The clip closest to the center of the viewport autoplays; everyone
// else stays paused on their poster frame. Same warm bone / ink palette
// and editorial vocabulary as the desktop version, retuned for narrow
// portrait screens (no side rail, no hover, no grid pagination).
//
// Memory note: on a phone we cannot afford to decode 30+ <video>
// elements at once. The MobileClip component only attaches a real
// <video> when the tile is within ±2 viewport-heights of the active
// clip; everything else renders just the poster image. This keeps
// scroll buttery and avoids Safari running out of media decoders.

const { useState: useStateM, useEffect: useEffectM, useRef: useRefM, useMemo: useMemoM } = React;

// ---------------------------------------------------------------------------
// One mobile clip tile — sized at the clip's native aspect ratio.
// Renders a <video> only when `near` is true. Plays only when `active`.
// ---------------------------------------------------------------------------
// Extracted-poster cache. Once we've seen a frame for a given src, we
// keep the data URL forever and use it as the static preview — even
// after the <video> for that tile is torn down. Listeners let other
// tiles re-render when their poster becomes available.
window.__clipExtractedPoster = window.__clipExtractedPoster || {};
window.__clipPosterListeners = window.__clipPosterListeners || new Set();
function notifyPosterListeners() {
  window.__clipPosterListeners.forEach(fn => { try { fn(); } catch (e) {} });
}

// Resolve "Foo.mp4" → "https://videos.spencer-russell.com/Foo.mp4" using
// the helper defined in Portfolio Site.html. Falls back to identity if
// somehow loaded standalone.
function resolveSrcM(src) {
  if (!src) return src;
  if (typeof window.resolveClipSrc === 'function') return window.resolveClipSrc(src);
  return src;
}

function MobileClip({ clip, idx, total, active, near, onMeasure }) {
  const wrapRef = useRefM(null);
  const videoRef = useRefM(null);
  // "attached" gates whether we mount a <video> at all. Toggles with
  // `near`, but stays true once flipped on so we don't repeatedly tear
  // down/re-create the element while the user oscillates near the edge.
  const [attached, setAttached] = useStateM(false);
  const [errored, setErrored] = useStateM(false);
  const [videoReady, setVideoReady] = useStateM(false);
  const [aspect, setAspect] = useStateM(
    () => (window.__clipAspect && window.__clipAspect[clip.src]) || 16 / 9
  );
  // Static poster — either provided on the clip, or extracted from the
  // first decoded frame and cached. Re-resolves whenever the global
  // cache fills in a new entry.
  const [extractedPoster, setExtractedPoster] = useStateM(
    () => (window.__clipExtractedPoster && window.__clipExtractedPoster[clip.src]) || null
  );
  useEffectM(() => {
    const fn = () => {
      const p = window.__clipExtractedPoster[clip.src];
      if (p && p !== extractedPoster) setExtractedPoster(p);
    };
    fn();
    window.__clipPosterListeners.add(fn);
    return () => window.__clipPosterListeners.delete(fn);
  }, [clip.src, extractedPoster]);

  // Pick up aspect updates from the global cache (populated by ClipMedia
  // anywhere else this clip mounts, e.g. desktop hover preload).
  useEffectM(() => {
    const fn = () => {
      const a = window.__clipAspect && window.__clipAspect[clip.src];
      if (a && isFinite(a) && a > 0 && a !== aspect) setAspect(a);
    };
    fn();
    (window.__clipAspectListeners = window.__clipAspectListeners || new Set()).add(fn);
    return () => window.__clipAspectListeners.delete(fn);
  }, [clip.src, aspect]);

  // Attach <video> when the tile is near the active one. R2 streams
  // mp4 with proper Accept-Ranges so we can use plain <video src> —
  // no fetch / data-URL workaround needed.
  useEffectM(() => {
    if (near && !attached) setAttached(true);
  }, [near, attached]);

  // Tear the <video> back down when the tile drifts far from active.
  // The poster stays visible underneath. We only tear down once we're
  // well outside the near band so brief overshoot doesn't churn.
  useEffectM(() => {
    if (!near && attached) {
      setAttached(false);
      setVideoReady(false);
    }
  }, [near, attached]);

  // Play / pause based on whether this is the active (centered) tile.
  useEffectM(() => {
    const v = videoRef.current;
    if (!v) return;
    if (active) v.play().catch(() => {});
    else v.pause();
  }, [active, attached]);

  // Capture aspect ratio + seed playhead on metadata.
  useEffectM(() => {
    const v = videoRef.current;
    if (!v || !attached) return;
    const onErr = () => setErrored(true);
    const onReady = () => {
      setVideoReady(true);
      // Extract a still preview from the first decoded frame and cache it
      // so every other tile can use it as a static poster (and so this
      // tile keeps a poster after we tear down its <video>).
      try {
        if (!window.__clipExtractedPoster[clip.src] && v.videoWidth && v.videoHeight) {
          // Cap canvas dimensions so we're not stashing 1280×720 base64.
          const maxW = 480;
          const ratio = v.videoWidth / v.videoHeight;
          const cw = Math.min(maxW, v.videoWidth);
          const ch = Math.round(cw / ratio);
          const c = document.createElement('canvas');
          c.width = cw; c.height = ch;
          const ctx = c.getContext('2d');
          ctx.drawImage(v, 0, 0, cw, ch);
          const url = c.toDataURL('image/jpeg', 0.78);
          window.__clipExtractedPoster[clip.src] = url;
          notifyPosterListeners();
        }
      } catch (e) { /* CORS / decode hiccup — fall back to bg color */ }
    };
    const onMeta = () => {
      if (v.videoWidth && v.videoHeight) {
        const ratio = v.videoWidth / v.videoHeight;
        window.__clipAspect[clip.src] = ratio;
        if (ratio !== aspect) setAspect(ratio);
        (window.__clipAspectListeners || new Set()).forEach(fn => { try { fn(); } catch (e) {} });
      }
      const dur = v.duration || 0;
      if (dur > 0) {
        const seed = parseInt(String(clip.id || idx).replace(/\D/g, ''), 10) || idx;
        v.currentTime = (seed * 0.31) % dur;
      }
      if (active) 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);
    };
  }, [attached, clip.id, clip.src, idx, active, aspect]);

  // Report position + size to the parent so it can pick the active tile.
  useEffectM(() => {
    if (!onMeasure) return;
    onMeasure(idx, wrapRef.current);
  });

  // Aspect-ratio container. Width 100% of column; height derived from aspect.
  return (
    <div
      ref={wrapRef}
      data-clip-tile={idx}
      style={{ display: 'flex', flexDirection: 'column', gap: 10 }}
    >
      <div style={{
        position: 'relative',
        width: '100%',
        aspectRatio: String(aspect),
        background: clip.bg || '#0a0a0a',
        overflow: 'hidden',
      }}>
        {/* Static poster. Either the clip's own poster (if provided) or
            an auto-extracted first frame cached after first decode. Stays
            visible whenever this tile isn't actively playing — that
            includes inactive tiles whose <video> never mounts. */}
        {(() => {
          const posterURL = clip.poster || extractedPoster;
          if (!posterURL || errored) return null;
          return (
            <img
              src={posterURL}
              alt=""
              aria-hidden="true"
              draggable={false}
              style={{
                position: 'absolute', inset: 0, width: '100%', height: '100%',
                objectFit: 'cover', display: 'block',
                opacity: videoReady && active ? 0 : 1,
                transition: 'opacity 320ms ease',
                pointerEvents: 'none',
              }}
            />
          );
        })()}
        {attached && !errored && (
          <video
            ref={videoRef}
            src={resolveSrcM(clip.src)}
            muted loop playsInline
            preload={clip.preload || 'metadata'}
            crossOrigin="anonymous"
            style={{
              position: 'absolute', inset: 0,
              width: '100%', height: '100%', objectFit: 'cover', display: 'block',
              opacity: videoReady && active ? 1 : 0,
              transition: 'opacity 320ms ease',
            }}
          />
        )}
        {errored && (
          <div style={{
            position: 'absolute', inset: 0, display: 'flex', alignItems: 'center', justifyContent: 'center',
            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)`,
          }}>
            {(clip.src || '').split('/').pop()}
          </div>
        )}
        {/* Subtle "now playing" indicator on the active tile only. */}
        <div style={{
          position: 'absolute', top: 10, right: 10,
          display: 'flex', alignItems: 'center', gap: 6,
          padding: '4px 8px',
          background: 'rgba(22,20,15,0.55)',
          backdropFilter: 'blur(6px)',
          WebkitBackdropFilter: 'blur(6px)',
          color: '#e8e3d8',
          fontSize: 9, letterSpacing: 1, textTransform: 'uppercase',
          opacity: active ? 1 : 0,
          transition: 'opacity 240ms ease',
          pointerEvents: 'none',
        }}>
          <span style={{
            width: 6, height: 6, borderRadius: '50%', background: '#e8e3d8',
            animation: 'mobNowPlaying 1.4s ease-in-out infinite',
          }} />
          Playing
        </div>
      </div>
      <div style={{
        display: 'flex', justifyContent: 'space-between', alignItems: 'baseline',
        fontSize: 11, letterSpacing: 0.6, textTransform: 'uppercase',
        padding: '0 16px',
      }}>
        <span style={{ fontVariantNumeric: 'tabular-nums', opacity: 0.5 }}>
          {String(idx + 1).padStart(3, '0')} / {String(total).padStart(3, '0')}
        </span>
        <span style={{ opacity: 0.85, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', marginLeft: 12 }}>
          {clip.label}
        </span>
      </div>
    </div>
  );
}

// ---------------------------------------------------------------------------
// DirBMobile — vertical scroll list. Determines which tile's center is
// closest to the viewport center on every scroll; that tile is "active"
// (autoplays). Tiles within ±NEAR_RANGE of the active one preload their
// video; everyone else is poster-only.
// ---------------------------------------------------------------------------
function DirBMobile({ width = 390, height = 844 }) {
  const scrollRef = useRefM(null);
  const tilesRef = useRefM(new Map()); // idx -> DOM node
  const [activeIdx, setActiveIdx] = useStateM(0);
  const [activeNav, setActiveNav] = useStateM('reel');
  const allClips = window.CLIPS;

  const NEAR_RANGE = 1; // preload video for active ± 1 tile

  // Background poster pass. After mount, walk through every clip and
  // grab a single decoded frame so the static previews fill in even for
  // clips far below the fold. Done serially with a small delay between
  // each one so we don't saturate the decode pipeline (or overwhelm
  // Safari's decoder pool on iPhone). Uses streamed <video src> against
  // the configured CDN — same path the visible tiles use.
  useEffectM(() => {
    let cancelled = false;
    const grabPoster = (clip) => new Promise((resolve) => {
      if (window.__clipExtractedPoster[clip.src] || !clip.src) return resolve();
      const v = document.createElement('video');
      v.muted = true;
      v.playsInline = true;
      v.preload = 'auto';
      // crossOrigin must be set BEFORE src so the request is made with
      // CORS headers (otherwise the canvas draw later will taint and
      // toDataURL() throws SecurityError).
      v.crossOrigin = 'anonymous';
      v.src = resolveSrcM(clip.src);
      const cleanup = () => {
        v.removeAttribute('src');
        try { v.load(); } catch (e) {}
        resolve();
      };
      const onReady = () => {
        try {
          if (v.videoWidth && v.videoHeight && !window.__clipExtractedPoster[clip.src]) {
            const maxW = 480;
            const ratio = v.videoWidth / v.videoHeight;
            const cw = Math.min(maxW, v.videoWidth);
            const ch = Math.round(cw / ratio);
            const c = document.createElement('canvas');
            c.width = cw; c.height = ch;
            c.getContext('2d').drawImage(v, 0, 0, cw, ch);
            window.__clipExtractedPoster[clip.src] = c.toDataURL('image/jpeg', 0.78);
            window.__clipAspect[clip.src] = ratio;
            (window.__clipAspectListeners || new Set()).forEach(fn => { try { fn(); } catch (e) {} });
            notifyPosterListeners();
          }
        } catch (e) {}
        cleanup();
      };
      v.addEventListener('loadeddata', onReady, { once: true });
      v.addEventListener('error', cleanup, { once: true });
      // Hard cap so a slow clip doesn't stall the queue.
      setTimeout(cleanup, 4000);
    });
    // Walk the catalog with a small delay between items so the device
    // isn't decoding 30 videos simultaneously on first load.
    (async () => {
      for (let i = 0; i < allClips.length; i++) {
        if (cancelled) return;
        await grabPoster(allClips[i]);
        await new Promise(r => setTimeout(r, 80));
      }
    })();
    return () => { cancelled = true; };
  }, [allClips]);

  // Stable measure callback so child useEffect doesn't churn on re-render.
  const measure = useMemoM(() => (idx, node) => {
    const map = tilesRef.current;
    if (node) map.set(idx, node);
    else map.delete(idx);
  }, []);

  // Compute which tile is closest to viewport center on scroll.
  useEffectM(() => {
    const root = scrollRef.current;
    if (!root) return;
    let raf = 0;
    const recompute = () => {
      raf = 0;
      const rootRect = root.getBoundingClientRect();
      const centerY = rootRect.top + rootRect.height / 2;
      let best = activeIdx;
      let bestDist = Infinity;
      tilesRef.current.forEach((node, idx) => {
        if (!node) return;
        const r = node.getBoundingClientRect();
        const tileCenter = r.top + r.height / 2;
        const d = Math.abs(tileCenter - centerY);
        if (d < bestDist) { bestDist = d; best = idx; }
      });
      if (best !== activeIdx) setActiveIdx(best);
      // Update section nav highlight from data-mob-section markers.
      const top = rootRect.top + 80;
      const sections = root.querySelectorAll('[data-mob-section]');
      let cur = 'reel';
      sections.forEach((el) => {
        if (el.getBoundingClientRect().top <= top) cur = el.getAttribute('data-mob-section');
      });
      if (cur !== activeNav) setActiveNav(cur);
    };
    const onScroll = () => {
      if (raf) return;
      raf = requestAnimationFrame(recompute);
    };
    root.addEventListener('scroll', onScroll, { passive: true });
    // Run once on mount + once after a tick so initial layout is captured.
    recompute();
    const t = setTimeout(recompute, 50);
    return () => {
      root.removeEventListener('scroll', onScroll);
      if (raf) cancelAnimationFrame(raf);
      clearTimeout(t);
    };
  }, [activeIdx, activeNav]);

  const goto = (id) => {
    const root = scrollRef.current;
    const el = root && root.querySelector(`#bm-${id}`);
    if (el) {
      const top = el.offsetTop - 12;
      root.scrollTo({ top, behavior: 'smooth' });
    }
  };

  // Color tokens — same as desktop direction B.
  const bg = '#e8e3d8';
  const ink = '#16140f';
  const muted = 'rgba(22,20,15,0.55)';
  const rule = 'rgba(22,20,15,0.18)';

  const sections = [
    { id: 'work',    n: '01', t: 'Index' },
    { id: 'about',   n: '02', t: 'About' },
    { id: 'contact', n: '03', t: 'Contact' },
  ];

  return (
    <div
      ref={scrollRef}
      style={{
        width, height,
        overflow: 'auto',
        background: bg, color: ink,
        fontFamily: '"Helvetica Neue", "Inter", Helvetica, Arial, sans-serif',
        position: 'relative',
        WebkitOverflowScrolling: 'touch',
      }}
    >
      <style>{`
        @keyframes mobNowPlaying {
          0%, 100% { opacity: 1; transform: scale(1); }
          50%      { opacity: 0.4; transform: scale(0.7); }
        }
        @keyframes bm-marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } }
      `}</style>

      {/* Sticky compact header — name + tiny section nav. */}
      <div style={{
        position: 'sticky', top: 0, zIndex: 20,
        background: bg,
        borderBottom: `1px solid ${rule}`,
        padding: '14px 16px 12px',
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      }}>
        <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: 1.2, textTransform: 'uppercase' }}>
          Spencer Russell <span style={{ opacity: 0.45, fontWeight: 400 }}>/ Motion</span>
        </div>
        <div style={{ display: 'flex', gap: 14 }}>
          {sections.map(s => (
            <button
              key={s.id}
              onClick={() => goto(s.id)}
              style={{
                background: 'transparent', border: 'none', padding: 0,
                fontFamily: 'inherit',
                fontSize: 10, letterSpacing: 0.6, textTransform: 'uppercase',
                fontVariantNumeric: 'tabular-nums',
                color: activeNav === s.id ? ink : muted,
                cursor: 'pointer',
              }}
            >
              {s.n}
            </button>
          ))}
        </div>
      </div>

      {/* Kinetic marquee — smaller on mobile but same energy. */}
      <div style={{ paddingTop: 24, paddingBottom: 18, borderBottom: `1px solid ${rule}`, overflow: 'hidden' }}>
        <div style={{ display: 'flex', whiteSpace: 'nowrap', animation: 'bm-marquee 28s linear infinite' }}>
          {[0, 1].map((k) => (
            <div key={k} style={{ display: 'flex', alignItems: 'baseline', gap: 14, paddingRight: 14 }}>
              <span style={{ fontSize: 56, fontWeight: 700, letterSpacing: -1.6, lineHeight: 1.05 }}>Motion Design</span>
              <span style={{ fontSize: 56, fontWeight: 200, letterSpacing: -1.6, lineHeight: 1.05, fontStyle: 'italic', opacity: 0.5 }}>/</span>
              <span style={{ fontSize: 56, fontWeight: 700, letterSpacing: -1.6, lineHeight: 1.05 }}>Cinema 4D</span>
              <span style={{ fontSize: 56, fontWeight: 200, letterSpacing: -1.6, lineHeight: 1.05, fontStyle: 'italic', opacity: 0.5 }}>/</span>
              <span style={{ fontSize: 56, fontWeight: 700, letterSpacing: -1.6, lineHeight: 1.05 }}>Redshift</span>
              <span style={{ fontSize: 56, fontWeight: 200, letterSpacing: -1.6, lineHeight: 1.05, fontStyle: 'italic', opacity: 0.5 }}>/</span>
            </div>
          ))}
        </div>
      </div>

      {/* Index section header */}
      <div
        id="bm-work"
        data-mob-section="work"
        style={{
          padding: '28px 16px 18px',
          display: 'flex', justifyContent: 'space-between', alignItems: 'baseline',
          borderBottom: `1px solid ${rule}`,
        }}
      >
        <div style={{ fontSize: 22, fontWeight: 700, letterSpacing: -0.6 }}>Index</div>
        <div style={{
          fontSize: 10, letterSpacing: 0.6, textTransform: 'uppercase', opacity: 0.55,
          fontVariantNumeric: 'tabular-nums',
        }}>
          {allClips.length} works · 2024 / 2026
        </div>
      </div>

      {/* The vertical reel. Each tile sized to its native aspect ratio. */}
      <div style={{
        display: 'flex', flexDirection: 'column',
        // Generous gap so each clip reads as its own moment.
        gap: 36,
        padding: '24px 0 24px',
      }}>
        {allClips.map((clip, idx) => {
          const dist = Math.abs(idx - activeIdx);
          const near = dist <= NEAR_RANGE;
          return (
            <MobileClip
              key={clip.id || clip.src || idx}
              clip={clip}
              idx={idx}
              total={allClips.length}
              active={idx === activeIdx}
              near={near}
              onMeasure={measure}
            />
          );
        })}
      </div>

      {/* About */}
      <div
        id="bm-about"
        data-mob-section="about"
        style={{ padding: '40px 16px 40px', borderTop: `1px solid ${rule}` }}
      >
        <div style={{
          fontSize: 10, letterSpacing: 1, textTransform: 'uppercase',
          opacity: 0.6, marginBottom: 16,
        }}>02 / About</div>
        <div style={{
          fontSize: 24, fontWeight: 700, letterSpacing: -0.6, lineHeight: 1.15,
          marginBottom: 24,
        }}>
          I build abstract studies, VJ clips, and the occasional commission{' '}
          <span style={{ opacity: 0.5 }}>
            using Cinema 4D, Redshift, xParticles, Octane, and After Effects.
          </span>
        </div>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 14, fontSize: 14, lineHeight: 1.6, opacity: 0.85 }}>
          {window.PORTFOLIO.bio.map((p, i) => <p key={i} style={{ margin: 0 }}>{p}</p>)}
        </div>
      </div>

      {/* Contact */}
      <div
        id="bm-contact"
        data-mob-section="contact"
        style={{ padding: '40px 16px 32px', borderTop: `1px solid ${rule}` }}
      >
        <div style={{
          fontSize: 10, letterSpacing: 1, textTransform: 'uppercase',
          opacity: 0.6, marginBottom: 16,
        }}>03 / Contact</div>
        <div>
          {[
            ['Email', window.PORTFOLIO.contact.email],
            ['Instagram', window.PORTFOLIO.contact.instagram],
            ['Are.na', window.PORTFOLIO.contact.arena],
            ['Location', 'Portland, OR'],
          ].map(([k, v]) => (
            <div
              key={k}
              style={{
                display: 'grid',
                gridTemplateColumns: '88px 1fr',
                gap: 14,
                padding: '12px 0',
                borderBottom: `1px solid ${rule}`,
                alignItems: 'baseline',
              }}
            >
              <span style={{ fontSize: 10, letterSpacing: 0.6, textTransform: 'uppercase', opacity: 0.55 }}>{k}</span>
              <span style={{
                fontSize: 15, fontWeight: 500, letterSpacing: -0.1,
                overflow: 'hidden', textOverflow: 'ellipsis',
              }}>{v}</span>
            </div>
          ))}
        </div>
        <div style={{
          marginTop: 36,
          fontSize: 9, letterSpacing: 0.6, textTransform: 'uppercase', opacity: 0.4,
        }}>© 2026 Spencer Russell</div>
      </div>
    </div>
  );
}

window.DirBMobile = DirBMobile;
