/**
 * LANE-RENDER — wildlife: dust piles, mosquito swarms, and parity mites (§2/§5, the
 * living hazards of M2 and The Correction).
 *
 * Piles are HF-dust glitter heaps (codex: quantizer sweepings) — they grow with `size`.
 * Swarms are pixel-gnat clouds that cluster over the machine they're degrading (`target`);
 * the machine's own flicker is EntityLayer's job, driven off the same `target` ids.
 * Parity mites are hospital-white beetles with a green checkmark eye that walk with an
 * unsettling gentleness (§5): they don't rampage, they FILE. Where one corrects your
 * product it leaves an infuriatingly tidy sparkle (Juice.tidy), the visual of the
 * 'repaired' event — which render() can't see, so it's derived from the mite's presence.
 *
 * All instanced from day one: one InstancedMesh for every pile, one for every gnat across
 * every swarm, one for every mite body, one for every mite eye.
 */
import * as THREE from 'three';
import type { SimSnapshot, WildlifeState } from '../contracts';
import type { Juice } from './juice';

const GNATS_PER_SWARM = 24;

/** HF dust: pale, faintly iridescent glitter. Emissive so heaps sparkle at iso distance. */
function pileMaterial(): THREE.MeshStandardMaterial {
  return new THREE.MeshStandardMaterial({
    color: 0xe8e0ff, emissive: 0xb0a0ff, emissiveIntensity: 0.5,
    roughness: 0.3, metalness: 0.6, flatShading: true,
  });
}

/** A crisp green ✓ on transparent ground — the mite's single, approving eye (§5). */
function checkmarkTexture(): THREE.CanvasTexture {
  const s = 64;
  const cv = document.createElement('canvas');
  cv.width = cv.height = s;
  const ctx = cv.getContext('2d')!;
  ctx.strokeStyle = '#39ff88';
  ctx.lineWidth = 9;
  ctx.lineCap = 'round';
  ctx.lineJoin = 'round';
  ctx.beginPath();
  ctx.moveTo(s * 0.2, s * 0.52);
  ctx.lineTo(s * 0.42, s * 0.74);
  ctx.lineTo(s * 0.82, s * 0.26);
  ctx.stroke();
  const tex = new THREE.CanvasTexture(cv);
  tex.colorSpace = THREE.SRGBColorSpace;
  return tex;
}

export class WildlifeLayer {
  readonly group = new THREE.Group();
  private piles: THREE.InstancedMesh;
  private gnats: THREE.InstancedMesh;
  private mites: THREE.InstancedMesh;
  private miteEyes: THREE.InstancedMesh;
  private pileCap = 64;
  private gnatCap = 64 * GNATS_PER_SWARM;
  private miteCap = 96;
  private dummy = new THREE.Object3D();
  private eyeCol = new THREE.Color();
  /** Per-mite last-seen {size,pos}, so the renderer can derive the two mite events it can't
   *  see: `repaired` (size crossing <1 → ≥1 = a correction landed) and "canned by a
   *  firewall" (a mite gone from the list while still unsated = filed, not fled). */
  private miteState = new Map<number, { size: number; x: number; z: number }>();
  /** Entity ids currently harassed by a swarm — EntityLayer reads this to flicker them. */
  readonly harassed = new Set<number>();

