// The terrain grid as geometry, in TypeScript — used two ways (docs/terrain-plan.md §1.3, §3.5):
//   • `heightAt` / `normalAt` — the SDK's own queries, identical on every host (bilinear on the SAME
//     triangle split the engines draw and collide);
//   • `buildTerrainChunk` — chunk meshes for hosts WITHOUT `_creator.terrainCreate` (web-lite, headless,
//     a wasm/AAR/Apple build predating the feature): the same grid → chunk → triangles rule
//     creator-gl/src/terrain.cpp applies (the i00–i11 diagonal, CCW seen from +Y, a triangle only when
//     its three samples are valid), as plain Geometry for setMesh. Normals use central differences over
//     the FULL grid (seamless chunks); the tangent frame is then the host's arbitrary one, so normal maps
//     read wrong on those hosts — the accepted fallback cost.
// Keep the hole / split rule in step with terrain.cpp buildChunk and Jolt's HeightFieldShape.

import { Geometry } from "./Geometry"

export type TerrainGrid = {
  sizeX: number
  sizeZ: number
  cellSize: number
  /** sizeX × sizeZ heights, row-major (z rows). */
  heights: Float32Array
  /** One byte per sample, 1 = hole; null = no holes. */
  holes: Uint8Array | null
}

const clampI = (v: number, hi: number): number => (v < 0 ? 0 : v > hi ? hi : v)

/** The sample at integer (x, z), clamped to the grid. */
export const sampleAt = (g: TerrainGrid, x: number, z: number): number =>
  g.heights[clampI(z, g.sizeZ - 1) * g.sizeX + clampI(x, g.sizeX - 1)]

export const isHole = (g: TerrainGrid, x: number, z: number): boolean =>
  g.holes !== null && g.holes[z * g.sizeX + x] !== 0

/** Surface normal at a sample from central differences (one-sided at the borders). Writes `out` (xyz). */
export const normalAt = (g: TerrainGrid, x: number, z: number, out: Float32Array | number[] = [ 0, 0, 0 ]): typeof out => {
  const x0 = x > 0 ? x - 1 : x, x1 = x + 1 < g.sizeX ? x + 1 : x
  const z0 = z > 0 ? z - 1 : z, z1 = z + 1 < g.sizeZ ? z + 1 : z
  const dx = (sampleAt(g, x1, z) - sampleAt(g, x0, z)) / ((x1 - x0) * g.cellSize)
  const dz = (sampleAt(g, x, z1) - sampleAt(g, x, z0)) / ((z1 - z0) * g.cellSize)
  const len = Math.hypot(dx, 1, dz)
  out[0] = -dx / len; out[1] = 1 / len; out[2] = -dz / len
  return out
}

/**
 * Height at a LOCAL (x, z) in metres, interpolated on the drawn triangles (the i00–i11 diagonal), so a
 * character's feet and a raycast agree with what is rendered to the millimetre. NaN inside a hole
 * triangle or outside the grid.
 */
export const heightAt = (g: TerrainGrid, x: number, z: number): number => {
  const fx = x / g.cellSize, fz = z / g.cellSize
  if (!(fx >= 0 && fz >= 0 && fx <= g.sizeX - 1 && fz <= g.sizeZ - 1)) return NaN
  let ix = Math.floor(fx), iz = Math.floor(fz)
  if (ix >= g.sizeX - 1) ix = g.sizeX - 2
  if (iz >= g.sizeZ - 1) iz = g.sizeZ - 2
  const u = fx - ix, v = fz - iz
  const i00 = iz * g.sizeX + ix
  const h00 = g.heights[i00], h10 = g.heights[i00 + 1], h01 = g.heights[i00 + g.sizeX], h11 = g.heights[i00 + g.sizeX + 1]
  if (g.holes !== null) {
    const holes = g.holes
    if (holes[i00] || holes[i00 + g.sizeX + 1]) return NaN
    // lower-left triangle (i00, i01, i11) when v >= u, else upper-right (i00, i11, i10)
    if (v >= u ? holes[i00 + g.sizeX] !== 0 : holes[i00 + 1] !== 0) return NaN
  }
  // plane of the triangle the point falls in
  return v >= u
    ? h00 + (h11 - h01) * u + (h01 - h00) * v
    : h00 + (h10 - h00) * u + (h11 - h10) * v
}

