React = require 'react'
createClass = require 'create-react-class'
PropTypes = require 'prop-types'
find = require 'lodash/find'
{ DOWN_ARROW, RIGHT_ARROW, LEFT_ARROW, UP_ARROW, ENTER, SPACE, ESCAPE, TAB } = require '../constants/keyboard'
{alphaNumericKeyCode} = require '../utils'
InputMixin = require '../mixins/input_mixin'
CircleXButton = React.createFactory(require './circle_x_button')
SelectInputCustomOptions = React.createFactory(require './select_input_custom_options')
Pvr = React.createFactory(require './pvr')

{div, select, option} = require 'react-dom-factories'

OPTION_PADDING = 10

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

  @props.options - [Array] - Required
  Full list of options to display on component

  @props.value - [String|Object] - Optional
  The value that corresponds to the option object with the matching value

  @props.selectText - [String] - Optional
  Text displayed as the default value

  @props.onChange - [Function] - Required
  Function fired when a change is made to the selection

  @props.tabIndex - [Number] - Optional
  Tab order index

  @props.disabled - [Boolean] - Optional
  Disabled state of the component

  @props.isFilter - [Boolean] - Optional
  Show/hide the filter typeahead input. Default is no

  @props.returnFullObjects - [Boolean] - Optional
  Determine whether `getValue` returns the full option object, or just the default value string

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

  @props.valueField - [String] - Optional
  The name of the key used to reference the value on the option object

  @props.labelField - [String] - Optional
  The name of the key used to reference the label on the option object

  @props.width - [Number] - Optional
  The width of the menu popover

  @props.height - [Number] - Optional
  The height of the menu popover

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

SelectInputCustom = createClass

  displayName: 'SelectInputCustom'
  
  mixins: [InputMixin]

  contextTypes:
    openOverlay: PropTypes.func
    closeOverlay: PropTypes.func
    clearValidationError: PropTypes.func
    addValidationError: PropTypes.func
    getValidationStatus: PropTypes.func
    toggleValidationError: PropTypes.func

  propTypes:
    options: PropTypes.array.isRequired
    selectText: PropTypes.string
    onChange: PropTypes.func.isRequired
    tabIndex: PropTypes.number
    disabled: PropTypes.bool
    isFilter: PropTypes.bool
    returnFullObjects: PropTypes.bool
    placeholder: PropTypes.string
    valueField: PropTypes.string
    labelField: PropTypes.string
    width: PropTypes.number
    height: PropTypes.number
    optionHeight: PropTypes.number
    value: PropTypes.oneOfType [
      PropTypes.string
      PropTypes.object
    ]
    nibColor: PropTypes.string


  getDefaultProps: ->
    options: []
    selectText: 'Select from list...'
    placeholder: 'Filter options'
    valueField: 'value'
    labelField: 'title'
    width: 250
    height: 400
    isFilter: no
    nibColor: 'white'
    optionHeight: 20
    returnFullObjects: no

  componentWillMount: ->
    {value} = @props
    @value = value
    @overlayId = null
    
  
  componentWillUnmount: ->
    clearInterval @timer

  render: ->
    {options, id, selectText, valueField, labelField, tabIndex, disabled, isFilter, value, returnFullObjects} = @props
    {valueHasChanged} = @state
    {error, forceShowAllErrors} = @context.getValidationStatus(@inputId)
    isValid = not error?

    outerClass = 'field-wrap filter-select'
    outerClass += " #{wrapperClass}" if wrapperClass?
    outerClass += ' invalid shrink' if not isValid and (valueHasChanged or forceShowAllErrors)
    outerClass += ' x-room' if value and not disabled
    
    optionItems = [
      option {
        key: "none"
        value: ""
      }, selectText
    ]

    options.forEach (o, i) =>
      optionItems.push option {
        key: i
        value: o[valueField]
      }, o[labelField]

    # When returnFullObjects is on, then overwrite the value (which will be ab object), 
    # with it's 'valueField' attribute
    if returnFullObjects and value? then value = value[valueField] or ''

    @value = value

    div {
      className: outerClass
    }, [
      select {
        key: 'select'
        ref: 'input'
        tabIndex: tabIndex
        id: id
        disabled: disabled
        value: value
        onChange: -> # So React doesn't complain about value with no onChange handler
        onMouseDown: @handleNativeEvent
        onKeyDown: @handleNativeEvent
      }, optionItems
      CircleXButton {
        key: 'clear'
        tabIndex: -1
        ref: (clearBtn) => @clearBtn = clearBtn
        onClick: @clearValue
      } if value and not disabled
      div {
        className: 'field-errors-show'
        key: 'textInputErrorsShow'
        ref: 'errorAnchor'
        onMouseOver: @handleErrorMouseOver
        onMouseOut: @handleErrorMouseOut
      }
    ]


  handleNativeEvent: (e) ->
    {keyCode, currentTarget} = e
    {options} = @props
    openKeys = [DOWN_ARROW, RIGHT_ARROW, LEFT_ARROW, UP_ARROW, ENTER, SPACE]
    closeKeys = [ESCAPE, TAB]

    # Handle the keyboard
    if keyCode?
      isOpenKey = openKeys.indexOf(keyCode) > -1
      isCloseKey = closeKeys.indexOf(keyCode) > -1
      alphaNum = alphaNumericKeyCode(keyCode)

      if isOpenKey or alphaNum
        e.preventDefault()
        e.stopPropagation()

        # For alphanumeric input, start filtering the element
        if alphaNum then filter = e.key

        @openOptionsList(currentTarget, filter)

      else if isCloseKey
        e.preventDefault() if keyCode isnt TAB
        if @overlayId?
          @context.closeOverlay {@overlayId} 
          @overlayId = null

      return

    # Handle mouse clicks
    e.preventDefault()
    
    @openOptionsList(currentTarget)

    # Attempt to reload the overlay if the list load after the user opens it
    if options.length is 0 
      @timer = setInterval =>
        if @props.options.length > 0
          @context.closeOverlay {@overlayId} 
          @openOptionsList(currentTarget)
          clearInterval @timer
      , 100

  openOptionsList: (anchor, filter) ->
    {value, options, labelField, valueField, height, width, placeholder, isFilter, nibColor, optionHeight, returnFullObjects} = @props

    if ((options.length + 1) * optionHeight) < height
      height = options.length * (optionHeight + OPTION_PADDING)

    if returnFullObjects and value? then value = value[valueField] or ''

    # Construct the options component
    @overlayId = @context.openOverlay
      component: Pvr
      noBackdrop: true      
      id: "popover"
      props:
        direction: 'below'
        width: width
        height: if height is 0 then 58 else height
        anchor: anchor
        nibColor: nibColor
        animateIn: no
        element: SelectInputCustomOptions {
          key: 'fso'
          options: @props.options
          onChange: @handleValueChange
          labelField: labelField
          valueField: valueField
          placeholder: placeholder
          value: value
          SelectEl: @
          isFilter: isFilter
          searchWidth: width - 10
          optionHeight: optionHeight
          OPTION_PADDING: OPTION_PADDING
          filter: filter
        }


  # Note: using slightly non-standard names for the next 2 methods in order to not conflict with the mixin
  handleValueChange: (value, cb) ->
    {onChange, validation, jsonPath} = @props
    {valueHasChanged} = @state

    @setState {valueHasChanged: yes}, =>
      @validate(validation, value) 
      
      @value = value
      onChange?(@getValue(), jsonPath)
      cb?()
      
      @focus()
      @fireDelayedAction()

  clearValue: -> @handleValueChange('')


module.exports = SelectInputCustom