/**
 * √3
 * @type {number}
 */
export const SQRT3 = 1.7320508075688772;

/**
 * √⅓
 * @type {number}
 */
export const SQRT1_3 = 0.5773502691896257;

/**
 * Bound a number between some minimum and maximum value, inclusively.
 * @param {number} num    The current value
 * @param {number} min    The minimum allowed value
 * @param {number} max    The maximum allowed value
 * @returns {number}      The clamped number
 */
export function clamp(num, min, max) {
  return Math.min(Math.max(num, min), max);
}

/**
 * Linear interpolation function
 * @param {number} a   An initial value when weight is 0.
 * @param {number} b   A terminal value when weight is 1.
 * @param {number} w   A weight between 0 and 1.
 * @returns {number}   The interpolated value between a and b with weight w.
 */
export function mix(a, b, w) {
  return a + (w * (b - a));
}

/**
 * Used by {@link nextAfter}.
 * @type {DataView}
 */
const nextAfterView = new DataView(new ArrayBuffer(8));

/**
 * Returns the number adjacent to `x` in the direction of `y`.
 * @param {number} x
 * @param {number} y
 * @returns {number}
 */
export function nextAfter(x, y) {
  x = Number(x);
  y = Number(y);
  if ( Number.isNaN(x) || Number.isNaN(y) ) return NaN;
  if ( x === y ) return x;
  if ( x === 0 ) return Math.sign(y) * Number.MIN_VALUE;
  nextAfterView.setFloat64(0, x, false);
  let bits = nextAfterView.getBigUint64(0, false);
  bits += BigInt(Math.sign(x) * Math.sign(y - x));
  nextAfterView.setBigUint64(0, bits, false);
  return nextAfterView.getFloat64(0, false);
}

/**
 * Returns the number adjacent to `x` in the direction of -Infinity.
 * @param {number} x
 * @returns {number}
 */
export function nextDown(x) {
  return nextAfter(x, -Infinity);
}

/**
 * Returns the number adjacent to `x` in the direction of +Infinity.
 * @param {number} x
 * @returns {number}
 */
export function nextUp(x) {
  return nextAfter(x, Infinity);
}

/**
 * Transform an angle in degrees to be bounded within the domain [0, 360)
 * @param {number} degrees  An angle in degrees
 * @returns {number}        The same angle on the range [0, 360)
 */
export function normalizeDegrees(degrees) {
  const d = degrees % 360;
  return d < 0 ? d + 360 : d;
}

/**
 * Transform an angle in radians to be bounded within the domain [-PI, PI]
 * @param {number} radians  An angle in degrees
 * @returns {number}        The same angle on the range [-PI, PI]
 */
export function normalizeRadians(radians) {
  const pi = Math.PI;
  const pi2 = pi * 2;
  return radians - (pi2 * Math.floor((radians + pi) / pi2));
}

/**
 * Transform an angle in radians to a number in degrees
 * @param {number} angle    An angle in radians
 * @returns {number}        An angle in degrees
 */
export function toDegrees(angle) {
  return angle * (180 / Math.PI);
}

/**
 * Transform an angle in degrees to an angle in radians
 * @param {number} angle    An angle in degrees
 * @returns {number}        An angle in radians
 */
export function toRadians(angle) {
  return angle * (Math.PI / 180);
}

/**
 * Returns the value of the oscillation between `a` and `b` at time `t`.
 * @param {number} a                              The minimium value of the oscillation
 * @param {number} b                              The maximum value of the oscillation
 * @param {number} t                              The time
 * @param {number} [p=1]                          The period (must be nonzero)
 * @param {(x: number) => number} [f=Math.cos]    The periodic function (its period must be 2π)
 * @returns {number}                              `((b - a) * (f(2π * t / p) + 1) / 2) + a`
 */
export function oscillation(a, b, t, p=1, f=Math.cos) {
  return ((b - a) * (f((2 * Math.PI * t) / p) + 1) / 2) + a;
}

// Define properties on the Math environment
Object.defineProperties(Math, {
  SQRT3: {value: SQRT3},
  SQRT1_3: {value: SQRT1_3},
  clamp: {
    value: clamp,
    configurable: true,
    writable: true
  },
  mix: {
    value: mix,
    configurable: true,
    writable: true
  },
  nextAfter: {
    value: nextAfter,
    configurable: true,
    writable: true
  },
  nextDown: {
    value: nextDown,
    configurable: true,
    writable: true
  },
  nextUp: {
    value: nextUp,
    configurable: true,
    writable: true
  },
  normalizeDegrees: {
    value: normalizeDegrees,
    configurable: true,
    writable: true
  },
  normalizeRadians: {
    value: normalizeRadians,
    configurable: true,
    writable: true
  },
  toDegrees: {
    value: toDegrees,
    configurable: true,
    writable: true
  },
  toRadians: {
    value: toRadians,
    configurable: true,
    writable: true
  },
  oscillation: {
    value: oscillation,
    configurable: true,
    writable: true
  }
});