/** The geometric normal at a LOCAL (x, z): the drawn triangle's normal (not the smoothed vertex one). */
export const faceNormalAt = (g: TerrainGrid, x: number, z: number, out: Float32Array | number[] = [ 0, 0, 0 ]): typeof out => {
  const fx = x / g.cellSize, fz = z / g.cellSize
  let ix = Math.floor(fx), iz = Math.floor(fz)
  ix = clampI(ix, g.sizeX - 2); iz = clampI(iz, g.sizeZ - 2)
  const u = fx - ix, v = fz - iz
  const i00 = iz * g.sizeX + ix
  const h00 = g.heights[i00], h10 = g.heights[i00 + 1], h01 = g.heights[i00 + g.sizeX], h11 = g.heights[i00 + g.sizeX + 1]
  const c = g.cellSize
  // gradients of the two planes (see heightAt)
  const dx = (v >= u ? h11 - h01 : h10 - h00) / c
  const dz = (v >= u ? h01 - h00 : h11 - h10) / c
  const len = Math.hypot(dx, 1, dz)
  out[0] = -dx / len; out[1] = 1 / len; out[2] = -dz / len
  return out
}

/** Chunks along X and Z for a grid: a chunk covers `chunk` cells (chunk + 1 samples). */
export const chunkCounts = (g: TerrainGrid, chunk: number): [number, number] =>
  [ Math.floor((g.sizeX - 2) / chunk) + 1, Math.floor((g.sizeZ - 2) / chunk) + 1 ]

/**
 * One chunk as a Geometry (positions, smooth normals, UV0 = local XZ metres, UV1 = the terrain's unit
 * square, u16 indices), or null when every cell of it is a hole.
 */
export const buildTerrainChunk = (g: TerrainGrid, cx: number, cz: number, chunk: number): Geometry | null => {
  const x0 = cx * chunk, z0 = cz * chunk
  const x1 = Math.min(x0 + chunk, g.sizeX - 1), z1 = Math.min(z0 + chunk, g.sizeZ - 1)
  const nx = x1 - x0 + 1, nz = z1 - z0 + 1
  const idx: number[] = []
  for (let z = z0; z < z1; z++) {
    for (let x = x0; x < x1; x++) {
      const i00 = (z - z0) * nx + (x - x0), i10 = i00 + 1, i01 = i00 + nx, i11 = i01 + 1
      const v00 = !isHole(g, x, z), v10 = !isHole(g, x + 1, z), v01 = !isHole(g, x, z + 1), v11 = !isHole(g, x + 1, z + 1)
      if (!(v00 && v11)) continue
      if (v01) idx.push(i00, i01, i11)
      if (v10) idx.push(i00, i11, i10)
    }
  }
  if (idx.length === 0) return null
  const count = nx * nz
  const vertices = new Float32Array(count * 3)
  const normals = new Float32Array(count * 3)
  const uv = new Float32Array(count * 2)
  const uv1 = new Float32Array(count * 2)
  const n = [ 0, 0, 0 ]
  const invX = 1 / (g.sizeX - 1), invZ = 1 / (g.sizeZ - 1)
  for (let z = z0; z <= z1; z++) {
    for (let x = x0; x <= x1; x++) {
      const i = (z - z0) * nx + (x - x0)
      vertices[i * 3] = x * g.cellSize
      vertices[i * 3 + 1] = g.heights[z * g.sizeX + x]
      vertices[i * 3 + 2] = z * g.cellSize
      normalAt(g, x, z, n)
      normals[i * 3] = n[0]; normals[i * 3 + 1] = n[1]; normals[i * 3 + 2] = n[2]
      uv[i * 2] = x * g.cellSize; uv[i * 2 + 1] = z * g.cellSize
      uv1[i * 2] = x * invX; uv1[i * 2 + 1] = z * invZ
    }
  }
  const geom = new Geometry(vertices, normals, Uint16Array.from(idx), uv)
  geom.uv1 = uv1
  return geom
}

// ---- ray queries ---------------------------------------------------------------------------------

export type GridHit = { x: number, y: number, z: number, distance: number, nx: number, ny: number, nz: number }

/** Möller–Trumbore against one triangle; the distance along the ray, or -1. */
const rayTriangle = (
  ox: number, oy: number, oz: number, dx: number, dy: number, dz: number,
  ax: number, ay: number, az: number, bx: number, by: number, bz: number, qx: number, qy: number, qz: number,
): number => {
  const e1x = bx - ax, e1y = by - ay, e1z = bz - az
  const e2x = qx - ax, e2y = qy - ay, e2z = qz - az
  const px = dy * e2z - dz * e2y, py = dz * e2x - dx * e2z, pz = dx * e2y - dy * e2x
  const det = e1x * px + e1y * py + e1z * pz
  if (Math.abs(det) < 1e-12) return -1
  const inv = 1 / det
  const tx = ox - ax, ty = oy - ay, tz = oz - az
  const u = (tx * px + ty * py + tz * pz) * inv
  if (u < -1e-6 || u > 1 + 1e-6) return -1
  const qxx = ty * e1z - tz * e1y, qyy = tz * e1x - tx * e1z, qzz = tx * e1y - ty * e1x
  const v = (dx * qxx + dy * qyy + dz * qzz) * inv
  if (v < -1e-6 || u + v > 1 + 1e-6) return -1
  const t = (e2x * qxx + e2y * qyy + e2z * qzz) * inv
  return t >= 0 ? t : -1
}

