import * as THREE from 'three';
import { clamp, lerp, mulberry32, smoothstep, TAU } from './util';

export interface Collider {
  x: number;
  z: number;
  r: number;
}

export interface PotSpot {
  x: number;
  z: number;
  y: number;
  /** Reachable on foot (gets a collider while intact). */
  ground: boolean;
}

export const GRASS_PATCHES: { x: number; z: number; r: number }[] = [
  { x: -3, z: -3, r: 3.6 },
  { x: 3.5, z: 2.5, r: 3.0 },
  { x: -8, z: 2, r: 2.6 },
  { x: 5, z: -10, r: 3.2 },
  { x: -3.5, z: 12.5, r: 2.8 },
  { x: -15, z: -6, r: 2.6 },
  { x: 0.5, z: -13.5, r: 3.0 },
  { x: 12, z: 12, r: 2.6 },
];

const GRASS_A = new THREE.Color('#4fae45');
const GRASS_B = new THREE.Color('#83cf5c');
const SAND = new THREE.Color('#d9c27e');
const SAND_DEEP = new THREE.Color('#a8854f');
const STONE_TINT = new THREE.Color('#a3b08b');

interface Butterfly {
  g: THREE.Group;
  wl: THREE.Mesh;
  wr: THREE.Mesh;
  ax: number;
  az: number;
  p1: number;
  p2: number;
  px: number;
  pz: number;
}

export class World {
  group = new THREE.Group();
  colliders: Collider[] = [];
  potSpots: PotSpot[] = [];
  slimeSpawns: { x: number; z: number; big: boolean }[] = [
    { x: 6, z: -4, big: true },
    { x: -6.5, z: -11, big: false },
    { x: 4, z: 7.5, big: false },
    { x: -13, z: -1, big: false },
    { x: 14.5, z: 4, big: false },
    { x: -1, z: 16, big: false },
  ];
  bounds = 18.4;
  pondX = -11.5;
  pondZ = 9.5;
  pondR = 5.4;
  waterY = -0.55;
  ruinX = 12;
  ruinZ = -12;
  slabTop = 0;

  private canopies: { g: THREE.Object3D; phase: number }[] = [];
  private water!: THREE.Mesh;
  private butterflies: Butterfly[] = [];
  private motes!: THREE.Points;
  private motesPos!: Float32Array;
  private t = 0;

  constructor() {
    this.build();
  }

  terrainHeight(x: number, z: number): number {
    let h =
      0.5 * Math.sin(x * 0.16) * Math.cos(z * 0.14) +
      0.3 * Math.sin(x * 0.33 + 1.7) * Math.sin(z * 0.29 + 0.6) +
      0.16 * Math.sin(x * 0.71) * Math.cos(z * 0.83 + 2.0);
    const pd = Math.hypot(x - this.pondX, z - this.pondZ);
    h -= smoothstep(clamp(1 - pd / this.pondR, 0, 1)) * 1.9;
    const rd = Math.hypot(x - this.ruinX, z - this.ruinZ);
    h = lerp(h, 0.18, smoothstep(clamp(1 - rd / 6.5, 0, 1)));
    return h;
  }

  /** Walkable ground height (terrain, or the ruin floor slab where it is higher). */
  groundY(x: number, z: number): number {
    const h = this.terrainHeight(x, z);
    if (Math.abs(x - this.ruinX) < 3.4 && Math.abs(z - this.ruinZ) < 3.4) {
      return Math.max(h, this.slabTop);
    }
    return h;
  }

  /** Push a circle out of all static colliders and clamp to bounds. Mutates and returns p. */
  resolve(p: { x: number; z: number }, r: number, extra?: Collider[]): { x: number; z: number } {
    const lists = extra ? [this.colliders, extra] : [this.colliders];
    for (const list of lists) {
      for (const c of list) {
        const dx = p.x - c.x;
        const dz = p.z - c.z;
        const d = Math.hypot(dx, dz);
        const min = r + c.r;
        if (d < min && d > 1e-5) {
          const k = (min - d) / d;
          p.x += dx * k;
          p.z += dz * k;
        }
      }
    }
    p.x = clamp(p.x, -this.bounds, this.bounds);
    p.z = clamp(p.z, -this.bounds, this.bounds);
    return p;
  }

  private build(): void {
    const rng = mulberry32(7);
    this.buildTerrain(rng);
    this.buildPond(rng);
    this.buildTreesAndRocks(rng);
    this.buildRuin();
    this.buildFlowers(rng);
    this.buildButterflies(rng);
    this.buildMotes(rng);
    // keep the pond's deep middle off-limits
    this.colliders.push({ x: this.pondX, z: this.pondZ, r: this.pondR - 1.5 });
  }

