const Container = require('./container')
const Placeholder = require('./placeholder')
const Sticky = require('./sticky')

const PositionSticky = {}

/**
 * Creates an instance of PositionSticky
 *
 * @param element
 * @returns {PositionSticky}
 */
PositionSticky.create = function create(element) {
  return Object.create(PositionSticky).init(element)
}

/**
 * Constructor method
 *
 * @param element {HTMLElement}
 * @returns {PositionSticky}
 */
PositionSticky.init = function init(element) {
  this.constructor   = PositionSticky
  this.placeholder   = Placeholder.create(element)
  this.sticky        = Sticky.create(element, this)
  this.container     = Container.create(element.parentNode)
  this.positionType  = 'static'
  this.isTicking     = false
  this.threshold     = null

  this.leftPositionWhenAbsolute = null
  this.leftPositionWhenFixed    = null
  this.latestKnownScrollY       = global.pageYOffset

  this.setOffsetTop()
  this.setOffsetBottom()
  this.setLeftPositionWhenAbsolute()
  this.setLeftPositionWhenFixed()
  this.calcThreshold()
  this.subscribeToWindowScroll()
  this.subscribeToWindowResize()

  this.update()

  return this
}

/**
 * Sets the distance that the sticky element will have from the top of viewport
 * when it becomes sticky
 */
PositionSticky.setOffsetTop = function setOffsetTop() {
  this.offsetTop = this.container.borderTopWidth + this.container.paddingTop
}

/**
 * Sets the amount to subtract in #_canStickyFitInContainer and also sets the
 * distance that the sticky element will have from the bottom of its container
 * when it is positioned absolutely
 */
PositionSticky.setOffsetBottom = function setOffsetBottom() {
  this.offsetBottom = this.container.borderBottomWidth + this.container.paddingBottom
}

/**
 * Calculates the point where the sticky behaviour should start
 */
PositionSticky.calcThreshold = function calcThreshold() {
  this.threshold = this.getStickyDistanceFromDocumentTop() - this.offsetTop
}

/**
 * Gets the element's distance from its offset parent's left
 * and subtracts any horizontal margins and saves it
 */
PositionSticky.setLeftPositionWhenAbsolute = function setLeftPositionWhenAbsolute() {
  const marginLeft = parseFloat(this.sticky.$element.css('margin-left'))
  this.leftPositionWhenAbsolute = this.sticky.$element.position().left - marginLeft
}

/**
 * Gets the element's distance from document left and saves it
 *
 * @todo Write a test that is covering when the page is scrolled
 */
PositionSticky.setLeftPositionWhenFixed = function setLeftPositionWhenFixed() {
  const marginLeft = parseFloat(this.sticky.$element.css('margin-left'))
  this.leftPositionWhenFixed = (global.pageXOffset + this.sticky.$element.offset().left) - marginLeft
}

/**
 * Attaches #_onScroll method to Window.onscroll event
 */
PositionSticky.subscribeToWindowScroll = function subscribeToWindowScroll() {
  global.addEventListener('scroll', this.onScroll.bind(this))
}

/**
 * Debounces the scroll event
 *
 * @see [Debouncing Scroll Events]{@link http://www.html5rocks.com/en/tutorials/speed/animations/#debouncing-scroll-events}
 *
 * @todo Don't run update when container is not visible
 */
PositionSticky.onScroll = function onScroll() {
  if (!this.isTicking) {
    this.latestKnownScrollY = global.pageYOffset
    this.isTicking = true
    global.requestAnimationFrame(this.update.bind(this))
  }
}

/**
 * Attaches #_onResize method to Window.onresize event
 */
PositionSticky.subscribeToWindowResize = function subscribeToWindowResize() {
  global.addEventListener('resize', this.onResize.bind(this))
}

/**
 * Debounces the resize event
 *
 * @see [Debouncing Scroll Events]{@link http://www.html5rocks.com/en/tutorials/speed/animations/#debouncing-scroll-events}
 * @instance
 * @private
 *
 * @todo Don't run update when container is not visible
 */
