###
Instructions for implementing this mixin

Requires 2 properties to be defined in the component it is mixed into:
  @ROW_HEIGHT - defaults to 30, the static height of each list item
  @SPACE_ABOVE_CONTENT - defaults to 68, The space in the app above of the list (for calculating the list height)
  **OPTIONAL
  @SPACE_BELOW_CONTENT - defaults to 0, The space in the app below the list (for calculating the list height)

Require the following props to be passed to the component it is mixed into:
  @props.records - the array of objects being rendered into the list
  @props.loadNextRecordset - a method that triggers the server GET for the next 'page' of records

Additional info
  After a records GET is complete, the doneWithFetch method must be called
  EG, if the component with this mixin is the 'results' ref, you would call the following from it's parent
  @refs.results.doneWithFetch({recordsExhausted, newSearch})
  The options object has two boolean attributes:
    recordsExhausted - means there are no more records to fetch from the server
    newSearch - means a new list is being rendered, and the scroll states should be reset
  
For examples of what the rende method of the results component should look like, look at one of the components that implements this mixin.


Public Methods to call from within render method

calculateRange
returns an object with start and end properties, which is the range of indexes from the records array that should be rendered

buildLoadingRow
returns an li component with a spinner for the end of the list

buildScrollBar
build the verical scrollbar, can pass optionaly className and key props

###
React = require 'react'
Animation = require 'ainojs-animation'
easing = require 'ainojs-easing'
Spinner = React.createFactory(require '../components/spinner')
{HOME, END, PAGE_UP, PAGE_DOWN, UP_ARROW, DOWN_ARROW} = require('../constants/keyboard')

{div, button, li} = React.DOM

SCROLL_THUMB_MIN = 25
HEADER_HEIGHT = 0

