/* PRAXIS Loop — app shell: altitude state machine, cosmos choreography, tweaks */
/* global React, ReactDOM, TopBar, StatusStrip, UniverseScreen, SectorScreen, StarScreen, MissionScreen, ReviewScreen,
          TweaksPanel, TweakSection, TweakSlider, TweakToggle, TweakRadio, TweakColor, useTweaks */
const { useState: useStateA, useEffect: useEffectA, useRef: useRefA, useCallback: useCallbackA } = React;

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "frame": "expedition",
  "motionDrama": 1,
  "density": "regular",
  "macroAccent": "#5CE6D5",
  "ambientSound": false
}/*EDITMODE-END*/;

/* camera per altitude */
function cameraFor(view, sector) {
  const s = sector || { x: 0.5, y: 0.5 };
  switch (view) {
    case 'universe': return { x: 0.5, y: 0.52, zoom: 1 };
    case 'sector':   return { x: s.x, y: s.y, zoom: 3.0 };
    case 'star':     return { x: s.x + 0.02, y: s.y - 0.01, zoom: 5.2 };
    case 'mission':  return { x: s.x + 0.03, y: s.y, zoom: 7.5 };
    case 'review':   return { x: s.x, y: s.y - 0.04, zoom: 1.9 };
    default:         return { x: 0.5, y: 0.52, zoom: 1 };
  }
}

