import * as THREE from 'three';
import { angDiff, clamp, easeOutBack, lerp, TAU } from './util';
import type { World } from './world';

type SlimeState = 'idle' | 'pre' | 'air' | 'land' | 'stun' | 'spawning';

export interface SlimeHit {
  slime: Slime;
  died: boolean;
  x: number;
  y: number;
  z: number;
}

const SMALL_COLOR = '#4ecf7e';
const BIG_COLOR = '#3fbf9f';
const GRAVITY = 22;

export class Slime {
  group = new THREE.Group();
  body: THREE.Mesh;
  mat: THREE.MeshStandardMaterial;
  shadow: THREE.Mesh;
  big: boolean;
  hp: number;
  r: number;
  x: number;
  z: number;
  h = 0; // hop height above ground
  vy = 0;
  vxz = new THREE.Vector2();
  state: SlimeState = 'spawning';
  t = 0;
  timer = 1;
  flashT = 0;
  lastHitId = -1;
  phase = Math.random() * TAU;
  yaw = 0;
  kb = new THREE.Vector2();

  constructor(x: number, z: number, big: boolean) {
    this.x = x;
    this.z = z;
    this.big = big;
    this.hp = big ? 3 : 2;
    this.r = big ? 0.78 : 0.46;
    this.mat = new THREE.MeshStandardMaterial({
      color: big ? BIG_COLOR : SMALL_COLOR,
      roughness: 0.32,
      metalness: 0,
      flatShading: true,
      transparent: true,
      opacity: 0.94,
      emissive: big ? BIG_COLOR : SMALL_COLOR,
      emissiveIntensity: 0.08,
    });
    this.body = new THREE.Mesh(new THREE.IcosahedronGeometry(this.r, 1), this.mat);
    this.body.scale.y = 0.82;
    this.body.position.y = this.r * 0.72;
    this.body.castShadow = true;
    this.group.add(this.body);

    const eyeGeo = new THREE.SphereGeometry(this.r * 0.16, 6, 5);
    const pupilGeo = new THREE.SphereGeometry(this.r * 0.085, 5, 4);
    const white = new THREE.MeshBasicMaterial({ color: '#ffffff' });
    const black = new THREE.MeshBasicMaterial({ color: '#1c2429' });
    for (const sx of [-0.38, 0.38]) {
      const eye = new THREE.Mesh(eyeGeo, white);
      eye.position.set(sx * this.r, this.r * 0.95, this.r * 0.72);
      const pupil = new THREE.Mesh(pupilGeo, black);
      pupil.position.set(sx * this.r, this.r * 0.95, this.r * 0.86);
      this.group.add(eye, pupil);
    }

    this.shadow = new THREE.Mesh(
      new THREE.CircleGeometry(this.r * 0.95, 16),
      new THREE.MeshBasicMaterial({ color: '#14290f', transparent: true, opacity: 0.25, depthWrite: false }),
    );
    this.shadow.rotation.x = -Math.PI / 2;
    this.shadow.position.y = 0.04;
    this.group.add(this.shadow);

    this.group.scale.setScalar(0.01);
    this.t = 0;
  }

  squash(sy: number): void {
    this.body.scale.set((1 / Math.sqrt(sy)) * 1.0, sy * 0.82, 1 / Math.sqrt(sy));
  }
}

/** Hopping slime mob with squash-and-stretch, knockback, and a big one that splits. */
export class Slimes {
  list: Slime[] = [];
  private respawnT = 0;

  constructor(
    private scene: THREE.Scene,
    private world: World,
  ) {
    for (const s of world.slimeSpawns) this.add(s.x, s.z, s.big);
  }

  add(x: number, z: number, big: boolean): Slime {
    const s = new Slime(x, z, big);
    this.scene.add(s.group);
    this.list.push(s);
    return s;
  }

  get alive(): number {
    return this.list.length;
  }

