// Particles, beams, shockwave rings, light flashes and screen shake — the juice.
import * as THREE from 'three';

const PCOUNT = 1600;
const BEAMS = 56; // lightning eats ~6 slots per arc
const RINGS = 10;
const FLASHES = 4;
const LINKS = 12;
const SCORCH = 24;
const DUST = 130;

interface BeamSlot {
  group: THREE.Group;
  coreMat: THREE.MeshBasicMaterial;
  glowMat: THREE.MeshBasicMaterial;
  life: number;
  max: number;
}

interface RingSlot {
  mesh: THREE.Mesh;
  mat: THREE.MeshBasicMaterial;
  t: number;
  dur: number;
  radius: number;
}

export class FX {
  shake = 0;

  private pos = new Float32Array(PCOUNT * 3);
  private vel = new Float32Array(PCOUNT * 3);
  private col = new Float32Array(PCOUNT * 3);
  private baseCol = new Float32Array(PCOUNT * 3);
  private life = new Float32Array(PCOUNT);
  private maxLife = new Float32Array(PCOUNT);
  private grav = new Float32Array(PCOUNT);
  private cursor = 0;
  private points: THREE.Points;
  private pGeo: THREE.BufferGeometry;

  private beams: BeamSlot[] = [];
  private beamCursor = 0;
  private links: { group: THREE.Group; coreMat: THREE.MeshBasicMaterial; glowMat: THREE.MeshBasicMaterial }[] = [];
  private linkUsed = 0;
  private rings: RingSlot[] = [];
  private ringCursor = 0;
  private flashes: THREE.PointLight[] = [];
  private scorches: { mesh: THREE.Mesh; mat: THREE.MeshBasicMaterial; t: number }[] = [];
  private scorchCursor = 0;
  private dust: THREE.Points;
  private dustPos: Float32Array;
  private dustVel: Float32Array;
  /** World point the dust volume drifts around (set to the camera target each frame). */
  dustCenter = new THREE.Vector3();

  private tmpColor = new THREE.Color();
  private tmpVec = new THREE.Vector3();

