// Mesh geometry: vertex / normal / index / uv buffers, plus the primitive builders. `Geometry.box()`
// etc. return a Geometry you can hand to Mesh.from(); Mesh.box() is sugar over them. Ported from
// worker/src/primitives/*.

import { cx, cy, cz, type Vec3Like } from "../math/vec"

export type MeshKind = "triangles" | "vertices" | "edges"

// local tuple helpers — geometry building works on raw Float32Array buffers, so it stays out of the
// Vec3 wrapper (no per-vertex allocation).
type Tup3 = [number, number, number]
const cross = (a: Tup3, b: Tup3): Tup3 => [ a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0] ]
const normalize = (a: Tup3): Tup3 => {
  const l = Math.hypot(a[0], a[1], a[2])
  return l === 0 ? [ 0, 0, 0 ] : [ a[0] / l, a[1] / l, a[2] / l ]
}

export class Geometry {
  vertices: Float32Array
  normals: Float32Array
  indices: Uint16Array
  uv: Float32Array
  /** Lightmap UV set (2 floats per vertex, non-overlapping, inside the unit square) — the
   *  `Lightmap` atlas samples a Mesh through it. The box and cylinder builders fill it (their `uv`
   *  tiles per face, which would fold every face onto the same texels); absent = the host reuses
   *  `uv`, which is right for a plane. */
  uv1?: Float32Array
  /** @internal native mesh-type enum (0 triangles / 1 edges / 2 vertices). */
  _meshKind = 0

  constructor(vertices: Float32Array, normals: Float32Array, indices: Uint16Array, uv: Float32Array) {
    this.vertices = vertices
    this.normals = normals
    this.indices = indices
    this.uv = uv
  }

  translate(x: number, y: number, z: number): this {
    for (let i = 0; i < this.vertices.length; i += 3) {
      this.vertices[i] += x; this.vertices[i + 1] += y; this.vertices[i + 2] += z
    }
    return this
  }
  scale(x: number, y: number, z: number): this {
    for (let i = 0; i < this.vertices.length; i += 3) {
      this.vertices[i] *= x; this.vertices[i + 1] *= y; this.vertices[i + 2] *= z
    }
    return this
  }
  scaleUV(x: number, y: number): this {
    for (let i = 0; i < this.uv.length; i += 2) { this.uv[i] *= x; this.uv[i + 1] *= y }
    return this
  }
  set kind(kind: MeshKind) {
    this._meshKind = kind === "edges" ? 1 : kind === "vertices" ? 2 : 0
  }

  /** A box; `size` (default 1) scales the vertices AND lays the lightmap chart out in proportion to
   *  the faces' areas (a floor's top face gets the texels, not its 1 m side strips). */
  static box(size: Vec3Like | number = 1): Geometry { return createCube(size) }
  static sphere(options?: SphereOptions): Geometry { return createSphere(options) }
  static cylinder(options?: CylinderOptions): Geometry { return createCylinder(options) }
  static plane(options?: PlaneOptions): Geometry { return createPlane(options) }
}

// Gutter between lightmap charts (fraction of the chart square): the bake dilates each rect by a
// few texels and the shader samples bilinearly, so neighbouring faces must not touch in uv1.
const LM_GUTTER = 0.03

type Rect = { x: number, y: number, w: number, h: number }

/** Shelf-pack rectangles (metres) into the unit square at the largest uniform scale — every face
 *  gets texels in proportion to its area, so a 64×1×64 floor's top face isn't squeezed into the
 *  same cell as its 1 m side strips. Returns rects in input order. */
const packCharts = (dims: [number, number][]): Rect[] => {
  const order = dims.map((_, i) => i).sort((a, b) => dims[b][1] - dims[a][1])
  const attempt = (scale: number): Rect[] | null => {
    const out: Rect[] = new Array(dims.length)
    let x = 0, y = 0, rowH = 0
    for (const i of order) {
      const w = dims[i][0] * scale, h = dims[i][1] * scale
      if (x > 0 && x + w > 1) { y += rowH + LM_GUTTER; x = 0; rowH = 0 }
      if (w > 1 || y + h > 1) return null
      out[i] = { x, y, w, h }
      x += w + LM_GUTTER
      rowH = Math.max(rowH, h)
    }
    return out
  }
  let lo = 0, hi = 1 / Math.max(...dims.map((d) => Math.max(d[0], d[1])))
  let best = attempt(hi)
  if (!best) {
    for (let it = 0; it < 40; it++) {
      const mid = (lo + hi) / 2
      const r = attempt(mid)
      if (r) { best = r; lo = mid } else hi = mid
    }
  }
  return best ?? dims.map(() => ({ x: 0, y: 0, w: 1, h: 1 }))
}

