// GPU particle system, as a Node. Created with a material; properties drive the native emitter.
// Ported from worker/src/components/Particles.ts — folded into the node, so (unlike the worker
// component) the system exists at construction and setters apply immediately.

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

type Range<T> = T | { min: T, max: T }

type VelocityValue = {
  speed?: Range<number>
  randomizeAngle?: { min: Vec3Like, max: Vec3Like }
} & ({ from: Vec3Like } | { to: Vec3Like } | { dir: Vec3Like } | {})

export type DynamicValue =
  | Range<number>
  | { value?: Range<number>, inc?: Float32Array, inc_b?: Float32Array, mul?: Float32Array, mul_b?: Float32Array }

export type DynamicColor =
  | Range<string>
  | { value?: Range<string>, mul: string, mul_b?: string, t: Float32Array }

type Shape = { type: "point", v: Vec3Like } | { type: "box", min: Vec3Like, max: Vec3Like }
type Noise = { strength?: number, frequency?: number, speed?: number }

export type ParticlesOptions = {
  material: Material
  rate?: Range<number>
  shape?: Shape
  startVelocity?: Vec3Like | VelocityValue
  gravity?: number
  drag?: number
  lifetime?: Range<number>
  color?: DynamicColor
  size?: DynamicValue
  rotation?: DynamicValue
  noise?: Noise | null
}

export class Particles extends Node {
  private _material: Material

  /** Custom curve params (slots 0..3); size/rotation alias slots 0/1. */
  readonly custom: DynamicValue[]

  constructor(options: ParticlesOptions) {
    super()
    this._material = options.material
    _creator.createParticleSystem(this.id, Material.idOf(options.material))

    const id = this.id
    this.custom = new Proxy([] as DynamicValue[], {
      set(target, prop: string, value: DynamicValue) {
        const i = parseInt(prop)
        if (Number.isNaN(i)) { (target as any)[prop] = value; return true }
        target[i] = value
        applyParam(id, i, value)
        return true
      },
    })

    if (options.rate !== undefined) this.rate = options.rate
    if (options.shape !== undefined) this.shape = options.shape
    if (options.startVelocity !== undefined) this.startVelocity = options.startVelocity
    if (options.gravity !== undefined) this.gravity = options.gravity
    if (options.drag !== undefined) this.drag = options.drag
    if (options.lifetime !== undefined) this.lifetime = options.lifetime
    if (options.color !== undefined) this.color = options.color
    if (options.size !== undefined) this.custom[0] = options.size
    if (options.rotation !== undefined) this.custom[1] = options.rotation
    if (options.noise) this.noise = options.noise
  }

  /** Emit `count` particles immediately (a burst). */
  spawn(count: number): this { _creator.spawnParticles(this.id, count); return this }

  set rate(val: Range<number>) {
    if (typeof val === "number") _creator.setParticleSystemRateValue(this.id, val)
  }

  set shape(shape: Shape) {
    if (shape.type === "point") _creator.setParticleSystemShapePoint(this.id, cx(shape.v), cy(shape.v), cz(shape.v))
    else _creator.setParticleSystemShapeBox(this.id, cx(shape.min), cy(shape.min), cz(shape.min), cx(shape.max), cy(shape.max), cz(shape.max))
  }

  set startVelocity(val: Vec3Like | VelocityValue) {
    if (Array.isArray(val) || val instanceof Float32Array || val instanceof Vec3) {
      _creator.setParticleSystemVelocityValue(this.id, 1, cx(val), cy(val), cz(val))
      return
    }
    const v = val as VelocityValue
    if ("from" in v) _creator.setParticleSystemVelocityValue(this.id, 2, cx(v.from), cy(v.from), cz(v.from))
    else if ("to" in v) _creator.setParticleSystemVelocityValue(this.id, 3, cx(v.to), cy(v.to), cz(v.to))
    else if ("dir" in v) _creator.setParticleSystemVelocityValue(this.id, 1, cx(v.dir), cy(v.dir), cz(v.dir))
    else _creator.setParticleSystemVelocityValue(this.id, 0, 0, 0, 0)
    if ("speed" in v && v.speed !== undefined) {
      if (typeof v.speed === "number") _creator.setParticleSystemVelocitySpeedValue(this.id, v.speed)
      else _creator.setParticleSystemVelocitySpeedRange(this.id, v.speed.min, v.speed.max)
    }
    if (v.randomizeAngle) {
      const a = v.randomizeAngle
      _creator.setParticleSystemVelocityRndAngle(this.id, cx(a.min), cy(a.min), cz(a.min), cx(a.max), cy(a.max), cz(a.max))
    }
  }