  private buildTerrain(rng: () => number): void {
    const seg = 64;
    const geo = new THREE.PlaneGeometry(40, 40, seg, seg);
    geo.rotateX(-Math.PI / 2);
    const pos = geo.attributes.position as THREE.BufferAttribute;
    const colors = new Float32Array(pos.count * 3);
    const c = new THREE.Color();
    for (let i = 0; i < pos.count; i++) {
      const x = pos.getX(i);
      const z = pos.getZ(i);
      const h = this.terrainHeight(x, z);
      pos.setY(i, h);
      const n = 0.5 + 0.5 * Math.sin(x * 0.55 + 1.3) * Math.cos(z * 0.6 - 0.4) + (rng() - 0.5) * 0.3;
      c.copy(GRASS_A).lerp(GRASS_B, clamp(0.25 + 0.55 * n, 0, 1));
      const pd = Math.hypot(x - this.pondX, z - this.pondZ);
      if (pd < this.pondR + 1.6) {
        const s = smoothstep(clamp((this.pondR + 1.6 - pd) / 2.4, 0, 1));
        c.lerp(h < this.waterY ? SAND_DEEP : SAND, s);
      }
      const rd = Math.hypot(x - this.ruinX, z - this.ruinZ);
      if (rd < 6) c.lerp(STONE_TINT, smoothstep(clamp(1 - rd / 6, 0, 1)) * 0.5);
      c.offsetHSL(0, 0, h * 0.025);
      colors[i * 3] = c.r;
      colors[i * 3 + 1] = c.g;
      colors[i * 3 + 2] = c.b;
    }
    geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
    geo.computeVertexNormals();
    const mat = new THREE.MeshLambertMaterial({ vertexColors: true, flatShading: true });
    const mesh = new THREE.Mesh(geo, mat);
    mesh.receiveShadow = true;
    this.group.add(mesh);
    this.group.add(this.buildSkirt(seg));
  }

  /** Earth-brown walls hanging off the terrain edge — sells the floating-diorama look. */
  private buildSkirt(seg: number): THREE.Mesh {
    const posArr: number[] = [];
    const colArr: number[] = [];
    const top = new THREE.Color('#7c5b33');
    const bot = new THREE.Color('#54401f');
    const bottomY = -3.2;
    const step = 40 / seg;
    const edges: [number, number, number, number][] = [
      [-20, -20, 1, 0], // along +x at z=-20
      [-20, 20, 1, 0], // along +x at z=+20
      [-20, -20, 0, 1], // along +z at x=-20
      [20, -20, 0, 1], // along +z at x=+20
    ];
    for (const [sx, sz, dx, dz] of edges) {
      for (let i = 0; i < seg; i++) {
        const x0 = sx + dx * step * i;
        const z0 = sz + dz * step * i;
        const x1 = sx + dx * step * (i + 1);
        const z1 = sz + dz * step * (i + 1);
        const h0 = this.terrainHeight(x0, z0);
        const h1 = this.terrainHeight(x1, z1);
        // two triangles per quad
        posArr.push(x0, h0, z0, x1, h1, z1, x0, bottomY, z0);
        posArr.push(x1, h1, z1, x1, bottomY, z1, x0, bottomY, z0);
        const cols = [top, top, bot, top, bot, bot];
        for (const cc of cols) colArr.push(cc.r, cc.g, cc.b);
      }
    }
    const geo = new THREE.BufferGeometry();
    geo.setAttribute('position', new THREE.Float32BufferAttribute(posArr, 3));
    geo.setAttribute('color', new THREE.Float32BufferAttribute(colArr, 3));
    geo.computeVertexNormals();
    return new THREE.Mesh(
      geo,
      new THREE.MeshLambertMaterial({ vertexColors: true, side: THREE.DoubleSide }),
    );
  }

