import * as THREE from 'three';
import { TAU } from './util';

interface P {
  i: number;
  age: number;
  life: number;
  x: number; y: number; z: number;
  vx: number; vy: number; vz: number;
  rx: number; ry: number; rz: number;
  wx: number; wy: number; wz: number;
  size: number;
  grav: number;
  drag: number;
  bounce: number;
  ground: number;
  flutter: number;
  grow: number;
  billboard: boolean;
  alignVel: boolean;
}

type SpawnOpts = Partial<P> & { x: number; y: number; z: number; life: number };

class Pool {
  mesh: THREE.InstancedMesh;
  private freeIdx: number[] = [];
  private live: P[] = [];
  private dummy = new THREE.Object3D();
  private zero = new THREE.Matrix4().makeScale(0, 0, 0);

  constructor(scene: THREE.Scene, geo: THREE.BufferGeometry, mat: THREE.Material, count: number) {
    this.mesh = new THREE.InstancedMesh(geo, mat, count);
    this.mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
    this.mesh.frustumCulled = false;
    for (let i = 0; i < count; i++) {
      this.freeIdx.push(i);
      this.mesh.setMatrixAt(i, this.zero);
    }
    scene.add(this.mesh);
  }

  spawn(o: SpawnOpts, color?: THREE.Color): void {
    const i = this.freeIdx.pop();
    if (i === undefined) return;
    this.live.push({
      i, age: 0,
      vx: 0, vy: 0, vz: 0,
      rx: 0, ry: 0, rz: 0,
      wx: 0, wy: 0, wz: 0,
      size: 1, grav: 0, drag: 0, bounce: 0, ground: -100,
      flutter: 0, grow: 0, billboard: false, alignVel: false,
      ...o,
    });
    if (color) {
      this.mesh.setColorAt(i, color);
      if (this.mesh.instanceColor) this.mesh.instanceColor.needsUpdate = true;
    }
  }

  update(dt: number, camQ: THREE.Quaternion): void {
    if (this.live.length === 0) return;
    const d = this.dummy;
    for (let k = this.live.length - 1; k >= 0; k--) {
      const p = this.live[k]!;
      p.age += dt;
      if (p.age >= p.life) {
        this.mesh.setMatrixAt(p.i, this.zero);
        this.freeIdx.push(p.i);
        this.live.splice(k, 1);
        continue;
      }
      p.vy -= p.grav * dt;
      if (p.drag > 0) {
        const f = Math.exp(-p.drag * dt);
        p.vx *= f; p.vy *= f; p.vz *= f;
      }
      p.x += p.vx * dt;
      p.y += p.vy * dt;
      p.z += p.vz * dt;
      if (p.flutter > 0) {
        p.x += Math.sin(p.age * 11 + p.i * 1.7) * p.flutter * dt;
        p.z += Math.cos(p.age * 9 + p.i * 2.3) * p.flutter * dt;
      }
      if (p.y < p.ground + p.size * 0.05 && p.vy < 0) {
        if (p.bounce > 0.01) {
          p.y = p.ground + p.size * 0.05;
          p.vy = -p.vy * p.bounce;
          p.vx *= 0.6;
          p.vz *= 0.6;
          p.wx *= 0.5; p.wy *= 0.5; p.wz *= 0.5;
        } else {
          p.y = p.ground + p.size * 0.05;
          p.vy = 0;
          p.vx *= 0.8;
          p.vz *= 0.8;
        }
      }
      p.rx += p.wx * dt;
      p.ry += p.wy * dt;
      p.rz += p.wz * dt;

      const fade = Math.min(1, (1 - p.age / p.life) * 3.5);
      let s = p.size * fade;
      if (p.grow > 0) s = p.size * (0.3 + (p.age / p.life) * p.grow) * fade;

      d.position.set(p.x, p.y, p.z);
      if (p.billboard) {
        d.quaternion.copy(camQ);
      } else if (p.alignVel) {
        d.quaternion.setFromUnitVectors(
          new THREE.Vector3(0, 1, 0),
          new THREE.Vector3(p.vx, p.vy, p.vz).normalize(),
        );
      } else {
        d.rotation.set(p.rx, p.ry, p.rz);
      }
      d.scale.setScalar(Math.max(0.001, s));
      d.updateMatrix();
      this.mesh.setMatrixAt(p.i, d.matrix);
    }
    this.mesh.instanceMatrix.needsUpdate = true;
  }
}