  get totalHp(): number {
    let n = 0;
    for (const s of this.list) n += s.hp;
    return n;
  }

  nearest(x: number, z: number): [number, number] | null {
    let best: Slime | null = null;
    let bd = Infinity;
    for (const s of this.list) {
      if (s.state === 'spawning') continue;
      const d = (s.x - x) * (s.x - x) + (s.z - z) * (s.z - z);
      if (d < bd) {
        bd = d;
        best = s;
      }
    }
    return best ? [best.x, best.z] : null;
  }

  /** Apply a blade arc; returns hits (with death info) for the game to juice up. */
  tryHit(
    hx: number,
    hz: number,
    bladeYaw: number,
    radius: number,
    halfArc: number,
    dmg: number,
    hitId: number,
  ): SlimeHit[] {
    const hits: SlimeHit[] = [];
    for (let i = this.list.length - 1; i >= 0; i--) {
      const s = this.list[i]!;
      if (s.state === 'spawning' || s.lastHitId === hitId) continue;
      const dx = s.x - hx;
      const dz = s.z - hz;
      const d = Math.hypot(dx, dz);
      if (d > radius + s.r * 0.5 || s.h > 1.3) continue;
      if (Math.abs(angDiff(Math.atan2(dx, dz), bladeYaw)) > halfArc + s.r / Math.max(0.6, d)) continue;
      s.lastHitId = hitId;
      s.hp -= dmg;
      const nx = d > 1e-4 ? dx / d : 0;
      const nz = d > 1e-4 ? dz / d : 1;
      const cy = this.world.groundY(s.x, s.z) + s.h + s.r * 0.7;
      const cx = s.x - nx * s.r * 0.8;
      const cz = s.z - nz * s.r * 0.8;
      const died = s.hp <= 0;
      if (died) {
        this.scene.remove(s.group);
        this.list.splice(i, 1);
        if (s.big) {
          // big slime splits into two smalls that pop outward
          for (const side of [-1, 1]) {
            const px = Math.cos(Math.atan2(nx, nz)) * side;
            const pz = -Math.sin(Math.atan2(nx, nz)) * side;
            const child = this.add(s.x + px * 0.7, s.z + pz * 0.7, false);
            child.state = 'air';
            child.h = 0.3;
            child.vy = 4.5;
            child.vxz.set(px * 2.8 + nx * 1.2, pz * 2.8 + nz * 1.2);
            child.group.scale.setScalar(1);
          }
        }
      } else {
        s.state = 'stun';
        s.t = 0;
        s.timer = 0.32;
        s.kb.set(nx * 8.5, nz * 8.5);
        s.flashT = 0.14;
        s.mat.emissive.set('#ffffff');
        s.mat.emissiveIntensity = 2.2;
      }
      hits.push({ slime: s, died, x: cx, y: cy, z: cz });
    }
    return hits;
  }