  constructor(private juice?: Juice) {
    this.group.name = 'wildlife';
    // A low, faceted heap; instance scale grows it with `size`.
    const pileGeo = new THREE.IcosahedronGeometry(0.5, 0);
    pileGeo.scale(1, 0.5, 1);
    this.piles = new THREE.InstancedMesh(pileGeo, pileMaterial(), this.pileCap);
    this.piles.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
    this.piles.castShadow = true;
    this.piles.frustumCulled = false;
    this.piles.count = 0;

    const gnatGeo = new THREE.PlaneGeometry(0.08, 0.08);
    const gnatMat = new THREE.MeshBasicMaterial({
      color: 0x101014, transparent: true, opacity: 0.85, side: THREE.DoubleSide,
    });
    this.gnats = new THREE.InstancedMesh(gnatGeo, gnatMat, this.gnatCap);
    this.gnats.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
    this.gnats.frustumCulled = false;
    this.gnats.count = 0;

    // Parity mite: a hospital-white carapace (faceted ellipsoid). Faintly self-lit so it
    // reads clinical-white on the dark ground rather than going grey.
    const miteGeo = new THREE.IcosahedronGeometry(0.18, 0);
    miteGeo.scale(1, 0.62, 1.4); // longer than wide: a beetle, not a bead
    const miteMat = new THREE.MeshStandardMaterial({
      color: 0xf4f6ff, emissive: 0xd6ddf2, emissiveIntensity: 0.32,
      roughness: 0.5, metalness: 0.1, flatShading: true,
    });
    this.mites = new THREE.InstancedMesh(miteGeo, miteMat, this.miteCap);
    this.mites.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
    this.mites.castShadow = true;
    this.mites.frustumCulled = false;
    this.mites.count = 0;

    const eyeGeo = new THREE.PlaneGeometry(0.16, 0.16);
    const eyeMat = new THREE.MeshBasicMaterial({
      map: checkmarkTexture(), transparent: true, depthWrite: false, side: THREE.DoubleSide,
    });
    this.miteEyes = new THREE.InstancedMesh(eyeGeo, eyeMat, this.miteCap);
    this.miteEyes.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
    this.miteEyes.frustumCulled = false;
    this.miteEyes.count = 0;

    this.group.add(this.piles, this.gnats, this.mites, this.miteEyes);
  }

  sync(wildlife: ReadonlyArray<WildlifeState> | undefined, timeSec: number): void {
    this.harassed.clear();
    const list = wildlife ?? [];
    let pileN = 0;
    let gnatN = 0;
    let miteN = 0;
    const seenMites = new Set<number>();

    for (const w of list) {
      if (w.kind === 'dust-pile') {
        if (pileN >= this.pileCap) continue;
        const s = 0.4 + THREE.MathUtils.clamp(w.size, 0, 1) * 1.3;
        this.dummy.position.set(w.pos.x + 0.5, 0.05 * s, w.pos.y + 0.5);
        this.dummy.rotation.set(0, w.id * 1.7, 0); // desync facets between heaps
        this.dummy.scale.set(s, s, s);
        this.dummy.updateMatrix();
        this.piles.setMatrixAt(pileN++, this.dummy.matrix);
      } else if (w.kind === 'parity-mite') {
        if (miteN >= this.miteCap) continue;
        seenMites.add(w.id);
        miteN = this.placeMite(w, timeSec, miteN);
      } else {
        if (w.target !== undefined) this.harassed.add(w.target);
        const cx = w.pos.x + 0.5;
        const cz = w.pos.y + 0.5;
        const density = Math.round(GNATS_PER_SWARM * (0.4 + w.size * 0.6));
        for (let i = 0; i < density && gnatN < this.gnatCap; i++) {
          // Each gnat orbits the swarm centre on its own little chaotic path.
          const ph = w.id * 3.1 + i * 0.7;
          const rad = 0.5 + 0.5 * Math.sin(timeSec * 3 + ph * 1.3);
          const a = timeSec * (1.5 + (i % 3) * 0.4) + ph;
          this.dummy.position.set(
            cx + Math.cos(a) * rad,
            0.5 + 0.4 * Math.sin(timeSec * 4 + ph) + w.size * 0.3,
            cz + Math.sin(a * 1.1) * rad,
          );
          this.dummy.rotation.set(0, -a, 0); // face roughly along travel
          this.dummy.scale.setScalar(1);
          this.dummy.updateMatrix();
          this.gnats.setMatrixAt(gnatN++, this.dummy.matrix);
        }
      }
    }

    // A mite that vanished from the list: sated ones (size≥1) reached the rim and left
    // carrying their correction — no cue. One still unsated was CANNED by a firewall
    // mid-repair (§5's counter): the product survived, so it reads as filed paperwork —
    // an infuriatingly tidy sparkle where the jar closed.
    for (const [id, st] of this.miteState) {
      if (seenMites.has(id)) continue;
      if (st.size < 1) this.juice?.tidy(st.x, st.z);
      this.miteState.delete(id);
    }

    this.piles.count = pileN;
    this.piles.instanceMatrix.needsUpdate = true;
    this.gnats.count = gnatN;
    this.gnats.instanceMatrix.needsUpdate = true;
    this.mites.count = miteN;
    this.mites.instanceMatrix.needsUpdate = true;
    this.miteEyes.count = miteN;
    this.miteEyes.instanceMatrix.needsUpdate = true;
    if (this.miteEyes.instanceColor) this.miteEyes.instanceColor.needsUpdate = true;
  }

