React = require 'react'
ReactDOM = require 'react-dom'
_ = require 'lodash'

MultiSelectOption = React.createFactory(require './multi_select_option')
SearchInput = React.createFactory(require './search_input')
{makeGuid} = require '../utils'

{div, button, ul, li} = React.DOM

###
Multi Select Props

@props.options - REQUIRED - Array
  An array of options for the user to click - can be a flat array (where each entry is both the value and label)
  or an array of objects, where the value is the `valueField` and the label is the `labelField` (see below)

@props.values - OPTIONAL - Array
  A flat array of values that the control initializes to. If the optons arrya is objects, each entry should be the 
  `valueField` value. Will default to an empty array

@props.labelField - OPTIONAL - String
  The attribute name from each option object that is the 'value`

@props.valueField - OPTIONAL - String
  The attribute name from each option object that is the user faciing label

@props.onChange - OPTIONAL - function
  Function/method to fire when the data changes

@props.filter - OPTIONAL - Boolean - default: 'auto'
  Show text filter? auto will show it whenever the options list is longer than 4

@props.allowDefault - OPTIONAL - Boolean - default: false
  Allow one entry to be set as the default

@props.valueOfDefault - OPTIONAL - used in conjunction w/ allowDefault
  The value of the option that is currently set as the default

@props.searchPlaceholder - OPTIONAL
  placeholder text for the add button

@props.editPlaceholder - OPTIONAL
  placeholder text for the filter field


@props.tabIndex - OPTIONAL
  tab order of the edi button

@props.returnFullObjects - OPTIONAL - Boolean - default: false
  whether or not the getFormData method should return a collection of selected objects or a flat array

@props.onRemove - OPTIONAL - function
  function call when an item is removed, will pass the removed item
###

