/**
 * @file 缓动函数
 * 提供常用的缓动函数。
 */

export type Definition = (t: number) => number
export type Names =
  | 'linear'
  | 'quad'
  | 'cubic'
  | 'inout'
  | 'exponential'
  | 'bounce'
  | 'easeInSine'
  | 'easeOutSine'
  | 'easeInOutSine'
  | 'easeInQuad'
  | 'easeOutQuad'
  | 'easeInOutQuad'
  | 'easeInCubic'
  | 'easeOutCubic'
  | 'easeInOutCubic'
  | 'easeInQuart'
  | 'easeOutQuart'
  | 'easeInOutQuart'
  | 'easeInQuint'
  | 'easeOutQuint'
  | 'easeInOutQuint'
  | 'easeInExpo'
  | 'easeOutExpo'
  | 'easeInOutExpo'
  | 'easeInCirc'
  | 'easeOutCirc'
  | 'easeInOutCirc'
  | 'easeInBack'
  | 'easeOutBack'
  | 'easeInOutBack'
  | 'easeInElastic'
  | 'easeOutElastic'
  | 'easeInOutElastic'
  | 'easeInBounce'
  | 'easeOutBounce'
  | 'easeInOutBounce'

export const linear: Definition = (t) => t
export const quad: Definition = (t) => t * t
export const cubic: Definition = (t) => t * t * t
export const inout: Definition = (t) => {
  if (t <= 0) {
    return 0
  }

  if (t >= 1) {
    return 1
  }

  const t2 = t * t
  const t3 = t2 * t
  return 4 * (t < 0.5 ? t3 : 3 * (t - t2) + t3 - 0.75)
}

export const exponential: Definition = (t) => {
  return 2 ** (10 * (t - 1)) // eslint-disable-line
}

export const bounce = ((t: number) => {
  for (let a = 0, b = 1; ; a += b, b /= 2) {
    if (t >= (7 - 4 * a) / 11) {
      const q = (11 - 6 * a - 11 * t) / 4
      return -q * q + b * b
    }
  }
}) as Definition

export const decorators = {
  reverse(f: Definition): Definition {
    return (t) => 1 - f(1 - t)
  },
  reflect(f: Definition): Definition {
    return (t) => 0.5 * (t < 0.5 ? f(2 * t) : 2 - f(2 - 2 * t))
  },
  clamp(f: Definition, n = 0, x = 1): Definition {
    return (t) => {
      const r = f(t)
      return r < n ? n : r > x ? x : r
    }
  },
  back(s = 1.70158): Definition {
    return (t) => t * t * ((s + 1) * t - s)
  },
  elastic(x = 1.5): Definition {
    return (t) => 2 ** (10 * (t - 1)) * Math.cos(((20 * Math.PI * x) / 3) * t)
  },
}

// Slight acceleration from zero to full speed
export function easeInSine(t: number) {
  return -1 * Math.cos(t * (Math.PI / 2)) + 1
}

// Slight deceleration at the end
export function easeOutSine(t: number) {
  return Math.sin(t * (Math.PI / 2))
}

// Slight acceleration at beginning and slight deceleration at end
export function easeInOutSine(t: number) {
  return -0.5 * (Math.cos(Math.PI * t) - 1)
}

// Accelerating from zero velocity
export function easeInQuad(t: number) {
  return t * t
}

// Decelerating to zero velocity
export function easeOutQuad(t: number) {
  return t * (2 - t)
}

// Acceleration until halfway, then deceleration
export function easeInOutQuad(t: number) {
  return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t
}

// Accelerating from zero velocity
export function easeInCubic(t: number) {
  return t * t * t
}

// Decelerating to zero velocity
export function easeOutCubic(t: number) {
  const t1 = t - 1
  return t1 * t1 * t1 + 1
}

// Acceleration until halfway, then deceleration
export function easeInOutCubic(t: number) {
  return t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1
}

// Accelerating from zero velocity
export function easeInQuart(t: number) {
  return t * t * t * t
}

// Decelerating to zero velocity
export function easeOutQuart(t: number) {
  const t1 = t - 1
  return 1 - t1 * t1 * t1 * t1
}

// Acceleration until halfway, then deceleration
export function easeInOutQuart(t: number) {
  const t1 = t - 1
  return t < 0.5 ? 8 * t * t * t * t : 1 - 8 * t1 * t1 * t1 * t1
}

// Accelerating from zero velocity
export function easeInQuint(t: number) {
  return t * t * t * t * t
}

// Decelerating to zero velocity
export function easeOutQuint(t: number) {
  const t1 = t - 1
  return 1 + t1 * t1 * t1 * t1 * t1
}

// Acceleration until halfway, then deceleration
export function easeInOutQuint(t: number) {
  const t1 = t - 1
  return t < 0.5 ? 16 * t * t * t * t * t : 1 + 16 * t1 * t1 * t1 * t1 * t1
}

// Accelerate exponentially until finish
export function easeInExpo(t: number) {
  if (t === 0) {
    return 0
  }

  return 2 ** (10 * (t - 1))
}

// Initial exponential acceleration slowing to stop
export function easeOutExpo(t: number) {
  if (t === 1) {
    return 1
  }

  return -(2 ** (-10 * t)) + 1
}

