// A renderable 3D node from raw geometry — the primitive shapes (box/sphere/cylinder/plane) and
// Mesh.from for a custom Geometry. A GLB import is a different kind: see Model.load.

import { cx, cy, cz, type Vec3Like } from "../math/vec"
import {
  Geometry,
  type CylinderOptions,
  type PlaneOptions,
  type SphereOptions,
} from "./Geometry"
import { Material } from "./Material"
import { Node } from "./Node"

/** Common transform/render options every primitive factory accepts. */
export type MeshOptions = {
  material?: Material
  position?: Vec3Like
  eulerAngles?: Vec3Like
  scale?: Vec3Like | number
  name?: string
  castShadows?: boolean
  receiveShadows?: boolean
}

export class Mesh extends Node {
  private _geometry?: Geometry
  private _materials: Material[] = []

  constructor(geometry?: Geometry, material?: Material) {
    super()
    if (geometry) {
      const mat = material ?? Material.unlit()
      this._geometry = geometry
      this._materials = [ mat ]
      _creator.setMesh(this.id, Material.idOf(mat), geometry.vertices, geometry.normals, geometry.indices, geometry.uv, geometry._meshKind)
    }
  }

  override get geometry(): Geometry | undefined { return this._geometry }

  get material(): Material { return this._materials[0] }
  set material(m: Material) { this.setMaterial(m, 0) }

  setMaterial(material: Material, index = 0): this {
    this._materials[index] = material
    _creator.setMaterial(this.id, Material.idOf(material), index)
    return this
  }
  getMaterial(index = 0): Material | null {
    const existing = this._materials[index]
    if (existing) return existing
    const id = _creator.getMaterial(this.id, index)
    if (id === 0) return null
    const mat = new Material(id)
    this._materials[index] = mat
    return mat
  }

  set culling(v: boolean) { _creator.setCulling(this.id, v) }
  set castShadows(v: boolean) { _creator.setCastShadows(this.id, v) }
  set receiveShadows(v: boolean) { _creator.setReceiveShadows(this.id, v) }

  // --- factories ---

  static box(options: MeshOptions & { size?: Vec3Like | number } = {}): Mesh {
    const geo = Geometry.box()
    const [ sx, sy, sz ] = sizeToVec(options.size ?? 1)
    if (sx !== 1 || sy !== 1 || sz !== 1) geo.scale(sx, sy, sz)
    return apply(new Mesh(geo, options.material), options)
  }

  static sphere(options: MeshOptions & SphereOptions = {}): Mesh {
    return apply(new Mesh(Geometry.sphere(options), options.material), options)
  }

  static cylinder(options: MeshOptions & CylinderOptions = {}): Mesh {
    return apply(new Mesh(Geometry.cylinder(options), options.material), options)
  }

  static plane(options: MeshOptions & PlaneOptions = {}): Mesh {
    return apply(new Mesh(Geometry.plane(options), options.material), options)
  }

  /** Wrap a custom Geometry. */
  static from(geometry: Geometry, options: MeshOptions = {}): Mesh {
    return apply(new Mesh(geometry, options.material), options)
  }
}

const sizeToVec = (s: Vec3Like | number): [number, number, number] => (typeof s === "number" ? [ s, s, s ] : [ cx(s), cy(s), cz(s) ])

const apply = (mesh: Mesh, o: MeshOptions): Mesh => {
  if (o.position) mesh.position = o.position
  if (o.eulerAngles) mesh.eulerAngles = o.eulerAngles
  if (o.scale !== undefined) mesh.scale = typeof o.scale === "number" ? [ o.scale, o.scale, o.scale ] : o.scale
  if (o.name !== undefined) mesh.name = o.name
  if (o.castShadows !== undefined) mesh.castShadows = o.castShadows
  if (o.receiveShadows !== undefined) mesh.receiveShadows = o.receiveShadows
  return mesh
}
