export type Easing = (v: number) => number
export type EasingModifier = (easing: Easing) => Easing

const DEFAULT_OVERSHOOT_STRENGTH = 1.525
const BOUNCE_FIRST_THRESHOLD = 4.0 / 11.0
const BOUNCE_SECOND_THRESHOLD = 8.0 / 11.0
const BOUNCE_THIRD_THRESHOLD = 9.0 / 10.0

// helper constants
const ca = 4356.0 / 361.0
const cb = 35442.0 / 1805.0
const cc = 16061.0 / 1805.0

export function linear(p: number): number {
  return p
}

export function bounceOut(p: number): number {
  if (p === 1 || p === 0) return p
  const p2 = p * p

  return p < BOUNCE_FIRST_THRESHOLD
    ? 7.5625 * p2
    : p < BOUNCE_SECOND_THRESHOLD
      ? 9.075 * p2 - 9.9 * p + 3.4
      : p < BOUNCE_THIRD_THRESHOLD
        ? ca * p2 - cb * p + cc
        : 10.8 * p * p - 20.52 * p + 10.72
}

export function bounceInOut(p: number): number {
  return p < 0.5
    ? 0.5 * (1.0 - bounceOut(1.0 - p * 2.0))
    : 0.5 * bounceOut(p * 2.0 - 1.0) + 0.5
}

// Accepts an easing function and returns a new one that outputs reversed values.
// Turns easeIn into easeOut.
export function reverseEasing(easing: Easing): Easing {
  return p => 1 - easing(1 - p)
}

// Accepts an easing function and returns a new one that outputs mirrored values for
// the second half of the animation. Turns easeIn into easeInOut.
export function mirrorEasing(easing: Easing): Easing {
  return p => (p <= 0.5 ? easing(2 * p) / 2 : (2 - easing(2 * (1 - p))) / 2)
}

// Creates an easing function that is based on the exponent of the provided `power`.
// The higher the `power`, the stronger the easing.
export function createExpoIn(power: number): Easing {
  return p => p ** power
}

// Creates an easing function that has a stronger overshoot the higher the provided `power`.
export function createBackIn(power: number): Easing {
  return p => p * p * ((power + 1) * p - power)
}

// Creates an easing function that pulls back a little before moving, and then
// has a `createBackIn`-based overshoot
export function createAnticipate(power: number): Easing {
  const backEasing = createBackIn(power)

  return p =>
    (p *= 2) < 1
      ? 0.5 * backEasing(p)
      : 0.5 * (2 - Math.pow(2, -10 * (p - 1)))
}

export function bounceIn(p: number): number {
  return 1 - bounceOut(1 - p)
}

export function easeIn(p: number): number {
  return p * p
}

export function easeOut(p: number): number {
  return 1 - easeIn(1 - p)
}

export function easeInOut(p: number): number {
  return p <= 0.5
    ? easeIn(2 * p) / 2
    : (2 - easeIn(2 * (1 - p))) / 2
}

export function circIn(p: number): number {
  return 1 - Math.sin(Math.acos(p))
}

export function circOut(p: number): number {
  return 1 - circIn(1 - p)
}

export function circInOut(p: number): number {
  return p <= 0.5
    ? circOut(2 * p) / 2
    : (2 - circOut(2 * (1 - p))) / 2
}

export function backIn(p: number): number {
  const power = DEFAULT_OVERSHOOT_STRENGTH
  return p * p * ((power + 1) * p - power)
}

export function backOut(p: number): number {
  return 1 - backIn(1 - p)
}

export function backInOut(p: number): number {
  return p <= 0.5
    ? backIn(2 * p) / 2
    : (2 - backIn(2 * (1 - p))) / 2
}

export function anticipate(p: number): number {
  const doubled = p * 2

  if (doubled < 1) {
    return 0.5 * backIn(doubled)
  }

  return 0.5 * (2 - Math.pow(2, -10 * (doubled - 1)))
}