// Persistent presenter chrome — left-rail act spine + top hairline timecode.
// Lean: reads document scroll for both the hairline progress and the active
// act. Clicking a spine dot scrolls to that act.

const { useState, useEffect } = window;

function TopHairline() {
  const [p, setP] = useState(0);
  useEffect(() => {
    const onScroll = () => {
      const doc = document.documentElement;
      const max = doc.scrollHeight - window.innerHeight;
      setP(max > 0 ? window.scrollY / max : 0);
    };
    window.addEventListener('scroll', onScroll, { passive: true });
    onScroll();
    return () => window.removeEventListener('scroll', onScroll);
  }, []);
  return (
    <div style={{
      position: 'fixed', top: 0, left: 0, right: 0, height: 2,
      background: 'rgba(255,255,255,0.06)', zIndex: 60, pointerEvents: 'none',
    }}>
      <div style={{
        height: '100%', width: `${p * 100}%`,
        background: 'linear-gradient(to right, var(--sage), var(--gold))',
      }} />
    </div>
  );
}

function Spine({ acts, active }) {
  const go = (id) => {
    const el = document.getElementById('act-' + id);
    if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
  };
  return (
    <nav
      aria-label="Acts"
      style={{
        position: 'fixed', right: 22, top: '50%',
        transform: 'translateY(-50%)',
        display: 'flex', flexDirection: 'column', gap: 14,
        zIndex: 60, padding: '18px 10px',
      }}
    >
      {acts.map((a, i) => {
        const isActive = i === active;
        return (
          <button
            key={a.id}
            onClick={() => go(a.id)}
            aria-label={`${a.roman}. ${a.title}`}
            title={`${a.roman}. ${a.title}`}
            style={{
              appearance: 'none', background: 'transparent', border: 'none',
              display: 'flex', alignItems: 'center', gap: 10, cursor: 'pointer', padding: 0,
              flexDirection: 'row-reverse',
            }}
          >
            <span style={{
              display: 'inline-block',
              width: isActive ? 10 : 6, height: isActive ? 10 : 6,
              borderRadius: '50%',
              background: isActive ? 'var(--gold)' : 'rgba(143,166,143,0.45)',
              boxShadow: isActive ? '0 0 12px rgba(201,168,76,0.7)' : 'none',
              transition: 'all 0.25s ease',
            }} />
            <span style={{
              fontFamily: 'var(--font-ui)', fontSize: 9,
              letterSpacing: '0.22em', textTransform: 'uppercase',
              color: isActive ? 'var(--parchment)' : 'var(--taupe)',
              opacity: isActive ? 1 : 0.55, transition: 'all 0.25s ease',
            }}>{a.roman}</span>
          </button>
        );
      })}
    </nav>
  );
}

// Mode toggle — Scroll Mode (cinematic, smooth, current default) vs Click
// Mode (snap, no tween, behaves like Keynote / PowerPoint).
//
// Scroll Mode:  900ms eased scroll-tween per clicker press, 0.18s opacity
//               transitions on cross-fades. The "deck-as-scroll-site" feel.
// Click Mode:   80ms scroll (effectively instant), 60ms transitions.
//               The "deck-as-slideshow" feel. For Svet if she wants tighter
//               control, or if the cinematic transitions feel slow on stage.
//
// State lives on window.__deckClickMode (boolean) AND in a body data-attr,
// AND is persisted to localStorage so the mode survives reloads.
function ModeToggle() {
  const [mode, setMode] = useState(() => {
    try {
      return localStorage.getItem('vv_deck_mode') || 'scroll';
    } catch {
      return 'scroll';
    }
  });
  useEffect(() => {
    window.__deckClickMode = (mode === 'click');
    document.body.dataset.deckMode = mode;
    try { localStorage.setItem('vv_deck_mode', mode); } catch {}
  }, [mode]);
  const isClick = mode === 'click';
  const toggle = () => setMode(isClick ? 'scroll' : 'click');
  return (
    <button
      onClick={toggle}
      aria-label={isClick ? 'Switch to Scroll Mode' : 'Switch to Click Mode'}
      title={isClick ? 'Switch to Scroll Mode (cinematic transitions)' : 'Switch to Click Mode (snap transitions)'}
      style={{
        position: 'fixed', bottom: 18, left: 130,
        display: 'inline-flex', alignItems: 'center', gap: 8,
        padding: '8px 14px',
        background: isClick ? 'rgba(201,168,76,0.18)' : 'rgba(7,50,66,0.55)',
        border: `1px solid ${isClick ? 'var(--gold)' : 'rgba(143,166,143,0.45)'}`,
        color: isClick ? 'var(--gold)' : 'var(--sage)',
        fontFamily: 'var(--font-ui)',
        fontSize: 10,
        letterSpacing: '0.22em',
        textTransform: 'uppercase',
        cursor: 'pointer',
        backdropFilter: 'blur(6px)',
        zIndex: 61,
        transition: 'all 0.2s ease',
      }}
    >
      <span style={{
        width: 8, height: 8, borderRadius: '50%',
        background: isClick ? 'var(--gold)' : 'var(--sage)',
        boxShadow: isClick ? '0 0 8px rgba(201,168,76,0.6)' : 'none',
      }} />
      <span>{isClick ? 'Click Mode' : 'Scroll Mode'}</span>
    </button>
  );
}

