// Small pooled meshes for the spell effects that need real geometry (the cast rune disc, the Ice Lance spear, the Frozen Orb shell).
// Everything is built once in the Vfx constructor and starts visible but parked far below the arena, so the shader warm-up
// compiles it; Vfx.warm() then hides it. Nothing here allocates per frame: temp vectors and quaternions are module scratch.
import * as THREE from "three";

const Z = new THREE.Vector3(0, 0, 1);
const qA = new THREE.Quaternion();
const qB = new THREE.Quaternion();
const PARK = -500;

const additive = (color: THREE.ColorRepresentation, opacity: number, extra: THREE.MeshBasicMaterialParameters = {}) =>
  new THREE.MeshBasicMaterial({ color, transparent: true, opacity, blending: THREE.AdditiveBlending, depthWrite: false, toneMapped: false, fog: false, side: THREE.DoubleSide, ...extra });

function runeTexture(): THREE.CanvasTexture {
  const s = 256;
  const c = document.createElement("canvas");
  c.width = c.height = s;
  const g = c.getContext("2d")!;
  g.translate(s / 2, s / 2);
  const grad = g.createRadialGradient(0, 0, 8, 0, 0, s / 2);
  grad.addColorStop(0, "rgba(255,255,255,0.16)");
  grad.addColorStop(0.75, "rgba(255,255,255,0.05)");
  grad.addColorStop(1, "rgba(255,255,255,0)");
  g.fillStyle = grad;
  g.fillRect(-s / 2, -s / 2, s, s);
  g.strokeStyle = "#fff";
  g.fillStyle = "#fff";
  g.lineWidth = 3;
  g.beginPath();
  g.arc(0, 0, 118, 0, Math.PI * 2);
  g.stroke();
  g.lineWidth = 1.6;
  g.beginPath();
  g.arc(0, 0, 100, 0, Math.PI * 2);
  g.stroke();
  g.beginPath();
  g.arc(0, 0, 58, 0, Math.PI * 2);
  g.stroke();
  for (let i = 0; i < 36; i++) {
    const a = (i / 36) * Math.PI * 2;
    const r0 = 102, r1 = i % 3 === 0 ? 116 : 108;
    g.beginPath();
    g.moveTo(Math.cos(a) * r0, Math.sin(a) * r0);
    g.lineTo(Math.cos(a) * r1, Math.sin(a) * r1);
    g.stroke();
  }
  // two triangles (a six-point star) and a dot at each tip
  g.lineWidth = 2;
  for (let t = 0; t < 2; t++) {
    g.beginPath();
    for (let k = 0; k < 3; k++) {
      const a = (k / 3) * Math.PI * 2 + t * (Math.PI / 3) - Math.PI / 2;
      const x = Math.cos(a) * 96, y = Math.sin(a) * 96;
      if (k === 0) g.moveTo(x, y);
      else g.lineTo(x, y);
    }
    g.closePath();
    g.stroke();
  }
  for (let k = 0; k < 6; k++) {
    const a = (k / 6) * Math.PI * 2 - Math.PI / 2;
    g.beginPath();
    g.arc(Math.cos(a) * 80, Math.sin(a) * 80, 3.4, 0, Math.PI * 2);
    g.fill();
  }
  const t = new THREE.CanvasTexture(c);
  t.colorSpace = THREE.SRGBColorSpace;
  t.anisotropy = 4;
  return t;
}

/** The ground circle under the hero while a cast-time spell charges: grows and brightens with the cast bar, pops on release. */
export class RuneDisc {
  readonly mesh: THREE.Mesh;
  private mat: THREE.MeshBasicMaterial;
  private tex: THREE.CanvasTexture;
  private spin = 0;
  private pop = 0;
  private lastK = 0;
  private lastX = 0;
  private lastZ = 0;

