const easeInOutQuad = (t, b, c, d) => {
  t /= d / 2
  if (t < 1) return (c / 2) * t * t + b
  t--
  return (-c / 2) * (t * (t - 2) - 1) + b
}

const jumper = () => {
  // private variable cache
  // no variables are created during a jump, preventing memory leaks

  let container // container element to be scrolled       (node)
  let element // element to scroll to                   (node)

  let start // where scroll starts                    (px)
  let stop // where scroll stops                     (px)

  let offset // adjustment from the stop position      (px)
  let easing // easing function                        (function)
  let a11y // accessibility support flag             (boolean)

  let distance // distance of scroll                     (px)
  let duration // scroll duration                        (ms)

  let timeStart // time scroll started                    (ms)
  let timeElapsed // time spent scrolling thus far          (ms)

  let next // next scroll position                   (px)

  let callback // to call when done scrolling            (function)

  // scroll position helper

  function location() {
    let top = container.scrollTop || container.scrollY || container.pageYOffset
    top = typeof top === 'undefined' ? 0 : top
    return top
  }

  // element offset helper

  function top(element) {
    const elementTop = element.getBoundingClientRect().top
    const containerTop = container.getBoundingClientRect
      ? container.getBoundingClientRect().top
      : 0

    return elementTop - containerTop + start
  }

  // scrollTo helper

  function scrollTo(top) {
    container.scrollTo
      ? container.scrollTo(0, top) // window
      : (container.scrollTop = top) // custom container
  }

  // rAF loop helper

  function loop(timeCurrent) {
    // store time scroll started, if not started already
    if (!timeStart) {
      timeStart = timeCurrent
    }

    // determine time spent scrolling so far
    timeElapsed = timeCurrent - timeStart

    // calculate next scroll position
    next = easing(timeElapsed, start, distance, duration)

    // scroll to it
    scrollTo(next)

    // check progress
    timeElapsed < duration
      ? requestAnimationFrame(loop) // continue scroll loop
      : done() // scrolling is done
  }

  // scroll finished helper

  function done() {
    // account for rAF time rounding inaccuracies
    scrollTo(start + distance)

    // if scrolling to an element, and accessibility is enabled
    if (element && a11y) {
      // add tabindex indicating programmatic focus
      element.setAttribute('tabindex', '-1')

      // focus the element
      element.focus()
    }

    // if it exists, fire the callback
    if (typeof callback === 'function') {
      callback()
    }

    // reset time for next jump
    timeStart = false
  }

  // API

  function jump(target, options = {}) {
    // resolve options, or use defaults
    duration = options.duration || 1000
    offset = options.offset || 0
    callback = options.callback // "undefined" is a suitable default, and won't be called
    easing = options.easing || easeInOutQuad
    a11y = options.a11y || false

    // resolve container
    switch (typeof options.container) {
      case 'object':
        // we assume container is an HTML element (Node)
        container = options.container
        break

      case 'string':
        container = document.querySelector(options.container)
        break

      default:
        container = window
    }

    // cache starting position
    start = location()

    // resolve target
    switch (typeof target) {
      // scroll from current position
      case 'number':
        element = undefined // no element to scroll to
        a11y = false // make sure accessibility is off
        stop = start + target
        break

      // scroll to element (node)
      // bounding rect is relative to the viewport
      case 'object':
        element = target
        stop = top(element)
        break

      // scroll to element (selector)
      // bounding rect is relative to the viewport
      case 'string':
        element = document.querySelector(target)
        stop = top(element)
        break
    }

    // resolve scroll distance, accounting for offset
    distance = stop - start + offset

    // resolve duration
    switch (typeof options.duration) {
      // number in ms
      case 'number':
        duration = options.duration
        break

      // function passed the distance of the scroll
      case 'function':
        duration = options.duration(distance)
        break
    }

    // start the loop
    requestAnimationFrame(loop)
  }

  // expose only the jump method
  return jump
}

// export singleton

const singleton = jumper()

export default singleton