import { defineComponent, h, PropType } from 'vue'

import { CElementCover } from '../element-cover'
import { CVirtualScroller } from '../virtual-scroller'

import { getNextSibling, getPreviousSibling } from '../../utils'

import type { Option, OptionsGroup } from './types'
import { getOptionLabel, highlightSubstring, isOptionDisabled, isOptionSelected } from './utils'

const CAutocompleteOptions = defineComponent({
  name: 'CAutocompleteOptions',
  props: {
    highlightOptionsOnSearch: Boolean,
    loading: Boolean,
    options: {
      type: Array as PropType<(Option | OptionsGroup)[]>,
      required: true,
    },
    optionsMaxHeight: [Number, String] as PropType<number | string>,
    scopedSlots: Object,
    searchNoResultsLabel: [Boolean, String] as PropType<boolean | string>,
    searchValue: String,
    selected: [Object, String, null] as PropType<Option | null>,
    virtualScroller: Boolean,
    visible: Boolean,
    visibleItems: {
      type: Number,
      default: 10,
    },
  },
  emits: ['optionClick'],
  setup(props, { emit }) {
    const handleKeyDown = (event: KeyboardEvent, option: Option) => {
      if (event.code === 'Space' || event.key === 'Enter') {
        event.preventDefault()
        emit('optionClick', option)
      }

      if (event.key === 'Down' || event.key === 'ArrowDown') {
        event.preventDefault()
        const target = event.target as HTMLElement
        const next = getNextSibling(
          target,
          '.autocomplete-option:not(.disabled):not(:disabled)'
        ) as HTMLElement | null

        if (next) {
          next.focus()
        }
      }

      if (event.key === 'Up' || event.key === 'ArrowUp') {
        event.preventDefault()
        const target = event.target as HTMLElement
        const prev = getPreviousSibling(
          target,
          '.autocomplete-option:not(.disabled):not(:disabled)'
        ) as HTMLElement | null

        if (prev) {
          prev.focus()
        }
      }

      if (event.key === 'Home' || event.key === 'End') {
        event.preventDefault()
        const target = event.target as HTMLElement
        const container = target.closest('.autocomplete-options') as HTMLElement | null

        if (!container) {
          return
        }

        const first = event.key === 'Home'
        const focusEdgeOption = () => {
          const options = container.querySelectorAll<HTMLElement>(
            '.autocomplete-option:not(.disabled):not(:disabled)'
          )

          if (options.length > 0) {
            options[first ? 0 : options.length - 1].focus()
          }
        }

        if (props.virtualScroller) {
          // The virtualized viewport only mounts a window of options — scroll it to
          // the edge so the true first/last options render, then focus after the
          // window has re-rendered (scroll event → state update → commit).
          container.scrollTop = first ? 0 : container.scrollHeight
          requestAnimationFrame(() => requestAnimationFrame(focusEdgeOption))
          return
        }

        focusEdgeOption()
      }
    }

    const createOption = (option: Option, index: number) =>
      h(
        'div',
        {
          class: [
            'autocomplete-option',
            {
              disabled: isOptionDisabled(option),
              selected: isOptionSelected(option, props.selected || null),
            },
          ],
          key: index,
          onClick: () => emit('optionClick', option),
          onKeydown: (event: KeyboardEvent) => handleKeyDown(event, option),
          onMousedown: (event: MouseEvent) => event.preventDefault(),
          role: 'option',
          'aria-selected': isOptionSelected(option, props.selected || null) ? 'true' : 'false',
          ...(isOptionDisabled(option) && { 'aria-disabled': 'true' }),
          tabindex: 0,
          ...(props.highlightOptionsOnSearch &&
            !props.scopedSlots?.['options'] && {
              innerHTML: highlightSubstring(getOptionLabel(option), props.searchValue),
            }),
        },
        props.highlightOptionsOnSearch
          ? undefined
          : props.scopedSlots && props.scopedSlots['options']
            ? h(props.scopedSlots['options'], { option: option })
            : getOptionLabel(option)
      )

    const createOptions = (options: (Option | OptionsGroup)[]) => {
      if (options.length === 0 && props.searchNoResultsLabel) {
        return h(
          'div',
          { class: 'autocomplete-options-empty', role: 'status' },
          props.searchNoResultsLabel
        )
      }

      return options.map((option: Option | OptionsGroup, index: number) => {
        if (typeof option !== 'string' && 'options' in option) {
          return h('div', { key: index }, [
            h('div', { class: 'autocomplete-optgroup-label' }, [
              props.scopedSlots && props.scopedSlots['options-groups']
                ? h(props.scopedSlots['options-groups'], { option: option })
                : option.label,
            ]),
            ...(option.options?.map((opt: Option, idx: number) => createOption(opt, idx)) || []),
          ])
        }

        return createOption(option as Option, index)
      })
    }

    return () => [
      props.visible && props.virtualScroller && props.options.length > 0
        ? h(
            CVirtualScroller,
            {
              class: 'autocomplete-options',
              visibleItems: props.visibleItems,
            },
            {
              default: () => createOptions(props.options),
            }
          )
        : h(
            'div',
            {
              class: 'autocomplete-options',
              ...(props.optionsMaxHeight !== 'auto' && {
                style: { maxHeight: props.optionsMaxHeight, overflow: 'scroll' },
              }),
            },
            createOptions(props.options)
          ),
      props.loading && h(CElementCover),
    ]
  },
})

export { CAutocompleteOptions }