const ALTITUDE = { universe: 0, sector: 1, star: 2, mission: 3, review: 2.5 };

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const [view, setView] = useStateA('universe');
  const [phase, setPhase] = useStateA('in');           // 'in' | 'out'
  const [focusSector, setFocusSector] = useStateA(null);
  const [selectedStar, setSelectedStar] = useStateA('net-bgp');
  const lex = window.PRAXIS.LEXICONS[t.frame] || window.PRAXIS.LEXICONS.expedition;

  /* mount cosmos once */
  useEffectA(() => {
    // entrance choreography only where frames actually flow: latch the class on
    // the first real frame and keep it (frozen/captured contexts never get it).
    requestAnimationFrame(() => document.documentElement.classList.add('motion-live'));
    // Stall watchdog: in frame-starved contexts (hidden tabs, capture iframes)
    // CSS animations freeze at currentTime 0 while `backwards` fill keeps the
    // element invisible. Timers still fire there, so detect animations making
    // zero progress between ticks and snap them to their end state. A live
    // animation always advances between ticks, so real motion is untouched.
    const seen = new WeakMap();
    const stallWatch = setInterval(() => {
      for (const a of document.getAnimations()) {
        if (a.playState !== 'running') { seen.delete(a); continue; }
        const prev = seen.get(a);
        if (prev !== undefined && prev === a.currentTime) {
          try { a.finish(); } catch (err) { /* infinite animations can't finish */ }
        }
        seen.set(a, a.currentTime);
      }
    }, 700);
    const canvas = document.getElementById('cosmos');
    window.PraxisCosmos.mount(canvas);
    window.PraxisCosmos.setSectors(window.PRAXIS.SECTORS, window.PRAXIS.EDGES);
    window.PraxisCosmos.setCamera({ x: 0.5, y: 0.2, zoom: 0.55 });
    window.PraxisCosmos.flyTo(cameraFor('universe'), 2400);   // opening establishing shot
    return () => clearInterval(stallWatch);
  }, []);

  /* apply tweak CSS vars */
  useEffectA(() => {
    const root = document.documentElement;
    root.style.setProperty('--motion-scale', String(t.motionDrama));
    root.style.setProperty('--density', t.density === 'dense' ? '0.85' : '1');
    root.style.setProperty('--cyan', t.macroAccent);
    root.style.setProperty('--cyan-fill', t.macroAccent + '1A');
    root.style.setProperty('--cyan-line', t.macroAccent + '52');
  }, [t.motionDrama, t.density, t.macroAccent]);

  /* sound */
  useEffectA(() => { window.PraxisSound.setEnabled(!!t.ambientSound); }, [t.ambientSound]);

  /* the descent/ascent — single navigation verb */
  const navLockRef = useRefA(false);
  const navigate = useCallbackA((to, sectorId) => {
    if (to === view || navLockRef.current) return;
    navLockRef.current = true;
    const sector = window.PRAXIS.SECTORS.find(s => s.id === (sectorId || focusSector)) || null;
    const descending = ALTITUDE[to] > ALTITUDE[view];
    descending ? window.PraxisSound.descend() : window.PraxisSound.ascend();
    setPhase('out');
    window.PraxisCosmos.flyTo(cameraFor(to, sector), 1100 * t.motionDrama);
    window.PraxisCosmos.setFocus(to === 'universe' ? null : (sector ? sector.id : null));
    const wait = Math.max(380 * t.motionDrama, 120);
    setTimeout(() => {
      setView(to);
      setPhase('in');
      navLockRef.current = false;
      if (to === 'universe') setFocusSector(f => f);  // keep last focus as memory
    }, wait);
  }, [view, focusSector, t.motionDrama]);

  /* universe label focus (no altitude change) */
  const focusOn = id => {
    setFocusSector(id);
    window.PraxisCosmos.setFocus(id);
    const s = window.PRAXIS.SECTORS.find(x => x.id === id);
    window.PraxisCosmos.flyTo({ x: 0.5 * 0.35 + s.x * 0.65, y: 0.52 * 0.35 + s.y * 0.65, zoom: 1.35 }, 900 * t.motionDrama);
  };

  /* wheel zoom + drag pan — the infinite-zoom navigation.
     Zoom INTO a nebula far enough and you descend; zoom OUT of a sector and you ascend. */
  useEffectA(() => {
    const PC = window.PraxisCosmos;
    const overUi = e => e.target && e.target.closest &&
      e.target.closest('.lp-inspector, .lp-topbar, .lp-status, .lp-card, .lp-rail, .lp-uni-left, button, textarea, input');
    let acc = 0, accT = 0;
    const bump = dy => {
      const now = performance.now();
      if (now - accT > 650) acc = 0;
      accT = now;
      acc += dy;
      return acc;
    };
    const onWheel = e => {
      if (overUi(e) || view === 'mission') return;
      e.preventDefault();
      if (view === 'universe') {
        const z = PC.zoomAt(e.clientX, e.clientY, Math.exp(-e.deltaY * 0.0016));
        if (z >= 2.85 && e.deltaY < 0) {
          // deep enough — descend into the nearest nebula
          let best = null, bestD = Infinity;
          for (const s of window.PRAXIS.SECTORS) {
            const p = PC.project(s.x, s.y);
            const d = Math.hypot(p.x - e.clientX, p.y - e.clientY);
            if (d < bestD) { bestD = d; best = s; }
          }
          if (best && bestD < Math.max(220, window.innerWidth * 0.2)) {
            setFocusSector(best.id);
            navigate('sector', best.id);
          }
        }
      } else if (view === 'sector') {
        if (e.deltaY > 0 && bump(e.deltaY) > 420) navigate('universe');
        else if (e.deltaY < 0 && bump(e.deltaY) < -420) navigate('star');
      } else if (view === 'star') {
        if (e.deltaY > 0 && bump(e.deltaY) > 420) navigate('sector');
      } else if (view === 'review') {
        if (e.deltaY > 0 && bump(e.deltaY) > 420) navigate('universe');
      }
    };
    // drag to pan (universe only)
    let dragging = false, lx = 0, ly = 0;
    const onDown = e => {
      if (view !== 'universe' || overUi(e) || e.button !== 0) return;
      dragging = true; lx = e.clientX; ly = e.clientY;
      document.body.style.cursor = 'grabbing';
    };
    const onMove = e => {
      if (!dragging) return;
      PC.panBy(e.clientX - lx, e.clientY - ly);
      lx = e.clientX; ly = e.clientY;
    };
    const onUp = () => { dragging = false; document.body.style.cursor = ''; };
    window.addEventListener('wheel', onWheel, { passive: false });
    window.addEventListener('pointerdown', onDown);
    window.addEventListener('pointermove', onMove);
    window.addEventListener('pointerup', onUp);
    return () => {
      window.removeEventListener('wheel', onWheel);
      window.removeEventListener('pointerdown', onDown);
      window.removeEventListener('pointermove', onMove);
      window.removeEventListener('pointerup', onUp);
      document.body.style.cursor = '';
    };
  }, [view, navigate]);

  /* keyboard: Esc ascends */
  useEffectA(() => {
    const onKey = e => {
      if (e.key !== 'Escape') return;
      if (view === 'sector') navigate('universe');
      else if (view === 'star') navigate('sector');
      else if (view === 'mission') navigate('star');
      else if (view === 'review') navigate('universe');
      else if (view === 'universe' && focusSector) {
        setFocusSector(null); window.PraxisCosmos.setFocus(null);
        window.PraxisCosmos.flyTo(cameraFor('universe'), 900 * t.motionDrama);
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [view, focusSector, navigate, t.motionDrama]);

  /* breadcrumb cosmology */
  const sectorName = (window.PRAXIS.SECTORS.find(s => s.id === focusSector) || {}).name || 'Networking';
  const crumbsByView = {
    universe: [lex.universe],
    sector:   [lex.universe, sectorName],
    star:     [lex.universe, sectorName, 'BGP and Internet Routing'],
    mission:  [lex.universe, sectorName, 'BGP and Internet Routing', lex.mission],
    review:   [lex.universe, sectorName, 'BGP and Internet Routing', lex.review],
  };
  const crumbs = crumbsByView[view];
  const crumbTargets = ['universe', 'sector', 'star', view === 'review' ? 'review' : 'mission'];

  const statusByView = {
    universe: [{ k: 'mastery', v: '62' }, { k: 'trend', v: '▲ rising', tone: 'var(--mastery)' }, { k: 'next review', v: 'Jun 14', tone: 'var(--info, var(--cyan))' }, { k: 'loop', v: 'closed', tone: 'var(--decay)' }],
    sector:   [{ k: 'sector avg', v: '62' }, { k: 'decaying', v: '1', tone: 'var(--decay)' }, { k: 'due today', v: 'BGP', tone: 'var(--decay)' }],
    star:     [{ k: 'mastery', v: '31', tone: 'var(--m-1)' }, { k: 'trend', v: '▼ 9', tone: 'var(--decay)' }, { k: 'evidence', v: '6' }],
    mission:  [{ k: 'stage', v: 'live', tone: 'var(--cyan)' }, { k: 'star', v: 'net-bgp' }, { k: 'projected', v: '31 → 48', tone: 'var(--m-3)' }],
    review:   [{ k: 'mastery', v: '48', tone: 'var(--m-3)' }, { k: 'trend', v: '▲ +17', tone: 'var(--mastery)' }, { k: 'loop', v: 'closed', tone: 'var(--mastery)' }],
  };
  const hints = {
    universe: '⌘K ask the universe · drag to pan · scroll to zoom · click a sector to concentrate',
    sector:   'click a star · esc ascends',
    star:     'esc ascends to the constellation',
    mission:  'the cosmos waits — esc aborts',
    review:   'esc returns to the universe',
  };

  /* cosmos treatment per altitude (dim/blur as you go deeper) */
  useEffectA(() => {
    const wrap = document.getElementById('cosmos-wrap');
    wrap.dataset.altitude = view;
  }, [view]);

  return (
    <React.Fragment>
      <TopBar lex={lex} crumbs={crumbs} sound={!!t.ambientSound}
        onSound={() => setTweak('ambientSound', !t.ambientSound)}
        onCrumb={i => { const target = crumbTargets[i]; if (target !== view) navigate(target); }} />

      <main className={`lp-stage ${phase === 'out' ? 'is-out' : 'is-in'}`} data-screen-label={`Praxis Loop · ${view}`}>
        {view === 'universe' ? (
          <UniverseScreen lex={lex} focus={focusSector} onFocus={focusOn}
            onDescend={id => navigate('sector', id)} />
        ) : null}
        {view === 'sector' ? (
          <SectorScreen lex={lex} sectorId={focusSector || 'networking'} selected={selectedStar}
            onSelect={setSelectedStar} onOpenStar={() => navigate('star')} />
        ) : null}
        {view === 'star' ? <StarScreen lex={lex} onEngage={() => navigate('mission')} /> : null}
        {view === 'mission' ? <MissionScreen lex={lex} onComplete={() => navigate('review')} /> : null}
        {view === 'review' ? <ReviewScreen lex={lex} onAscend={() => navigate('universe')} /> : null}
      </main>

      <StatusStrip items={statusByView[view]} hint={hints[view]} />

      <TweaksPanel>
        <TweakSection label="Narrative frame" />
        <TweakRadio label="Frame" value={t.frame}
          options={['cosmology', 'expedition', 'observatory']}
          onChange={v => setTweak('frame', v)} />
        <TweakSection label="Motion" />
        <TweakSlider label="Drama" value={t.motionDrama} min={0.4} max={1.8} step={0.1}
          onChange={v => setTweak('motionDrama', v)} />
        <TweakSection label="Surface" />
        <TweakRadio label="Density" value={t.density} options={['regular', 'dense']}
          onChange={v => setTweak('density', v)} />
        <TweakColor label="Macro accent" value={t.macroAccent}
          options={['#5CE6D5', '#6BB8FF', '#B08CFF', '#7FE08A']}
          onChange={v => setTweak('macroAccent', v)} />
        <TweakSection label="Atmosphere" />
        <TweakToggle label="Ambient sound" value={!!t.ambientSound}
          onChange={v => setTweak('ambientSound', v)} />
      </TweaksPanel>
    </React.Fragment>
  );
}

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