import * as THREE from 'three';
import type { App } from './core/app';
import type { Game } from './game/game';

/**
 * Dev harness. The game is driven by mouse gestures over a 3D scene, which makes
 * "does it actually work" hard to answer by inspection — so this drives real
 * input through the real code paths, deterministically, at a chosen dt.
 *
 * Also the escape hatch for headless/embedded browsers that never fire rAF:
 * everything here calls app.step() directly.
 *
 * Dev builds only.
 */
export function installDevHarness(app: App, game: Game): void {
  const kitchen = game.kitchenView;
  const v = new THREE.Vector3();
  const toNdc = (x: number, y: number, z: number): [number, number] => {
    v.set(x, y, z).project(app.camera);
    return [v.x, v.y];
  };
  const run = (frames: number, dt = 1 / 60) => {
    for (let i = 0; i < frames; i++) app.step(dt);
  };
  const tap = (code: string) => {
    window.dispatchEvent(new KeyboardEvent('keydown', { code }));
    app.step(1 / 60);
    window.dispatchEvent(new KeyboardEvent('keyup', { code }));
  };
  const point = (x: number, y: number, z: number, down: boolean) => {
    const n = toNdc(x, y, z);
    app.input.ndc.set(n[0], n[1]);
    app.input.down = down;
  };

  const harness = {
    app,
    game,
    kitchen,
    THREE,
    run,
    tap,
    point,
    toNdc,

    /** Drop the lever, brown for `seconds`, pop, and wait for it to land. */
    toast(seconds = 10, power?: number) {
      if (power !== undefined) kitchen.power = power;
      tap('Space');
      run(Math.round(seconds * 60));
      tap('Space');
      run(200);
      return harness.stats();
    },

    dip() {
      const hit = (kitchen as unknown as { potHit: THREE.Mesh | null }).potHit;
      const wp = new THREE.Vector3(3.15, 0.29, 0.45);
      hit?.getWorldPosition(wp);
      point(wp.x, wp.y, wp.z, true);
      run(4);
      app.input.down = false;
      app.step(1 / 60);
    },

    /**
     * Raster the whole slice. `speed` is world-units/sec — the thing that
     * actually decides how thick the spread goes on.
     */
    spread(passes = 9, speed = 0.55, angle?: number) {
      const sl = kitchen.currentSlice;
      if (angle !== undefined) kitchen.knife.angle = angle;
      harness.dip();
      const stepDist = speed / 60;
      for (let p = 0; p < passes; p++) {
        const z = 0.55 - 0.44 + (p / Math.max(1, passes - 1)) * 0.88;
        const steps = Math.ceil(0.96 / stepDist);
        for (let s = 0; s <= steps; s++) {
          if (kitchen.knife.load < 0.02) harness.dip();
          const x = 1.45 + (p % 2 ? 1 : -1) * (0.48 - s * stepDist);
          point(x, sl.topY, z, true);
          app.step(1 / 60);
        }
      }
      app.input.down = false;
      harness.park();
      return harness.stats();
    },

    /** Swirl the knife in the jar — remixes a separated spread. */
    swirl(circles = 3) {
      const hit = (kitchen as unknown as { potHit: THREE.Mesh | null }).potHit;
      const wp = new THREE.Vector3(3.15, 0.56, 0.45);
      hit?.getWorldPosition(wp);
      const steps = Math.round(circles * 48);
      for (let i = 0; i <= steps; i++) {
        const a = (i / 48) * Math.PI * 2;
        point(wp.x + Math.cos(a) * 0.2, wp.y, wp.z + Math.sin(a) * 0.2, true);
        app.step(1 / 60);
      }
      app.input.down = false;
      app.step(1 / 60);
    },

    /** Park the cursor off the toast so a screenshot isn't full of knife. */
    park() {
      point(2.5, kitchen.currentSlice.topY, 1.3, false);
      app.step(1 / 60);
    },

    stats() {
      const sl = kitchen.currentSlice;
      const b = sl.browning.stats(sl.mask);
      const s = sl.spread.stats(sl.mask);
      const d = sl.damage.stats(sl.mask);
      const r = (n: number) => +n.toFixed(3);
      return {
        phase: kitchen.phase,
        warmth: r(sl.warmth),
        browning: { mean: r(b.mean), stdev: r(b.stdev), max: r(b.max) },
        char: r(sl.browning.fraction(sl.mask, (x) => x > 0.85)),
        spread: { mean: r(s.mean), stdev: r(s.stdev), max: r(s.max) },
        coverage: r(sl.spread.fraction(sl.mask, (x) => x > 0.02)),
        damage: r(d.mean),
        load: r(kitchen.knife.load),
        angle: r(kitchen.knife.angle),
        mode: kitchen.lastFx?.mode ?? 'idle',
      };
    },

    /** Move the camera somewhere for a screenshot without disturbing the sim. */
    look(pos: [number, number, number], at: [number, number, number]) {
      app.camera.position.set(...pos);
      app.camera.lookAt(new THREE.Vector3(...at));
      app.renderer.render(app.scene, app.camera);
    },
  };

  (window as unknown as { __t: unknown }).__t = harness;
}