// Exponential acceleration and deceleration
export function easeInOutExpo(t: number) {
  if (t === 0 || t === 1) {
    return t
  }

  const scaledTime = t * 2
  const scaledTime1 = scaledTime - 1

  if (scaledTime < 1) {
    return 0.5 * 2 ** (10 * scaledTime1)
  }

  return 0.5 * (-(2 ** (-10 * scaledTime1)) + 2)
}

// Increasing velocity until stop
export function easeInCirc(t: number) {
  const scaledTime = t / 1
  return -1 * (Math.sqrt(1 - scaledTime * t) - 1)
}

// Start fast, decreasing velocity until stop
export function easeOutCirc(t: number) {
  const t1 = t - 1
  return Math.sqrt(1 - t1 * t1)
}

// Fast increase in velocity, fast decrease in velocity
export function easeInOutCirc(t: number) {
  const scaledTime = t * 2
  const scaledTime1 = scaledTime - 2

  if (scaledTime < 1) {
    return -0.5 * (Math.sqrt(1 - scaledTime * scaledTime) - 1)
  }

  return 0.5 * (Math.sqrt(1 - scaledTime1 * scaledTime1) + 1)
}

// Slow movement backwards then fast snap to finish
export function easeInBack(t: number, magnitude = 1.70158) {
  return t * t * ((magnitude + 1) * t - magnitude)
}

// Fast snap to backwards point then slow resolve to finish
export function easeOutBack(t: number, magnitude = 1.70158) {
  const scaledTime = t / 1 - 1

  return (
    scaledTime * scaledTime * ((magnitude + 1) * scaledTime + magnitude) + 1
  )
}

// Slow movement backwards, fast snap to past finish, slow resolve to finish
export function easeInOutBack(t: number, magnitude = 1.70158) {
  const scaledTime = t * 2
  const scaledTime2 = scaledTime - 2

  const s = magnitude * 1.525

  if (scaledTime < 1) {
    return 0.5 * scaledTime * scaledTime * ((s + 1) * scaledTime - s)
  }

  return 0.5 * (scaledTime2 * scaledTime2 * ((s + 1) * scaledTime2 + s) + 2)
}

// Bounces slowly then quickly to finish
export function easeInElastic(t: number, magnitude = 0.7) {
  if (t === 0 || t === 1) {
    return t
  }

  const scaledTime = t / 1
  const scaledTime1 = scaledTime - 1

  const p = 1 - magnitude
  const s = (p / (2 * Math.PI)) * Math.asin(1)

  return -(
    2 ** (10 * scaledTime1) * // eslint-disable-line
    Math.sin(((scaledTime1 - s) * (2 * Math.PI)) / p)
  )
}

// Fast acceleration, bounces to zero
export function easeOutElastic(t: number, magnitude = 0.7) {
  const p = 1 - magnitude
  const scaledTime = t * 2

  if (t === 0 || t === 1) {
    return t
  }

  const s = (p / (2 * Math.PI)) * Math.asin(1)
  return (
    2 ** (-10 * scaledTime) * // eslint-disable-line
      Math.sin(((scaledTime - s) * (2 * Math.PI)) / p) +
    1
  )
}

// Slow start and end, two bounces sandwich a fast motion
export function easeInOutElastic(t: number, magnitude = 0.65) {
  const p = 1 - magnitude

  if (t === 0 || t === 1) {
    return t
  }

  const scaledTime = t * 2
  const scaledTime1 = scaledTime - 1

  const s = (p / (2 * Math.PI)) * Math.asin(1)

  if (scaledTime < 1) {
    return (
      -0.5 *
      (2 ** (10 * scaledTime1) * // eslint-disable-line
        Math.sin(((scaledTime1 - s) * (2 * Math.PI)) / p))
    )
  }

  return (
    2 ** (-10 * scaledTime1) * // eslint-disable-line
      Math.sin(((scaledTime1 - s) * (2 * Math.PI)) / p) *
      0.5 +
    1
  )
}

// Bounce to completion
export function easeOutBounce(t: number) {
  const scaledTime = t / 1

  if (scaledTime < 1 / 2.75) {
    return 7.5625 * scaledTime * scaledTime
  }
  if (scaledTime < 2 / 2.75) {
    const scaledTime2 = scaledTime - 1.5 / 2.75
    return 7.5625 * scaledTime2 * scaledTime2 + 0.75
  }
  if (scaledTime < 2.5 / 2.75) {
    const scaledTime2 = scaledTime - 2.25 / 2.75
    return 7.5625 * scaledTime2 * scaledTime2 + 0.9375
  }
  {
    const scaledTime2 = scaledTime - 2.625 / 2.75
    return 7.5625 * scaledTime2 * scaledTime2 + 0.984375
  }
}

// Bounce increasing in velocity until completion
export function easeInBounce(t: number) {
  return 1 - easeOutBounce(1 - t)
}

// Bounce in and bounce out
export function easeInOutBounce(t: number) {
  if (t < 0.5) {
    return easeInBounce(t * 2) * 0.5
  }

  return easeOutBounce(t * 2 - 1) * 0.5 + 0.5
}
