// SequenceHero — 3D scroll-triggered image sequence
//
// Architecture:
//   • Three.js (window.THREE from CDN) — WebGL rendering via CanvasTexture
//   • GSAP ScrollTrigger — pins the section, maps scroll progress → frame index
//   • LERP render loop — smooth frame interpolation regardless of scroll speed
//   • Progressive batched preload — frame 0 first, then 6-at-a-time
//   • Graceful fallback — if /assets/sequence/frame_0001.webp returns 404
//     the component transparently shows the existing hero video with no
//     visible change to the rest of the page.
//
// Drop JPEG/WebP frames in:  /assets/sequence/frame_0001.webp … frame_NNNN.webp
// Change frameCount prop to match your total frame count.

(function () {
  'use strict';

  const { useRef, useEffect, useState, useCallback } = React;

  /* ─── zero-pads n to `d` digits ─── */
  const pad = (n, d) => String(n).padStart(d, '0');

  /* ═══════════════════════════════════════════════════════════
     SequenceHero
     ═══════════════════════════════════════════════════════════ */
  function SequenceHero({
    /* asset config */
    folderPath     = '/assets/sequence',
    frameCount     = 88,
    filePrefix     = 'frame_',
    filePad        = 4,
    fileExt        = 'png',
    /* performance */
    batchSize      = 6,               // parallel loads per batch
    lerpFactor     = 0.11,            // 0-1; lower = more lag, silkier feel
    /* scroll */
    scrollPerFrame = 18,              // px of scroll per frame
    /* overlay texts — change at these scroll-progress thresholds */
    overlayTexts = [
      { from: 0.00, line1: 'MASTER THE',  line2: 'FRAME',      sub: 'Vancouver Film Maker'    },
      { from: 0.38, line1: 'CRAFT THE',   line2: 'STORY',      sub: 'Stage 1 · Fundamentals'  },
      { from: 0.72, line1: 'OWN THE',     line2: 'PRODUCTION', sub: 'Stage 3 · Professional'  },
    ],
    /* children are rendered as an overlay above the canvas */
    children,
    className = '',
  }) {

    /* ─── refs (avoid re-renders in hot paths) ─── */
    const wrapRef   = useRef(null);   // section element → ScrollTrigger trigger
    const canvasRef = useRef(null);   // WebGL canvas
    const framesArr = useRef([]);     // Image[] cache
    const threeCtx  = useRef({});     // Three.js objects
    const rafId     = useRef(null);
    const stRef     = useRef(null);   // ScrollTrigger instance
    const targetF   = useRef(0);      // desired frame (from scroll)
    const lerpedF   = useRef(0);      // interpolated frame (in render loop)

    /* ─── reactive state ─── */
    const [pct,      setPct]      = useState(0);          // preload %
    const [phase,    setPhase]    = useState('loading');  // 'loading' | 'ready' | 'fallback'
    const [overlayI, setOverlayI] = useState(0);          // active overlay text index

    /* ─── frame URL ─── */
    const frameUrl = (i) =>
      `${folderPath}/${filePrefix}${pad(i + 1, filePad)}.${fileExt}`;

    /* ─────────────────────────────────────────────
       PRELOAD  (progressive, batched)
       ───────────────────────────────────────────── */
    const preload = useCallback(async () => {
      const cache = new Array(frameCount).fill(null);
      framesArr.current = cache;
      let n = 0;

      const load1 = (i) => new Promise((res) => {
        const img   = new Image();
        img.crossOrigin = 'anonymous';
        img.onload  = () => { cache[i] = img; setPct(Math.round(++n / frameCount * 100)); res(img); };
        img.onerror = () => {             setPct(Math.round(++n / frameCount * 100)); res(null);  };
        img.src = frameUrl(i);
      });

      /* frame 0 first — gate success before continuing */
      const first = await load1(0);
      if (!first) { setPhase('fallback'); return; }

      /* load the rest in batches of batchSize */
      for (let i = 1; i < frameCount; i += batchSize) {
        const batch = [];
        for (let j = i; j < Math.min(i + batchSize, frameCount); j++) batch.push(load1(j));
        await Promise.all(batch);
      }

      setPhase('ready');
    }, []); // eslint-disable-line react-hooks/exhaustive-deps

    /* ─────────────────────────────────────────────
       THREE.JS SCENE SETUP
       ───────────────────────────────────────────── */
    const initThree = useCallback(() => {
      const THREE = window.THREE;
      if (!THREE || !canvasRef.current) return;

      const cv  = canvasRef.current;
      const W   = cv.clientWidth  || window.innerWidth;
      const H   = cv.clientHeight || window.innerHeight;
      const dpr = Math.min(window.devicePixelRatio, 2);

      /* Renderer — let CSS set canvas size (false = don't override CSS) */
      const renderer = new THREE.WebGLRenderer({ canvas: cv, antialias: true, alpha: false });
      renderer.setSize(W, H, false);
      renderer.setPixelRatio(dpr);
      if (THREE.SRGBColorSpace) renderer.outputColorSpace = THREE.SRGBColorSpace;

      /* Camera — PerspectiveFOV=55 at z=1.6 covers a 2-unit tall plane */
      const camera = new THREE.PerspectiveCamera(55, W / H, 0.1, 100);
      camera.position.z = 1.6;

      const scene = new THREE.Scene();

      /* Off-screen canvas — image frames are drawn here, then uploaded
         as a CanvasTexture.  HiDPI-aware but capped so it stays fast. */
      const offW   = Math.round(Math.min(W * dpr, 2560));
      const offH   = Math.round(offW * (H / W));
      const off    = document.createElement('canvas');
      off.width    = offW;
      off.height   = offH;
      const offCtx = off.getContext('2d', { alpha: false, willReadFrequently: false });

      const tex = new THREE.CanvasTexture(off);
      tex.minFilter     = THREE.LinearFilter;
      tex.magFilter     = THREE.LinearFilter;
      tex.generateMipmaps = false;

      /* Plane sized to fill the entire viewport (cover-fit math) */
      const fovR  = (55 * Math.PI) / 180;
      const dist  = 1.6;
      const pH    = 2 * Math.tan(fovR / 2) * dist;        // plane height in world units
      const pW    = pH * (W / H);                          // plane width

      const geo  = new THREE.PlaneGeometry(pW, pH);
      const mat  = new THREE.MeshBasicMaterial({ map: tex, toneMapped: false });
      const mesh = new THREE.Mesh(geo, mat);
      scene.add(mesh);

      threeCtx.current = { renderer, camera, scene, tex, off, offCtx, mat, geo };
    }, []);

    /* ─────────────────────────────────────────────
       DRAW ONE FRAME  (blit Image → CanvasTexture)
       ───────────────────────────────────────────── */
    const drawFrame = useCallback((idx) => {
      const { off, offCtx, tex } = threeCtx.current;
      if (!offCtx) return;

      const cache = framesArr.current;
      let img = cache[idx];
      if (!img) {
        /* nearest loaded frame below idx */
        let k = idx;
        while (k > 0 && !cache[k]) k--;
        img = cache[k];
      }
      if (!img) return;

      offCtx.drawImage(img, 0, 0, off.width, off.height);
      tex.needsUpdate = true;
    }, []);

    /* ─────────────────────────────────────────────
       RENDER LOOP  (rAF + LERP)
       ───────────────────────────────────────────── */
    const startLoop = useCallback(() => {
      const { renderer, camera, scene } = threeCtx.current;
      if (!renderer) return;

      let lastIdx = -1;

      const tick = () => {
        /* LERP toward target frame — prevents jitter on fast scrolls */
        lerpedF.current += (targetF.current - lerpedF.current) * lerpFactor;
        const idx = Math.round(Math.min(Math.max(lerpedF.current, 0), frameCount - 1));

        if (idx !== lastIdx) {
          drawFrame(idx);
          lastIdx = idx;

          /* sync overlay text label */
          const p = idx / (frameCount - 1);
          let oi = 0;
          overlayTexts.forEach((t, i) => { if (p >= t.from) oi = i; });
          setOverlayI(oi);
        }

        /* subtle 3D camera drift — gives depth / parallax feel */
        const p = targetF.current / (frameCount - 1);
        camera.rotation.y = Math.sin(p * Math.PI) * 0.018;
        camera.rotation.x = p * -0.012;
        camera.position.z = 1.6 + p * 0.05;

        renderer.render(scene, camera);
        rafId.current = requestAnimationFrame(tick);
      };

      rafId.current = requestAnimationFrame(tick);
    }, [frameCount, lerpFactor, drawFrame, overlayTexts]);

    /* ─────────────────────────────────────────────
       EFFECTS
       ───────────────────────────────────────────── */

    /* kick off preload on mount */
    useEffect(() => { preload(); }, []); // eslint-disable-line

    /* once preload succeeds, wire up Three.js + GSAP */
    useEffect(() => {
      if (phase !== 'ready') return;

      initThree();
      drawFrame(0);
      startLoop();

      const section = wrapRef.current;
      if (section) {
        /* fade hero title/cta out during the first 12 % of the sequence */
        gsap.to(
          section.querySelectorAll('.hero-content, .hero-eyebrow, .scroll-indicator'),
          {
            opacity: 0,
            y: -28,
            ease: 'none',
            scrollTrigger: {
              trigger: section,
              start: 'top top',
              end: `+=${frameCount * scrollPerFrame * 0.12}px`,
              scrub: true,
            },
          }
        );

        /* main pin + scrub */
        stRef.current = ScrollTrigger.create({
          trigger: section,
          start:   'top top',
          end:     `+=${frameCount * scrollPerFrame}px`,
          pin:     true,
          scrub:   true,          // exact scroll → frame, LERP handles smoothness
          onUpdate: (self) => {
            targetF.current = self.progress * (frameCount - 1);
          },
        });
      }

      /* resize handler */
      const onResize = () => {
        const { renderer, camera } = threeCtx.current;
        const cv = canvasRef.current;
        if (!renderer || !cv) return;
        const W = cv.clientWidth, H = cv.clientHeight;
        renderer.setSize(W, H, false);
        camera.aspect = W / H;
        camera.updateProjectionMatrix();
        ScrollTrigger.refresh();
      };
      window.addEventListener('resize', onResize);

      return () => {
        cancelAnimationFrame(rafId.current);
        stRef.current?.kill();
        window.removeEventListener('resize', onResize);
        const { renderer, mat, tex, geo } = threeCtx.current;
        geo?.dispose(); tex?.dispose(); mat?.dispose(); renderer?.dispose();
      };
    }, [phase]); // eslint-disable-line

    /* ─────────────────────────────────────────────
       RENDER
       ───────────────────────────────────────────── */
    const ot = overlayTexts[overlayI];

    return (
      <div
        ref={wrapRef}
        className={`seq-hero ${className}`}
        style={{ position: 'relative', width: '100%', height: '100vh', overflow: 'hidden', background: '#0F0F0F' }}
      >
        {/* ── WebGL canvas ── */}
        <canvas
          ref={canvasRef}
          style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', display: 'block', zIndex: 0 }}
        />

        {/* ── cinematic grade overlay ── */}
        <div className="seq-grade" aria-hidden="true" />

        {/* ── scroll-synced 3D text labels ── */}
        {phase === 'ready' && (
          <div className="seq-text-wrap" key={overlayI} aria-hidden="true">
            <p className="seq-tl seq-tl-1">{ot.line1}</p>
            <p className="seq-tl seq-tl-2">{ot.line2}</p>
            <div className="seq-bar" />
            <p className="seq-sub">{ot.sub}</p>
          </div>
        )}

        {/* ── children layer (nav, hero-content, sparkles, etc.) ── */}
        <div className="seq-children-layer">
          {children}
        </div>

        {/* ── loading overlay ── */}
        {phase === 'loading' && (
          <div className="seq-loader" aria-live="polite">
            <div className="seq-loader-logo">VFM</div>
            <div className="seq-loader-eyeline">Preparing your experience</div>
            <div className="seq-loader-track">
              <div className="seq-loader-fill" style={{ width: `${pct}%` }} />
            </div>
            <div className="seq-loader-num">{pct}%</div>
          </div>
        )}

        {/* ── fallback: original video (shown when no sequence images found) ── */}
        {phase === 'fallback' && (
          <>
            <video
              autoPlay muted loop playsInline
              style={{
                position: 'absolute', inset: 0, width: '100%', height: '100%',
                objectFit: 'cover', zIndex: 0,
                opacity: .82, filter: 'saturate(1.12) contrast(1.12) brightness(.82)',
                transform: 'scale(1.04)',
                animation: 'menuVideoDrift 18s ease-in-out infinite alternate',
              }}
            >
              <source src="assets/vfm-main-menu-bg.webm" type="video/webm" />
              <source src="assets/vfm-main-menu-bg.mp4"  type="video/mp4" />
            </video>
            <div className="main-menu-grade" />
          </>
        )}

        {/* ── scroll nudge ── */}
        {phase === 'ready' && (
          <div className="seq-scroll-nudge" aria-hidden="true">
            <span className="seq-scroll-label">Scroll</span>
            <span className="seq-scroll-line" />
          </div>
        )}
      </div>
    );
  }

  window.SequenceHero = SequenceHero;
})();
