/* ============================================================
   BeavrDam cinematic scroll-scrub engine
   ============================================================ */

const CLIPS = [
  { dir:"uploads/scenes/hero",       frames:  1, startFrame:1, endFrame:1,  accent:"#FF8C00", scrub: 50, dwell: 90, ext:"png" },        // 1 Hero — war room (hi-res still)
  { dir:"uploads/scenes/about",      frames:  1, startFrame:1, endFrame:1,  accent:"#2BE08A", scrub: 40, dwell:100, ext:"png" },        // 2 About Beaver Solutions (hi-res still)
  { dir:"uploads/scenes/howitworks", frames:  1, startFrame:1, endFrame:1,  accent:"#2BE08A", scrub: 40, dwell:100, ext:"png" },        // 3 How it works — CONNECT/HUNT/SHIP/WAKE UP (hi-res still)
  { dir:"uploads/frames/asset-3",    frames:121,                            accent:"#2BE08A", scrub: 90, dwell: 95 },                   // 4 Research closeup (ezgif sequence)
  { dir:"uploads/scenes/sales",      frames:  1, startFrame:1, endFrame:1,  accent:"#FF8C00", scrub: 40, dwell:100, ext:"png" },        // 5 Sales — Outbound that moves (hi-res still)
  { dir:"uploads/frames/asset-6",    frames:121,                            accent:"#2F6BFF", scrub: 90, dwell:100 },                   // 6 Enforcer (ezgif sequence — flagged: need hi-res PNG)
  { dir:"uploads/scenes/captain",    frames:  1, startFrame:1, endFrame:1,  accent:"#A855F7", scrub: 40, dwell:110, ext:"png" },        // 7 Captain orchestration (hi-res still)
  { dir:"uploads/scenes/final",      frames:  1, startFrame:1, endFrame:1,  accent:"#FF8C00", scrub: 40, dwell:130, ext:"png" },        // 8 Final CTA stage (hi-res still)
];

/* image-sequence frame path helper — frameNum is 1-indexed to match file names; ext defaults to jpg */
const framePath = (clip, frameNum) => `${clip.dir}/ezgif-frame-${String(frameNum).padStart(3,'0')}.${clip.ext||'jpg'}`;

const clamp = (v,a,b)=>Math.max(a,Math.min(b,v));
const smooth = (t)=>{ t=clamp(t,0,1); return t*t*(3-2*t); };
const lerp = (a,b,t)=>a+(b-a)*t;

/* ---------- dust particle field (canvas) ---------- */
function startDust(canvas){
  const ctx = canvas.getContext('2d');
  let w,h,dpr,parts=[];
  function resize(){
    dpr = Math.min(window.devicePixelRatio||1, 2);
    w = canvas.width = innerWidth*dpr;
    h = canvas.height = innerHeight*dpr;
    canvas.style.width = innerWidth+'px';
    canvas.style.height = innerHeight+'px';
    const n = Math.round((innerWidth*innerHeight)/26000);
    parts = Array.from({length:n}, ()=>({
      x:Math.random()*w, y:Math.random()*h,
      r:(Math.random()*1.6+0.4)*dpr,
      sx:(Math.random()-0.5)*0.12*dpr, sy:(-Math.random()*0.22-0.04)*dpr,
      a:Math.random()*0.5+0.15, tw:Math.random()*Math.PI*2
    }));
  }
  resize(); addEventListener('resize', resize);
  let accent = '#FF8C00';
  function tick(){
    ctx.clearRect(0,0,w,h);
    const amb = parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--ambient'))||1;
    for(const p of parts){
      p.x+=p.sx; p.y+=p.sy; p.tw+=0.02;
      if(p.y<-10)p.y=h+10; if(p.x<-10)p.x=w+10; if(p.x>w+10)p.x=-10;
      const fl = (Math.sin(p.tw)*0.4+0.6);
      ctx.globalAlpha = clamp(p.a*fl*amb,0,1);
      ctx.fillStyle = accent;
      ctx.beginPath(); ctx.arc(p.x,p.y,p.r,0,7); ctx.fill();
    }
    requestAnimationFrame(tick);
  }
  tick();
  return { setAccent:(c)=>accent=c };
}

