// One geometry drawn many times — grass tufts, crates, debris, coins, a crowd of identical props —
// with a transform per instance and a handful of draw calls. Hardware instancing (Filament's
// InstanceBuffer): the engine applies each instance's local matrix, so any regular material works.
//
// Transforms live in a CPU-side Float32Array (16 floats per instance, column-major, local to this
// node). `setAt` / `hideAt` / `setMatrixAt` edit it and track the dirty range; `commit()` uploads
// that range in one bridge call — call it once per frame after your edits, not per instance.
// The native side splits the instances into chunks of the platform's per-draw cap (64 native,
// 8 on WebGL) on child entities, and keeps a bounding box per chunk so frustum culling stays on.

import { Mat4, type Mat4Like } from "../math/mat4"
import { Quat, type QuatLike } from "../math/quat"
import { cx, cy, cz, type Vec3Like } from "../math/vec"
import type { Geometry } from "./Geometry"
import { Material } from "./Material"
import type { MeshOptions } from "./Mesh"
import { Node } from "./Node"

/** One instance's placement, local to the InstancedMesh node. Omitted fields mean identity. */
export type InstanceTransform = {
  position?: Vec3Like
  /** Quaternion; wins over `eulerAngles`. */
  rotation?: QuatLike
  /** Degrees (YXZ), like `Node.eulerAngles`. */
  eulerAngles?: Vec3Like
  scale?: Vec3Like | number
}

const DEG = Math.PI / 180

export class InstancedMesh extends Node {
  /** Instance capacity, fixed at construction. Unset instances are hidden. */
  readonly count: number
  private _geometry: Geometry
  private _material: Material
  private _data: Float32Array
  private _lo = Infinity
  private _hi = -1

  constructor(geometry: Geometry, count: number, options: MeshOptions = {}) {
    super()
    if (!(count >= 1)) throw new Error("InstancedMesh: count must be ≥ 1")
    this.count = Math.floor(count)
    this._geometry = geometry
    this._material = options.material ?? Material.unlit()
    this._data = new Float32Array(16 * this.count)
    _creator.createInstancedMesh(this.id, Material.idOf(this._material), geometry.vertices, geometry.normals, geometry.indices, geometry.uv, this.count)
    if (options.position) this.position = options.position
    if (options.eulerAngles) this.eulerAngles = options.eulerAngles
    if (options.scale !== undefined) this.scale = typeof options.scale === "number" ? [ options.scale, options.scale, options.scale ] : options.scale
    if (options.name !== undefined) this.name = options.name
    if (options.castShadows !== undefined || options.receiveShadows !== undefined) {
      _creator.setInstancedMeshShadows(this.id, options.castShadows ?? true, options.receiveShadows ?? true)
    }
  }

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

  get material(): Material { return this._material }
  set material(m: Material) {
    this._material = m
    _creator.setInstancedMeshMaterial(this.id, Material.idOf(m))
  }

  /** Shadow flags for all instances (both at once — the engine sets them per draw). */
  setShadows(cast: boolean, receive = cast): this {
    _creator.setInstancedMeshShadows(this.id, cast, receive)
    return this
  }

  private _touch(i: number): void {
    if (i < this._lo) this._lo = i
    if (i > this._hi) this._hi = i
  }

  /** Place instance `i`. Pending until `commit()`. */
  setAt(i: number, t: InstanceTransform): this {
    if (i < 0 || i >= this.count) throw new RangeError(`InstancedMesh: index ${i} out of 0..${this.count - 1}`)
    const rot = t.rotation ?? (t.eulerAngles
      ? Quat.fromEuler(cx(t.eulerAngles) * DEG, cy(t.eulerAngles) * DEG, cz(t.eulerAngles) * DEG)
      : Quat.identity)
    const m = Mat4.compose(t.position ?? [ 0, 0, 0 ], rot, t.scale ?? 1).toArray()
    this._data.set(m, i * 16)
    this._touch(i)
    return this
  }

  /** Place instance `i` from a 4×4 column-major matrix (local to this node). */
  setMatrixAt(i: number, m: Mat4Like): this {
    if (i < 0 || i >= this.count) throw new RangeError(`InstancedMesh: index ${i} out of 0..${this.count - 1}`)
    const a = m instanceof Mat4 ? m.toArray() : m
    for (let k = 0; k < 16; k++) this._data[i * 16 + k] = a[k]
    this._touch(i)
    return this
  }

  /** Hide instance `i` (a zero matrix — no geometry is emitted for it). */
  hideAt(i: number): this {
    if (i < 0 || i >= this.count) throw new RangeError(`InstancedMesh: index ${i} out of 0..${this.count - 1}`)
    this._data.fill(0, i * 16, i * 16 + 16)
    this._touch(i)
    return this
  }

  /** Bulk write: `matrices` holds 16 floats per instance, starting at instance `offset`. */
  setMatrices(matrices: ArrayLike<number>, offset = 0): this {
    const n = Math.min(Math.floor(matrices.length / 16), this.count - offset)
    if (n <= 0) return this
    this._data.set(Array.prototype.slice.call(matrices, 0, n * 16), offset * 16)
    this._touch(offset)
    this._touch(offset + n - 1)
    return this
  }

  /** The 4×4 of instance `i` as stored (a copy). */
  matrixAt(i: number): Mat4 {
    return Mat4.from(this._data.subarray(i * 16, i * 16 + 16))
  }

  /** Upload every change since the last commit (one bridge call covering the dirty range). */
  commit(): this {
    if (this._hi < this._lo) return this
    const lo = this._lo, hi = this._hi
    this._lo = Infinity
    this._hi = -1
    _creator.setInstanceTransforms(this.id, this._data.subarray(lo * 16, (hi + 1) * 16), lo)
    return this
  }

  /** Whether edits are waiting for `commit()`. */
  get dirty(): boolean { return this._hi >= this._lo }
}