PositionSticky.onResize = function onResize() {
  if (!this.isTicking) {
    global.requestAnimationFrame(this.refresh.bind(this))
    this.isTicking = true
  }
}

PositionSticky.isStatic = function isStatic() {
  return this.positionType === 'static'
}

PositionSticky.makeStatic = function makeStatic() {
  this.sticky.$element.css({
    position: 'static',
    width:    '',
    left:     '',
    top:      '',
  })
  this.placeholder.$element.hide()
  this.positionType = 'static'
}

PositionSticky.isFixed = function isFixed() {
  return this.positionType === 'fixed'
}

PositionSticky.makeFixed = function makeFixed() {
  this.sticky.$element.css({
    position: 'fixed',
    top:      `${this.offsetTop}px`,
    bottom:   '',
    left:     `${this.leftPositionWhenFixed}px`,
  })
  this.placeholder.$element.show()
  this.positionType = 'fixed'
}

PositionSticky.isAbsolute = function isAbsolute() {
  return this.positionType === 'absolute'
}

PositionSticky.makeAbsolute = function makeAbsolute() {
  this.sticky.$element.css({
    position: 'absolute',
    top:      'auto',
    bottom:   `${this.container.paddingBottom}px`,
    left:     `${this.leftPositionWhenAbsolute}px`,
  })
  this.placeholder.$element.show()
  this.positionType = 'absolute'
}

/**
 * This is the main method that runs on every animation frame during scroll.
 * It starts with checking whether the element is within the static range.
 * If not, it checks whether the element is within the fixed range.
 * Otherwise, it positions the element absolutely.
 */
PositionSticky.update = function update() {
  this.isTicking = false

  if (this.isBelowThreshold()) {
    if (!this.isStatic()) {
      this.makeStatic()
    }
  } else if (this.canStickyFitInContainer()) {
    if (!this.isFixed()) {
      this.makeFixed()
    }
  } else if (!this.isAbsolute()) {
    this.makeAbsolute()
  }
}

/**
 * Returns true when the page hasn't been scrolled to the threshold point yet.
 * Otherwise, returns false.
 *
 * @returns {boolean}
 */
PositionSticky.isBelowThreshold = function isBelowThreshold() {
  if (this.latestKnownScrollY < this.threshold) {
    return true
  }
  return false
}

/**
 * Checks whether the element can fit inside the visible portion of the container or not
 *
 * @returns {boolean}
 */
PositionSticky.canStickyFitInContainer = function canStickyFitInContainer() {
  return this.getAvailableSpaceInContainer() >= this.sticky.boundingBoxHeight
}

/**
 * Calculates the height of the visible portion of the container
 * that can be used to fit the sticky element
 *
 * @returns {number}
 */
PositionSticky.getAvailableSpaceInContainer = function getAvailableSpaceInContainer() {
  return this.container.$element[0].getBoundingClientRect().bottom - this.offsetBottom - this.offsetTop
}

/**
 * Calculates sticky element's total offset from the document top.
 * It uses placeholder if it is called when the sticky element is
 * not static (e.g. through #refresh)
 *
 * @returns {number}
 */
PositionSticky.getStickyDistanceFromDocumentTop = function getStickyDistanceFromDocumentTop() {
  const $element = (this.isStatic() ? this.sticky.$element : this.placeholder.$element)
  return $element.offset().top
}

/**
 * Re-measures the cached positions/dimensions that are used during scroll
 */
PositionSticky.refresh = function refresh() {
  this.sticky.refresh()
  this.placeholder.refresh()
  this.calcThreshold()
  this.isTicking = false

  if (!this.isStatic()) {
    this.makeStatic()
    if (this.isBelowThreshold()) return
  }

  if (this.canStickyFitInContainer()) {
    this.setLeftPositionWhenFixed()
    this.makeFixed()
  } else {
    this.setLeftPositionWhenAbsolute()
    this.makeAbsolute()
  }
}

module.exports = PositionSticky
