// Copyright 2026 Leo Cheng
// SPDX-License-Identifier: Apache-2.0

///|
/// The ratio of a circle's circumference to its diameter.
pub let pi : Float = 3.14159265358979323846

///|
/// A full turn in radians: `2 * pi`. The period of every angle here.
pub let tau : Float = 6.28318530717958647692

///|
/// Half a turn in radians: `pi / 2`. A quarter of `tau`.
pub let half_pi : Float = 1.57079632679489661923

///|
/// A quarter turn in radians: `pi / 4`, the angle whose tangent is one.
pub let quarter_pi : Float = 0.78539816339744830962

///|
/// The absolute tolerance `approx` falls back on when the caller gives none.
///
/// It is a compromise: large enough to cover the rounding of a handful of
/// operations at this precision, small enough that values a user would call
/// different do not compare equal.
pub let default_epsilon : Float = 1.0e-6

///|
/// The smallest angle, in radians, that `approx` still calls a difference.
let angle_epsilon : Float = 1.0e-6

///|
/// Degrees per radian: `180 / pi`.
let degrees_per_radian : Float = 57.29577951308232087680

///|
/// Radians per degree: `pi / 180`.
let radians_per_degree : Float = 0.01745329251994329577

///|
/// `degrees` converted to radians.
pub fn to_radians(degrees : Float) -> Float {
  degrees * radians_per_degree
}

///|
/// `radians` converted to degrees.
pub fn to_degrees(radians : Float) -> Float {
  radians * degrees_per_radian
}

///|
/// The point `t` of the way from `a` to `b`: `a` at zero, `b` at one.
///
/// `t` outside `0..1` extrapolates, which is what callers building animation
/// curves usually want; clamp the result if it is not.
pub fn lerp(a : Float, b : Float, t : Float) -> Float {
  a + (b - a) * t
}

///|
/// Where `value` sits between `start` and `end`, as a fraction: the inverse of
/// `lerp`.
///
/// Returns zero when the two ends are equal — there is no fraction between
/// them, and zero is the answer that keeps `lerp` from producing a NaN.
pub fn inverse_lerp(start : Float, end : Float, value : Float) -> Float {
  let span : Float = end - start
  if span == 0.0 {
    0.0
  } else {
    (value - start) / span
  }
}

///|
/// `value` taken from the range `from_start..from_end` and placed in
/// `to_start..to_end`.
///
/// It is `lerp(to_start, to_end, inverse_lerp(from_start, from_end, value))`:
/// the value keeps its position within the range, and values outside the source
/// range land outside the destination range by the same proportion.
pub fn remap(
  value : Float,
  from_start : Float,
  from_end : Float,
  to_start : Float,
  to_end : Float,
) -> Float {
  lerp(to_start, to_end, inverse_lerp(from_start, from_end, value))
}

///|
/// A smooth step from zero at `start` to one at `end`, zero slope at both ends.
///
/// The curve is `3t² - 2t³` over the normalised position, which is the cheapest
/// easing whose first derivative vanishes at both ends — no visible corner where
/// the animation starts or stops.
///
/// A zero-width range has no slope to smooth: it steps at `start`.
pub fn smoothstep(start : Float, end : Float, value : Float) -> Float {
  if start == end {
    if value < start {
      0.0
    } else {
      1.0
    }
  } else {
    let t = clamp01((value - start) / (end - start))
    t * t * (3.0 - 2.0 * t)
  }
}

///|
/// `value` folded into `0..span`.
///
/// Angles and texture coordinates repeat, and a value that has run past the end
/// of its range has to come back to the start rather than stop there. A span of
/// zero or less has nothing to fold into and gives zero.
///
/// ```mbt check
/// test {
///   assert_eq(@moonmath.wrap(7.5, 5.0), 2.5)
///   assert_eq(@moonmath.wrap(-1.0, 5.0), 4.0)
/// }
/// ```
pub fn wrap(value : Float, span : Float) -> Float {
  if span <= 0.0 {
    0.0
  } else {
    let folded : Float = value - span * (value / span).floor()
    // `floor` of a value already in range gives the value back unchanged, but
    // rounding can still land exactly on the span; fold that one step back so
    // the result stays half-open.
    if folded >= span {
      0.0
    } else {
      folded
    }
  }
}

///|
/// `value` folded into `start..end`.
///
/// `wrap(value - start, end - start) + start`, with the same half-open result.
pub fn wrap_range(value : Float, start : Float, end : Float) -> Float {
  start + wrap(value - start, end - start)
}

///|
/// Whether two numbers are the same to within `eps`.
///
/// Without an epsilon the comparison uses `default_epsilon`, scaled up for
/// values whose magnitude would swamp an absolute tolerance. Equality itself is
/// exact — `==` on two numbers that took different paths to the same place
/// rarely holds — so this is the test callers want for results of arithmetic.
///
/// ```mbt check
/// test {
///   let a : Float = 0.1 + 0.2
///   assert_true(@moonmath.approx(a, 0.3))
///   assert_false(@moonmath.approx(a, 0.31))
///   assert_true(@moonmath.approx(a, 0.31, eps=0.02))
/// }
/// ```
pub fn approx(a : Float, b : Float, eps? : Float) -> Bool {
  let limit : Float = match eps {
    Some(eps) => eps
    None => default_epsilon
  }
  if a == b {
    true
  } else if a.is_nan() || b.is_nan() {
    false
  } else {
    (a - b).abs() <= limit
  }
}

///|
/// Whether two angles — in radians, wrapping at `tau` — point the same way.
///
/// `0` and `tau` are the same direction, and so are `-pi` and `pi`; comparing
/// them as plain numbers says otherwise.
pub fn approx_angle(a : Float, b : Float, eps? : Float) -> Bool {
  let limit : Float = match eps {
    Some(eps) => eps
    None => angle_epsilon
  }
  let difference = (wrap(a - b + half_pi, tau) - half_pi).abs()
  difference <= limit
}

///|
/// `value` pinned to `0..1`.
pub fn clamp01(value : Float) -> Float {
  if value < 0.0 {
    0.0
  } else if value > 1.0 {
    1.0
  } else {
    value
  }
}