  constructor(private scene: THREE.Scene) {
    // --- particle pool ---
    this.pGeo = new THREE.BufferGeometry();
    this.pos.fill(0);
    for (let i = 0; i < PCOUNT; i++) this.pos[i * 3 + 1] = -999;
    this.pGeo.setAttribute('position', new THREE.BufferAttribute(this.pos, 3));
    this.pGeo.setAttribute('color', new THREE.BufferAttribute(this.col, 3));
    const pMat = new THREE.PointsMaterial({
      size: 0.14,
      vertexColors: true,
      transparent: true,
      blending: THREE.AdditiveBlending,
      depthWrite: false,
      toneMapped: false,
    });
    this.points = new THREE.Points(this.pGeo, pMat);
    this.points.frustumCulled = false;
    scene.add(this.points);

    // --- beam pool ---
    const coreGeo = new THREE.BoxGeometry(0.05, 0.05, 1);
    const glowGeo = new THREE.BoxGeometry(0.17, 0.17, 1);
    for (let i = 0; i < BEAMS; i++) {
      const coreMat = new THREE.MeshBasicMaterial({
        color: 0xffffff,
        transparent: true,
        toneMapped: false,
        depthWrite: false,
      });
      const glowMat = new THREE.MeshBasicMaterial({
        color: 0xffdd55,
        transparent: true,
        opacity: 0.35,
        blending: THREE.AdditiveBlending,
        toneMapped: false,
        depthWrite: false,
      });
      const group = new THREE.Group();
      group.add(new THREE.Mesh(coreGeo, coreMat));
      group.add(new THREE.Mesh(glowGeo, glowMat));
      group.visible = false;
      scene.add(group);
      this.beams.push({ group, coreMat, glowMat, life: 0, max: 1 });
    }

    // --- persistent link-laser pool (enemy↔enemy floor wires, repositioned each frame) ---
    const linkCoreGeo = new THREE.BoxGeometry(0.04, 0.04, 1);
    const linkGlowGeo = new THREE.BoxGeometry(0.15, 0.15, 1);
    for (let i = 0; i < LINKS; i++) {
      const coreMat = new THREE.MeshBasicMaterial({
        color: 0xfff4bb, transparent: true, toneMapped: false, depthWrite: false,
      });
      const glowMat = new THREE.MeshBasicMaterial({
        color: 0xffe066, transparent: true, opacity: 0.3,
        blending: THREE.AdditiveBlending, toneMapped: false, depthWrite: false,
      });
      const group = new THREE.Group();
      group.add(new THREE.Mesh(linkCoreGeo, coreMat));
      group.add(new THREE.Mesh(linkGlowGeo, glowMat));
      group.visible = false;
      scene.add(group);
      this.links.push({ group, coreMat, glowMat });
    }

    // --- shockwave ring pool ---
    const ringGeo = new THREE.RingGeometry(0.82, 1, 48);
    for (let i = 0; i < RINGS; i++) {
      const mat = new THREE.MeshBasicMaterial({
        color: 0xffaa55,
        transparent: true,
        opacity: 0,
        blending: THREE.AdditiveBlending,
        side: THREE.DoubleSide,
        depthWrite: false,
        toneMapped: false,
      });
      const mesh = new THREE.Mesh(ringGeo, mat);
      mesh.rotation.x = -Math.PI / 2;
      mesh.visible = false;
      scene.add(mesh);
      this.rings.push({ mesh, mat, t: 1, dur: 1, radius: 1 });
    }

    // --- reusable flash lights ---
    for (let i = 0; i < FLASHES; i++) {
      const l = new THREE.PointLight(0xffaa44, 0, 9, 1.8);
      l.position.set(0, 1, 0);
      scene.add(l);
      this.flashes.push(l);
    }

    // --- scorch decal pool (canvas radial gradient, fades over ~8s) ---
    const cv = document.createElement('canvas');
    cv.width = cv.height = 64;
    const cx = cv.getContext('2d');
    if (cx) {
      const grad = cx.createRadialGradient(32, 32, 4, 32, 32, 32);
      grad.addColorStop(0, 'rgba(0,0,0,0.9)');
      grad.addColorStop(0.55, 'rgba(0,0,0,0.55)');
      grad.addColorStop(1, 'rgba(0,0,0,0)');
      cx.fillStyle = grad;
      cx.fillRect(0, 0, 64, 64);
    }
    const scorchTex = new THREE.CanvasTexture(cv);
    const scorchGeo = new THREE.CircleGeometry(1, 24);
    for (let i = 0; i < SCORCH; i++) {
      const mat = new THREE.MeshBasicMaterial({
        map: scorchTex, transparent: true, opacity: 0,
        depthWrite: false, color: 0x000000,
      });
      const mesh = new THREE.Mesh(scorchGeo, mat);
      mesh.rotation.x = -Math.PI / 2;
      mesh.visible = false;
      scene.add(mesh);
      this.scorches.push({ mesh, mat, t: 99 });
    }

    // --- ambient dust motes: slow drift in a volume around dustCenter ---
    this.dustPos = new Float32Array(DUST * 3);
    this.dustVel = new Float32Array(DUST * 3);
    for (let i = 0; i < DUST; i++) {
      this.dustPos[i * 3] = (Math.random() - 0.5) * 26;
      this.dustPos[i * 3 + 1] = 0.2 + Math.random() * 3.2;
      this.dustPos[i * 3 + 2] = (Math.random() - 0.5) * 26;
      this.dustVel[i * 3] = (Math.random() - 0.5) * 0.22;
      this.dustVel[i * 3 + 1] = 0.04 + Math.random() * 0.1;
      this.dustVel[i * 3 + 2] = (Math.random() - 0.5) * 0.22;
    }
    const dustGeo = new THREE.BufferGeometry();
    dustGeo.setAttribute('position', new THREE.BufferAttribute(this.dustPos, 3));
    this.dust = new THREE.Points(
      dustGeo,
      new THREE.PointsMaterial({
        color: 0x8aa6c8, size: 0.05, transparent: true, opacity: 0.5,
        blending: THREE.AdditiveBlending, depthWrite: false, toneMapped: false,
      })
    );
    this.dust.frustumCulled = false;
    scene.add(this.dust);
  }

