// ScrollCrab.jsx — Ferris hops out of the banner and walks the page as you scroll.
// Sprite sheets and numbers come from ferris-tower-3d/source/web_pack.py.
const FERRIS = {
  anchor: [0.3851, 0.5757], // sprite centre in the banner, as a fraction of its size
  spriteScale: 1.0802,      // sprite width / banner width at hop frame 0
  strideFrac: 0.0682,       // on-screen body travel per half walk cycle / sprite width
  sheets: {
    big:   { src: 'assets/ferris-sprites.webp', cols: 6, rows: 9 },
    back:  { src: 'assets/ferris-hopback.webp', cols: 6, rows: 6 },
    small: { src: 'assets/ferris-walk.webp',    cols: 8, rows: 8 },
  },
  // name: [sheet, first frame, frame count]
  clips: {
    hop: ['big', 0, 40], bannerWave: ['big', 40, 12],
    hopBack: ['back', 0, 32], // same hop from frame HOP_SHARED on, but lands facing away
    walk: ['small', 0, 12], back: ['small', 12, 12], turn: ['small', 24, 12],
    wave: ['small', 36, 12], look: ['small', 48, 12],
  },
};
const HOP_SHARED = 8;          // crouch frames both hops have in common
const BOX = 384;              // element size; frames are scaled into it with background-size
const JUMP_MS = 900, TURN_MS = 450, CLIP_MS = 1100;
const TURN_AFTER_PX = 30;     // scroll this far the other way before he turns around
// Hysteresis: he jumps out once you scroll past outAt, and only jumps home again near the very top.
const jumpLimits = vh => ({ outAt: Math.min(160, vh * 0.2), homeAt: 24 });
// 'pause': scrolling just stopped; 'first': first idle move after that; 'next': between idle moves.
const idleDelay = kind => kind === 'pause' ? 2500 : kind === 'first' ? 4000 + Math.random() * 5000 : 7000 + Math.random() * 8000;

const ease = t => t * t * (3 - 2 * t);
const clamp01 = t => Math.min(1, Math.max(0, t));
const mod = (a, n) => ((a % n) + n) % n;

