# Select — accessible headless select widget
#
# Keyboard: ArrowDown/Up to navigate, Enter/Space to select, Escape to close,
#           Home/End for first/last, typeahead to jump by character.
#
# Exposes $open and $placeholder on button, $highlighted and $selected on options.
# Ships zero CSS — style entirely via attribute selectors in your stylesheet.
#
# Usage:
#   Select value <=> selectedValue, @change: (=> handle(event.detail))
#     option value: "a", "Option A"
#     option value: "b", "Option B"

export Select = component
  @value := null
  @placeholder := 'Select...'
  @disabled := false

  open := false
  highlightedIndex := -1
  typeaheadBuffer := ''
  typeaheadTimer := null
  _listId =! "sel-#{Math.random().toString(36).slice(2, 8)}"

  getOpt: (o) -> o.dataset.value ?? o.value

  options ~=
    return [] unless @_slot
    Array.from(@_slot.querySelectorAll('[data-value], option[value]') or [])

  selectedLabel ~=
    if @value?
      el = options.find (o) -> @getOpt(o) is String(@value)
      el?.textContent?.trim() or String(@value)
    else
      @placeholder

  toggle: ->
    return if @disabled
    if open then @close() else @openMenu()

  openMenu: ->
    open = true
    highlightedIndex = Math.max(0, options.findIndex (o) -> @getOpt(o) is String(@value))
    requestAnimationFrame =>
      @_position()
      @_focusHighlighted()

  close: ->
    open = false
    highlightedIndex = -1
    @_trigger?.focus()

  isDisabled: (opt) -> opt?.hasAttribute?('data-disabled') or opt?.disabled

  selectIndex: (idx) ->
    opt = options[idx]
    return unless opt
    return if @isDisabled(opt)
    @value = @getOpt(opt)
    @emit 'change', @value
    @close()

  onTriggerKeydown: (e) ->
    return if @disabled
    switch e.key
      when 'ArrowDown', 'ArrowUp', 'Enter', ' '
        e.preventDefault()
        @openMenu()
      when 'Escape'
        e.preventDefault()
        @close() if open

  _nextEnabled: (from, dir) ->
    len = options.length
    i = from
    loop len
      i = (i + dir) %% len
      return i unless @isDisabled(options[i])
    from

  onListKeydown: (e) ->
    len = options.length
    return unless len

    switch e.key
      when 'ArrowDown'
        e.preventDefault()
        highlightedIndex = @_nextEnabled(highlightedIndex, 1)
        @_focusHighlighted()
      when 'ArrowUp'
        e.preventDefault()
        highlightedIndex = @_nextEnabled(highlightedIndex, -1)
        @_focusHighlighted()
      when 'Home'
        e.preventDefault()
        highlightedIndex = 0
        @_focusHighlighted()
      when 'End'
        e.preventDefault()
        highlightedIndex = len - 1
        @_focusHighlighted()
      when 'Enter', ' '
        e.preventDefault()
        @selectIndex(highlightedIndex)
      when 'Escape'
        e.preventDefault()
        @close()
      when 'Tab'
        @close()
      else
        if e.key.length is 1
          @_typeahead(e.key)

  _typeahead: (char) ->
    clearTimeout typeaheadTimer if typeaheadTimer
    typeaheadBuffer += char.toLowerCase()
    typeaheadTimer = setTimeout (-> typeaheadBuffer = ''), 500
    idx = options.findIndex (o) -> o.textContent.trim().toLowerCase().startsWith(typeaheadBuffer)
    if idx >= 0
      highlightedIndex = idx
      @_focusHighlighted()

  _focusHighlighted: ->
    opt = options[highlightedIndex]
    opt?.focus()

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

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

  render
    .

      # Button
      button ref: "_trigger", role: "combobox"
        aria-expanded: !!open
        aria-haspopup: "listbox"
        aria-controls: open ? _listId : undefined
        $open: open?!
        $placeholder: (!@value)?!
        $disabled: @disabled?!
        disabled: @disabled
        @click: @toggle
        @keydown: @onTriggerKeydown
        span selectedLabel

      # Hidden slot for reading option definitions
      . ref: "_slot", style: "display:none"
        slot

      # Dropdown listbox
      if open
        div ref: "_list", id: _listId, role: "listbox", style: "position:fixed;visibility:hidden"
          $open: true
          @keydown: @onListKeydown
          for opt, idx in options
            div role: "option"
              tabindex: "-1"
              $value: @getOpt(opt)
              $highlighted: (idx is highlightedIndex)?!
              $selected: (@getOpt(opt) is String(@value))?!
              $disabled: @isDisabled(opt)?!
              aria-selected: @getOpt(opt) is String(@value)
              aria-disabled: @isDisabled(opt)?!
              @click: (=> @selectIndex(idx))
              @mouseenter: (=> highlightedIndex = idx)
              = opt.textContent
