// combat/weapons.js (Lane B) — the lysozyme cannon and the antacid torpedo.
//
// Pooling law: nothing is allocated after construction. Each weapon owns a fixed-capacity
// InstancedMesh; firing wakes a slot, expiry sleeps it, and `count` is set to the live
// prefix each frame. One draw call per projectile type, forever, no matter the fight.
//
// Projectiles carry an `s` that is advanced LINEARLY (never re-projected). It is only a
// broad-phase hint for the s-bucket lookup — the actual hit test is a world-space distance
// compare, so a few units of drift over a pellet's 1.15 s life costs nothing and saves us a
// world.project() per pellet per frame.

import * as THREE from 'three';
import { BALANCE as B } from './balance.js';
import { emissiveMat, EMISSIVE } from '../flight/emissive.js';

const _v = new THREE.Vector3(), _q = new THREE.Quaternion(), _m = new THREE.Matrix4();
const _scale = new THREE.Vector3(), _dir = new THREE.Vector3();
const FORWARD = new THREE.Vector3(0, 0, 1);

// A fixed-capacity instanced pool. Every visual in combat/ is one of these.
function makePool(scene, geo, mat, cap, makeSlot) {
  const mesh = new THREE.InstancedMesh(geo, mat, cap);
  mesh.frustumCulled = false;                 // projectiles live around the camera anyway
  mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
  mesh.count = 0;
  scene.add(mesh);
  const slots = Array.from({ length: cap }, makeSlot);
  return {
    mesh, slots,
    take() { for (const s of slots) if (!s.alive) { s.alive = true; return s; } return null; },
    // Pack live slots into the front of the instance buffer; count is the live prefix.
    flush(write) {
      let n = 0;
      for (const s of slots) if (s.alive) write(s, n++);
      mesh.count = n;
      mesh.instanceMatrix.needsUpdate = true;
      return n;
    },
    dispose() { scene.remove(mesh); mesh.dispose(); geo.dispose(); mat.dispose(); },
  };
}