/** Fading additive ribbon that follows the sword blade. */
export class SwordTrail {
  private static readonly MAX = 24;
  private samples: { bx: number; by: number; bz: number; tx: number; ty: number; tz: number; age: number }[] = [];
  private geo = new THREE.BufferGeometry();
  private posAttr: THREE.BufferAttribute;
  private colAttr: THREE.BufferAttribute;
  mesh: THREE.Mesh;
  private maxAge = 0.16;

  constructor(scene: THREE.Scene) {
    const max = SwordTrail.MAX;
    this.posAttr = new THREE.BufferAttribute(new Float32Array(max * 2 * 3), 3);
    this.posAttr.setUsage(THREE.DynamicDrawUsage);
    this.colAttr = new THREE.BufferAttribute(new Float32Array(max * 2 * 3), 3);
    this.colAttr.setUsage(THREE.DynamicDrawUsage);
    this.geo.setAttribute('position', this.posAttr);
    this.geo.setAttribute('color', this.colAttr);
    const idx: number[] = [];
    for (let i = 0; i < max - 1; i++) {
      const a = i * 2;
      idx.push(a, a + 1, a + 2, a + 1, a + 3, a + 2);
    }
    this.geo.setIndex(idx);
    this.geo.setDrawRange(0, 0);
    const mat = new THREE.MeshBasicMaterial({
      vertexColors: true,
      transparent: true,
      blending: THREE.AdditiveBlending,
      depthWrite: false,
      side: THREE.DoubleSide,
      toneMapped: false,
    });
    this.mesh = new THREE.Mesh(this.geo, mat);
    this.mesh.frustumCulled = false;
    scene.add(this.mesh);
  }

  push(base: THREE.Vector3, tip: THREE.Vector3): void {
    this.samples.push({ bx: base.x, by: base.y, bz: base.z, tx: tip.x, ty: tip.y, tz: tip.z, age: 0 });
    if (this.samples.length > SwordTrail.MAX) this.samples.shift();
  }

  update(dt: number): void {
    for (const s of this.samples) s.age += dt;
    while (this.samples.length > 0 && this.samples[0]!.age > this.maxAge) this.samples.shift();
    const n = this.samples.length;
    if (n < 2) {
      this.geo.setDrawRange(0, 0);
      return;
    }
    for (let i = 0; i < n; i++) {
      const s = this.samples[i]!;
      const b = 1 - s.age / this.maxAge;
      const glow = b * b;
      this.posAttr.setXYZ(i * 2, s.bx, s.by, s.bz);
      this.posAttr.setXYZ(i * 2 + 1, s.tx, s.ty, s.tz);
      // white core fading through pale cyan to black (additive = invisible)
      this.colAttr.setXYZ(i * 2, glow * 0.55, glow * 0.75, glow * 0.8);
      this.colAttr.setXYZ(i * 2 + 1, glow, glow, glow * 0.95);
    }
    this.posAttr.needsUpdate = true;
    this.colAttr.needsUpdate = true;
    this.geo.setDrawRange(0, (n - 1) * 6);
  }
}

interface Ring {
  mesh: THREE.Mesh;
  mat: THREE.MeshBasicMaterial;
  age: number;
  life: number;
  scale: number;
}

/** All transient visual juice: particle pools, impact rings, the sword trail. */
export class Effects {
  trail: SwordTrail;
  private confetti: Pool;
  private shards: Pool;
  private drops: Pool;
  private puffs: Pool;
  private sparks: Pool;
  private rings: Ring[] = [];
  private camQ = new THREE.Quaternion();

