// 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
  /** @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
  }

  static box(): Geometry { return createCube() }
  static sphere(options?: SphereOptions): Geometry { return createSphere(options) }
  static cylinder(options?: CylinderOptions): Geometry { return createCylinder(options) }
  static plane(options?: PlaneOptions): Geometry { return createPlane(options) }
}

// --- cube ---
const createCube = (): Geometry => {
  const vertices = new Float32Array(6 * 4 * 3)
  const normals = new Float32Array(6 * 4 * 3)
  const uv = new Float32Array(6 * 4 * 3)
  const indices = new Uint16Array(6 * 6)
  let cnt = 0, indicesCnt = 0
  const sides = [ -0.5, 0.5 ]
  for (let i = 0; i < 6; i++) {
    const s = cnt
    const side = i % 2 === 0 ? -0.5 : 0.5
    for (const a of sides) {
      for (const b of sides) {
        uv.set([ a + 0.5, b + 0.5 ], cnt * 2)
        if (i === 0 || i === 1) { vertices.set([ side, b, -a ], cnt * 3); normals.set([ side * 2, 0, 0 ], cnt * 3) }
        if (i === 2 || i === 3) { vertices.set([ a, side, -b ], cnt * 3); normals.set([ 0, side * 2, 0 ], cnt * 3) }
        if (i === 4 || i === 5) { vertices.set([ a, b, side ], 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
  }
  return new Geometry(vertices, normals, indices, uv)
}

// --- 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)
  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)
        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
    vertices.set([ 0, side, 0 ], cnt * 3); normals.set([ 0, side * 2, 0 ], cnt * 3); 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)
      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++
    }
  }
  return new Geometry(vertices, normals, indices, uv)
}

// --- 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)
}
