# Autocomplete — accessible headless suggestion input
#
# Like Combobox but the input value IS the value (no selection from a list).
# Suggestions are shown as the user types; selecting a suggestion fills the input.
# Ships zero CSS.
#
# Usage:
#   Autocomplete value <=> city, items: cities, @filter: filterCities

acCollator = new Intl.Collator(undefined, { sensitivity: 'base' })

export Autocomplete = component
  @value := ''
  @items := []
  @placeholder := 'Type to search...'
  @disabled := false

  open := false

  filteredItems ~=
    q = @value.trim()
    return @items unless q
    @items.filter (item) ->
      label = if typeof item is 'string' then item else (item.label or item.name or String(item))
      acCollator.compare(label.slice(0, q.length), q) is 0

  _listId =! "ac-list-#{Math.random().toString(36).slice(2, 8)}"

  _getItems: ->
    return [] unless @_list
    Array.from(@_list.querySelectorAll('[role="option"]'))

  _updateHighlight: ->
    idx = @_hlIdx
    opts = @_getItems()
    opts.forEach (el, ndx) ->
      el.id = "#{@_listId}-opt-#{ndx}" unless el.id
      el.toggleAttribute 'data-highlighted', ndx is idx
    activeId = if idx >= 0 and opts[idx] then opts[idx].id else undefined
    if @_input
      if activeId then @_input.setAttribute 'aria-activedescendant', activeId
      else @_input.removeAttribute 'aria-activedescendant'
    opts[idx]?.scrollIntoView({ block: 'nearest' })

  openMenu: ->
    open = true
    @_hlIdx = -1
    setTimeout => @_position(), 0

  close: ->
    open = false
    @_hlIdx = -1

  _position: ->
    return unless @_input and @_list
    tr = @_input.getBoundingClientRect()
    @_list.style.position = 'fixed'
    @_list.style.left = "#{tr.left}px"
    @_list.style.top = "#{tr.bottom + 2}px"
    @_list.style.minWidth = "#{tr.width}px"
    fl = @_list.getBoundingClientRect()
    if fl.bottom > window.innerHeight
      @_list.style.top = "#{tr.top - fl.height - 2}px"

  selectIndex: (idx) ->
    item = filteredItems[idx]
    return unless item
    label = if typeof item is 'string' then item else (item.label or item.name or String(item))
    @value = label
    @_input?.value = label
    @emit 'select', item
    @close()

  onInput: (e) ->
    newVal = e.target.value
    return if newVal is @value
    @value = newVal
    open = true
    @_hlIdx = if filteredItems.length > 0 then 0 else -1
    setTimeout =>
      @_position()
      @_updateHighlight()
    , 0

  onKeydown: (e) ->
    len = filteredItems.length
    switch e.key
      when 'ArrowDown'
        e.preventDefault()
        @openMenu() unless open
        if len
          @_hlIdx = (@_hlIdx + 1) %% len
          @_updateHighlight()
      when 'ArrowUp'
        e.preventDefault()
        @openMenu() unless open
        if len
          @_hlIdx = if @_hlIdx <= 0 then len - 1 else @_hlIdx - 1
          @_updateHighlight()
      when 'Enter'
        e.preventDefault()
        @selectIndex(@_hlIdx) if @_hlIdx >= 0
      when 'Escape'
        e.preventDefault()
        @close()
      when 'Tab'
        @close()

  ~>
    if open
      onDown = (e) =>
        unless @_input?.contains(e.target) or @_list?.contains(e.target)
          @close()
      document.addEventListener 'mousedown', onDown
      return -> document.removeEventListener 'mousedown', onDown

  mounted: ->
    @_hlIdx = -1
    @_input.value = @value if @_input and @value

  render
    . $open: open?!

      input ref: "_input", role: "combobox", type: "text"
        autocomplete: "off"
        aria-expanded: !!open
        aria-haspopup: "listbox"
        aria-autocomplete: "list"
        aria-controls: open ? _listId : undefined
        $disabled: @disabled?!
        disabled: @disabled
        placeholder: @placeholder
        @input: @onInput

      if open and filteredItems.length > 0
        div ref: "_list", id: _listId, role: "listbox", $open: true, style: "position:fixed"
          for item, idx in filteredItems
            div role: "option", tabindex: "-1"
              @click: (=> @selectIndex(idx))
              @mouseenter: (=> @_hlIdx = idx; @_updateHighlight())
              "#{if typeof item is 'string' then item else (item.label or item.name or String(item))}"