  addShake(amount: number): void {
    this.shake = Math.min(1.2, this.shake + amount);
  }

  spawnParticle(
    x: number, y: number, z: number,
    vx: number, vy: number, vz: number,
    color: number, life: number, gravity = 9
  ): void {
    const i = this.cursor;
    this.cursor = (this.cursor + 1) % PCOUNT;
    this.pos[i * 3] = x; this.pos[i * 3 + 1] = y; this.pos[i * 3 + 2] = z;
    this.vel[i * 3] = vx; this.vel[i * 3 + 1] = vy; this.vel[i * 3 + 2] = vz;
    this.tmpColor.set(color);
    this.baseCol[i * 3] = this.tmpColor.r;
    this.baseCol[i * 3 + 1] = this.tmpColor.g;
    this.baseCol[i * 3 + 2] = this.tmpColor.b;
    this.life[i] = life;
    this.maxLife[i] = life;
    this.grav[i] = gravity;
  }

  /** Burst of sparks at a point (laser impacts, small hits). */
  sparks(p: THREE.Vector3, color: number, count = 8, speed = 3): void {
    for (let i = 0; i < count; i++) {
      const a = Math.random() * Math.PI * 2;
      const s = speed * (0.3 + Math.random() * 0.7);
      this.spawnParticle(
        p.x, p.y, p.z,
        Math.cos(a) * s, (Math.random() * 0.9 + 0.3) * s, Math.sin(a) * s,
        color, 0.25 + Math.random() * 0.3, 12
      );
    }
  }

  beam(a: THREE.Vector3, b: THREE.Vector3, color: number, life = 0.12): void {
    const slot = this.beams[this.beamCursor];
    this.beamCursor = (this.beamCursor + 1) % BEAMS;
    const len = a.distanceTo(b);
    if (len < 0.01) return;
    slot.group.visible = true;
    slot.group.position.copy(a).add(b).multiplyScalar(0.5);
    slot.group.lookAt(b);
    slot.group.scale.set(1, 1, len);
    slot.coreMat.color.set(0xffffff).lerp(this.tmpColor.set(color), 0.25);
    slot.coreMat.opacity = 1;
    slot.glowMat.color.set(color);
    slot.glowMat.opacity = 0.4;
    slot.life = life;
    slot.max = life;
  }

  /** Jagged electric arc between two points, drawn from beam segments. */
  lightning(a: THREE.Vector3, b: THREE.Vector3, color = 0xaaddff, life = 0.18): void {
    const segs = 6;
    const dir = this.tmpVec.copy(b).sub(a);
    const len = dir.length();
    if (len < 0.05) return;
    // perpendicular (XZ) for the jitter
    const px = -dir.z / len;
    const pz = dir.x / len;
    let prev = a.clone();
    for (let i = 1; i <= segs; i++) {
      const f = i / segs;
      const p = a.clone().lerp(b, f);
      if (i < segs) {
        const j = (Math.random() - 0.5) * Math.min(0.8, len * 0.22);
        p.x += px * j;
        p.z += pz * j;
        p.y += (Math.random() - 0.5) * 0.25;
      }
      this.beam(prev, p, color, life * (0.8 + Math.random() * 0.4));
      prev = p;
    }
  }

  /** Short bright cone of particles at a weapon muzzle. */
  muzzle(p: THREE.Vector3, dir: THREE.Vector3, color: number): void {
    for (let i = 0; i < 5; i++) {
      const spread = 0.45;
      this.spawnParticle(
        p.x, p.y, p.z,
        dir.x * (2.5 + Math.random() * 2) + (Math.random() - 0.5) * spread,
        0.4 + Math.random() * 0.8,
        dir.z * (2.5 + Math.random() * 2) + (Math.random() - 0.5) * spread,
        color, 0.08 + Math.random() * 0.07, 2
      );
    }
  }