/**
 * Nearest hit of a LOCAL ray (origin + direction, metres) on the drawn triangles — the same
 * i00–i11 split and hole rule as `heightAt`, so a pick lands on what is rendered. Walks the cells
 * the ray crosses (2D DDA on XZ), testing each cell's two triangles; holes let the ray through.
 * Null when it misses (or never enters the grid within `maxDistance`).
 */
export const gridRaycast = (g: TerrainGrid, ox: number, oy: number, oz: number, dx: number, dy: number, dz: number, maxDistance = 1e5): GridHit | null => {
  const c = g.cellSize, W = (g.sizeX - 1) * c, D = (g.sizeZ - 1) * c
  let t0 = 0, t1 = maxDistance
  const clip = (o: number, d: number, hi: number): boolean => {
    if (Math.abs(d) < 1e-12) return o >= 0 && o <= hi
    let a = -o / d, b = (hi - o) / d
    if (a > b) { const s = a; a = b; b = s }
    if (a > t0) t0 = a
    if (b < t1) t1 = b
    return t0 <= t1
  }
  if (!clip(ox, dx, W) || !clip(oz, dz, D)) return null
  const sx = ox + dx * t0, sz = oz + dz * t0
  let ix = Math.min(g.sizeX - 2, Math.max(0, Math.floor(sx / c)))
  let iz = Math.min(g.sizeZ - 2, Math.max(0, Math.floor(sz / c)))
  const stepX = dx > 1e-12 ? 1 : dx < -1e-12 ? -1 : 0
  const stepZ = dz > 1e-12 ? 1 : dz < -1e-12 ? -1 : 0
  const tDeltaX = stepX ? Math.abs(c / dx) : Infinity, tDeltaZ = stepZ ? Math.abs(c / dz) : Infinity
  let tMaxX = stepX > 0 ? t0 + ((ix + 1) * c - sx) / dx : stepX < 0 ? t0 + (ix * c - sx) / dx : Infinity
  let tMaxZ = stepZ > 0 ? t0 + ((iz + 1) * c - sz) / dz : stepZ < 0 ? t0 + (iz * c - sz) / dz : Infinity
  const holes = g.holes
  let t = t0
  for (let guard = 0; guard < g.sizeX + g.sizeZ + 2 && t <= t1 + 1e-9; guard++) {
    const i00 = iz * g.sizeX + ix, i10 = i00 + 1, i01 = i00 + g.sizeX, i11 = i01 + 1
    const x0 = ix * c, x1 = x0 + c, z0 = iz * c, z1 = z0 + c
    const h00 = g.heights[i00], h10 = g.heights[i10], h01 = g.heights[i01], h11 = g.heights[i11]
    const ok00 = !holes || holes[i00] === 0, ok11 = !holes || holes[i11] === 0
    let best = -1, tri = 0
    if (ok00 && ok11) {
      // lower-left (i00, i01, i11) and upper-right (i00, i11, i10) — both CCW seen from +Y
      if (!holes || holes[i01] === 0) {
        const d = rayTriangle(ox, oy, oz, dx, dy, dz, x0, h00, z0, x0, h01, z1, x1, h11, z1)
        if (d >= 0 && (best < 0 || d < best)) { best = d; tri = 0 }
      }
      if (!holes || holes[i10] === 0) {
        const d = rayTriangle(ox, oy, oz, dx, dy, dz, x0, h00, z0, x1, h11, z1, x1, h10, z0)
        if (d >= 0 && (best < 0 || d < best)) { best = d; tri = 1 }
      }
    }
    if (best >= 0 && best <= t1 + 1e-6) {
      // the hit triangle's normal
      let ax: number, ay: number, az: number, bx: number, by: number, bz: number
      if (tri === 0) { ax = 0; ay = h01 - h00; az = c; bx = c; by = h11 - h00; bz = c }
      else { ax = c; ay = h11 - h00; az = c; bx = c; by = h10 - h00; bz = 0 }
      let nx = ay * bz - az * by, ny = az * bx - ax * bz, nz = ax * by - ay * bx
      const l = Math.hypot(nx, ny, nz) || 1
      nx /= l; ny /= l; nz /= l
      return { x: ox + dx * best, y: oy + dy * best, z: oz + dz * best, distance: best, nx, ny, nz }
    }
    if (tMaxX < tMaxZ) { t = tMaxX; ix += stepX; tMaxX += tDeltaX }
    else { t = tMaxZ; iz += stepZ; tMaxZ += tDeltaZ }
    if (ix < 0 || iz < 0 || ix > g.sizeX - 2 || iz > g.sizeZ - 2) break
  }
  return null
}