  set gravity(val: number) { _creator.setParticleSystemGravityValue(this.id, val) }
  set drag(val: number) { _creator.setParticleSystemDragValue(this.id, val) }

  set lifetime(val: Range<number>) {
    if (typeof val === "number") _creator.setParticleSystemLifetimeValue(this.id, val)
    else _creator.setParticleSystemLifetimeRange(this.id, val.min, val.max)
  }

  set size(val: DynamicValue) { this.custom[0] = val }
  set rotation(val: DynamicValue) { this.custom[1] = val }

  set color(value: DynamicColor) {
    if (typeof value === "string") _creator.setParticleSystemColorValue(this.id, value)
    else if ("min" in value) _creator.setParticleSystemColorRange(this.id, value.min, value.max)
    else {
      if (typeof value.value === "string") _creator.setParticleSystemColorValue(this.id, value.value)
      else if (value.value) _creator.setParticleSystemColorRange(this.id, value.value.min, value.value.max)
      if (value.mul) _creator.setParticleSystemColorCurveA(this.id, value.mul, value.t)
      if (value.mul_b) _creator.setParticleSystemColorCurveB(this.id, value.mul_b, value.t)
    }
  }

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

  set noise(noise: Noise | null) {
    if (noise) _creator.setParticleSystemNoise(this.id, noise.strength ?? 1, noise.frequency ?? 1, noise.speed ?? 1)
    else _creator.disableParticleSystemNoise(this.id)
  }
}

const applyParam = (id: number, i: number, value: DynamicValue): void => {
  if (typeof value === "number") {
    _creator.setParticleSystemParamValue(id, i, value)
  } else if ("min" in value) {
    _creator.setParticleSystemParamRange(id, i, value.min, value.max)
  } else {
    if (typeof value.value === "number") _creator.setParticleSystemParamValue(id, i, value.value)
    else if (value.value) _creator.setParticleSystemParamRange(id, i, value.value.min, value.value.max)
    if (value.inc) _creator.setParticleSystemParamCurveA(id, i, value.inc, 1)
    if (value.inc_b) _creator.setParticleSystemParamCurveB(id, i, value.inc_b)
    if (value.mul) _creator.setParticleSystemParamCurveA(id, i, value.mul, 0)
    if (value.mul_b) _creator.setParticleSystemParamCurveB(id, i, value.mul_b)
  }
}

// --- curve builders (unchanged from worker) ---

export const dynamicColor = (baseValue: Range<string>, ...args: (number | string | { min: string, max: string })[]): DynamicColor => {
  const t = new Float32Array(args.length / 2)
  const mul: string[] = []
  let mul_b: string[] | undefined
  for (let i = 0; i < args.length / 2; i++) {
    if (typeof args[i * 2] !== "number") throw new Error(`dynamicColor: arg ${i * 2} must be a number`)
    t[i] = args[i * 2] as number
    const val = args[i * 2 + 1]
    if (typeof val === "string") { mul[i] = val; if (mul_b) mul_b[i] = val }
    else if (typeof val === "number") throw new Error(`dynamicColor: arg ${i * 2 + 1} must be a string`)
    else { mul[i] = val.min; if (!mul_b) mul_b = [ ...mul ]; mul_b[i] = val.max }
  }
  return { value: baseValue, mul: mul.join("\0"), mul_b: mul_b?.join("\0"), t }
}

export const dynamic = (baseValue: Range<number>, type: "multiply" | "inc", ...args: (number | { min: number, max: number })[]): DynamicValue => {
  const inc = new Float32Array(args.length)
  let inc_b: Float32Array | undefined
  for (let i = 0; i < args.length; i += 2) {
    if (typeof args[i] !== "number") throw new Error(`dynamic: arg ${i} must be a number`)
    inc[i] = args[i] as number
    if (inc_b) inc_b[i] = args[i] as number
    const val = args[i + 1]
    if (typeof val === "number") { inc[i + 1] = val; if (inc_b) inc_b[i + 1] = val }
    else { inc[i + 1] = val.min; if (!inc_b) inc_b = new Float32Array(inc); inc_b[i + 1] = val.max }
  }
  return type === "inc" ? { value: baseValue, inc, inc_b } : { value: baseValue, mul: inc, mul_b: inc_b }
}