  constructor(scene: THREE.Scene) {
    this.trail = new SwordTrail(scene);

    this.confetti = new Pool(
      scene,
      new THREE.PlaneGeometry(0.13, 0.17),
      new THREE.MeshLambertMaterial({ side: THREE.DoubleSide }),
      160,
    );
    this.shards = new Pool(
      scene,
      new THREE.TetrahedronGeometry(0.12),
      new THREE.MeshLambertMaterial({ flatShading: true }),
      64,
    );
    this.drops = new Pool(
      scene,
      new THREE.IcosahedronGeometry(0.075, 0),
      new THREE.MeshLambertMaterial({ flatShading: true }),
      96,
    );
    this.puffs = new Pool(
      scene,
      new THREE.PlaneGeometry(0.55, 0.55),
      new THREE.MeshBasicMaterial({
        map: Effects.puffTexture(),
        transparent: true,
        opacity: 0.42,
        depthWrite: false,
      }),
      48,
    );
    this.sparks = new Pool(
      scene,
      new THREE.PlaneGeometry(0.05, 0.3),
      new THREE.MeshBasicMaterial({ toneMapped: false, side: THREE.DoubleSide }),
      64,
    );

    const ringGeo = new THREE.RingGeometry(0.42, 0.56, 26);
    for (let i = 0; i < 6; i++) {
      const mat = new THREE.MeshBasicMaterial({
        color: '#ffffff',
        transparent: true,
        opacity: 0,
        blending: THREE.AdditiveBlending,
        depthWrite: false,
        side: THREE.DoubleSide,
        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, age: 0, life: 0, scale: 1 });
    }
  }

  private static puffTexture(): THREE.CanvasTexture {
    const c = document.createElement('canvas');
    c.width = 64;
    c.height = 64;
    const g = c.getContext('2d')!;
    const grad = g.createRadialGradient(32, 32, 4, 32, 32, 30);
    grad.addColorStop(0, 'rgba(255,255,255,1)');
    grad.addColorStop(1, 'rgba(255,255,255,0)');
    g.fillStyle = grad;
    g.fillRect(0, 0, 64, 64);
    return new THREE.CanvasTexture(c);
  }

  ring(x: number, y: number, z: number, scale = 1): void {
    const r = this.rings.find((rr) => rr.life <= 0) ?? this.rings[0]!;
    r.mesh.position.set(x, y, z);
    r.mesh.visible = true;
    r.age = 0;
    r.life = 0.28;
    r.scale = scale;
  }

  grassBurst(x: number, y: number, z: number): void {
    const c = new THREE.Color();
    for (let i = 0; i < 6; i++) {
      const a = Math.random() * TAU;
      c.setHSL(0.3 + Math.random() * 0.06, 0.65, 0.32 + Math.random() * 0.22);
      this.confetti.spawn(
        {
          x, y: y + 0.25, z,
          life: 0.65 + Math.random() * 0.4,
          vx: Math.sin(a) * (0.8 + Math.random() * 1.2),
          vy: 2.2 + Math.random() * 1.6,
          vz: Math.cos(a) * (0.8 + Math.random() * 1.2),
          grav: 6.5,
          flutter: 1.4,
          ground: y,
          rx: Math.random() * TAU, ry: Math.random() * TAU, rz: Math.random() * TAU,
          wx: 6 + Math.random() * 8, wy: 5 + Math.random() * 6, wz: 6 + Math.random() * 8,
          size: 0.8 + Math.random() * 0.5,
        },
        c,
      );
    }
  }

  potBurst(x: number, y: number, z: number, ground: number): void {
    const c = new THREE.Color();
    for (let i = 0; i < 7; i++) {
      const a = Math.random() * TAU;
      c.setHSL(0.05 + Math.random() * 0.025, 0.5, 0.4 + Math.random() * 0.18);
      this.shards.spawn(
        {
          x, y: y + 0.3, z,
          life: 1.0 + Math.random() * 0.35,
          vx: Math.sin(a) * (1.4 + Math.random() * 1.8),
          vy: 2.4 + Math.random() * 2.2,
          vz: Math.cos(a) * (1.4 + Math.random() * 1.8),
          grav: 15,
          bounce: 0.35,
          ground,
          wx: 8 + Math.random() * 12, wy: 6 + Math.random() * 9, wz: 8 + Math.random() * 12,
          size: 0.7 + Math.random() * 0.7,
        },
        c,
      );
    }
    this.dust(x, y + 0.2, z, 5, '#cdb38f');
  }