  constructor(scene: THREE.Scene, renderer: THREE.WebGLRenderer) {
    this.tex = runeTexture();
    renderer.initTexture(this.tex);
    this.mat = additive(new THREE.Color(0.35, 1.0, 1.5), 0, { map: this.tex });
    this.mesh = new THREE.Mesh(new THREE.PlaneGeometry(1, 1).rotateX(-Math.PI / 2), this.mat);
    this.mesh.position.y = PARK;
    this.mesh.renderOrder = 22;
    this.mesh.frustumCulled = false;
    scene.add(this.mesh);
  }
  tint(r: number, g: number, b: number) {
    this.mat.color.setRGB(r, g, b);
  }
  /** k = cast progress 0..1 while charging. */
  charge(x: number, z: number, k: number, dt: number) {
    this.spin += dt * (0.8 + k * 3.2);
    this.lastK = k;
    this.lastX = x;
    this.lastZ = z;
    this.mesh.visible = true;
    this.mesh.position.set(x, 0.065, z);
    this.mesh.rotation.y = this.spin;
    this.mesh.scale.setScalar(1.1 + k * 1.5);
    this.mat.opacity = 0.18 + k * 0.5;
  }
  /** The spell left the staff: a quick flare, then the disc fades. */
  release() {
    this.pop = 1;
  }
  update(dt: number, charging: boolean) {
    if (charging) {
      this.pop = 0;
      return;
    }
    if (this.pop > 0) {
      this.pop = Math.max(0, this.pop - dt * 3.2);
      this.spin += dt * 5;
      this.mesh.position.set(this.lastX, 0.065, this.lastZ);
      this.mesh.rotation.y = this.spin;
      this.mesh.scale.setScalar(2.6 + (1 - this.pop) * 1.1);
      this.mat.opacity = this.pop * 0.7;
    } else if (this.mesh.visible) this.hide();
  }
  hide() {
    this.mesh.visible = false;
  }
  park() {
    this.mesh.visible = false;
    this.mesh.position.y = 0.065;
  }
  dispose(scene: THREE.Scene) {
    scene.remove(this.mesh);
    this.mesh.geometry.dispose();
    this.mat.dispose();
    this.tex.dispose();
  }
}

/** Ice Lance bodies: a faceted spear with an additive shell and two crossed fins, spun about its axis. */
export class SpearPool {
  private groups: THREE.Group[] = [];
  private used: boolean[] = [];
  private geos: THREE.BufferGeometry[] = [];
  private mats: THREE.Material[] = [];