export function createWeapons({ scene, world, bus, rng, player, targets }) {
  const rand = rng ? rng('weapons') : () => 0.5;

  // --- lysozyme cannon --------------------------------------------------------------
  const pelletGeo = new THREE.SphereGeometry(B.cannon.radius, 6, 4);
  const cannon = makePool(scene, pelletGeo, emissiveMat(EMISSIVE.neutral, { additive: true }),
    B.cannon.pool, () => ({ alive: false, pos: new THREE.Vector3(), vel: new THREE.Vector3(), s: 0, sSpeed: 0, life: 0 }));

  // --- antacid torpedo --------------------------------------------------------------
  const torpGeo = new THREE.SphereGeometry(B.torpedo.radius, 8, 6);
  const torps = makePool(scene, torpGeo, emissiveMat(EMISSIVE.acid, { additive: true }),
    B.torpedo.pool, () => ({ alive: false, pos: new THREE.Vector3(), vel: new THREE.Vector3(), s: 0, sSpeed: 0, life: 0 }));

  // --- blasts (kills + torpedo detonations) -----------------------------------------
  const blastGeo = new THREE.SphereGeometry(1, 10, 8);
  const blasts = makePool(scene, blastGeo, emissiveMat(EMISSIVE.hostileShot, { additive: true, opacity: 0.7 }),
    24, () => ({ alive: false, pos: new THREE.Vector3(), t: 0, dur: 0.35, size: 1 }));

  const heat = { value: 0, locked: false };
  let fireTimer = 0, torpTimer = 0;
  let ammo = B.torpedo.ammo;

  function spawnBlast(pos, size, dur = 0.35) {
    const b = blasts.take();
    if (!b) return;
    b.pos.copy(pos); b.t = 0; b.size = size; b.dur = dur;
  }

  function fireCannon() {
    const p = cannon.take();
    if (!p) return;
    _dir.copy(player.aimPoint).sub(player.position).normalize();
    _dir.x += (rand() - 0.5) * B.cannon.spread;
    _dir.y += (rand() - 0.5) * B.cannon.spread;
    _dir.normalize();
    p.pos.copy(player.position);
    p.vel.copy(_dir).multiplyScalar(B.cannon.speed);
    p.life = B.cannon.life;
    p.s = player.state.s;
    p.sSpeed = p.vel.dot(world.sample(p.s).tan);
    heat.value += B.cannon.heatPerShot;
    bus.emit('audio:cue', { name: 'cannon' });
  }

  function fireTorpedo() {
    const t = torps.take();
    if (!t) return;
    _dir.copy(player.aimPoint).sub(player.position).normalize();
    t.pos.copy(player.position);
    t.vel.copy(_dir).multiplyScalar(B.torpedo.speed);
    t.life = B.torpedo.life;
    t.s = player.state.s;
    t.sSpeed = t.vel.dot(world.sample(t.s).tan);
    ammo--;
    bus.emit('audio:cue', { name: 'torpedo' });
  }

  // The dual-use bit (GDD): a torpedo both damages and neutralizes acid for ~10 s.
  // Lanes A and C consume this; we emit it faithfully whether or not anyone listens yet.
  function detonate(t) {
    spawnBlast(t.pos, B.torpedo.splash, 0.5);
    const s = THREE.MathUtils.clamp(t.s, 0, world.length);
    bus.emit('level:neutralize', { s, radius: B.torpedo.neutralizeRadius, duration: B.torpedo.neutralizeDuration });
    bus.emit('audio:cue', { name: 'torpedo_blast' });
    targets.forEachNear(s, B.torpedo.splash, (e) => {
      const d = e.pos.distanceTo(t.pos);
      if (d > B.torpedo.splash + e.radius) return;
      const falloff = 1 - d / (B.torpedo.splash + e.radius);
      targets.hurt(e, B.torpedo.damage * falloff, 'torpedo');
    });
    t.alive = false;
  }

  function stepProjectile(p, dt) {
    p.pos.addScaledVector(p.vel, dt);
    p.s += p.sSpeed * dt;             // broad-phase hint only — see header
    p.life -= dt;
  }

  function writeProjectile(p, i, mesh, stretch) {
    _dir.copy(p.vel).normalize();
    _q.setFromUnitVectors(FORWARD, _dir);
    _scale.set(1, 1, stretch);
    mesh.setMatrixAt(i, _m.compose(p.pos, _q, _scale));
  }

  function update(dt) {
    const st = player.state;

    // heat: cool always, lock at overheatAt, unlock at resumeAt (hysteresis)
    heat.value = Math.max(0, heat.value - B.cannon.coolRate * dt);
    if (heat.value >= B.cannon.overheatAt && !heat.locked) {
      heat.locked = true;
      bus.emit('audio:cue', { name: 'overheat' });
    }
    if (heat.locked && heat.value <= B.cannon.resumeAt) heat.locked = false;

    const intent = player.intent;
    fireTimer -= dt;
    torpTimer -= dt;
    if (st.alive && intent.fire && !heat.locked) {
      // catch up in fixed steps so rate of fire is frame-rate independent
      let guard = 8;
      while (fireTimer <= 0 && !heat.locked && guard--) {
        fireCannon();
        fireTimer += 1 / B.cannon.rof;
        if (heat.value >= B.cannon.overheatAt) heat.locked = true;
      }
    }
    if (fireTimer < 0) fireTimer = 0;
    if (st.alive && intent.fire2 && torpTimer <= 0 && ammo > 0) {
      fireTorpedo();
      torpTimer = 1 / B.torpedo.rof;
    }

    // --- cannon pellets: step, hit-test, expire ---
    for (const p of cannon.slots) {
      if (!p.alive) continue;
      stepProjectile(p, dt);
      let hit = false;
      targets.forEachNear(p.s, B.cannon.radius, (e) => {
        if (hit) return;
        const r = e.radius + B.cannon.radius;
        if (p.pos.distanceToSquared(e.pos) > r * r) return;
        hit = true;
        targets.hurt(e, B.cannon.damage, 'cannon');
        spawnBlast(p.pos, 1.1, 0.16);
      });
      if (hit || p.life <= 0) p.alive = false;
    }
    cannon.flush((p, i) => writeProjectile(p, i, cannon.mesh, 3.2));

    // --- torpedoes: step, proximity-detonate, expire ---
    for (const t of torps.slots) {
      if (!t.alive) continue;
      stepProjectile(t, dt);
      let boom = t.life <= 0;
      if (!boom) {
        targets.forEachNear(t.s, B.torpedo.radius, (e) => {
          const r = e.radius + B.torpedo.radius;
          if (t.pos.distanceToSquared(e.pos) <= r * r) boom = true;
        });
      }
      if (!boom && world.collide(t.pos, B.torpedo.radius)) boom = true;
      if (boom) detonate(t);
    }
    torps.flush((t, i) => writeProjectile(t, i, torps.mesh, 2.0));

    // --- blasts: expand and fade ---
    for (const b of blasts.slots) {
      if (!b.alive) continue;
      b.t += dt;
      if (b.t >= b.dur) b.alive = false;
    }
    blasts.flush((b, i) => {
      const k = b.t / b.dur;
      const r = b.size * (0.35 + k * 0.9);
      blasts.mesh.setMatrixAt(i, _m.compose(b.pos, _q.identity(), _scale.setScalar(r)));
    });
  }

  return {
    update,
    spawnBlast,
    get heat() { return heat.value; },
    get heatMax() { return B.cannon.overheatAt; },
    get overheated() { return heat.locked; },
    get ammo() { return ammo; },
    get ammoMax() { return B.torpedo.ammoMax; },
    addAmmo(n) { ammo = Math.min(B.torpedo.ammoMax, ammo + n); },
    get liveCount() { return cannon.mesh.count + torps.mesh.count; },
    dispose() { cannon.dispose(); torps.dispose(); blasts.dispose(); },
  };
}
