// Scalar / general math helpers, exposed to user code as the `Mathf` global. Vector & quaternion
// helpers live with the engine that uses them (vec2 in g2/, vec3+quat in gl/). `smoothDamp` works on
// both numbers and numeric arrays here (no gl-matrix), which covers 2D (vec2) and 3D position damping.

export type Damper<T extends number | number[]> = ((target: T, dt: number) => T) & {
  /** Snap the damper's internal current value (e.g. on teleport). */
  setValue(value: T): void
}

const clamp = (v: number, min: number, max: number): number => (v < min ? min : v > max ? max : v)

const smoothDampNumber = (initial: number, smoothTime: number, maxSpeed: number): Damper<number> => {
  let current = initial
  let velocity = 0
  const damp = ((target: number, dt: number): number => {
    smoothTime = Math.max(0.0001, smoothTime)
    const omega = 2 / smoothTime
    const x = omega * dt
    const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x)
    const maxDelta = maxSpeed * smoothTime
    let delta = clamp(current - target, -maxDelta, maxDelta)
    const newTarget = current - delta
    const temp = (velocity + omega * delta) * dt
    velocity = (velocity - omega * temp) * exp
    current = newTarget + (delta + temp) * exp
    return current
  }) as Damper<number>
  damp.setValue = (v) => { current = v; velocity = 0 }
  return damp
}

function smoothDamp(initial: number, smoothTime: number, maxSpeed?: number): Damper<number>
function smoothDamp(initial: number[], smoothTime: number, maxSpeed?: number): Damper<number[]>
function smoothDamp(initial: any, smoothTime: number, maxSpeed = Infinity): any {
  return typeof initial === "number"
    ? smoothDampNumber(initial, smoothTime, maxSpeed)
    : smoothDampArray(initial, smoothTime, maxSpeed)
}

const smoothDampArray = (initial: number[], smoothTime: number, maxSpeed: number): Damper<number[]> => {
  const n = initial.length
  let current = initial.slice()
  const velocity = new Array(n).fill(0)
  const damp = ((target: number[], dt: number): number[] => {
    smoothTime = Math.max(0.0001, smoothTime)
    const omega = 2 / smoothTime
    const x = omega * dt
    const exp = 1 / (1 + x + 0.48 * x * x + 0.235 * x * x * x)
    const maxDelta = maxSpeed * smoothTime

    const delta = new Array(n)
    let mag = 0
    for (let i = 0; i < n; i++) { delta[i] = current[i] - target[i]; mag += delta[i] * delta[i] }
    mag = Math.sqrt(mag)
    if (mag > maxDelta && mag > 0) {
      const s = maxDelta / mag
      for (let i = 0; i < n; i++) delta[i] *= s
    }

    const out = new Array(n)
    for (let i = 0; i < n; i++) {
      const newTarget = current[i] - delta[i]
      const temp = (velocity[i] + omega * delta[i]) * dt
      velocity[i] = (velocity[i] - omega * temp) * exp
      out[i] = newTarget + (delta[i] + temp) * exp
    }
    current = out
    return out.slice()
  }) as Damper<number[]>
  damp.setValue = (v) => { current = v.slice(); velocity.fill(0) }
  return damp
}

export const Mathf = {
  PI: Math.PI,
  TAU: Math.PI * 2,
  EPSILON: 1e-6,
  DEG2RAD: Math.PI / 180,
  RAD2DEG: 180 / Math.PI,

  deg2rad: (deg: number): number => deg * (Math.PI / 180),
  rad2deg: (rad: number): number => rad * (180 / Math.PI),

  clamp,
  clamp01: (v: number): number => clamp(v, 0, 1),
  lerp: (a: number, b: number, t: number): number => a + (b - a) * t,
  inverseLerp: (a: number, b: number, v: number): number => (a === b ? 0 : (v - a) / (b - a)),
  remap: (v: number, inMin: number, inMax: number, outMin: number, outMax: number): number =>
    outMin + ((v - inMin) / (inMax - inMin)) * (outMax - outMin),

  /** Move `a` toward `b` by at most `maxDelta`. */
  moveTowards: (a: number, b: number, maxDelta: number): number => {
    const d = b - a
    return Math.abs(d) <= maxDelta ? b : a + Math.sign(d) * maxDelta
  },
  /** Frame-rate-independent exponential approach (good default: lambda 5..15). */
  damp: (a: number, b: number, lambda: number, dt: number): number => a + (b - a) * (1 - Math.exp(-lambda * dt)),

  sign: Math.sign,
  /** Loop a value into [0, length). */
  repeat: (t: number, length: number): number => clamp(t - Math.floor(t / length) * length, 0, length),
  /** Ping-pong a value in [0, length]. */
  pingPong: (t: number, length: number): number => {
    const r = clamp(t - Math.floor(t / (length * 2)) * (length * 2), 0, length * 2)
    return length - Math.abs(r - length)
  },

  random: (min = 0, max = 1): number => min + Math.random() * (max - min),
  randomInt: (min: number, max: number): number => Math.floor(min + Math.random() * (max - min + 1)),

  /**
   * Critically-damped follow (Unity SmoothDamp). Returns a stateful damper you call every frame:
   *   const d = Mathf.smoothDamp([x, y], 0.15)
   *   pos = d(targetPos, dt)
   * Works on a number or a numeric array (vec2 / vec3).
   */
  smoothDamp,
}