function Landing(){
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const tref = React.useRef(t);
  tref.current = t;

  const vids = React.useRef([]);
  const stageRef = React.useRef(null);
  const offsets = React.useRef([]);    // [{start, scrubEnd, dwellEnd}]
  const total = React.useRef(1);
  const loaded = React.useRef({});     // i -> 'load'|'ready'
  const dustApi = React.useRef(null);
  const activeRef = React.useRef(-1);
  const [ready, setReady] = React.useState(false);
  const [prog, setProg] = React.useState(0);

  /* preload a clip's frames into browser cache + show first frame */
  const ensure = React.useCallback((i)=>{
    if(i<0 || i>=CLIPS.length) return;
    if(loaded.current[i]) return;
    loaded.current[i]='load';
    const c = CLIPS[i];
    const startFrame = c.startFrame ?? 1;
    const endFrame = c.endFrame ?? c.frames;
    // warm cache with parallel Image() preloads, only for the kept range
    let pendingFirst = true;
    for(let f=startFrame; f<=endFrame; f++){
      const img = new Image();
      if(pendingFirst){
        img.onload = ()=>{ loaded.current[i]='ready'; pendingFirst=false; };
        pendingFirst=false;
      }
      img.src = framePath(c, f);
    }
    // point the visible <img> at the first kept frame so it shows immediately
    const el = vids.current[i];
    if(el && !el.src) el.src = framePath(c, startFrame);
  },[]);

  /* position the hotspot layer to exactly match the contain-fitted final clip frame */
  const positionHotspots = React.useCallback(()=>{
    const hs=document.getElementById('hotspots'); if(!hs) return;
    const W=window.innerWidth, H=window.innerHeight, a=1924/1076;
    let rw,rh,ox,oy;
    if(W/H > a){ rh=H; rw=H*a; oy=0; ox=(W-rw)/2; }
    else { rw=W; rh=W/a; ox=0; oy=(H-rh)/2; }
    hs.style.left=ox+'px'; hs.style.top=oy+'px'; hs.style.width=rw+'px'; hs.style.height=rh+'px';
  },[]);

  /* recompute scroll geometry — only enabled scenes */
  const layout = React.useCallback(()=>{
    const vh = window.innerHeight;
    const pace = tref.current.pacing || 1;
    const scenes = tref.current.scenes || CLIPS.map(()=>true);
    let seq = CLIPS.map((c,i)=>i).filter(i=>scenes[i]);
    if(seq.length===0) seq=[0];
    let acc = 0; const out=[];
    for(const ci of seq){
      const c = CLIPS[ci];
      const s = c.scrub/100*vh*pace;
      const d = c.dwell/100*vh*pace;
      out.push({ clip:ci, start:acc, scrubEnd:acc+s, dwellEnd:acc+s+d, scrub:s, dwell:d });
      acc += s+d;
    }
    offsets.current = out;
    total.current = acc;
    const track = document.getElementById('track');
    if(track) track.style.height = (acc + vh) + 'px'; // +vh tail so last dwell fully holds
    positionHotspots();
  },[]);

  /* main scroll render loop */
  React.useEffect(()=>{
    const root = document.documentElement;
    let raf=0, pending=true;

    // boot
    layout();
    const firstCi = (offsets.current[0] && offsets.current[0].clip) || 0;
    ensure(firstCi);
    if(offsets.current[1]) ensure(offsets.current[1].clip);
    dustApi.current = startDust(document.getElementById('dust'));

    const v0 = vids.current[firstCi];
    const reveal = ()=>{ setReady(true); setTimeout(()=>{ const l=document.getElementById('loader'); if(l)l.classList.add('gone'); }, 60); };
    if(v0){
      if(v0.complete && v0.naturalWidth>0) reveal();
      else v0.addEventListener('load', reveal, {once:true});
    }
    // loader progress feedback
    const lp = document.querySelector('#loader .lbar i');
    let lpv=8; const lpt=setInterval(()=>{ lpv=Math.min(96,lpv+Math.random()*16); if(lp)lp.style.width=lpv+'%'; if(ready){lp.style.width='100%';clearInterval(lpt);} },180);

    const apply = ()=>{
      const off = offsets.current;
      if(!off.length){ raf=requestAnimationFrame(loop); return; }
      const y = window.scrollY || window.pageYOffset;
      const vh = window.innerHeight;

      // find active segment
      let seg = off.length-1, p=1, dwellP=1;
      for(let k=0;k<off.length;k++){
        const o=off[k];
        if(y < o.dwellEnd){
          seg=k;
          if(y<=o.scrubEnd){ p=o.scrub? (y-o.start)/o.scrub : 1; dwellP=0; }
          else { p=1; dwellP=o.dwell? (y-o.scrubEnd)/o.dwell : 1; }
          break;
        }
      }
      p=clamp(p,0,1); dwellP=clamp(dwellP,0,1);

      const ci = off[seg].clip;                 // active clip index
      ensure(ci);
      if(off[seg-1]) ensure(off[seg-1].clip);
      if(off[seg+1]) ensure(off[seg+1].clip);
      if(off[seg+2]) ensure(off[seg+2].clip);
      const clip = CLIPS[ci];

      // accent + ambient css vars
      if(activeRef.current!==ci){
        root.style.setProperty('--accent', clip.accent);
        if(dustApi.current) dustApi.current.setAccent(clip.accent);
      }

      // image-sequence visibility + frame scrub
      vids.current.forEach((el,idx)=>{
        if(!el) return;
        if(idx===ci){
          if(!el.classList.contains('show')) el.classList.add('show');
          const c = CLIPS[idx];
          const startFrame = c.startFrame ?? 1;
          const endFrame = c.endFrame ?? c.frames;
          const range = endFrame - startFrame;
          const frameNum = clamp(startFrame + Math.round(p*range), startFrame, endFrame);
          if(el.dataset.frame !== String(frameNum)){
            el.src = framePath(c, frameNum);
            el.dataset.frame = String(frameNum);
          }
        } else {
          if(el.classList.contains('show')) el.classList.remove('show');
        }
      });

      // glow intensity grows while dwelling on a scene
      const gi = (tref.current.accent ?? 1) * lerp(0.55, 1.15, dwellP);
      root.style.setProperty('--intensity', gi.toFixed(3));
      root.style.setProperty('--ambient', String(tref.current.ambient ?? 1));

      // hero overlay (segment 0)
      const heroEl = document.getElementById('hero');
      const cueEl  = document.getElementById('cue');
      if(heroEl){
        const o0 = off[0];
        const hp = clamp((y - o0.start)/(o0.scrub*0.7), 0, 1);
        heroEl.style.opacity = (ci===0 ? (1-smooth(hp)) : 0).toFixed(3);
        heroEl.style.transform = `translateY(${(-smooth(hp)*40).toFixed(1)}px)`;
        if(cueEl){ const cp = clamp(y/(o0.scrub*0.35),0,1); cueEl.style.opacity=(ci===0?(1-cp):0).toFixed(2); }
      }

      // logo fade with scroll
      const brand = document.getElementById('brand');
      if(brand){
        const f = clamp(y/(off[0].dwellEnd), 0, 1);
        brand.style.opacity = lerp(1, 0.32, f).toFixed(3);
      }

      // clickable hotspots over baked buttons — live only on the final clip's held frame
      const hs = document.getElementById('hotspots');
      if(hs){
        let live = (ci===CLIPS.length-1 && p>=0.9);
        const footer = document.querySelector('footer');
        if(footer && footer.getBoundingClientRect().top < vh*0.85) live=false;
        hs.classList.toggle('live', live);
      }

      // progress rail
      const pr = clamp(y/ (total.current), 0, 1);
      const prog = document.getElementById('progress');
      if(prog) prog.style.width = (pr*100).toFixed(2)+'%';

      activeRef.current=ci;
    };

    const loop = ()=>{ if(pending){ apply(); pending=false; } raf=requestAnimationFrame(loop); };
    const onScroll = ()=>{ pending=true; };
    const onResize = ()=>{ layout(); pending=true; };
    addEventListener('scroll', onScroll, {passive:true});
    addEventListener('resize', onResize);
    raf=requestAnimationFrame(loop);
    pending=true;

    return ()=>{ cancelAnimationFrame(raf); removeEventListener('scroll',onScroll); removeEventListener('resize',onResize); clearInterval(lpt); };
  },[]);

  /* relayout when pacing or enabled scenes change */
  React.useEffect(()=>{ layout(); window.dispatchEvent(new Event('scroll')); }, [t.pacing, t.scenes]);

  return (
    <>
      {/* fixed stage */}
      <div id="stage" ref={stageRef}>
        {CLIPS.map((c,i)=>(
          <img key={i} className={i===CLIPS.length-1?"clip clip-final":"clip"} alt=""
               ref={el=>vids.current[i]=el} aria-hidden="true" draggable="false" />
        ))}
      </div>

      {/* ambient */}
      <div className="fx fx-glow"></div>
      <canvas id="dust"></canvas>
      <div className="fx fx-grain"></div>
      <div className="fx fx-haze"></div>
      <div className="fx fx-vignette"></div>

      {/* chrome */}
      <div className="ui brand" id="brand">
        <img src="assets/logo_mark.png" alt="" />
        <b>beavr<span>dam</span></b>
      </div>

      {/* hero */}
      <div className="ui hero" id="hero" data-screen-label="Hero">
        <div className="kicker">{t.heroKicker}</div>
        <ParticleHero
          align="center"
          words={(t.heroWords||"").split(",").map(s=>s.trim()).filter(Boolean)}
          palette={[{r:226,g:232,b:240},{r:226,g:232,b:240},{r:255,g:140,b:0}]} />
        <p dangerouslySetInnerHTML={{__html:t.heroSub}}></p>
      </div>
      <div className="ui cue" id="cue">
        <div className="mouse"></div>
        <span>{t.scrollCue}</span>
      </div>

      {/* clickable hotspots over the baked CTA buttons in the final frame */}
      <div id="hotspots" data-screen-label="Final CTA">
        <a className="hotspot app" href="https://app.beaver.solutions/login" target="_blank" rel="noopener" aria-label="Open the app"><span>Open app</span></a>
        <a className="hotspot demo" href="Contact.html" aria-label="Book a demo"><span>Book a demo</span></a>
      </div>

      {/* progress rail */}
      <div className="ui progress" id="progress"></div>

      {/* scroll track (height set by JS) */}
      <div id="track"></div>

      {/* legal footer */}
      <footer>
        <div className="f-brand">
          <img src="assets/logo_mark.png" alt="" />
          <span>The whole dam crew, running your outbound.</span>
        </div>
        <nav>
          <a href="About.html" target="_blank" rel="noopener">About</a>
          <a href="Privacy.html" target="_blank" rel="noopener">Privacy</a>
          <a href="Terms.html" target="_blank" rel="noopener">Terms</a>
          <a href="/contact">Contact</a>
        </nav>
        <div className="copy">© 2026 Beaver Solutions. All rights reserved. · hello@beaver.solutions</div>
      </footer>

      <Tweaks t={t} setTweak={setTweak} />
    </>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<Landing/>);