  /** Dark fading scorch mark on the floor (explosions, big impacts). */
  scorch(p: THREE.Vector3, radius = 1): void {
    const slot = this.scorches[this.scorchCursor];
    this.scorchCursor = (this.scorchCursor + 1) % SCORCH;
    slot.mesh.visible = true;
    // tiny y-jitter so stacked decals don't z-fight
    slot.mesh.position.set(p.x, 0.045 + this.scorchCursor * 0.0012, p.z);
    slot.mesh.rotation.z = Math.random() * Math.PI * 2;
    slot.mesh.scale.setScalar(radius);
    slot.t = 0;
  }

  /** Call once per frame before re-adding links: hides every link beam. */
  beginLinks(): void {
    for (let i = 0; i < this.linkUsed; i++) this.links[i].group.visible = false;
    this.linkUsed = 0;
  }

  /** Persistent floor laser between two bots; lives until the next beginLinks(). */
  link(a: THREE.Vector3, b: THREE.Vector3, color: number, time: number): void {
    if (this.linkUsed >= LINKS) return;
    const slot = this.links[this.linkUsed++];
    const len = Math.hypot(b.x - a.x, b.z - a.z);
    if (len < 0.05) return;
    slot.group.visible = true;
    slot.group.position.set((a.x + b.x) / 2, 0.12, (a.z + b.z) / 2);
    slot.group.lookAt(b.x, 0.12, b.z);
    slot.group.scale.set(1, 1, len);
    const pulse = 0.75 + 0.25 * Math.sin(time * 9 + a.x * 3);
    slot.coreMat.opacity = pulse;
    slot.glowMat.color.set(color);
    slot.glowMat.opacity = 0.32 * pulse;
  }

  ring(p: THREE.Vector3, radius: number, color = 0xffbb66, dur = 0.45): void {
    const slot = this.rings[this.ringCursor];
    this.ringCursor = (this.ringCursor + 1) % RINGS;
    slot.mesh.visible = true;
    slot.mesh.position.set(p.x, 0.06, p.z);
    slot.mat.color.set(color);
    slot.t = 0;
    slot.dur = dur;
    slot.radius = radius;
  }

  flash(p: THREE.Vector3, color: number, intensity: number): void {
    let best = this.flashes[0];
    for (const f of this.flashes) if (f.intensity < best.intensity) best = f;
    best.color.set(color);
    best.position.set(p.x, 0.9, p.z);
    best.intensity = intensity;
  }

  /** The big one: shockwave + embers + sparks + light flash + shake. */
  explosion(p: THREE.Vector3, radius = 2.2, power = 1): void {
    this.ring(p, radius * 1.25, 0xffc070, 0.5);
    this.ring(p, radius * 0.7, 0xfff0c0, 0.32);
    const emberColors = [0xffd27a, 0xff8a3c, 0xff5526, 0xffeeaa];
    const n = Math.floor(60 * power);
    for (let i = 0; i < n; i++) {
      const a = Math.random() * Math.PI * 2;
      const s = (1.5 + Math.random() * 5) * (radius / 2.2);
      const up = 1.2 + Math.random() * 3.6;
      this.spawnParticle(
        p.x, p.y + 0.15, p.z,
        Math.cos(a) * s, up, Math.sin(a) * s,
        emberColors[(Math.random() * emberColors.length) | 0],
        0.5 + Math.random() * 0.7, 8
      );
    }
    // fast white-hot core sparks
    for (let i = 0; i < 14 * power; i++) {
      const a = Math.random() * Math.PI * 2;
      const s = 5 + Math.random() * 6;
      this.spawnParticle(
        p.x, p.y + 0.2, p.z,
        Math.cos(a) * s, 1 + Math.random() * 2.5, Math.sin(a) * s,
        0xfff6dd, 0.18 + Math.random() * 0.2, 4
      );
    }
    this.flash(p, 0xff9944, 45 * power);
    this.addShake(0.3 * power);
  }

  /** Random camera offset for the current shake amount. Call once per frame. */
  shakeOffset(out: THREE.Vector3): THREE.Vector3 {
    out.set(
      (Math.random() - 0.5) * this.shake * 0.5,
      (Math.random() - 0.5) * this.shake * 0.3,
      (Math.random() - 0.5) * this.shake * 0.5
    );
    return out;
  }