  /**
   * One mite. `size` is the sim's state channel (§5): 0 = hunting, 0<size<1 = on station
   * mid-repair, size≥1 = sated and carrying the correction home. The tell tracks it —
   * a hunting mite drifts with a slow, deliberate patrol (the wrongness is the calm, not
   * the speed); on station it stops and only trembles while the checkmark eye glows up to
   * completion; sated it moves off. The eye is tilted to meet the iso camera. Crossing
   * into sated fires the tidy 'repaired' sparkle where the product used to be.
   */
  private placeMite(w: WildlifeState, timeSec: number, n: number): number {
    const cx = w.pos.x + 0.5;
    const cz = w.pos.y + 0.5;
    const size = w.size;
    const onStation = size > 0 && size < 1;
    const ph = w.id * 2.3999; // golden-angle desync
    // On station it dwells (a tiny tremor); otherwise it patrols a slow, patient circle.
    const amp = onStation ? 0.05 : 0.26;
    const rate = onStation ? 0.9 : 0.55;
    const gait = timeSec * rate + ph;
    const px = cx + Math.cos(gait) * amp;
    const pz = cz + Math.sin(gait) * amp;
    const yaw = -gait; // face along the patrol tangent
    const bob = Math.abs(Math.sin(gait * 3)) * (onStation ? 0.012 : 0.03);

    this.dummy.position.set(px, 0.14 + bob, pz);
    this.dummy.rotation.set(0, yaw, 0);
    this.dummy.scale.setScalar(1);
    this.dummy.updateMatrix();
    this.mites.setMatrixAt(n, this.dummy.matrix);

    // Eye: forward of the carapace (local -Z, so world = (-sinYaw,-cosYaw)) and up, laid
    // back ~45° so the checkmark meets the iso camera instead of lying flat on the floor.
    const fwd = 0.14;
    this.dummy.position.set(px - Math.sin(yaw) * fwd, 0.27 + bob, pz - Math.cos(yaw) * fwd);
    this.dummy.rotation.set(-Math.PI / 4, yaw, 0);
    this.dummy.scale.setScalar(1);
    this.dummy.updateMatrix();
    this.miteEyes.setMatrixAt(n, this.dummy.matrix);
    // The approval glows in as the correction completes: dim while hunting, ramping on
    // station, full when sated. Per-instance via instanceColor (multiplies the map).
    const glow = size <= 0 ? 0.5 : size < 1 ? 0.5 + 0.5 * size : 1;
    this.miteEyes.setColorAt(n, this.eyeCol.setRGB(glow, glow, glow));

    // Derive the `repaired` event (render() can't see events): the correction lands the
    // instant the mite crosses from repairing into sated.
    const prev = this.miteState.get(w.id);
    if (prev && prev.size < 1 && size >= 1) this.juice?.tidy(cx, cz);
    this.miteState.set(w.id, { size, x: cx, z: cz });
    return n + 1;
  }
}

/** Convenience: which entities a snapshot's swarms are harassing (for EntityLayer). */
export function harassedIn(snap: SimSnapshot): Set<number> {
  const out = new Set<number>();
  for (const w of snap.wildlife ?? []) {
    if (w.kind === 'mosquito-swarm' && w.target !== undefined) out.add(w.target);
  }
  return out;
}
