# 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.
# Uses native `popover="auto"` + anchor positioning.
# 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:: any := null
  @placeholder:: string := "Select..."
  @disabled:: boolean := 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 => @_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) ->
    return unless options.length
    ARIA.listNav e,
      next:    => highlightedIndex = @_nextEnabled(highlightedIndex,  1); @_focusHighlighted()
      prev:    => highlightedIndex = @_nextEnabled(highlightedIndex, -1); @_focusHighlighted()
      first:   => highlightedIndex = 0;                  @_focusHighlighted()
      last:    => highlightedIndex = options.length - 1; @_focusHighlighted()
      select:  => @selectIndex(highlightedIndex)
      dismiss: => @close()
      tab:     => @close()
      char:    => @_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: ->
    el = @_list?.querySelectorAll('[role="option"]')?.[highlightedIndex]
    el?.focus()
    el?.scrollIntoView { block: 'nearest' }

  _applyPlacement: ->
    ARIA.position @_trigger, @_list, placement: 'bottom start', offset: 4, matchWidth: true

  ~>
    if @_list
      @_list.setAttribute 'popover', 'auto'
      @_applyPlacement()
    ARIA.bindPopover open, (=> @_list), ((isOpen) => open = isOpen), (=> @_trigger)

  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
      div ref: "_list", id: _listId, role: "listbox", style: "position:fixed;margin:0;inset:auto"
        $open: open?!
        @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