// --- cube ---
const createCube = (size: Vec3Like | number = 1): Geometry => {
  const sx = typeof size === "number" ? size : cx(size)
  const sy = typeof size === "number" ? size : cy(size)
  const sz = typeof size === "number" ? size : cz(size)
  const vertices = new Float32Array(6 * 4 * 3)
  const normals = new Float32Array(6 * 4 * 3)
  const uv = new Float32Array(6 * 4 * 2)
  const uv1 = new Float32Array(6 * 4 * 2)
  const indices = new Uint16Array(6 * 6)
  let cnt = 0, indicesCnt = 0
  const sides = [ -0.5, 0.5 ]
  // face extents along the (a, b) parameters below: ±x faces span (z, y), ±y faces (x, z), ±z faces (x, y)
  const charts = packCharts([ [ sz, sy ], [ sz, sy ], [ sx, sz ], [ sx, sz ], [ sx, sy ], [ sx, sy ] ])
  for (let i = 0; i < 6; i++) {
    const s = cnt
    const side = i % 2 === 0 ? -0.5 : 0.5
    const c = charts[i]
    for (const a of sides) {
      for (const b of sides) {
        uv.set([ a + 0.5, b + 0.5 ], cnt * 2)
        uv1.set([ c.x + (a + 0.5) * c.w, c.y + (b + 0.5) * c.h ], cnt * 2)
        if (i === 0 || i === 1) { vertices.set([ side * sx, b * sy, -a * sz ], cnt * 3); normals.set([ side * 2, 0, 0 ], cnt * 3) }
        if (i === 2 || i === 3) { vertices.set([ a * sx, side * sy, -b * sz ], cnt * 3); normals.set([ 0, side * 2, 0 ], cnt * 3) }
        if (i === 4 || i === 5) { vertices.set([ a * sx, b * sy, side * sz ], cnt * 3); normals.set([ 0, 0, side * 2 ], cnt * 3) }
        cnt++
      }
    }
    const s1 = s + 1, s2 = s + 2, s3 = s + 3
    if (i % 2 === 0) indices.set([ s, s1, s2, s3, s2, s1 ], indicesCnt)
    else indices.set([ s2, s1, s, s1, s2, s3 ], indicesCnt)
    indicesCnt += 6
  }
  const g = new Geometry(vertices, normals, indices, uv)
  g.uv1 = uv1
  return g
}

// --- sphere ---
export type SphereOptions = { radius?: number, widthSegments?: number, heightSegments?: number }
const createSphere = (options: SphereOptions = {}): Geometry => {
  const { radius = 0.5, widthSegments = 32, heightSegments = 32 } = options
  const verticesCount = (widthSegments + 1) * (heightSegments - 1) + widthSegments * 2
  const vertices = new Float32Array(verticesCount * 3)
  const normals = new Float32Array(verticesCount * 3)
  const uvs = new Float32Array(verticesCount * 2)
  const indices = new Uint16Array(widthSegments * (heightSegments - 2) * 6 + widthSegments * 3 * 2)
  const phiLength = Math.PI * 2, thetaLength = Math.PI
  let vertexCnt = 0, indicesCnt = 0
  for (let iy = 1; iy < heightSegments; iy++) {
    const v = iy / heightSegments
    for (let ix = 0; ix <= widthSegments; ix++) {
      const u = ix / widthSegments
      const point: [number, number, number] = [
        -Math.cos(u * phiLength) * Math.sin(v * thetaLength),
        Math.cos(v * thetaLength),
        Math.sin(u * phiLength) * Math.sin(v * thetaLength),
      ]
      vertices.set([ point[0] * radius, point[1] * radius, point[2] * radius ], vertexCnt * 3)
      normals.set(point, vertexCnt * 3)
      uvs.set([ u, 1 - v ], vertexCnt * 2)
      if (iy > 1 && ix > 0) {
        const s = vertexCnt - 1
        const s1 = vertexCnt - (widthSegments + 1) - 1
        indices.set([ s + 1, s1, s, s1, s + 1, s1 + 1 ], indicesCnt)
        indicesCnt += 6
      }
      vertexCnt++
    }
  }
  const sides = [ -radius, radius ]
  const circleStart2 = vertexCnt - (widthSegments + 1)
  for (const side of sides) {
    const circleStart = side > 0 ? 0 : circleStart2
    for (let ix = 0; ix < widthSegments; ix++) {
      const u = (ix + 0.5) / widthSegments
      vertices.set([ 0, side, 0 ], vertexCnt * 3)
      normals.set([ 0, side < 0 ? -1 : 1, 0 ], vertexCnt * 3)
      uvs.set([ u, side < 0 ? 0 : 1 ], vertexCnt * 2)
      const s = circleStart + ix
      if (side > 0) indices.set([ s, s + 1, vertexCnt ], indicesCnt)
      else indices.set([ vertexCnt, s + 1, s ], indicesCnt)
      indicesCnt += 3
      vertexCnt++
    }
  }
  return new Geometry(vertices, normals, indices, uvs)
}

