/**
 * LANE-RENDER — juice. The difference between placing a machine and a machine appearing.
 *
 * The renderer never sees events (render() gets only the snapshot), so every cue here is
 * derived from frame-to-frame deltas: an entity that wasn't there last frame was placed;
 * one that vanished was demolished; a rise in `shippedTotal` is a shipment. EntityLayer
 * owns the meshes so it does the scale-pop itself and calls in for the puff/burst; the
 * shipment flash is driven from index.ts.
 *
 * One InstancedMesh particle pool for the whole game — cheap, instanced, everywhere.
 */
import * as THREE from 'three';

const MAX = 512;
const GRAVITY = -6;

interface P {
  x: number; y: number; z: number;
  vx: number; vy: number; vz: number;
  life: number; ttl: number;
  size: number;
  r: number; g: number; b: number;
}

export class Juice {
  readonly group = new THREE.Group();
  private mesh: THREE.InstancedMesh;
  private pool: P[] = [];
  private live: P[] = [];
  private dummy = new THREE.Object3D();
  private color = new THREE.Color();

  constructor() {
    this.group.name = 'juice';
    const geo = new THREE.PlaneGeometry(1, 1);
    const mat = new THREE.MeshBasicMaterial({
      transparent: true, depthWrite: false, side: THREE.DoubleSide,
      blending: THREE.AdditiveBlending, vertexColors: true,
    });
    this.mesh = new THREE.InstancedMesh(geo, mat, MAX);
    this.mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
    this.mesh.frustumCulled = false;
    this.mesh.count = 0;
    // per-instance colour
    this.mesh.instanceColor = new THREE.InstancedBufferAttribute(new Float32Array(MAX * 3), 3);
    this.group.add(this.mesh);
    for (let i = 0; i < MAX; i++) {
      this.pool.push({ x: 0, y: 0, z: 0, vx: 0, vy: 0, vz: 0, life: 0, ttl: 1, size: 0.1, r: 1, g: 1, b: 1 });
    }
  }

  private emit(
    x: number, z: number, n: number, spread: number, up: number,
    size: number, ttl: number, col: number,
  ): void {
    this.color.setHex(col);
    for (let i = 0; i < n; i++) {
      const p = this.pool.pop();
      if (!p) break; // pool exhausted — drop the rest rather than allocate
      const a = Math.random() * Math.PI * 2;
      const s = Math.random() * spread;
      p.x = x; p.y = 0.2; p.z = z;
      p.vx = Math.cos(a) * s;
      p.vz = Math.sin(a) * s;
      p.vy = up * (0.6 + Math.random() * 0.8);
      p.life = 0;
      p.ttl = ttl * (0.7 + Math.random() * 0.6);
      p.size = size * (0.7 + Math.random() * 0.6);
      p.r = this.color.r; p.g = this.color.g; p.b = this.color.b;
      this.live.push(p);
    }
  }

  /** Placement: a soft ring of dust kicked outward. Pairs with EntityLayer's scale-pop. */
  puff(cx: number, cz: number): void {
    this.emit(cx, cz, 14, 2.2, 1.5, 0.18, 0.5, 0xbfb3a0);
  }

  /** Demolition: a sharper, brighter burst. */
  burst(cx: number, cz: number): void {
    this.emit(cx, cz, 22, 4.0, 3.0, 0.22, 0.55, 0xff6a3a);
  }

  /** Shipment: a quick upward flash of the shipped item's colour at the uplink. */
  shipFlash(cx: number, cz: number, col: number): void {
    this.emit(cx, cz, 10, 1.0, 5.0, 0.2, 0.45, col);
  }

  update(dt: number, camera: THREE.Camera): void {
    // integrate + recycle
    for (let i = this.live.length - 1; i >= 0; i--) {
      const p = this.live[i];
      p.life += dt;
      if (p.life >= p.ttl) {
        this.live[i] = this.live[this.live.length - 1];
        this.live.pop();
        this.pool.push(p);
        continue;
      }
      p.vy += GRAVITY * dt;
      p.x += p.vx * dt; p.y += p.vy * dt; p.z += p.vz * dt;
      if (p.y < 0.05) { p.y = 0.05; p.vy *= -0.3; p.vx *= 0.6; p.vz *= 0.6; }
    }

    // write instances, billboarded to the camera, fading over life
    const q = camera.quaternion;
    for (let i = 0; i < this.live.length; i++) {
      const p = this.live[i];
      const k = 1 - p.life / p.ttl;
      const s = p.size * (0.4 + k * 0.6);
      this.dummy.position.set(p.x, p.y, p.z);
      this.dummy.quaternion.copy(q);
      this.dummy.scale.setScalar(s);
      this.dummy.updateMatrix();
      this.mesh.setMatrixAt(i, this.dummy.matrix);
      this.mesh.setColorAt(i, this.color.setRGB(p.r * k, p.g * k, p.b * k));
    }
    this.mesh.count = this.live.length;
    this.mesh.instanceMatrix.needsUpdate = true;
    if (this.mesh.instanceColor) this.mesh.instanceColor.needsUpdate = true;
  }
}