  update(dt: number): void {
    // particles
    for (let i = 0; i < PCOUNT; i++) {
      if (this.life[i] <= 0) continue;
      this.life[i] -= dt;
      if (this.life[i] <= 0) {
        this.pos[i * 3 + 1] = -999;
        this.col[i * 3] = this.col[i * 3 + 1] = this.col[i * 3 + 2] = 0;
        continue;
      }
      const i3 = i * 3;
      this.vel[i3 + 1] -= this.grav[i] * dt;
      this.pos[i3] += this.vel[i3] * dt;
      this.pos[i3 + 1] += this.vel[i3 + 1] * dt;
      this.pos[i3 + 2] += this.vel[i3 + 2] * dt;
      // ember floor bounce
      if (this.pos[i3 + 1] < 0.02 && this.vel[i3 + 1] < 0) {
        this.pos[i3 + 1] = 0.02;
        this.vel[i3 + 1] *= -0.38;
        this.vel[i3] *= 0.7;
        this.vel[i3 + 2] *= 0.7;
      }
      const f = this.life[i] / this.maxLife[i];
      const fade = f * f;
      this.col[i3] = this.baseCol[i3] * fade;
      this.col[i3 + 1] = this.baseCol[i3 + 1] * fade;
      this.col[i3 + 2] = this.baseCol[i3 + 2] * fade;
    }
    (this.pGeo.attributes.position as THREE.BufferAttribute).needsUpdate = true;
    (this.pGeo.attributes.color as THREE.BufferAttribute).needsUpdate = true;

    // beams
    for (const b of this.beams) {
      if (!b.group.visible) continue;
      b.life -= dt;
      if (b.life <= 0) {
        b.group.visible = false;
        continue;
      }
      const f = b.life / b.max;
      b.coreMat.opacity = f;
      b.glowMat.opacity = 0.4 * f;
    }

    // rings
    for (const r of this.rings) {
      if (!r.mesh.visible) continue;
      r.t += dt;
      if (r.t >= r.dur) {
        r.mesh.visible = false;
        continue;
      }
      const f = r.t / r.dur;
      const ease = 1 - Math.pow(1 - f, 3);
      const s = Math.max(0.05, r.radius * ease);
      r.mesh.scale.set(s, s, 1);
      r.mat.opacity = 0.9 * (1 - f);
    }

    // flash lights
    for (const f of this.flashes) {
      if (f.intensity > 0.01) f.intensity *= Math.exp(-dt * 9);
      else f.intensity = 0;
    }

    // scorch decals: quick fade-in, slow fade-out
    for (const s of this.scorches) {
      if (!s.mesh.visible) continue;
      s.t += dt;
      if (s.t > 8) {
        s.mesh.visible = false;
        s.mat.opacity = 0;
        continue;
      }
      s.mat.opacity = s.t < 0.15 ? (s.t / 0.15) * 0.85 : 0.85 * (1 - (s.t - 0.15) / 7.85);
    }

    // dust motes: drift inside a box around dustCenter, wrap at the edges
    const R = 14;
    for (let i = 0; i < DUST; i++) {
      const i3 = i * 3;
      this.dustPos[i3] += this.dustVel[i3] * dt;
      this.dustPos[i3 + 1] += this.dustVel[i3 + 1] * dt;
      this.dustPos[i3 + 2] += this.dustVel[i3 + 2] * dt;
      const dx = this.dustPos[i3] - this.dustCenter.x;
      const dz = this.dustPos[i3 + 2] - this.dustCenter.z;
      if (dx > R) this.dustPos[i3] -= R * 2;
      else if (dx < -R) this.dustPos[i3] += R * 2;
      if (dz > R) this.dustPos[i3 + 2] -= R * 2;
      else if (dz < -R) this.dustPos[i3 + 2] += R * 2;
      if (this.dustPos[i3 + 1] > 3.6) this.dustPos[i3 + 1] = 0.15;
    }
    (this.dust.geometry.attributes.position as THREE.BufferAttribute).needsUpdate = true;

    // shake decay
    this.shake *= Math.exp(-dt * 5.5);
    if (this.shake < 0.001) this.shake = 0;
  }
}
