// Fluent 2D / 3D vectors — the SDK's user-facing vector math (no gl-matrix, no out-params, no imports).
//
// Design (see PLAN.md): a vector is a *mutable little struct* — `v.x = 3`, `v.set(x, y, z)` mutate in
// place — but every *method* is PURE: `a.add(b)` returns a NEW vector and never touches `a`. So chains
// (`dir.normalize().scale(s)`) can't clobber their source, and the immutable-method style is the most
// amenable to a future escape-analysis / scalar-replacement compiler pass. Backing is plain number
// fields (not a Float32Array), which the optimizer can trivially scalar-replace.
//
// "Never wrap away the metal": vectors expose `.x/.y/.z`, are iterable (`const [x, y] = v`, `[...v]`),
// and every method / constructor accepts a raw tuple just as well as a wrapper. Reads off a `*Like`
// input go through the `cx/cy/cz` helpers below, which work uniformly on a wrapper (`.x`) or an
// array-like (`[0]`), so callers can always pass `[0, 1, 0]` where a vector is expected.

export type Vec2Like = Vec2 | readonly [number, number] | number[] | Float32Array
export type Vec3Like = Vec3 | readonly [number, number, number] | number[] | Float32Array

/** Read the x / first component of any vector-like (wrapper, tuple, or typed array). `?? ` is safe on
 *  0 and NaN — it only falls through on `undefined` (i.e. a non-wrapper that has no `.x`). */
export const cx = (v: { x?: number } | ArrayLike<number>): number => (v as { x?: number }).x ?? (v as ArrayLike<number>)[0]
export const cy = (v: { y?: number } | ArrayLike<number>): number => (v as { y?: number }).y ?? (v as ArrayLike<number>)[1]
export const cz = (v: { z?: number } | ArrayLike<number>): number => (v as { z?: number }).z ?? (v as ArrayLike<number>)[2]

/** Relative-epsilon compare (matches gl-matrix's `equals`), so the oracle tests agree to the bit. */
const approx = (a: number, b: number, eps: number): boolean => Math.abs(a - b) <= eps * Math.max(1, Math.abs(a), Math.abs(b))

// ---------------------------------------------------------------------------------------------------
// Vec2
// ---------------------------------------------------------------------------------------------------

export class Vec2 {
  x: number
  y: number

  constructor(x: number | Vec2Like = 0, y = 0) {
    if (typeof x === "number") { this.x = x; this.y = y }
    else { this.x = cx(x); this.y = cy(x) }
  }

  // --- mutation (the only methods that change `this`) ---
  /** Mutate the components in place. Returns `this`. */
  set(x: number, y: number): this { this.x = x; this.y = y; return this }
  /** Copy another vector's components into this one. Returns `this`. */
  copy(v: Vec2Like): this { this.x = cx(v); this.y = cy(v); return this }

  // --- pure arithmetic (each returns a NEW Vec2) ---
  add(v: Vec2Like): Vec2 { return new Vec2(this.x + cx(v), this.y + cy(v)) }
  sub(v: Vec2Like): Vec2 { return new Vec2(this.x - cx(v), this.y - cy(v)) }
  mul(v: Vec2Like): Vec2 { return new Vec2(this.x * cx(v), this.y * cy(v)) }
  div(v: Vec2Like): Vec2 { return new Vec2(this.x / cx(v), this.y / cy(v)) }
  scale(s: number): Vec2 { return new Vec2(this.x * s, this.y * s) }
  /** `this + v * s` (fused multiply-add). */
  scaleAndAdd(v: Vec2Like, s: number): Vec2 { return new Vec2(this.x + cx(v) * s, this.y + cy(v) * s) }
  negate(): Vec2 { return new Vec2(-this.x, -this.y) }

  // --- products / measures ---
  dot(v: Vec2Like): number { return this.x * cx(v) + this.y * cy(v) }
  /** 2D cross product → the scalar z of the 3D cross (signed parallelogram area). */
  cross(v: Vec2Like): number { return this.x * cy(v) - this.y * cx(v) }
  length(): number { return Math.hypot(this.x, this.y) }
  lengthSq(): number { return this.x * this.x + this.y * this.y }
  distanceTo(v: Vec2Like): number { return Math.hypot(this.x - cx(v), this.y - cy(v)) }
  distanceSqTo(v: Vec2Like): number { const dx = this.x - cx(v), dy = this.y - cy(v); return dx * dx + dy * dy }