module.exports = 
  
  # Scroll bar properties
  numOfScreens: 1
  translate: 0
  height: 0

  getInitialState: ->
    scrollY: 0
    contentAreaHeight: 0
    maxY: 0
    noResults: no
    recordsExhausted: no

  componentWillReceiveProps: (nextProps) ->
    if not nextProps.records.length
      @setState
        noResults: true

  getContentAreaHeight: ->
    return window.innerHeight - @SPACE_ABOVE_CONTENT - @SPACE_BELOW_CONTENT

  calculateContentAreaHeight: -> 
    @setState
      contentAreaHeight: @getContentAreaHeight()

  calculateMaxY: (visibleRowCount, recordsExhausted = no) ->
    if visibleRowCount?
      buffer = if recordsExhausted then 0 else @ROW_HEIGHT
      contentHeight = (visibleRowCount * @ROW_HEIGHT) + buffer

    contentAreaHeight = @getContentAreaHeight()

    if contentHeight < contentAreaHeight then return null
    else return (0 - contentHeight) + contentAreaHeight

  componentWillMount: ->
    # Create defaults for required properties if they haven't been defined
    @ROW_HEIGHT = 30 unless @ROW_HEIGHT?
    @SPACE_ABOVE_CONTENT = 68 unless @SPACE_ABOVE_CONTENT?
    @SPACE_BELOW_CONTENT = 0 unless @SPACE_BELOW_CONTENT?
    @oldLength = null

  componentDidMount: ->
    @calculateContentAreaHeight()
    window.addEventListener('resize', @calculateContentAreaHeight)
    window.addEventListener('keydown', @handleScrollKeys)

  componentWillUnmount: ->
    window.removeEventListener('resize', @calculateContentAreaHeight)
    window.removeEventListener('keydown', @handleScrollKeys)

    if @animation?.isAnimating() then @animation.end()

  checkContentPosition: (e) ->
    # Only run this method if there are already results populated
    unless @props.records.length then return

    @handleScrollY(e) if e?

    # Once at the bottom 250px of the page, fetch more records
    # Only fetch if records aren't exhausted
    {scrollY, maxY, recordsExhausted} = @state
    {records} = @props
    if scrollY - maxY <= 250
      newLength = records.length
      if newLength isnt @oldLength and not recordsExhausted
        @oldLength = newLength
        @props.loadNextRecordset()

  doneWithFetch: (options) ->
    {recordsExhausted, newSearch} = options

    state =
      maxY: @calculateMaxY(@props.records.length, recordsExhausted)
      recordsExhausted: recordsExhausted
    
    if newSearch
      state.scrollY = 0
      @oldLength = null

    @setState state


  calculateRange: (records, buffer = 1) ->
    {contentAreaHeight, scrollY} = @state

    # The number of rows that can fit in the content area, with some buffer
    start = Math.floor(-scrollY / @ROW_HEIGHT) - buffer
    end = Math.ceil((-(scrollY - contentAreaHeight)) / @ROW_HEIGHT) + buffer

    {
      start: if start >= 0 then start else 0
      end: if end <= records.length then end else records.length
    }


  buildLoadingRow: (index) ->
    li {
      key: 'loadingRow'
      className: 'grid grid-pad full result-row loading-row'
      style:
        height: @ROW_HEIGHT
        top: @ROW_HEIGHT * index
    }, Spinner {length: 5}


  # Scroll Bar Methods

  handleScrollKeys: (e) ->
    {keyCode, metaKey, ctrlKey} = e
    return unless keyCode in [HOME, END, PAGE_UP, PAGE_DOWN, UP_ARROW, DOWN_ARROW]
    
    switch keyCode 
      when HOME then @animateScrollTo 0
      when END then @animateScrollTo @state.maxY
      when PAGE_UP then @page 1
      when PAGE_DOWN then @page -1
      when UP_ARROW
        if metaKey or ctrlKey then @page 1
        else @handleScrollY {deltaY: -@ROW_HEIGHT}, @checkContentPosition
      when DOWN_ARROW
        if metaKey or ctrlKey then @page -1
        else @handleScrollY {deltaY: @ROW_HEIGHT}, @checkContentPosition
  
  page: (direction) ->
    {scrollY, maxY} = @state
    pageDistance = window.innerHeight - @SPACE_ABOVE_CONTENT - @SPACE_BELOW_CONTENT
    newScrollPos = scrollY + (pageDistance * direction)
    newScrollPos = if newScrollPos > 0 then 0 else newScrollPos
    newScrollPos = if newScrollPos < maxY then maxY else newScrollPos
    @animateScrollTo newScrollPos   

  animateScrollTo: (newScrollPos) ->
    if @animation?.isAnimating() then @animation.end()
    
    {scrollY} = @state 

    @animation = new Animation 
      duration: 300
      easing: easing('easeOutCirc')
    .init {scrollY}
    .on 'frame', @onFrame
    .on 'complete', @checkContentPosition
    .animateTo {scrollY: newScrollPos}

  onFrame: (e) ->
    @setState e.values

  clickScroll: (e) ->
    @scrollPos = e.clientY
    document.addEventListener('mousemove', @handleMouseMoveY, false)
    document.addEventListener('mouseup', @handleMouseUpY, false)

  # Ends a drag of the vertical scroll thumb button
  handleMouseUpY: (e) ->
    document.removeEventListener('mousemove', @handleMouseMoveY, false)
    document.removeEventListener('mouseup', @handleMouseUpY, false)

    @handleScrollY 
      deltaY: 0 - (@scrollPos - e.clientY) * @numOfScreens

    @checkContentPosition()

  # handles drag of the vertical scroll thumb button
  handleMouseMoveY: (e) ->
    @handleScrollY 
      deltaY: 0 - (@scrollPos - e.clientY) * @numOfScreens
    @scrollPos = e.clientY

  handleScrollY: (e, cb) ->
    {scrollY, maxY} = @state
    {deltaY} = e

    newState = {}

    if deltaY isnt 0
      y = scrollY - deltaY
      newY = if y < 0 then y else 0
      newY = if newY > maxY then newY else maxY

      newState.scrollY = newY

    @setState newState, -> cb?()

  handleTrackClick: (e) ->
    {scrollY, maxY} = @state
    {clientY} = e

    deltaY = (->
      if clientY - @SPACE_ABOVE_CONTENT - @SPACE_BELOW_CONTENT > @height + @translate then @height 
      else if clientY - @SPACE_ABOVE_CONTENT  - @SPACE_BELOW_CONTENT < @translate then -@height
      else null
    )()

    unless deltaY? then return

    @handleScrollY 
      deltaY: deltaY * 2

    @scrollPos = clientY


  buildScrollBar: (props = {}) ->
    {className, key} = props
    {scrollY, maxY, contentAreaHeight} = @state

    finalClass = "thumb-btn-vertical-track"
    finalClass += " #{className}" if className?
    
    # Height of the scroll track
    availableHeight = contentAreaHeight

    # Number of screen heights in the content area
    @numOfScreens = (-maxY + contentAreaHeight) / availableHeight if availableHeight

    visibleClass = if @numOfScreens <= 1 then ' is-hidden' else ''

    # Thumb button height
    @height = availableHeight / @numOfScreens

    # 25px is the smallest
    if @height < SCROLL_THUMB_MIN then @height = SCROLL_THUMB_MIN

    # Empty space left in the track when the thumb button is in it
    availableTrackSpace = availableHeight - @height

    # The percent the content is currently scrolled
    scrollPercent = scrollY / maxY

    # How far down to move the scrollBar
    @translate = scrollPercent * availableTrackSpace

    div {
      key: key or 'vScroll'
      className: finalClass
      onClick: @handleTrackClick
    },
      button {
        key: 'thumb-btn-vertical'
        ref: 'scrollButton'
        className: 'thumb-btn-vertical' + visibleClass
        type: 'button'
        style:
          transform: "translateY(#{@translate}px)"
          WebkitTransform: "translateY(#{@translate}px)"
          msTransform: "translateY(#{@translate}px)"
          top: HEADER_HEIGHT + 1
          height: @height
        onMouseDown: @clickScroll
      }