  private buildPond(rng: () => number): void {
    const geo = new THREE.CircleGeometry(this.pondR, 28);
    geo.rotateX(-Math.PI / 2);
    const mat = new THREE.MeshStandardMaterial({
      color: '#3fb5e6',
      transparent: true,
      opacity: 0.82,
      roughness: 0.15,
      metalness: 0,
    });
    this.water = new THREE.Mesh(geo, mat);
    this.water.position.set(this.pondX, this.waterY, this.pondZ);
    this.group.add(this.water);

    const padGeo = new THREE.CylinderGeometry(0.34, 0.38, 0.04, 7);
    const padMat = new THREE.MeshLambertMaterial({ color: '#3f9b3f', flatShading: true });
    for (let i = 0; i < 3; i++) {
      const a = rng() * TAU;
      const r = this.pondR * (0.35 + rng() * 0.4);
      const pad = new THREE.Mesh(padGeo, padMat);
      pad.position.set(this.pondX + Math.sin(a) * r, this.waterY + 0.03, this.pondZ + Math.cos(a) * r);
      pad.rotation.y = rng() * TAU;
      pad.scale.setScalar(0.8 + rng() * 0.5);
      this.group.add(pad);
      if (i === 0) {
        const bud = new THREE.Mesh(
          new THREE.SphereGeometry(0.09, 6, 5),
          new THREE.MeshLambertMaterial({ color: '#f792b9' }),
        );
        bud.position.copy(pad.position);
        bud.position.y += 0.1;
        this.group.add(bud);
      }
    }
  }

  private buildTreesAndRocks(rng: () => number): void {
    const trunkMat = new THREE.MeshLambertMaterial({ color: '#7c5128', flatShading: true });
    const greens = ['#3fa84e', '#57bd52', '#2f9445'].map((g) => new THREE.MeshLambertMaterial({ color: g, flatShading: true }));
    const trees: [number, number][] = [
      [-16, -14], [-12.5, -16.5], [15.5, 8.5], [17, 2.5], [-17, 3.5],
      [6.5, 16], [-4.5, -17], [16.5, -5], [9.5, 15.5],
    ];
    for (const [tx, tz] of trees) {
      const s = 0.9 + rng() * 0.45;
      const tree = new THREE.Group();
      const trunk = new THREE.Mesh(new THREE.CylinderGeometry(0.16, 0.26, 1.25, 7), trunkMat);
      trunk.position.y = 0.6;
      trunk.castShadow = true;
      tree.add(trunk);
      const canopy = new THREE.Group();
      canopy.position.y = 1.15;
      const blobs: [number, number, number, number][] = [
        [0, 0.7, 0, 1.35],
        [0.55, 1.35, 0.25, 0.95],
        [-0.5, 1.2, -0.3, 0.85],
        [0.1, 1.9, -0.1, 0.7],
      ];
      for (let i = 0; i < blobs.length; i++) {
        const b = blobs[i]!;
        const blob = new THREE.Mesh(new THREE.IcosahedronGeometry(b[3], 0), greens[i % greens.length]!);
        blob.position.set(b[0], b[1], b[2]);
        blob.scale.y = 0.85;
        blob.rotation.set(rng() * TAU, rng() * TAU, 0);
        blob.castShadow = true;
        canopy.add(blob);
      }
      tree.add(canopy);
      tree.position.set(tx, this.terrainHeight(tx, tz) - 0.05, tz);
      tree.scale.setScalar(s);
      tree.rotation.y = rng() * TAU;
      this.group.add(tree);
      this.canopies.push({ g: canopy, phase: rng() * TAU });
      this.colliders.push({ x: tx, z: tz, r: 0.55 * s });
    }

    const bushMatA = new THREE.MeshLambertMaterial({ color: '#4cb24c', flatShading: true });
    const bushMatB = new THREE.MeshLambertMaterial({ color: '#3c9e4e', flatShading: true });
    const bushes: [number, number][] = [[-7.5, 15.5], [8.5, 4.5], [-10, -14], [2, 16.5], [17, 12], [-16.5, 12.5]];
    for (const [bx, bz] of bushes) {
      const bush = new THREE.Mesh(new THREE.IcosahedronGeometry(0.62, 0), rng() < 0.5 ? bushMatA : bushMatB);
      bush.position.set(bx, this.terrainHeight(bx, bz) + 0.28, bz);
      bush.scale.set(1, 0.72, 1);
      bush.rotation.y = rng() * TAU;
      bush.castShadow = true;
      this.group.add(bush);
      this.colliders.push({ x: bx, z: bz, r: 0.55 });
    }

    const rockMat = new THREE.MeshLambertMaterial({ color: '#97a0a8', flatShading: true });
    const rocks: [number, number, number][] = [
      [-2.5, 6.5, 0.7], [7.5, -7, 0.55], [1.5, 11.5, 0.5], [-8.5, -8.5, 0.8],
      [14, 15, 0.65], [-14.5, 16, 0.5], [16, -12, 0.75],
    ];
    for (const [rx, rz, rr] of rocks) {
      const rock = new THREE.Mesh(new THREE.IcosahedronGeometry(rr, 0), rockMat);
      rock.position.set(rx, this.terrainHeight(rx, rz) + rr * 0.35, rz);
      rock.scale.set(1, 0.72, 1);
      rock.rotation.set(rng() * TAU, rng() * TAU, rng() * 0.3);
      rock.castShadow = true;
      this.group.add(rock);
      this.colliders.push({ x: rx, z: rz, r: rr * 0.9 });
    }
  }