  normalize(): Vec2 {
    const l = Math.hypot(this.x, this.y)
    return l === 0 ? new Vec2(0, 0) : new Vec2(this.x / l, this.y / l)
  }
  lerp(v: Vec2Like, t: number): Vec2 { return new Vec2(this.x + (cx(v) - this.x) * t, this.y + (cy(v) - this.y) * t) }
  clamp(min: Vec2Like, max: Vec2Like): Vec2 {
    return new Vec2(clampN(this.x, cx(min), cx(max)), clampN(this.y, cy(min), cy(max)))
  }
  min(v: Vec2Like): Vec2 { return new Vec2(Math.min(this.x, cx(v)), Math.min(this.y, cy(v))) }
  max(v: Vec2Like): Vec2 { return new Vec2(Math.max(this.x, cx(v)), Math.max(this.y, cy(v))) }
  /** Left-hand perpendicular: (-y, x), a 90° CCW turn. */
  perp(): Vec2 { return new Vec2(-this.y, this.x) }
  /** Rotate by `rad` (CCW) around `origin` (default 0). */
  rotate(rad: number, origin: Vec2Like = ZERO2): Vec2 {
    const ox = cx(origin), oy = cy(origin)
    const c = Math.cos(rad), s = Math.sin(rad)
    const dx = this.x - ox, dy = this.y - oy
    return new Vec2(ox + dx * c - dy * s, oy + dx * s + dy * c)
  }
  /** The vector's heading in radians: `atan2(y, x)`. */
  heading(): number { return Math.atan2(this.y, this.x) }
  /** Angle (radians) between this and `v`. */
  angle(v: Vec2Like): number {
    const mag = Math.sqrt(this.lengthSq() * (cx(v) * cx(v) + cy(v) * cy(v)))
    return mag === 0 ? 0 : Math.acos(clampN(this.dot(v) / mag, -1, 1))
  }

  withX(x: number): Vec2 { return new Vec2(x, this.y) }
  withY(y: number): Vec2 { return new Vec2(this.x, y) }

  // --- interop ---
  equals(v: Vec2Like, eps = 1e-6): boolean { return approx(this.x, cx(v), eps) && approx(this.y, cy(v), eps) }
  clone(): Vec2 { return new Vec2(this.x, this.y) }
  toArray(): [number, number] { return [ this.x, this.y ] }
  *[Symbol.iterator](): Iterator<number> { yield this.x; yield this.y }

  static from(v: Vec2Like): Vec2 { return new Vec2(cx(v), cy(v)) }
  static get zero(): Vec2 { return new Vec2(0, 0) }
  static get one(): Vec2 { return new Vec2(1, 1) }
  static get up(): Vec2 { return new Vec2(0, 1) }
  static get down(): Vec2 { return new Vec2(0, -1) }
  static get left(): Vec2 { return new Vec2(-1, 0) }
  static get right(): Vec2 { return new Vec2(1, 0) }
}

// ---------------------------------------------------------------------------------------------------
// Vec3
// ---------------------------------------------------------------------------------------------------

export class Vec3 {
  x: number
  y: number
  z: number

  constructor(x: number | Vec3Like = 0, y = 0, z = 0) {
    if (typeof x === "number") { this.x = x; this.y = y; this.z = z }
    else { this.x = cx(x); this.y = cy(x); this.z = cz(x) }
  }

  // --- mutation ---
  set(x: number, y: number, z: number): this { this.x = x; this.y = y; this.z = z; return this }
  copy(v: Vec3Like): this { this.x = cx(v); this.y = cy(v); this.z = cz(v); return this }

