// flight/dev.js (Lane B) — harness boot for dev.html. Mirrors the parts of Lane F's boot.js
// that Lane B needs, and NOTHING else. Not shipped; not the game's entry point.
//
// It exists so flight/combat can be flown, tuned and screenshotted while boot.js still has
// no player wiring. The exact snippet F needs to paste into boot.js is in LANE_B_NOTES.md;
// when that lands, this file's only remaining job is isolated feel-tuning.

import * as THREE from 'three';
import { parseFlags } from '../core/flags.js';
import { createRng } from '../core/rng.js';
import { createBus } from '../core/bus.js';
import { createWorld as createStubWorld, STUB_LEVEL } from '../stub/world_stub.js';
import { createPlayer } from './player.js';
import { createCombat } from '../combat/index.js';

const flags = parseFlags();
const bus = createBus();
const rng = createRng(flags.seed ?? STUB_LEVEL.seed);

const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);

const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, innerWidth / innerHeight, 0.1, 600);

const world = await createStubWorld(STUB_LEVEL, { rng });
scene.add(world.group);
scene.background = new THREE.Color(world.biomeAt(0).palette.void);

const player = createPlayer({ scene, world, bus, rng, flags, camera, dom: renderer.domElement });
const combat = createCombat({ scene, world, bus, rng, flags, player });

// --- dev-only level pump ----------------------------------------------------------------
// Emits `level:event` as the player crosses each event's s. NOBODY owns this in the shipped
// build yet — TECH.md specifies the event but not its emitter. Flagged to F in NOTES; this
// local copy keeps Lane B unblocked without implementing another lane's side.
function createLevelPump(level, b) {
  const evts = [...(level.events ?? [])].sort((a, x) => a.s - x.s);
  let i = 0;
  return {
    update(s) { while (i < evts.length && s >= evts[i].s) b.emit('level:event', evts[i++]); },
    reset() { i = 0; },
  };
}
const pump = createLevelPump(world.level, bus);

// --- DBG (mirrors boot.js so NOTES numbers are measured the same way) --------------------
const dbgEl = document.getElementById('dbg');
const hintEl = document.getElementById('hint');
let frames = 0, fpsT = 0, fps = 0;
renderer.info.autoReset = false;
const stats = { draws: 0, tris: 0 };
let hud = null;
bus.on('player:state', (s) => (hud = s));
let cstate = null;
bus.on('combat:state', (s) => (cstate = s));

window.DBG = {
  get draws() { return stats.draws; },
  get tris() { return stats.tris; },
  get fps() { return fps; },
  world, player, combat, bus, flags, renderer, scene, camera,
  // Leak test (charter: "everything pooled + disposed, leak-test enter/exit"). Builds and
  // tears down N player+combat pairs and reports GPU resource deltas via renderer.info.
  leakTest(cycles = 5) {
    // Render FIRST: renderer.info.memory only counts geometries actually uploaded to the
    // GPU, so a baseline taken before any render reads 0 and the harness's own persistent
    // objects then look like a leak on the next sample. Ask me how I know.
    renderer.render(scene, camera);
    const before = { ...renderer.info.memory, children: scene.children.length };
    for (let i = 0; i < cycles; i++) {
      const p = createPlayer({ scene, world, bus, rng, flags, camera, dom: renderer.domElement });
      const c = createCombat({ scene, world, bus, rng, flags, player: p });
      c.enemies.spawn('floater', { s: 10 }); c.enemies.spawn('turret', { s: 20 });
      p.update(1 / 60); c.update(1 / 60);
      renderer.render(scene, camera);
      c.dispose(); p.dispose();
    }
    const after = { ...renderer.info.memory, children: scene.children.length };
    return { before, after,
      leaked: { geometries: after.geometries - before.geometries,
                textures: after.textures - before.textures,
                children: after.children - before.children } };
  },
  shot(name = 'shot') {
    const a = document.createElement('a');
    a.download = `${name}.png`;
    a.href = renderer.domElement.toDataURL('image/png');
    a.click();
  },
  // helpers for eyeballing behaviours without flying to them
  spawn: (type, at = 12) => combat.enemies.spawn(type, { s: player.state.s + at }),
  tp: (s) => { player.state.s = s; pump.reset(); },

  // Synchronous fixed-timestep advance. Two reasons this exists:
  //  1. requestAnimationFrame does not run in a hidden/on-demand-rendered tab, so the loop
  //     can't be exercised from an automated browser session without it.
  //  2. It's deterministic — DBG.step(120, 1/60) is exactly 2 s of game time every time,
  //     independent of frame pacing, so measurements quoted in NOTES are reproducible.
  step(n = 1, dt = 1 / 60) {
    for (let i = 0; i < n; i++) tick(dt);
    renderer.render(scene, camera);
    stats.draws = renderer.info.render.calls;
    stats.tris = renderer.info.render.triangles;
    renderer.info.reset();
    return { s: player.state.s, draws: stats.draws, tris: stats.tris };
  },
  get state() { return hud; },
  get combatState() { return cstate; },
};

addEventListener('resize', () => {
  camera.aspect = innerWidth / innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(innerWidth, innerHeight);
});

// The one true update order (mirror it in boot.js — see LANE_B_NOTES §-> Lane F):
//   player -> level pump -> combat -> world
// player first so combat hit-tests against this frame's ship position; world last so its
// shader time matches the s we just moved to.
function tick(dt) {
  player.update(dt);
  pump.update(player.state.s);
  combat.update(dt);
  world.update(dt, player.state.s);
}

let last = performance.now();
function frame(now) {
  const dt = Math.min((now - last) / 1000, 0.05);
  last = now;

  tick(dt);

  renderer.render(scene, camera);
  stats.draws = renderer.info.render.calls;
  stats.tris = renderer.info.render.triangles;
  renderer.info.reset();

  hintEl.classList.toggle('gone', !!document.pointerLockElement || flags.shots);

  frames++; fpsT += dt;
  if (fpsT >= 0.5) {
    fps = Math.round(frames / fpsT); frames = 0; fpsT = 0;
    if (!flags.shots && hud) {
      dbgEl.textContent =
        `${fps} fps · ${stats.draws} draws · ${(stats.tris / 1000) | 0}k tris\n` +
        `s ${hud.s.toFixed(0)}/${hud.length | 0}  spd ${hud.speed.toFixed(1)} (flow ${hud.flow})  thr ${hud.throttle.toFixed(2)}` +
        `${hud.surfing ? '  ◄SURF►' : ''}${hud.iframes ? '  [i]' : ''}\n` +
        `coat ${hud.coat.toFixed(0)}/${hud.coatMax}  hull ${hud.hull.toFixed(0)}/${hud.hullMax}  ` +
        `boost ${hud.boostReady ? 'READY' : hud.boostCd.toFixed(1)}\n` +
        (cstate ? `heat ${cstate.heat.toFixed(0)}/${cstate.heatMax}${cstate.overheated ? ' OVERHEAT' : ''}  ` +
          `torp ${cstate.ammo}/${cstate.ammoMax}  score ${cstate.score}  kills ${cstate.kills}  live ${cstate.enemies}` : '');
    }
  }
  requestAnimationFrame(frame);
}
if (flags.shots) dbgEl.style.display = 'none';
requestAnimationFrame(frame);