  dust(x: number, y: number, z: number, n: number, color = '#d8cdb8'): void {
    const c = new THREE.Color(color);
    for (let i = 0; i < n; i++) {
      const a = Math.random() * TAU;
      this.puffs.spawn(
        {
          x: x + Math.sin(a) * 0.15, y, z: z + Math.cos(a) * 0.15,
          life: 0.4 + Math.random() * 0.25,
          vx: Math.sin(a) * (0.5 + Math.random() * 0.7),
          vy: 0.5 + Math.random() * 0.5,
          vz: Math.cos(a) * (0.5 + Math.random() * 0.7),
          grow: 2.4,
          billboard: true,
          size: 0.7 + Math.random() * 0.6,
        },
        c,
      );
    }
  }

  gelBurst(x: number, y: number, z: number, color: THREE.Color, n = 6, power = 1): void {
    for (let i = 0; i < n; i++) {
      const a = Math.random() * TAU;
      this.drops.spawn(
        {
          x, y: y + 0.2, z,
          life: 0.55 + Math.random() * 0.35,
          vx: Math.sin(a) * (1.6 + Math.random() * 2.4) * power,
          vy: (2.2 + Math.random() * 2.4) * power,
          vz: Math.cos(a) * (1.6 + Math.random() * 2.4) * power,
          grav: 13,
          bounce: 0.25,
          ground: y,
          size: 0.7 + Math.random() * 0.8,
        },
        color,
      );
    }
  }

  sparkBurst(x: number, y: number, z: number, n = 8): void {
    const c = new THREE.Color();
    for (let i = 0; i < n; i++) {
      const a = (i / n) * TAU + Math.random() * 0.5;
      const sp = 4.5 + Math.random() * 3.5;
      c.setHSL(0.13 + Math.random() * 0.04, 1, 0.75 + Math.random() * 0.2);
      this.sparks.spawn(
        {
          x, y, z,
          life: 0.18 + Math.random() * 0.1,
          vx: Math.sin(a) * sp,
          vy: 0.6 + Math.random() * 1.4,
          vz: Math.cos(a) * sp,
          drag: 7,
          alignVel: true,
          size: 0.7 + Math.random() * 0.6,
        },
        c,
      );
    }
  }

  sparkle(x: number, y: number, z: number): void {
    const c = new THREE.Color('#fff8c8');
    this.sparks.spawn(
      {
        x: x + (Math.random() - 0.5) * 0.2, y, z: z + (Math.random() - 0.5) * 0.2,
        life: 0.25,
        vx: (Math.random() - 0.5) * 0.8,
        vy: 1.2 + Math.random(),
        vz: (Math.random() - 0.5) * 0.8,
        alignVel: true,
        size: 0.5,
      },
      c,
    );
  }

  deathPop(x: number, y: number, z: number, color: THREE.Color): void {
    this.gelBurst(x, y, z, color, 11, 1.4);
    this.sparkBurst(x, y + 0.3, z, 6);
    this.ring(x, y + 0.15, z, 1.4);
  }

  update(dt: number, camera: THREE.Camera): void {
    camera.getWorldQuaternion(this.camQ);
    this.trail.update(dt);
    this.confetti.update(dt, this.camQ);
    this.shards.update(dt, this.camQ);
    this.drops.update(dt, this.camQ);
    this.puffs.update(dt, this.camQ);
    this.sparks.update(dt, this.camQ);
    for (const r of this.rings) {
      if (r.life <= 0) continue;
      r.age += dt;
      if (r.age >= r.life) {
        r.life = 0;
        r.mesh.visible = false;
        continue;
      }
      const k = r.age / r.life;
      r.mesh.scale.setScalar((0.35 + k * 2.4) * r.scale);
      r.mat.opacity = (1 - k) * 0.85;
    }
  }
}