  private buildRuin(): void {
    const baseH = 0.18;
    this.slabTop = baseH + 0.12;
    const stoneMat = new THREE.MeshLambertMaterial({ color: '#b9beb4', flatShading: true });
    const paleMat = new THREE.MeshLambertMaterial({ color: '#c6cabe', flatShading: true });

    const slab = new THREE.Mesh(new THREE.BoxGeometry(6.8, 0.5, 6.8), stoneMat);
    slab.position.set(this.ruinX, baseH - 0.13, this.ruinZ);
    slab.receiveShadow = true;
    slab.castShadow = true;
    this.group.add(slab);

    const cols: [number, number, number][] = [
      [9.6, -14.4, 1.7],
      [14.4, -14.4, 1.05],
      [14.4, -9.6, 2.0],
    ];
    for (const [cx, cz, ch] of cols) {
      const col = new THREE.Mesh(new THREE.CylinderGeometry(0.38, 0.46, ch, 7), paleMat);
      col.position.set(cx, this.slabTop + ch / 2, cz);
      col.rotation.z = 0.03;
      col.castShadow = true;
      this.group.add(col);
      this.colliders.push({ x: cx, z: cz, r: 0.62 });
    }

    const fallen = new THREE.Mesh(new THREE.CylinderGeometry(0.36, 0.4, 2.4, 7), paleMat);
    fallen.position.set(10.2, this.slabTop + 0.3, -9.8);
    fallen.rotation.z = Math.PI / 2;
    fallen.rotation.y = 0.25;
    fallen.castShadow = true;
    this.group.add(fallen);
    this.colliders.push({ x: 9.4, z: -10, r: 0.55 });
    this.colliders.push({ x: 11, z: -9.7, r: 0.55 });

    const ledge = new THREE.Mesh(new THREE.BoxGeometry(3.0, 0.62, 1.5), stoneMat);
    ledge.position.set(12, this.slabTop + 0.31, -14.3);
    ledge.castShadow = true;
    ledge.receiveShadow = true;
    this.group.add(ledge);
    const ledgeTop = this.slabTop + 0.62;
    for (const lx of [11, 12, 13]) this.colliders.push({ x: lx, z: -14.3, r: 0.85 });

    // pots: on the ruin floor, up on the ledge, and a couple strays in the field
    const floorSpots: [number, number][] = [
      [10.5, -10.7], [13.5, -11.2], [11.0, -13.0], [13.3, -13.3], [9.9, -12.3],
    ];
    for (const [px, pz] of floorSpots) this.potSpots.push({ x: px, z: pz, y: this.slabTop, ground: true });
    for (const lx of [11.1, 12, 12.9]) this.potSpots.push({ x: lx, z: -14.3, y: ledgeTop, ground: false });
    for (const [px, pz] of [[-2.5, -6.5], [5.5, 9.0]] as [number, number][]) {
      this.potSpots.push({ x: px, z: pz, y: this.groundY(px, pz), ground: true });
    }
  }

  private buildFlowers(rng: () => number): void {
    const head = new THREE.SphereGeometry(0.07, 6, 5);
    head.translate(0, 0.17, 0);
    const stem = new THREE.CylinderGeometry(0.012, 0.018, 0.16, 4);
    stem.translate(0, 0.08, 0);
    const count = 70;
    const headMesh = new THREE.InstancedMesh(head, new THREE.MeshLambertMaterial(), count);
    const stemMesh = new THREE.InstancedMesh(stem, new THREE.MeshLambertMaterial({ color: '#3c8a3c' }), count);
    const colors = ['#fffdf2', '#ffd84f', '#ff9fb5', '#b8a5ff'].map((c) => new THREE.Color(c));
    const dummy = new THREE.Object3D();
    for (let i = 0; i < count; i++) {
      const patch = GRASS_PATCHES[Math.floor(rng() * GRASS_PATCHES.length)]!;
      const a = rng() * TAU;
      const r = patch.r * (0.75 + rng() * 0.55);
      const x = patch.x + Math.sin(a) * r;
      const z = patch.z + Math.cos(a) * r;
      const y = this.groundY(x, z);
      if (y < this.waterY + 0.15) {
        dummy.scale.setScalar(0);
      } else {
        dummy.scale.setScalar(0.8 + rng() * 0.6);
      }
      dummy.position.set(x, y, z);
      dummy.rotation.set(0, rng() * TAU, 0);
      dummy.updateMatrix();
      headMesh.setMatrixAt(i, dummy.matrix);
      stemMesh.setMatrixAt(i, dummy.matrix);
      headMesh.setColorAt(i, colors[Math.floor(rng() * colors.length)]!);
    }
    this.group.add(headMesh, stemMesh);
  }

