/**
 * Deterministic, smooth pseudo-randomness.
 *
 * The whole NABDH telemetry model is a *pure function* of (asset profile, timestamp,
 * scenario). That property is what makes the platform coherent: recorded history, the
 * live stream and the Energy Time Machine forecast are all the same function evaluated
 * at different times, so charts join up seamlessly and a demo replays identically.
 *
 * Plain `Math.random()` would break all of that, so we use hash-based value noise:
 * `noise(seed, x)` is continuous in `x` (no jitter between ticks) yet unpredictable
 * across seeds, giving realistic-looking drift rather than white noise.
 */

/** 32-bit integer hash (variant of MurmurHash3's finaliser). */
export function hash32(n: number): number {
  let h = n | 0
  h ^= h >>> 16
  h = Math.imul(h, 0x85ebca6b)
  h ^= h >>> 13
  h = Math.imul(h, 0xc2b2ae35)
  h ^= h >>> 16
  return h >>> 0
}

/** Hash two integers together — used to keep separate channels independent. */
export function hash2(a: number, b: number): number {
  return hash32(Math.imul(a | 0, 0x27d4eb2d) ^ (b | 0))
}

/** Deterministic float in [0, 1) for an integer lattice point. */
export function unitHash(seed: number, x: number): number {
  return hash2(seed, x) / 4294967296
}

/** Smoothstep easing, so interpolated noise has a continuous first derivative. */
function smooth(t: number): number {
  return t * t * (3 - 2 * t)
}

/**
 * 1-D value noise in [-1, 1]. Continuous in `x`; one "unit" of x is one noise period.
 */
export function noise1(seed: number, x: number): number {
  const i = Math.floor(x)
  const f = x - i
  const a = unitHash(seed, i)
  const b = unitHash(seed, i + 1)
  return (a + (b - a) * smooth(f)) * 2 - 1
}

/**
 * Fractal (multi-octave) noise in roughly [-1, 1]. Adds fine detail on top of slow
 * drift, which is what real telemetry looks like.
 */
export function fbm(seed: number, x: number, octaves = 3): number {
  let total = 0
  let amplitude = 1
  let frequency = 1
  let norm = 0
  for (let o = 0; o < octaves; o += 1) {
    total += noise1(seed + o * 7919, x * frequency) * amplitude
    norm += amplitude
    amplitude *= 0.5
    frequency *= 2.17
  }
  return total / norm
}

/** A stable pseudo-random value for a discrete entity (no time component). */
export function seededUnit(seed: number, salt = 0): number {
  return unitHash(seed, salt * 104729 + 17)
}

/** A stable pseudo-random value in [min, max] for a discrete entity. */
export function seededRange(seed: number, salt: number, min: number, max: number): number {
  return min + seededUnit(seed, salt) * (max - min)
}

/** Stable pseudo-random integer in [min, max]. */
export function seededInt(seed: number, salt: number, min: number, max: number): number {
  return Math.floor(seededRange(seed, salt, min, max + 0.999999))
}

/** Stable pseudo-random pick from a list. */
export function seededPick<T>(seed: number, salt: number, items: readonly T[]): T {
  return items[seededInt(seed, salt, 0, items.length - 1)]
}

/**
 * A small mulberry32 generator, for the seed script where a *sequence* is more
 * convenient than a lattice. Still fully deterministic.
 */
export function mulberry32(seed: number): () => number {
  let a = seed >>> 0
  return () => {
    a = (a + 0x6d2b79f5) >>> 0
    let t = a
    t = Math.imul(t ^ (t >>> 15), t | 1)
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61)
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296
  }
}

export function clamp(value: number, min: number, max: number): number {
  return value < min ? min : value > max ? max : value
}

/** Maps `value` from one range to another, clamped to the destination range. */
export function mapRange(
  value: number,
  inMin: number,
  inMax: number,
  outMin: number,
  outMax: number,
): number {
  if (inMax === inMin) return outMin
  const t = clamp((value - inMin) / (inMax - inMin), 0, 1)
  return outMin + t * (outMax - outMin)
}

export function round(value: number, decimals = 2): number {
  const f = 10 ** decimals
  return Math.round(value * f) / f
}
