React = require 'react'
createClass = require 'create-react-class'
PropTypes = require 'prop-types'
SearchInput = React.createFactory(require './search_input')
Spinner = React.createFactory(require './spinner')
{ DOWN_ARROW, UP_ARROW, TAB, ESCAPE, ENTER } = require '../constants/keyboard'

_filter = require 'lodash/filter'


{div, ul, li} = require 'react-dom-factories'

###&
  @general
  Filter select options component. This component lives on the overlay layer, and requires integrated context methods closeOverlay and openOverlay within the application.

  @props.filter - [String] - Optional
  Initialize the component overlay with a filter value. This will start filtering option labels based on this value.

  @props.placeholder - [String] - Optional
  Placeholder value for the filter input

  @props.onChange - [Function] - Required
  Function that is fired when a selection change is made
  
  @props.options - [Array] - Optional
  Array of options to render in the select

  @props.optionHeight - [Number] - Optional
  The fixed height of each menu option

  @props.value - [String|Number] - Optional
  The value of the currently selected option object

  @props.noResultsText - [String] - Optional
  Text displayed in the menu when no results match the filter input value

  @props.SelectEl - [Function] - Optional
  Reference to the select menu component that opens this overlay. If provided, focus will be directed back to the input when closing the overlay

  @props.onChangeFilter - [Function] - Optional
  Function fired when the filter input changes

  @props.searchWidth - [Number] - Optional
  Width of the search input. Default is 250
&###

SelectInputCustomOptions = createClass

  displayName: 'SelectInputCustomOptions'

  contextTypes:
    closeOverlay: PropTypes.func

  propTypes:
    filter: PropTypes.string
    placeholder: PropTypes.string
    onChange: PropTypes.func.isRequired
    options: PropTypes.array
    optionHeight: PropTypes.number
    OPTION_PADDING: PropTypes.number
    value: PropTypes.string
    noResultsText: PropTypes.string
    SelectEl: PropTypes.object
    valueField: PropTypes.string
    labelField: PropTypes.string
    isFilter: PropTypes.bool
    searchWidth: PropTypes.number

  getDefaultProps: ->
    filter: ''
    optionHeight: 20
    isFilter: no
    searchWidth: 250


  getInitialState: ->
    filterValue: ''
    focusedOptionIndex: -1
    options: @props.options.slice(0)

  componentDidMount: ->
    { filter, SelectEl } = @props

    # If using a filter, focus that on mount
    if filter
      @handleFilterChange filter
    else
      SelectEl?.refs.input?.blur()

    if @textInput?
      @textInput.focus()

    # Add arrow key handlers
    window.addEventListener('keydown', @handleKeyDown)

  componentWillUnmount: ->
    window.removeEventListener('keydown', @handleKeyDown)

  render: ->
    passedOptions = @props.options
    { filterValue, options } = @state
    { placeholder, value, noResultsText, isFilter, searchWidth, labelField } = @props
    selectOptions = []

    optionListClass = if isFilter then 'options-list' else 'options-list no-filter'

    # Render options elements
    options.forEach (o, i) =>
      selectOptions.push(@processOption(o, i))

    div {
      className: "select-options"
      onClick: @handleOptionsClick
    }, [
      SearchInput {
        key: 'input'
        ref: (input) => @textInput = input
        id: "filter"
        term: filterValue
        handleChange: @handleFilterChange
        placeholder: placeholder
        wrapClass: 'custom-filter-select'
        width: searchWidth
      } if isFilter
      if passedOptions.length is 0 # Assume loading state when the options lenght is 0
        Spinner {
          key: 'spinner'
          length: 7
          radius: 7
          lines: 12
          width: 2
        }
      else if selectOptions.length is 0 # No mathes to the filter term
        div {
          key: 'no-results'
          className: 'no-results'
        }, if noResultsText? then noResultsText else t 'No matches found'
      else
        ul {
          key: 'options'
          className: optionListClass
          ref: (optionsList) => @optionsList = optionsList
          onScroll: @handleScroll
        }, selectOptions
    ]

  processOption: (opt, index) ->
    { options, focusedOptionIndex } = @state
    { value, optionHeight, labelField, valueField } = @props
    optionClass = "option"

    if index is focusedOptionIndex then optionClass += " is-focused"
    if value is opt[valueField] then optionClass += " is-selected"

    return li {
      key: index
      onClick: @handleClick.bind(@, opt)
      onMouseOver: @handleMouseOver.bind(@, index)
      onMouseOut: @handleMouseOut.bind(@, index)
      className: optionClass
      title: opt[labelField]
      style:
        height: optionHeight
        lineHeight: "#{optionHeight}px"
    }, opt[labelField]


  handleKeyDown: (e) ->
    { options, focusedOptionIndex, isFilter } = @state
    { SelectEl, labelField, valueField } = @props
    adjust = 0

    switch e.keyCode
      when UP_ARROW
        e.preventDefault()
        adjust = -1
        break
      when DOWN_ARROW
        e.preventDefault()
        adjust = 1
        break
      when TAB
        e.preventDefault()
        return @context.closeOverlay({ refocusEl: SelectEl })
      when ESCAPE
        e.preventDefault()
        return @context.closeOverlay({ refocusEl: SelectEl })
      when ENTER
        e.preventDefault()
        currentOption = options[focusedOptionIndex]
        if currentOption?
          return @props.onChange(currentOption[valueField], @context.closeOverlay)
        return @context.closeOverlay({ refocusEl: SelectEl })
      else
        e.stopPropagation()
        break

    newIndex = focusedOptionIndex + adjust
    if newIndex < 0
      newIndex = 0
    else if newIndex >= options.length - 1
      newIndex = options.length - 1

    @setState
      focusedOptionIndex: newIndex
    , =>
      @textInput.focus() if isFilter
      @adjustScrollPosition(newIndex) if @optionsList?
    
  adjustScrollPosition: (newIndex) ->
    { optionHeight, OPTION_PADDING } = @props
    fullHeight = optionHeight + OPTION_PADDING
    { scrollTop, clientHeight } = @optionsList

    newIndexTop = fullHeight * newIndex
    newIndexBottom = newIndexTop + fullHeight
    
    isOffBottom = newIndexBottom > scrollTop + clientHeight
    isOffTop = newIndexTop < scrollTop

    adjust = 0

    if isOffBottom then adjust = newIndexBottom - (scrollTop + clientHeight)
    else if isOffTop then adjust = 0 - (scrollTop - newIndexTop)

    # Adjust the scrollTop position
    @optionsList.scrollTop = scrollTop + adjust if adjust isnt 0

  handleFilterChange: (filterValue) ->
    {options, labelField} = @props
    
    options = (opt for opt in options when opt[labelField].toLowerCase().search(filterValue.toLowerCase()) > -1)

    @setState {filterValue, options}
    , =>
      @props.onChangeFilter?(filterValue)
    
  handleClick: (option, e) ->
    {valueField, onChange} = @props
    onChange(option[valueField], @context.closeOverlay)

  handleMouseOver: (focusedOptionIndex, e) -> @setState {focusedOptionIndex}

  handleMouseOut: (focusedOptionIndex, e) -> @setState {focusedOptionIndex: -1}


  handleOptionsClick: (e) ->
    e.stopPropagation()

  handleScroll: (e) ->
    e.stopPropagation()


module.exports = SelectInputCustomOptions