// Always-available link to the Appendix (fashion-school directory + archive
// network reference). Discrete bottom-left pin; opens in a new tab so the
// presenter doesn't lose deck position. Per Doug's feedback after pulling
// the inline directory button — the appendix is valuable, just shouldn't
// crowd a slide.
function AppendixPin() {
  return (
    <a
      href="Appendix.html"
      target="_blank"
      rel="noopener noreferrer"
      aria-label="Open the Appendix · 42-school directory + archive network"
      title="Appendix · directory + archive network"
      style={{
        position: 'fixed', bottom: 18, left: 18,
        display: 'inline-flex', alignItems: 'center', gap: 8,
        padding: '8px 14px',
        background: 'rgba(7,50,66,0.55)',
        border: '1px solid rgba(201,168,76,0.4)',
        color: 'var(--gold)',
        fontFamily: 'var(--font-ui)',
        fontSize: 10,
        letterSpacing: '0.22em',
        textTransform: 'uppercase',
        textDecoration: 'none',
        backdropFilter: 'blur(6px)',
        zIndex: 61,
        transition: 'all 0.2s ease',
      }}
      onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(201,168,76,0.18)'; }}
      onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(7,50,66,0.55)'; }}
    >
      <span>Appendix</span>
      <span style={{ fontFamily: 'var(--font-display)', fontSize: 12 }}>↗</span>
    </a>
  );
}

function FullscreenToggle() {
  const [isFull, setIsFull] = useState(false);
  useEffect(() => {
    const onChange = () => setIsFull(!!document.fullscreenElement);
    document.addEventListener('fullscreenchange', onChange);
    return () => document.removeEventListener('fullscreenchange', onChange);
  }, []);
  const toggle = () => {
    if (document.fullscreenElement) document.exitFullscreen();
    else document.documentElement.requestFullscreen();
  };
  return (
    <button
      onClick={toggle}
      aria-label={isFull ? 'Exit fullscreen' : 'Enter fullscreen'}
      title={isFull ? 'Exit fullscreen' : 'Enter fullscreen'}
      style={{
        position: 'fixed', top: 18, right: 18,
        width: 36, height: 36,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
        background: 'rgba(7,50,66,0.5)',
        border: '1px solid rgba(201,168,76,0.35)',
        color: 'var(--gold)',
        fontFamily: 'var(--font-display)', fontSize: 16,
        cursor: 'pointer',
        zIndex: 61,
        backdropFilter: 'blur(6px)',
        transition: 'all 0.2s ease',
      }}
      onMouseEnter={(e) => { e.currentTarget.style.background = 'rgba(201,168,76,0.18)'; }}
      onMouseLeave={(e) => { e.currentTarget.style.background = 'rgba(7,50,66,0.5)'; }}
    >
      {isFull ? '⤢' : '⛶'}
    </button>
  );
}

