import * as THREE from 'three';
import { angDiff, easeOutBack, mulberry32, TAU } from './util';
import { GRASS_PATCHES, World } from './world';

const ALIVE = 0;
const CUT = 1;
const GROWING = 2;

interface Tuft {
  x: number;
  y: number;
  z: number;
  rot: number;
  s: number;
  state: number;
  t: number;
}

/** Instanced field of cuttable grass tufts with shader wind sway. */
export class GrassField {
  mesh: THREE.InstancedMesh;
  totalCut = 0;
  private tufts: Tuft[] = [];
  private timeU = { value: 0 };
  private dummy = new THREE.Object3D();

  constructor(scene: THREE.Scene, world: World) {
    const rng = mulberry32(99);
    const geo = this.buildTuftGeometry(rng);

    for (const patch of GRASS_PATCHES) {
      const step = 0.62;
      for (let gx = -patch.r; gx <= patch.r; gx += step) {
        for (let gz = -patch.r; gz <= patch.r; gz += step) {
          const jx = gx + (rng() - 0.5) * 0.45;
          const jz = gz + (rng() - 0.5) * 0.45;
          const d = Math.hypot(jx, jz);
          if (d > patch.r * (0.75 + rng() * 0.3)) continue;
          const x = patch.x + jx;
          const z = patch.z + jz;
          if (Math.abs(x) > 18.5 || Math.abs(z) > 18.5) continue;
          const y = world.groundY(x, z);
          if (y < world.waterY + 0.12) continue;
          if (Math.abs(x - world.ruinX) < 3.6 && Math.abs(z - world.ruinZ) < 3.6) continue;
          let blocked = false;
          for (const c of world.colliders) {
            if (Math.hypot(x - c.x, z - c.z) < c.r + 0.25) {
              blocked = true;
              break;
            }
          }
          if (blocked) continue;
          this.tufts.push({ x, y, z, rot: rng() * TAU, s: 0.85 + rng() * 0.4, state: ALIVE, t: 0 });
        }
      }
    }

    const mat = new THREE.MeshLambertMaterial({
      vertexColors: true,
      side: THREE.DoubleSide,
      emissive: '#1d4413',
    });
    mat.onBeforeCompile = (shader) => {
      shader.uniforms.uTime = this.timeU;
      shader.vertexShader =
        'uniform float uTime;\n' +
        shader.vertexShader.replace(
          '#include <begin_vertex>',
          `#include <begin_vertex>
          {
            vec4 gwp = instanceMatrix * vec4(transformed, 1.0);
            float gsway = sin(uTime * 2.1 + gwp.x * 0.9 + gwp.z * 0.7) * 0.6
                        + sin(uTime * 3.6 + gwp.z * 1.4 + gwp.x * 0.3) * 0.4;
            transformed.x += gsway * 0.28 * transformed.y;
            transformed.z += cos(uTime * 1.8 + gwp.x * 1.1) * 0.16 * transformed.y;
          }`,
        );
    };

    this.mesh = new THREE.InstancedMesh(geo, mat, this.tufts.length);
    this.mesh.instanceMatrix.setUsage(THREE.DynamicDrawUsage);
    this.mesh.receiveShadow = true;
    for (let i = 0; i < this.tufts.length; i++) this.applyMatrix(i, 1);
    scene.add(this.mesh);
  }

  private buildTuftGeometry(rng: () => number): THREE.BufferGeometry {
    const pos: number[] = [];
    const col: number[] = [];
    const blades = 6;
    const c = new THREE.Color();
    for (let i = 0; i < blades; i++) {
      const a = (i / blades) * TAU + rng() * 0.9;
      const lean = 0.1 + rng() * 0.17;
      const h = 0.32 + rng() * 0.2;
      const bx = Math.sin(a) * 0.08;
      const bz = Math.cos(a) * 0.08;
      const w = 0.04 + rng() * 0.022;
      const px = Math.cos(a) * w;
      const pz = -Math.sin(a) * w;
      pos.push(bx - px, 0, bz - pz, bx + px, 0, bz + pz, bx + Math.sin(a) * lean, h, bz + Math.cos(a) * lean);
      const g = 0.85 + rng() * 0.45;
      c.setRGB(0.3 * g, 0.72 * g, 0.24 * g);
      for (let k = 0; k < 3; k++) col.push(c.r, c.g, c.b);
    }
    const geo = new THREE.BufferGeometry();
    geo.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3));
    geo.setAttribute('color', new THREE.Float32BufferAttribute(col, 3));
    geo.computeVertexNormals();
    return geo;
  }

  private applyMatrix(i: number, scale: number): void {
    const tuft = this.tufts[i]!;
    this.dummy.position.set(tuft.x, tuft.y, tuft.z);
    this.dummy.rotation.set(0, tuft.rot, 0);
    this.dummy.scale.setScalar(tuft.s * scale);
    this.dummy.updateMatrix();
    this.mesh.setMatrixAt(i, this.dummy.matrix);
    this.mesh.instanceMatrix.needsUpdate = true;
  }

  get remaining(): number {
    let n = 0;
    for (const t of this.tufts) if (t.state === ALIVE) n++;
    return n;
  }

  nearest(x: number, z: number): [number, number] | null {
    let best: Tuft | null = null;
    let bd = Infinity;
    for (const t of this.tufts) {
      if (t.state !== ALIVE) continue;
      const d = (t.x - x) * (t.x - x) + (t.z - z) * (t.z - z);
      if (d < bd) {
        bd = d;
        best = t;
      }
    }
    return best ? [best.x, best.z] : null;
  }

  /**
   * Cut alive tufts inside the blade's current angular window.
   * Calls onCut(x, y, z) per tuft; returns the number cut.
   */
  cutInArc(
    hx: number,
    hz: number,
    bladeYaw: number,
    radius: number,
    halfArc: number,
    onCut: (x: number, y: number, z: number) => void,
  ): number {
    let n = 0;
    const r2 = radius * radius;
    for (let i = 0; i < this.tufts.length; i++) {
      const t = this.tufts[i]!;
      if (t.state !== ALIVE) continue;
      const dx = t.x - hx;
      const dz = t.z - hz;
      if (dx * dx + dz * dz > r2) continue;
      if (Math.abs(angDiff(Math.atan2(dx, dz), bladeYaw)) > halfArc) continue;
      t.state = CUT;
      t.t = 18 + Math.random() * 6;
      this.applyMatrix(i, 0);
      this.totalCut++;
      n++;
      onCut(t.x, t.y, t.z);
    }
    return n;
  }

  update(dt: number): void {
    this.timeU.value += dt;
    for (let i = 0; i < this.tufts.length; i++) {
      const t = this.tufts[i]!;
      if (t.state === CUT) {
        t.t -= dt;
        if (t.t <= 0) {
          t.state = GROWING;
          t.t = 0;
        }
      } else if (t.state === GROWING) {
        t.t += dt / 0.5;
        if (t.t >= 1) {
          t.state = ALIVE;
          this.applyMatrix(i, 1);
        } else {
          this.applyMatrix(i, Math.max(0.01, easeOutBack(t.t)));
        }
      }
    }
  }
}