  // --- pure arithmetic ---
  add(v: Vec3Like): Vec3 { return new Vec3(this.x + cx(v), this.y + cy(v), this.z + cz(v)) }
  sub(v: Vec3Like): Vec3 { return new Vec3(this.x - cx(v), this.y - cy(v), this.z - cz(v)) }
  mul(v: Vec3Like): Vec3 { return new Vec3(this.x * cx(v), this.y * cy(v), this.z * cz(v)) }
  div(v: Vec3Like): Vec3 { return new Vec3(this.x / cx(v), this.y / cy(v), this.z / cz(v)) }
  scale(s: number): Vec3 { return new Vec3(this.x * s, this.y * s, this.z * s) }
  /** `this + v * s` (fused multiply-add). */
  scaleAndAdd(v: Vec3Like, s: number): Vec3 { return new Vec3(this.x + cx(v) * s, this.y + cy(v) * s, this.z + cz(v) * s) }
  negate(): Vec3 { return new Vec3(-this.x, -this.y, -this.z) }

  // --- products / measures ---
  dot(v: Vec3Like): number { return this.x * cx(v) + this.y * cy(v) + this.z * cz(v) }
  cross(v: Vec3Like): Vec3 {
    const bx = cx(v), by = cy(v), bz = cz(v)
    return new Vec3(this.y * bz - this.z * by, this.z * bx - this.x * bz, this.x * by - this.y * bx)
  }
  length(): number { return Math.hypot(this.x, this.y, this.z) }
  lengthSq(): number { return this.x * this.x + this.y * this.y + this.z * this.z }
  distanceTo(v: Vec3Like): number { return Math.hypot(this.x - cx(v), this.y - cy(v), this.z - cz(v)) }
  distanceSqTo(v: Vec3Like): number {
    const dx = this.x - cx(v), dy = this.y - cy(v), dz = this.z - cz(v)
    return dx * dx + dy * dy + dz * dz
  }

  normalize(): Vec3 {
    const l = Math.hypot(this.x, this.y, this.z)
    return l === 0 ? new Vec3(0, 0, 0) : new Vec3(this.x / l, this.y / l, this.z / l)
  }
  lerp(v: Vec3Like, t: number): Vec3 {
    return new Vec3(this.x + (cx(v) - this.x) * t, this.y + (cy(v) - this.y) * t, this.z + (cz(v) - this.z) * t)
  }
  clamp(min: Vec3Like, max: Vec3Like): Vec3 {
    return new Vec3(clampN(this.x, cx(min), cx(max)), clampN(this.y, cy(min), cy(max)), clampN(this.z, cz(min), cz(max)))
  }
  min(v: Vec3Like): Vec3 { return new Vec3(Math.min(this.x, cx(v)), Math.min(this.y, cy(v)), Math.min(this.z, cz(v))) }
  max(v: Vec3Like): Vec3 { return new Vec3(Math.max(this.x, cx(v)), Math.max(this.y, cy(v)), Math.max(this.z, cz(v))) }
  /** Reflect this vector about a (unit) `normal`: `v - 2·(v·n)·n`. */
  reflect(normal: Vec3Like): Vec3 {
    const d = 2 * this.dot(normal)
    return new Vec3(this.x - d * cx(normal), this.y - d * cy(normal), this.z - d * cz(normal))
  }
  /** Vector projection of this onto `onto`. */
  project(onto: Vec3Like): Vec3 {
    const ox = cx(onto), oy = cy(onto), oz = cz(onto)
    const denom = ox * ox + oy * oy + oz * oz
    if (denom === 0) return new Vec3(0, 0, 0)
    const k = this.dot(onto) / denom
    return new Vec3(ox * k, oy * k, oz * k)
  }
  /** Angle (radians) between this and `v`. */
  angle(v: Vec3Like): number {
    const bx = cx(v), by = cy(v), bz = cz(v)
    const mag = Math.sqrt(this.lengthSq() * (bx * bx + by * by + bz * bz))
    return mag === 0 ? 0 : Math.acos(clampN(this.dot(v) / mag, -1, 1))
  }