  update(dt: number, heroX: number, heroZ: number): { x: number; z: number; r: number }[] {
    // gentle respawn so the field never goes quiet
    if (this.list.length < 3) {
      this.respawnT += dt;
      if (this.respawnT > 14) {
        this.respawnT = 0;
        const spawns = this.world.slimeSpawns;
        const pick = spawns[Math.floor(Math.random() * spawns.length)]!;
        if (Math.hypot(pick.x - heroX, pick.z - heroZ) > 6) this.add(pick.x, pick.z, false);
      }
    } else {
      this.respawnT = 0;
    }

    const touchers: { x: number; z: number; r: number }[] = [];
    for (const s of this.list) {
      s.t += dt;
      if (s.flashT > 0) {
        s.flashT -= dt;
        if (s.flashT <= 0) {
          s.mat.emissive.set(s.big ? BIG_COLOR : SMALL_COLOR);
          s.mat.emissiveIntensity = 0.08;
        } else {
          s.mat.emissiveIntensity = 2.2 * (s.flashT / 0.14);
        }
      }

      const dx = heroX - s.x;
      const dz = heroZ - s.z;
      const distToHero = Math.hypot(dx, dz);

      switch (s.state) {
        case 'spawning': {
          const k = clamp(s.t / 0.4, 0, 1);
          s.group.scale.setScalar(Math.max(0.01, easeOutBack(k)));
          if (k >= 1) {
            s.state = 'idle';
            s.timer = 0.5 + Math.random();
          }
          break;
        }
        case 'idle': {
          const wob = Math.sin(s.t * 4.5 + s.phase) * 0.07;
          s.squash(1 + wob);
          s.timer -= dt;
          if (s.timer <= 0) {
            s.state = 'pre';
            s.t = 0;
          }
          if (distToHero < 9) {
            const ty = Math.atan2(dx, dz);
            s.yaw += angDiff(s.yaw, ty) * Math.min(1, 5 * dt);
          }
          break;
        }
        case 'pre': {
          s.squash(1 - 0.3 * clamp(s.t / 0.14, 0, 1));
          if (s.t >= 0.14) {
            s.state = 'air';
            s.vy = s.big ? 4.6 : 5.3;
            const speed = s.big ? 2.0 : 2.9;
            let a: number;
            if (distToHero < 11) {
              a = Math.atan2(dx, dz) + (Math.random() - 0.5) * 0.5;
            } else {
              a = Math.random() * TAU;
            }
            s.vxz.set(Math.sin(a) * speed, Math.cos(a) * speed);
            s.yaw = a;
          }
          break;
        }
        case 'air': {
          s.vy -= GRAVITY * dt;
          s.h += s.vy * dt;
          s.x += s.vxz.x * dt;
          s.z += s.vxz.y * dt;
          const p = this.world.resolve({ x: s.x, z: s.z }, s.r * 0.8);
          s.x = p.x;
          s.z = p.z;
          s.squash(1 + clamp(s.vy * 0.05, -0.18, 0.3));
          if (s.h <= 0 && s.vy < 0) {
            s.h = 0;
            s.state = 'land';
            s.t = 0;
          }
          break;
        }
        case 'land': {
          const k = clamp(s.t / 0.24, 0, 1);
          s.squash(lerp(0.62, 1, easeOutBack(k)));
          if (k >= 1) {
            s.state = 'idle';
            s.timer = (s.big ? 1.2 : 0.8) + Math.random() * 0.9;
          }
          break;
        }
        case 'stun': {
          s.x += s.kb.x * dt;
          s.z += s.kb.y * dt;
          s.kb.multiplyScalar(Math.exp(-6.5 * dt));
          const p = this.world.resolve({ x: s.x, z: s.z }, s.r * 0.8);
          s.x = p.x;
          s.z = p.z;
          if (s.h > 0 || s.vy > 0) {
            s.vy -= GRAVITY * dt;
            s.h = Math.max(0, s.h + s.vy * dt);
          }
          const k = clamp(s.t / s.timer, 0, 1);
          s.squash(lerp(0.62, 1, k) + Math.sin(k * 14) * 0.08 * (1 - k));
          if (k >= 1) {
            s.state = 'idle';
            s.timer = 0.4 + Math.random() * 0.5;
          }
          break;
        }
      }

      const gy = this.world.groundY(s.x, s.z);
      s.group.position.set(s.x, gy + s.h, s.z);
      s.group.rotation.y = s.yaw;
      s.shadow.scale.setScalar(clamp(1 - s.h * 0.35, 0.4, 1));
      (s.shadow.material as THREE.MeshBasicMaterial).opacity = 0.25 * clamp(1 - s.h * 0.4, 0.2, 1);
      s.shadow.position.y = 0.04 - s.h; // pin to ground while body hops

      if (s.state !== 'spawning' && s.h < 0.6) {
        touchers.push({ x: s.x, z: s.z, r: s.r });
      }
    }
    return touchers;
  }
}