// Clicker / keyboard advance — one beat (100vh) per press, tweened over
// ~900ms so scroll-scrubbed reveals (staggered cards, fades) have time to
// play. The browser's native `behavior: 'smooth'` is ~300ms, which rushes
// multi-element stagger. We animate by hand with rAF + easeOutCubic.
// Presses during an active tween are ignored (debounce via `animating`) so
// clicker-mashing doesn't stack scroll targets.
//
// Beat-snap stabilizer (added 2026-04-29): if the user wheel/trackpad-
// scrolls and rests partway between two beat anchors, the deck would
// otherwise leave the inter-act transition fade stuck mid-flight (e.g.
// both stages at 0.4 opacity, neither dominant). After scroll stops, we
// detect the rest position and auto-snap to the nearest 100vh boundary —
// so a transition, once initiated, always resolves.
function useClickerNavigation() {
  useEffect(() => {
    let animating = false;
    let snapTimer = null;
    const BEAT_DURATION_SCROLL = 900;     // ms per beat step in scroll mode
    const BEAT_DURATION_CLICK  = 80;      // ms in click mode (effectively instant)
    const SNAP_DURATION = 380;     // ms — short tween for the rest-snap
    const SNAP_DEBOUNCE = 220;     // ms after scroll stops before snapping
    const SNAP_TOLERANCE_PX = 4;   // don't snap if within 4px of a boundary
    const JUMP_DURATION = 1400;    // ms for Home/End
    const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3);

    // Resolve duration at the moment of tween — supports toggling Scroll/Click
    // mode mid-deck. window.__deckClickMode is owned by ModeToggle.
    const resolveBeatDuration = () =>
      window.__deckClickMode ? BEAT_DURATION_CLICK : BEAT_DURATION_SCROLL;

    const tweenTo = (target, duration) => {
      if (animating) return;
      animating = true;
      const start = window.scrollY;
      const clamped = Math.max(0, Math.min(
        document.documentElement.scrollHeight - window.innerHeight,
        target
      ));
      const delta = clamped - start;
      if (delta === 0) { animating = false; return; }
      const t0 = performance.now();
      const tick = (now) => {
        const r = Math.min(1, (now - t0) / duration);
        window.scrollTo(0, start + delta * easeOutCubic(r));
        if (r < 1) requestAnimationFrame(tick);
        else animating = false;
      };
      requestAnimationFrame(tick);
    };

    // Snap the user to the nearest beat anchor (every 100vh from doc top).
    // Skipped while the clicker is mid-tween to avoid fighting the active
    // animation.
    const snapToNearestBeat = () => {
      if (animating) return;
      const vh = window.innerHeight;
      if (vh <= 0) return;
      const current = window.scrollY;
      const nearest = Math.round(current / vh) * vh;
      if (Math.abs(current - nearest) > SNAP_TOLERANCE_PX) {
        tweenTo(nearest, SNAP_DURATION);
      }
    };

    // Each press advances one beat (100vh of scroll). With sticky stages
    // each viewport-step lands on a beat boundary; clicker-mashing through
    // an act sweeps its reveals at presenter pace.
    const step = (dir) => tweenTo(window.scrollY + dir * window.innerHeight, resolveBeatDuration());

    const onKey = (e) => {
      const t = e.target;
      if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable)) return;
      switch (e.key) {
        case 'PageDown':
        case 'ArrowDown':
        case 'ArrowRight':
        case ' ':
          e.preventDefault(); step(1); break;
        case 'PageUp':
        case 'ArrowUp':
        case 'ArrowLeft':
          e.preventDefault(); step(-1); break;
        case 'Home':
          e.preventDefault();
          tweenTo(0, JUMP_DURATION); break;
        case 'End':
          e.preventDefault();
          tweenTo(document.documentElement.scrollHeight, JUMP_DURATION); break;
      }
    };

    // After any scroll event, schedule a snap. The clicker tween itself
    // fires scroll events as it tweens, but `animating` short-circuits
    // snapping mid-tween. Wheel/trackpad scrolls always pass through.
    const onScroll = () => {
      clearTimeout(snapTimer);
      snapTimer = setTimeout(snapToNearestBeat, SNAP_DEBOUNCE);
    };

    window.addEventListener('keydown', onKey);
    window.addEventListener('scroll', onScroll, { passive: true });
    return () => {
      window.removeEventListener('keydown', onKey);
      window.removeEventListener('scroll', onScroll);
      clearTimeout(snapTimer);
    };
  }, []);
}

function PresenterChrome({ acts, active }) {
  useClickerNavigation();
  return (<><TopHairline /><Spine acts={acts} active={active} /><FullscreenToggle /><AppendixPin /><ModeToggle /></>);
}

Object.assign(window, { PresenterChrome });