  private buildButterflies(rng: () => number): void {
    const wing = new THREE.PlaneGeometry(0.14, 0.2);
    wing.translate(0.07, 0, 0);
    const cols = ['#fff6d8', '#ffd84f', '#c9a8ff', '#9fd8ff', '#ffb3c8'];
    for (let i = 0; i < 5; i++) {
      const mat = new THREE.MeshBasicMaterial({
        color: cols[i % cols.length],
        side: THREE.DoubleSide,
      });
      const g = new THREE.Group();
      const wl = new THREE.Mesh(wing, mat);
      const wr = new THREE.Mesh(wing, mat);
      wr.rotation.y = Math.PI;
      g.add(wl, wr);
      const patch = GRASS_PATCHES[Math.floor(rng() * GRASS_PATCHES.length)]!;
      this.group.add(g);
      this.butterflies.push({
        g, wl, wr,
        ax: patch.x, az: patch.z,
        p1: rng() * TAU, p2: rng() * TAU,
        px: patch.x, pz: patch.z,
      });
    }
  }

  private buildMotes(rng: () => number): void {
    const n = 60;
    this.motesPos = new Float32Array(n * 3);
    for (let i = 0; i < n; i++) {
      this.motesPos[i * 3] = (rng() - 0.5) * 36;
      this.motesPos[i * 3 + 1] = 0.3 + rng() * 3;
      this.motesPos[i * 3 + 2] = (rng() - 0.5) * 36;
    }
    const geo = new THREE.BufferGeometry();
    geo.setAttribute('position', new THREE.BufferAttribute(this.motesPos, 3));
    const mat = new THREE.PointsMaterial({
      color: '#ffe9b0',
      size: 0.07,
      transparent: true,
      opacity: 0.5,
      depthWrite: false,
    });
    this.motes = new THREE.Points(geo, mat);
    this.motes.frustumCulled = false;
    this.group.add(this.motes);
  }

  update(dt: number): void {
    this.t += dt;
    const t = this.t;
    for (const c of this.canopies) {
      c.g.rotation.z = Math.sin(t * 1.1 + c.phase) * 0.03;
      c.g.rotation.x = Math.cos(t * 0.9 + c.phase) * 0.022;
    }
    this.water.position.y = this.waterY + Math.sin(t * 1.3) * 0.03;

    for (const b of this.butterflies) {
      const x = b.ax + Math.sin(t * 0.45 + b.p1) * 2.4 + Math.sin(t * 0.21 + b.p2) * 1.4;
      const z = b.az + Math.cos(t * 0.38 + b.p2) * 2.2 + Math.cos(t * 0.27 + b.p1) * 1.2;
      const y = this.groundY(x, z) + 0.75 + Math.sin(t * 1.4 + b.p1) * 0.28;
      b.g.position.set(x, y, z);
      const flap = 0.35 + Math.abs(Math.sin(t * 14 + b.p1)) * 0.85;
      b.wl.rotation.y = flap;
      b.wr.rotation.y = Math.PI - flap;
      b.g.rotation.y = Math.atan2(x - b.px, z - b.pz);
      b.px = x;
      b.pz = z;
    }

    const pos = this.motesPos;
    for (let i = 0; i < pos.length; i += 3) {
      pos[i] = pos[i]! + Math.sin(t * 0.4 + i) * 0.12 * dt;
      pos[i + 1] = pos[i + 1]! + 0.1 * dt;
      pos[i + 2] = pos[i + 2]! + Math.cos(t * 0.33 + i * 1.7) * 0.12 * dt;
      if (pos[i + 1]! > 3.6) pos[i + 1] = 0.3;
    }
    (this.motes.geometry.attributes.position as THREE.BufferAttribute).needsUpdate = true;
  }
}