  // --- rotation ---
  /** Rotate this point around the X axis through `origin` (default 0) by `rad`. */
  rotateX(rad: number, origin: Vec3Like = ZERO3): Vec3 {
    const ox = cx(origin), oy = cy(origin), oz = cz(origin)
    const py = this.y - oy, pz = this.z - oz, c = Math.cos(rad), s = Math.sin(rad)
    return new Vec3(this.x, oy + py * c - pz * s, oz + py * s + pz * c)
  }
  rotateY(rad: number, origin: Vec3Like = ZERO3): Vec3 {
    const ox = cx(origin), oy = cy(origin), oz = cz(origin)
    const px = this.x - ox, pz = this.z - oz, c = Math.cos(rad), s = Math.sin(rad)
    return new Vec3(ox + pz * s + px * c, this.y, oz + pz * c - px * s)
  }
  rotateZ(rad: number, origin: Vec3Like = ZERO3): Vec3 {
    const ox = cx(origin), oy = cy(origin), oz = cz(origin)
    const px = this.x - ox, py = this.y - oy, c = Math.cos(rad), s = Math.sin(rad)
    return new Vec3(ox + px * c - py * s, oy + px * s + py * c, this.z)
  }
  /** Rotate this vector by a quaternion (x, y, z, w). Accepts a `Quat` or a raw `[x,y,z,w]`. */
  rotate(q: ArrayLike<number> | { x: number, y: number, z: number, w: number }): Vec3 {
    const qx = cx(q as Vec3Like), qy = cy(q as Vec3Like), qz = cz(q as Vec3Like)
    const qw = (q as { w?: number }).w ?? (q as ArrayLike<number>)[3]
    const vx = this.x, vy = this.y, vz = this.z
    // t = 2 * (q.xyz × v)
    let tx = qy * vz - qz * vy, ty = qz * vx - qx * vz, tz = qx * vy - qy * vx
    tx += tx; ty += ty; tz += tz
    // v + w·t + q.xyz × t
    return new Vec3(
      vx + qw * tx + qy * tz - qz * ty,
      vy + qw * ty + qz * tx - qx * tz,
      vz + qw * tz + qx * ty - qy * tx,
    )
  }
  /** Transform this point by a column-major mat4 (a `Mat4`, its `.m`, or a raw length-16 array),
   *  applying translation and the perspective divide. */
  transform(m: ArrayLike<number> | { m: ArrayLike<number> }): Vec3 {
    const a = (m as { m?: ArrayLike<number> }).m ?? (m as ArrayLike<number>)
    const x = this.x, y = this.y, z = this.z
    let w = a[3] * x + a[7] * y + a[11] * z + a[15]
    w = w || 1
    return new Vec3(
      (a[0] * x + a[4] * y + a[8] * z + a[12]) / w,
      (a[1] * x + a[5] * y + a[9] * z + a[13]) / w,
      (a[2] * x + a[6] * y + a[10] * z + a[14]) / w,
    )
  }

  withX(x: number): Vec3 { return new Vec3(x, this.y, this.z) }
  withY(y: number): Vec3 { return new Vec3(this.x, y, this.z) }
  withZ(z: number): Vec3 { return new Vec3(this.x, this.y, z) }

  // --- interop ---
  equals(v: Vec3Like, eps = 1e-6): boolean {
    return approx(this.x, cx(v), eps) && approx(this.y, cy(v), eps) && approx(this.z, cz(v), eps)
  }
  clone(): Vec3 { return new Vec3(this.x, this.y, this.z) }
  toArray(): [number, number, number] { return [ this.x, this.y, this.z ] }
  *[Symbol.iterator](): Iterator<number> { yield this.x; yield this.y; yield this.z }

  static from(v: Vec3Like): Vec3 { return new Vec3(cx(v), cy(v), cz(v)) }
  static get zero(): Vec3 { return new Vec3(0, 0, 0) }
  static get one(): Vec3 { return new Vec3(1, 1, 1) }
  static get up(): Vec3 { return new Vec3(0, 1, 0) }
  static get down(): Vec3 { return new Vec3(0, -1, 0) }
  static get left(): Vec3 { return new Vec3(-1, 0, 0) }
  static get right(): Vec3 { return new Vec3(1, 0, 0) }
  /** Forward is −Z (the engine's cameras look down −Z; `node.forward` is −Z at identity). */
  static get forward(): Vec3 { return new Vec3(0, 0, -1) }
  static get back(): Vec3 { return new Vec3(0, 0, 1) }
}

const clampN = (v: number, lo: number, hi: number): number => (v < lo ? lo : v > hi ? hi : v)
const ZERO2 = new Vec2(0, 0)
const ZERO3 = new Vec3(0, 0, 0)
