import type { Vec3 } from "../../engine/math/Vec3"
import { STRIDE, type Mesh } from "../../engine/scene/Mesh"
import type { Material } from "../../engine/render/Material"
import type { Prefab } from "../../engine/scene/Prefab"

const TAU = Math.PI * 2

/** One procedural boulder. `radius` is the overall size; `seed` drives the
 *  per-rock lumpiness and squash so no two look alike. It sits partly sunk into
 *  the ground at `position`, like a real rock. */
export type Boulder = {
  /** Resting point on the ground (the rock is centered a bit above and buried). */
  position: Vec3
  radius: number
  seed: number
}

/**
 * Low-poly boulder geometry in the same faceted flat-shaded style as the rest of
 * the world. A squashed, per-vertex-jittered sphere reads as an angular chunk of
 * rock once flat shading gives each face its own tone. Radial jitter is kept
 * seam- and pole-safe (the longitude wrap and both poles reuse one value) so the
 * rock never cracks open. `build` appends into a caller-owned mesh, so a whole
 * field of boulders batches into a single draw call.
 */
export namespace Boulder {
  export function create(material: Material): Prefab<Boulder> {
    return {
      position: (boulder) => boulder.position,
      bakeNear: (boulder, batch) => build(boulder, batch.mesh(material)),
      bakeFar: (boulder, batch) =>
        build(boulder, batch.mesh(material), "impostor"),
      collider(boulder) {
        if (boulder.radius <= 0.7) {
          return null
        }
        return {
          shape: "box",
          minX: boulder.position.x - boulder.radius,
          maxX: boulder.position.x + boulder.radius,
          minZ: boulder.position.z - boulder.radius,
          maxZ: boulder.position.z + boulder.radius,
          top: boulder.position.y + boulder.radius * 0.7,
          standable: false,
        }
      },
    }
  }

  /** `lod` "impostor" bakes a coarser rock (fewer facets) for far chunks. */
  export function build(
    boulder: Boulder,
    mesh: Mesh,
    lod: "full" | "impostor" = "full",
  ): void {
    const rand = rng(boulder.seed)
    const seg = lod === "impostor" ? 4 : 5
    const rings = lod === "impostor" ? 2 : 4
    const r = boulder.radius
    // Squat and slightly oval, so it reads as a rock, not a ball.
    const sx = r * (0.8 + rand() * 0.5)
    const sy = r * (0.55 + rand() * 0.3)
    const sz = r * (0.8 + rand() * 0.5)
    const cx = boulder.position.x
    const cz = boulder.position.z
    // Center lifted less than the half-height, so the base sinks into the ground.
    const cy = boulder.position.y + sy * 0.55

    const jitter = jitterGrid(seg, rings, rand)
    const start = mesh.verts.length / STRIDE
    for (let ir = 0; ir <= rings; ir++) {
      const phi = (ir / rings) * Math.PI
      const cyv = Math.cos(phi)
      const crv = Math.sin(phi)
      for (let is = 0; is <= seg; is++) {
        const theta = (is / seg) * TAU
        const j = jitter[ir][is]
        mesh.verts.push(
          cx + crv * Math.cos(theta) * sx * j,
          cy + cyv * sy * j,
          cz + crv * Math.sin(theta) * sz * j,
          (is / seg) * 1.5,
          (ir / rings) * 1.5,
        )
      }
    }
    const row = seg + 1
    for (let ir = 0; ir < rings; ir++) {
      for (let is = 0; is < seg; is++) {
        const p = start + ir * row + is
        mesh.indices.push(p, p + 1, p + row + 1, p, p + row + 1, p + row)
      }
    }
  }

  /** Per-vertex radial scale in ~0.72..1.14 for a chunky, angular surface. The
   *  longitude seam (last column == first) and each pole row (one shared value)
   *  match so the mesh stays closed. */
  function jitterGrid(
    seg: number,
    rings: number,
    rand: () => number,
  ): number[][] {
    const grid: number[][] = []
    for (let ir = 0; ir <= rings; ir++) {
      const pole = ir === 0 || ir === rings
      grid[ir] = []
      for (let is = 0; is <= seg; is++) {
        if (is === seg || (pole && is > 0)) {
          grid[ir][is] = grid[ir][0]
        } else {
          grid[ir][is] = 0.72 + rand() * 0.42
        }
      }
    }
    return grid
  }

  /** Deterministic 0..1 generator (mulberry32) seeded per boulder. */
  function rng(seed: number): () => number {
    let a = seed >>> 0
    return () => {
      a = (a + 0x6D2B79F5) | 0
      let t = Math.imul(a ^ (a >>> 15), 1 | a)
      t ^= t + Math.imul(t ^ (t >>> 7), 61 | t)
      return ((t ^ (t >>> 14)) >>> 0) / 4294967296
    }
  }
}