MultiSelect = React.createClass
  
  displayName: 'MultiSelect'

  contextTypes:
    clearValidationError: React.PropTypes.func
    addValidationError: React.PropTypes.func
    getValidationStatus: React.PropTypes.func
    toggleValidationError: React.PropTypes.func
    
  getDefaultProps: ->
    {
      filter: 'auto'
      allowDefault: false
      editPlaceholder: 'Edit Items'
      searchPlaceholder: 'Filter Items'
      tabIndex: -1
      returnFullObjects: no
      isInPopover: no
      onRemove: ->
    }


  render: ->
    {selected, notSelected, isActive, theDefault, filterTerm, valueHasChanged} = @state
    {editPlaceholder, searchPlaceholder, allowDefault, filter, tabIndex, disabled} = @props
    {getValidationStatus} = @context
    {error, forceShowAllErrors} = getValidationStatus(@inputId)
    
    isValid = not error?

    outerClass = 'multiselect field-wrap' 
    outerClass += ' invalid' if not isValid and (valueHasChanged or forceShowAllErrors)

    selectedOptions = []
    otherOptions = []
    
    selectedOptions.push(MultiSelectOption {
      key: option.value
      option: option
      allowDefault: allowDefault
      isActive: isActive
      setValues: @setValues
      disabled: disabled
      setDefault: @setDefault
      tabIndex: tabIndex
      isTheDefault: option is theDefault
      onRemove: @props.onRemove
      onBlur: if i is 0 then @blur else null
    }) for option, i in selected

    if selectedOptions.length is 0
      selectedOptions.push(li {
        key: 'none'
        className: 'multiselect-none'
      }, ['None'])

    visible = _.filter notSelected, {isVisible: yes}

    if isActive
      otherOptions.push(MultiSelectOption {
        key: option.value
        option: option
        allowDefault: allowDefault
        isActive: isActive
        tabIndex: tabIndex
        setValues: @setValues
        onBlur: if i is visible.length - 1 then @blur else null
      }) for option, i in visible

    buttonText = "+ #{editPlaceholder}"

    div {
      className: outerClass
      onClick: @toggleOn
    }, [
      ul {
        key: 'selectedList'
        ref: 'selectedList'
        className: 'multiselect-list-in'
      }, selectedOptions
      SearchInput {
        ref: 'filterField'
        key: 'filter-input'
        placeholder: searchPlaceholder
        handleChange: @handlefilter
        wrapClass: 'multi-select-filter'
        width: '100%'
        focusOnMount: yes
        disabled: disabled
        tabIndex: tabIndex
        term: filterTerm
      } if @filterShouldBeShown()
      button {
        key: 'addButton'
        className: 'multiselect-edit'
        tabIndex: tabIndex
        onFocus: @toggleOn
      }, buttonText unless isActive or disabled
      ul {
        key: 'notSelectedList'
        ref: 'notSelectedList'
        className: 'multiselect-list-out'
      }, otherOptions if isActive
      div {
        key: 'errors'
        className: 'field-errors-show'
        ref: 'errorAnchor'
        onMouseOver: @handleErrorMouseOver
        onMouseOut: @handleErrorMouseOut
      }
    ]

  componentWillMount: ->
    # create a unique id for this input
    # used by the validation framework to group validation errors
    @inputId = makeGuid()


  componentDidMount: -> 
    document.addEventListener('click', @blur)
    {selected} = @state
    {validation} = @props
    
    @validate validation, selected

  componentWillReceiveProps: (nextProps) ->
    {validation} = nextProps
    {selected} = @state
    
    validationChanged = (
      if typeof validation is 'function' then no
      else not _.isEqual(validation?.messages, @props.validation?.messages)
    )

    # Run validaiton if the validation changes
    if validationChanged then @validate(validation, selected) 

  
  componentWillUnmount: -> 
    document.removeEventListener('click', @blur)
    @context.clearValidationError @inputId
  
  getInitialState: ->
    {options, values, labelField, valueField, valueOfDefault, allowDefault} = @props
    
    @allOptions = [] 

    for option in options
      newOption =
        isSelected: false
        isVisible: true
      if typeof option is 'object'
        unless (valueField? and labelField?) or (option.value? and options.label?)
          return console?.error 'MultiSelect requires labelField and valueField props when the options array is made up of objects'
        newOption.value = option[valueField]
        newOption.label = option[labelField]
      else 
        newOption.label = option.toString()
        newOption.value = option

      newOption.isSelected = true if values? and values.indexOf(newOption.value) isnt -1

      @allOptions.push newOption

      theDefault = _.find(@allOptions, {value: valueOfDefault})
      selected = _.filter(@allOptions, {isSelected: true})

      unless theDefault?
        if selected.length? then theDefault = selected[0]
    
    {
      selected: selected or []
      notSelected: _.filter(@allOptions, {isSelected: false}) or []
      isActive: false
      theDefault: theDefault
      filterTerm: ''
      valueHasChanged: no
    }

  getValue: -> @getFormData()

  getFormData: ->
    selectedValues = _.map(_.filter(@allOptions, {isSelected: true}), 'value')
    unless @props.returnFullObjects then return selectedValues
      
    (option for option in @props.options when selectedValues.indexOf(option[@props.valueField]) isnt -1)

  toggleOn: (e) ->
    if @props.disabled then return
    e.nativeEvent.stopImmediatePropagation?()
    if not @state.isActive
      @setState
        isActive: true
      , =>
        @focusFirstOption() unless @filterShouldBeShown()

  focusFirstOption: ->
    # Find the first option in the list and focus it
    # This is done because of keyboard navigation with TAB key
    # When tabbing around, the dom re-renders and resets the tab order once the options are visible
    firstUnselected = @refs.notSelectedList.getElementsByTagName('button')[0]
    firstSelected = @refs.selectedList.getElementsByTagName('button')[0]

    if firstUnselected? then firstUnselected.focus()

    # If there's no unselected first item, focus the first selected item
    else firstSelected.focus()


  blur: ->
    @setState
      isActive: false

  setDefault: (newDefault) ->
    @setState
      theDefault: newDefault
    

  setValues: () ->
    {theDefault} = @state
    {validation} = @props
    
    selected = _.filter(@allOptions, {isSelected: true})

    if selected.indexOf(theDefault) is -1
      if selected.length then theDefault = selected[0]

    @setState
      selected: _.filter(@allOptions, {isSelected: true})
      notSelected: _.filter(@allOptions, {isSelected: false})
      theDefault: theDefault
      valueHasChanged: yes
    , ->
      if @filterShouldBeShown() then @refs.filterField.focus()
      else @focusFirstOption()

      @props.onChange?()
      @validate validation, @state.selected

  handlefilter: (term) ->
    notSelected = _.filter(@allOptions, {isSelected: false})
    if term is ''
      item.isVisible = true for item in @allOptions
    else 
      for item in notSelected
        if item.label.toLowerCase().search(term.toLowerCase()) isnt -1 then item.isVisible = true
        else item.isVisible = false

    @setState
      filterTerm: term
      notSelected: notSelected

  filterShouldBeShown: ->
    @state.isActive and (@props.filter is true or (@props.filter is 'auto' and @allOptions.length > 4))

  validate: (validation, value) ->
    return if validation is off 

    # Run validation and show any auto show messages
    if typeof validation is 'function' then validationError = validation(value)
    # validation can also be passed as a static array
    else validationError = validation

    {isInPopover} = @props
    {addValidationError, clearValidationError} = @context

    if validationError? then addValidationError(@refs.errorAnchor, validationError, @inputId, isInPopover)
    else clearValidationError(@inputId, isInPopover)

  handleErrorMouseOver: -> 
    {isInPopover} = @props
    {toggleValidationError} = @context
    toggleValidationError @inputId, on, isInPopover

  handleErrorMouseOut: ->
    {isInPopover} = @props
    {toggleValidationError} = @context
    toggleValidationError @inputId, off, isInPopover


module.exports = MultiSelect