// --- cylinder ---
export type CylinderOptions = { radius?: number, radiusTop?: number, radiusBottom?: number, edges?: number, smooth?: boolean }
const createCylinder = (options: CylinderOptions = {}): Geometry => {
  const { edges = 32, smooth = true, radius = 0.5, radiusTop = radius, radiusBottom = radius } = options
  const verticesCount = edges * 4 + (edges + 1) * 2
  const vertices = new Float32Array(verticesCount * 3)
  const normals = new Float32Array(verticesCount * 3)
  const uv = new Float32Array(verticesCount * 2)
  // lightmap chart: the side strip across the upper half, the two caps as discs in the lower half
  const uv1 = new Float32Array(verticesCount * 2)
  const indices = new Uint16Array(edges * 6 + edges * 6)
  const sides = [ -0.5, 0.5 ]
  let cnt = 0, indicesCnt = 0
  for (let i = 0; i < edges; i++) {
    const s = cnt
    const normalX = Math.cos((i + 0.5) / edges * Math.PI * 2)
    const normalY = Math.sin((i + 0.5) / edges * Math.PI * 2)
    for (let j = 0; j < 2; j++) {
      const x = Math.cos((i + j) / edges * Math.PI * 2)
      const y = Math.sin((i + j) / edges * Math.PI * 2)
      for (const side of sides) {
        if (side > 0) vertices.set([ x * radiusTop, side, y * radiusTop ], cnt * 3)
        else vertices.set([ x * radiusBottom, side, y * radiusBottom ], cnt * 3)
        if (smooth) normals.set([ x, 0, y ], cnt * 3)
        else normals.set([ normalX, 0, normalY ], cnt * 3)
        uv.set([ (i + j) / edges, side + 0.5 ], cnt * 2)
        uv1.set([ (i + j) / edges, 0.55 + (side + 0.5) * 0.43 ], cnt * 2)
        cnt++
      }
    }
    const s1 = s + 1, s2 = (s + 2) % (edges * 4), s3 = (s + 3) % (edges * 4)
    indices.set([ s, s1, s2, s3, s2, s1 ], indicesCnt)
    indicesCnt += 6
  }
  for (const side of sides) {
    const center = cnt
    const capU = side > 0 ? 0.75 : 0.25   // lightmap disc centre per cap
    vertices.set([ 0, side, 0 ], cnt * 3); normals.set([ 0, side * 2, 0 ], cnt * 3)
    uv1.set([ capU, 0.25 ], cnt * 2); cnt++
    for (let i = 0; i < edges; i++) {
      const x = Math.cos(i / edges * Math.PI * 2), y = Math.sin(i / edges * Math.PI * 2)
      if (side > 0) vertices.set([ x * radiusTop, side, y * radiusTop ], cnt * 3)
      else vertices.set([ x * radiusBottom, side, y * radiusBottom ], cnt * 3)
      normals.set([ 0, side * 2, 0 ], cnt * 3)
      uv1.set([ capU + x * 0.22, 0.25 + y * 0.22 ], cnt * 2)
      if (side < 0) indices.set([ center, center + i + 1, center + 1 + ((i + 1) % edges) ], indicesCnt)
      else indices.set([ center + 1 + ((i + 1) % edges), center + i + 1, center ], indicesCnt)
      indicesCnt += 3
      cnt++
    }
  }
  const g = new Geometry(vertices, normals, indices, uv)
  g.uv1 = uv1
  return g
}

// --- plane ---
export type PlaneOptions = { normal?: Vec3Like }
const createPlane = (options: PlaneOptions = {}): Geometry => {
  const normal: Tup3 = options.normal ? [ cx(options.normal), cy(options.normal), cz(options.normal) ] : [ 0, 0, 1 ]
  const vertices = new Float32Array(4 * 3)
  const normals = new Float32Array(4 * 3)
  const uvs = new Float32Array(4 * 2)
  const sides = [ -0.5, 0.5 ]
  let cnt = 0
  let upVector: Tup3, rightVec: Tup3
  if (normal[1] === 0 && normal[2] === 0) { upVector = [ 0, 1, 0 ]; rightVec = [ 0, 0, -normal[0] ] }
  else if (normal[0] === 0 && normal[2] === 0) { upVector = [ 0, 0, 1 ]; rightVec = [ -normal[1], 0, 0 ] }
  else if (normal[0] === 0 && normal[1] === 0) { upVector = [ 0, 1, 0 ]; rightVec = [ normal[2], 0, 0 ] } // тут было верно
  else {
    const temp: Tup3 = Math.abs(normal[0]) < 0.5 ? [ 1, 0, 0 ] : [ 0, 0, 1 ]
    upVector = normalize(cross(temp, normal))
    rightVec = normalize(cross(upVector, normal))
  }
  for (const a of sides) {
    for (const b of sides) {
      normals.set(normal, cnt * 3)
      uvs.set([ a + 0.5, b + 0.5 ], cnt * 2)
      vertices.set([
        rightVec[0] * a + upVector[0] * b,
        rightVec[1] * a + upVector[1] * b,
        rightVec[2] * a + upVector[2] * b,
      ], cnt * 3)
      cnt++
    }
  }
  const indices = new Uint16Array([ 2, 1, 0, 1, 2, 3 ])
  return new Geometry(vertices, normals, indices, uvs)
}