  constructor(scene: THREE.Scene, n = 5) {
    const prof = [new THREE.Vector2(0, 0.62), new THREE.Vector2(0.05, 0.3), new THREE.Vector2(0.085, 0.02), new THREE.Vector2(0.05, -0.26), new THREE.Vector2(0, -0.46)];
    const body = new THREE.LatheGeometry(prof, 6).rotateX(Math.PI / 2);
    const fin = new THREE.ShapeGeometry(new THREE.Shape([new THREE.Vector2(0, 0.5), new THREE.Vector2(0.08, 0.0), new THREE.Vector2(0, -0.5), new THREE.Vector2(-0.08, 0.0)]));
    // the shape lives in XY with its length along Y: turn the length onto Z, one fin in the XZ plane and one in the YZ plane
    const finA = fin.clone().rotateX(Math.PI / 2);
    const finB = fin.clone().rotateX(Math.PI / 2).rotateZ(Math.PI / 2);
    this.geos.push(body, fin, finA, finB);
    const core = new THREE.MeshStandardMaterial({ color: 0xbdefff, emissive: 0x1d9bff, emissiveIntensity: 1.6, roughness: 0.2, metalness: 0.15, flatShading: true });
    const shell = additive(new THREE.Color(0.12, 0.55, 1.0), 0.3);
    const fins = additive(new THREE.Color(0.3, 0.7, 1.0), 0.28);
    this.mats.push(core, shell, fins);
    for (let i = 0; i < n; i++) {
      const g = new THREE.Group();
      const m0 = new THREE.Mesh(body, core);
      const m1 = new THREE.Mesh(body, shell);
      m1.scale.set(1.7, 1.7, 1.1);
      const m2 = new THREE.Mesh(finA, fins);
      const m3 = new THREE.Mesh(finB, fins);
      for (const m of [m0, m1, m2, m3]) {
        m.frustumCulled = false;
        g.add(m);
      }
      m1.renderOrder = m2.renderOrder = m3.renderOrder = 41;
      g.scale.setScalar(1.7);
      g.position.y = PARK;
      scene.add(g);
      this.groups.push(g);
      this.used.push(false);
    }
  }
  take(): number {
    for (let i = 0; i < this.used.length; i++)
      if (!this.used[i]) {
        this.used[i] = true;
        return i;
      }
    return -1;
  }
  free(i: number) {
    if (i < 0) return;
    this.used[i] = false;
    this.groups[i].visible = false;
  }
  freeAll() {
    for (let i = 0; i < this.used.length; i++) this.free(i);
  }
  place(i: number, x: number, y: number, z: number, dx: number, dy: number, dz: number, spin: number) {
    const g = this.groups[i];
    g.visible = true;
    g.position.set(x, y, z);
    qA.setFromUnitVectors(Z, tmpDir.set(dx, dy, dz).normalize());
    qB.setFromAxisAngle(Z, spin);
    g.quaternion.copy(qA).multiply(qB);
  }
  park() {
    for (const g of this.groups) {
      g.visible = false;
      g.position.y = 0;
    }
  }
  dispose(scene: THREE.Scene) {
    for (const g of this.groups) scene.remove(g);
    for (const x of [...this.geos, ...this.mats]) x.dispose();
  }
}
const tmpDir = new THREE.Vector3();

/** Frozen Orb body: two counter-rotating wire crystals and a solid core, around the sim orb. */
export class OrbShell {
  private g = new THREE.Group();
  private a: THREE.Mesh;
  private b: THREE.Mesh;
  private c: THREE.Mesh;
  private geos: THREE.BufferGeometry[] = [];
  private mats: THREE.Material[] = [];

  constructor(scene: THREE.Scene) {
    const ico1 = new THREE.IcosahedronGeometry(0.78, 1);
    const ico2 = new THREE.IcosahedronGeometry(0.98, 0);
    const ico3 = new THREE.IcosahedronGeometry(0.46, 1);
    const wire1 = additive(new THREE.Color(0.5, 1.5, 2.2), 0.75, { wireframe: true });
    const wire2 = additive(new THREE.Color(0.25, 0.8, 1.5), 0.55, { wireframe: true });
    const core = additive(new THREE.Color(0.9, 1.8, 2.3), 0.5);
    this.geos.push(ico1, ico2, ico3);
    this.mats.push(wire1, wire2, core);
    this.a = new THREE.Mesh(ico1, wire1);
    this.b = new THREE.Mesh(ico2, wire2);
    this.c = new THREE.Mesh(ico3, core);
    for (const m of [this.a, this.b, this.c]) {
      m.frustumCulled = false;
      m.renderOrder = 42;
      this.g.add(m);
    }
    this.g.position.y = PARK;
    scene.add(this.g);
  }
  place(x: number, y: number, z: number, t: number) {
    this.g.visible = true;
    this.g.position.set(x, y, z);
    this.a.rotation.set(t * 2.1, t * 1.3, 0);
    this.b.rotation.set(-t * 1.1, t * 0.7, t * 0.5);
    const pulse = 1 + Math.sin(t * 9) * 0.05;
    this.c.scale.setScalar(pulse);
  }
  hide() {
    this.g.visible = false;
  }
  park() {
    this.g.visible = false;
    this.g.position.y = 0;
  }
  dispose(scene: THREE.Scene) {
    scene.remove(this.g);
    for (const x of [...this.geos, ...this.mats]) x.dispose();
  }
}