const ScrollCrab = () => {
  const ref = React.useRef(null);
  const spriteRef = React.useRef(null);
  const [ready, setReady] = React.useState(false);

  React.useEffect(() => {
    const srcs = Object.values(FERRIS.sheets).map(s => s.src);
    let left = srcs.length;
    srcs.forEach(src => { const img = new Image(); img.onload = () => { if (--left === 0) setReady(true); }; img.src = src; });
  }, []);

  React.useEffect(() => {
    if (!ready) return;
    const el = ref.current, sprite = spriteRef.current;
    const still = window.matchMedia('(prefers-reduced-motion: reduce)');
    let raf = 0, sheet = null, idleTimer = 0;
    let lastScroll = window.scrollY, towardUp = 0, towardDown = 0;
    let facing = 0, facingTarget = 0, lastTick = 0; // 0 = facing us, 1 = walking away
    let idle = null; // { clip, start }
    let perch = window.scrollY > jumpLimits(window.innerHeight).outAt ? 'out' : 'home';
    let jump = null; // { dir: 'out' | 'home', start, homeward }

    const show = (clip, i, x, y, size) => {
      const [name, first, count] = FERRIS.clips[clip];
      const s = FERRIS.sheets[name];
      if (sheet !== name) {
        sheet = name;
        sprite.style.backgroundImage = `url(${s.src})`;
        sprite.style.backgroundSize = `${s.cols * 100}% ${s.rows * 100}%`;
      }
      const f = first + Math.min(count - 1, Math.max(0, i));
      const col = f % s.cols, row = Math.floor(f / s.cols);
      sprite.style.backgroundPosition = `${(col / (s.cols - 1)) * 100}% ${(row / (s.rows - 1)) * 100}%`;
      el.style.transform = `translate3d(${x - BOX / 2}px, ${y - BOX / 2}px, 0) scale(${size / BOX})`;
    };

    const draw = now => {
      raf = 0;
      const banner = document.getElementById('ferris-banner');
      const r = banner && banner.getBoundingClientRect();
      if (!r || !r.width) { el.style.opacity = 0; return; }
      el.style.opacity = 1;

      const vw = window.innerWidth, vh = window.innerHeight, scroll = window.scrollY;
      const home = { x: r.left + FERRIS.anchor[0] * r.width, y: r.top + FERRIS.anchor[1] * r.height, size: r.width * FERRIS.spriteScale };
      if (still.matches) return show('hop', 0, home.x, home.y, home.size);

      // The jump plays in its own time once triggered; crossing back mid-jump queues the return.
      const { outAt, homeAt } = jumpLimits(vh);
      if (jump && now - jump.start >= JUMP_MS) {
        perch = jump.dir;
        facing = facingTarget = 0;
        jump = null;
      }
      if (!jump && perch === 'home' && scroll > outAt) {
        jump = { dir: 'out', start: now, homeward: false }; idle = null;
      } else if (!jump && perch === 'out' && scroll < homeAt) {
        // Walking away when he gets home: jump straight back with a half spin instead of turning first.
        jump = { dir: 'home', start: now, homeward: facing >= 0.5 }; idle = null;
      }

      // Time-based parts: turning around and idle clips.
      const dt = lastTick ? Math.min(50, now - lastTick) : 0;
      lastTick = now;
      if (facing !== facingTarget) {
        facing = facingTarget > facing ? Math.min(1, facing + dt / TURN_MS) : Math.max(0, facing - dt / TURN_MS);
      }
      const clipT = idle ? (now - idle.start) / CLIP_MS : 0;
      if (idle && clipT >= 1) { idle = null; scheduleIdle('next'); }
      const busy = jump || facing !== facingTarget || idle;

      // He rides the right edge like the scrollbar thumb. The leg frame follows y, so feet stay planted.
      const size = Math.min(150, Math.max(90, vw * 0.1));
      const x = document.documentElement.clientWidth - size * 0.32 - 6;
      const y0 = size * 0.45 + 8, y1 = vh - size * 0.35;
      const y = y0 + (y1 - y0) * clamp01(scroll / Math.max(1, document.documentElement.scrollHeight - vh));
      const step = y / (2 * FERRIS.strideFrac * size);
      const walkN = FERRIS.clips.walk[2];

      if (jump) {
        const t = clamp01((now - jump.start) / JUMP_MS);
        const u = jump.dir === 'out' ? t : 1 - t; // the way home plays the hop backwards
        const k = ease(u), i = Math.round(u * (FERRIS.clips.hop[2] - 1));
        const px = home.x + (x - home.x) * k, py = home.y + (y - home.y) * k, ps = home.size + (size - home.size) * k;
        if (jump.homeward && i >= HOP_SHARED) show('hopBack', i - HOP_SHARED, px, py, ps);
        else show('hop', i, px, py, ps);
      } else if (perch === 'home') {
        if (idle && idle.clip === 'bannerWave') show('bannerWave', Math.floor(clipT * 12), home.x, home.y, home.size);
        else show('hop', 0, home.x, home.y, home.size);
      } else if (facing > 0 && facing < 1) {
        show('turn', Math.round(facing * 11), x, y, size);
      } else if (facing === 1) {
        show('back', Math.floor(mod(-step, 1) * walkN), x, y, size);
      } else if (idle) {
        show(idle.clip, Math.floor(clipT * 12), x, y, size);
      } else {
        show('walk', Math.floor(mod(step, 1) * walkN), x, y, size);
      }
      if (busy) raf = requestAnimationFrame(draw);
    };
    const schedule = () => { if (!raf) raf = requestAnimationFrame(draw); };

    const atBottom = () => window.scrollY >= document.documentElement.scrollHeight - window.innerHeight - 2;

    // After a pause he turns to face you, then now and then waves or glances around.
    const scheduleIdle = kind => {
      clearTimeout(idleTimer);
      idleTimer = setTimeout(() => {
        if (jump) return scheduleIdle('next');
        if (perch === 'home') { idle = { clip: 'bannerWave', start: performance.now() }; return schedule(); }
        if (atBottom()) return scheduleIdle('next'); // stays facing up the page
        if (facingTarget === 1) { facingTarget = 0; schedule(); return scheduleIdle('first'); }
        idle = { clip: Math.random() < 0.5 ? 'wave' : 'look', start: performance.now() };
        schedule();
      }, idleDelay(kind));
    };

    const aim = () => {
      if (perch !== 'out' || jump) return;     // on the pedestal or mid-jump he faces us
      if (atBottom()) facingTarget = 1;        // end of the track: turn round, ready to walk back up
      else if (towardUp > TURN_AFTER_PX) facingTarget = 1;
      else if (towardDown > TURN_AFTER_PX) facingTarget = 0;
    };

    const onScroll = () => {
      const scroll = window.scrollY, delta = scroll - lastScroll;
      lastScroll = scroll;
      if (delta < 0) { towardUp -= delta; towardDown = 0; } else if (delta > 0) { towardDown += delta; towardUp = 0; }
      aim();
      idle = null;
      scheduleIdle('pause');
      schedule();
    };

    aim();
    facing = facingTarget;
    draw(performance.now());
    scheduleIdle('pause');
    window.addEventListener('scroll', onScroll, { passive: true });
    window.addEventListener('resize', schedule);
    still.addEventListener('change', schedule);
    const banner = document.getElementById('ferris-banner');
    banner && banner.addEventListener('load', schedule);
    return () => {
      cancelAnimationFrame(raf);
      clearTimeout(idleTimer);
      window.removeEventListener('scroll', onScroll);
      window.removeEventListener('resize', schedule);
      still.removeEventListener('change', schedule);
      banner && banner.removeEventListener('load', schedule);
    };
  }, [ready]);

  if (!ready) return null;
  return (
    <div ref={ref} aria-hidden="true" style={{
      position: 'fixed', top: 0, left: 0, zIndex: 50, pointerEvents: 'none',
      width: BOX, height: BOX, transformOrigin: 'center', opacity: 0, willChange: 'transform',
    }}>
      {/* Breathing: keyframes in index.html, anchored at his feet (~65% down the frame). */}
      <div ref={spriteRef} className="ferris-breathe" style={{ width: '100%', height: '100%', backgroundRepeat: 'no-repeat', transformOrigin: '50% 65%' }} />
    </div>
  );
};
window.ScrollCrab = ScrollCrab;
