// Styles
import './VSelect.sass'

// Components
import { VDialogTransition } from '@/components/transitions'
import { VAvatar } from '@/components/VAvatar'
import { VCheckboxBtn } from '@/components/VCheckbox'
import { VChip } from '@/components/VChip'
import { VDefaultsProvider } from '@/components/VDefaultsProvider'
import { VDivider } from '@/components/VDivider'
import { VIcon } from '@/components/VIcon'
import { useInputIcon } from '@/components/VInput/InputIcon'
import { VList, VListItem, VListSubheader } from '@/components/VList'
import { VMenu } from '@/components/VMenu'
import { VSheet } from '@/components/VSheet'
import { makeVTextFieldProps, VTextField } from '@/components/VTextField/VTextField'
import { VVirtualScroll } from '@/components/VVirtualScroll'
import { VHighlight } from '@/labs/VHighlight'

// Composables
import { useFocusRepair } from './useFocusRepair'
import { useScrolling } from './useScrolling'
import { useFocusGroups } from '../../composables/focusGroups'
import { useAutocomplete } from '@/composables/autocomplete'
import { makeFilterProps, useFilter } from '@/composables/filter'
import { useForm } from '@/composables/form'
import { forwardRefs } from '@/composables/forwardRefs'
import { IconValue } from '@/composables/icons'
import { makeItemsProps, useItems } from '@/composables/list-items'
import { useLocale } from '@/composables/locale'
import { makeMenuActivatorProps, useMenuActivator } from '@/composables/menuActivator'
import { useProxiedModel } from '@/composables/proxiedModel'
import { makeTransitionProps } from '@/composables/transition'

// Utilities
import { computed, mergeProps, nextTick, ref, shallowRef, toRef, watch } from 'vue'
import {
  camelizeProps,
  checkPrintable,
  deepEqual,
  ensureValidVNode,
  genericComponent,
  IN_BROWSER,
  matchesSelector,
  omit,
  propsFactory,
  useRender,
  wrapInArray,
} from '@/util'

// Types
import type { Component, PropType, Ref } from 'vue'
import type { VFieldSlots } from '@/components/VField/VField'
import type { VInputSlots } from '@/components/VInput/VInput'
import type { ListItem } from '@/composables/list-items'
import type { GenericProps, SelectItemKey } from '@/util'

type Primitive = string | number | boolean | symbol

type Val <T, ReturnObject extends boolean> = [T] extends [Primitive]
  ? T
  : (ReturnObject extends true ? T : any)

type Value <T, ReturnObject extends boolean, Multiple extends boolean> =
  Multiple extends true
    ? readonly Val<T, ReturnObject>[]
    : Val<T, ReturnObject> | null

export const makeSelectProps = propsFactory({
  chips: Boolean,
  closableChips: Boolean,
  eager: Boolean,
  hideNoData: Boolean,
  hideSelected: Boolean,
  listProps: {
    type: Object as PropType<VList['$props']>,
  },
  menu: Boolean,
  menuElevation: [Number, String],
  menuIcon: {
    type: IconValue,
    default: '$dropdown',
  },
  menuProps: {
    type: Object as PropType<VMenu['$props']>,
  },
  multiple: Boolean,
  noDataText: {
    type: String,
    default: '$vuetify.noDataText',
  },
  openOnClear: Boolean,
  itemColor: String,
  noAutoScroll: Boolean,

  ...makeMenuActivatorProps(),
  ...makeItemsProps({ itemChildren: false }),
}, 'Select')

export const makeVSelectProps = propsFactory({
  search: String,

  ...makeFilterProps({ filterKeys: ['title'] }),
  ...makeSelectProps(),
  ...omit(makeVTextFieldProps({
    modelValue: null,
    role: 'combobox',
  }), ['validationValue', 'dirty']),
  ...makeTransitionProps({ transition: { component: VDialogTransition as Component } }),
}, 'VSelect')

type ItemType<T> = T extends readonly (infer U)[] ? U : never

export const VSelect = genericComponent<new <
  T extends readonly any[],
  Item = ItemType<T>,
  ReturnObject extends boolean = false,
  Multiple extends boolean = false,
  V extends Value<Item, ReturnObject, Multiple> = Value<Item, ReturnObject, Multiple>
>(
  props: {
    items?: T
    itemTitle?: SelectItemKey<ItemType<T>>
    itemValue?: SelectItemKey<ItemType<T>>
    itemProps?: SelectItemKey<ItemType<T>>
    returnObject?: ReturnObject
    multiple?: Multiple
    modelValue?: V | null
    'onUpdate:modelValue'?: (value: V) => void
  },
  slots: Omit<VInputSlots & VFieldSlots, 'default'> & {
    item: { item: Item, internalItem: ListItem<Item>, index: number, props: Record<string, unknown> }
    chip: { item: Item, internalItem: ListItem<Item>, index: number, props: Record<string, unknown> }
    selection: { item: Item, internalItem: ListItem<Item>, index: number }
    subheader: { props: Record<string, unknown>, index: number }
    divider: { props: Record<string, unknown>, index: number }
    'prepend-item': never
    'append-item': never
    'no-data': never
    'menu-header': { search: Ref<string | undefined>, filteredItems: ListItem<Item>[] }
    'menu-footer': { search: Ref<string | undefined>, filteredItems: ListItem<Item>[] }
  }
) => GenericProps<typeof props, typeof slots>>()({
  name: 'VSelect',

  props: makeVSelectProps(),

  emits: {
    'update:focused': (focused: boolean) => true,
    'update:modelValue': (value: any) => true,
    'update:menu': (ue: boolean) => true,
    'update:search': (value: string) => true,
  },

  setup (props, { slots }) {
    const { t } = useLocale()
    const vTextFieldRef = ref<VTextField>()
    const vMenuRef = ref<VMenu>()
    const headerRef = ref<HTMLElement>()
    const footerRef = ref<HTMLElement>()
    const vVirtualScrollRef = ref<VVirtualScroll>()
    const { items, transformIn, transformOut } = useItems(props)
    const search = useProxiedModel(props, 'search', '')
    const { filteredItems, getMatches } = useFilter(props, items, () => search.value)
    const model = useProxiedModel(
      props,
      'modelValue',
      [],
      v => transformIn(v === null ? [null] : wrapInArray(v)),
      v => {
        const transformed = transformOut(v)
        return props.multiple ? transformed : (transformed[0] ?? null)
      }
    )
    const counterValue = computed(() => {
      return typeof props.counterValue === 'function' ? props.counterValue(model.value)
        : typeof props.counterValue === 'number' ? props.counterValue
        : model.value.length
    })
    const form = useForm(props)
    const autocomplete = useAutocomplete(props)
    const selectedValues = computed(() => model.value.map(selection => selection.value))
    const isFocused = shallowRef(false)
    const closableChips = toRef(() => props.closableChips && !form.isReadonly.value && !form.isDisabled.value)
    const { InputIcon } = useInputIcon(props)

    let keyboardLookupPrefix = ''
    let keyboardLookupIndex = 0
    let keyboardLookupLastTime: number
    let openedByKeyboard = false
    let openedByArrow: 'next' | 'prev' | null = null

    const displayItems = computed(() => {
      const baseItems = search.value ? filteredItems.value : items.value
      if (props.hideSelected) {
        return baseItems.filter(item => !model.value.some(s => (props.valueComparator || deepEqual)(s, item)))
      }
      return baseItems
    })

    const menuDisabled = computed(() => (
      (props.hideNoData && !displayItems.value.length) ||
      form.isReadonly.value || form.isDisabled.value
    ))
    const _menu = useProxiedModel(props, 'menu')
    const menu = computed({
      get: () => _menu.value,
      set: v => {
        if (_menu.value && !v && vMenuRef.value?.ΨopenChildren.size) return
        if (v && menuDisabled.value) return
        _menu.value = v
      },
    })

    const { menuId, ariaExpanded, ariaControls } = useMenuActivator(props, menu)

    const computedMenuProps = computed(() => {
      return {
        ...props.menuProps,
        activatorProps: {
          ...(props.menuProps?.activatorProps || {}),
          'aria-haspopup': 'listbox', // Set aria-haspopup to 'listbox'
        },
      }
    })

    const listRef = ref<VList>()
    const listEvents = useScrolling(listRef, vTextFieldRef)
    const repairOrphanedFocus = useFocusRepair(
      menu,
      () => vMenuRef.value?.contentEl,
      () => vTextFieldRef.value?.controlRef,
    )
    const { onTabKeydown } = useFocusGroups({
      groups: [
        { type: 'element' as const, contentRef: headerRef },
        { type: 'list' as const, contentRef: listRef, displayItemsCount: () => displayItems.value.length },
        { type: 'element' as const, contentRef: footerRef },
      ],
      onLeave: () => {
        menu.value = false
        vTextFieldRef.value?.focus()
      },
    })

    function onClear (e: MouseEvent | KeyboardEvent) {
      if (props.openOnClear) {
        menu.value = true
      }
    }
    function onMousedownControl () {
      if (menuDisabled.value) return

      openedByKeyboard = false
      openedByArrow = null
      menu.value = !menu.value
    }

    function onMenuKeydown (e: KeyboardEvent) {
      if (e.key === 'Tab') {
        onTabKeydown(e)
      }

      if (listRef.value?.$el.contains(e.target) && checkPrintable(e)) {
        onKeydown(e)
      }
    }

    function onKeydown (e: KeyboardEvent) {
      if (!e.key || form.isReadonly.value) return

      if (['Enter', ' ', 'ArrowDown', 'ArrowUp', 'Home', 'End'].includes(e.key)) {
        e.preventDefault()
      }

      if (['ArrowDown', 'ArrowUp'].includes(e.key)) {
        openedByArrow = e.key === 'ArrowDown' ? 'next' : 'prev'
      } else if (['Enter', ' '].includes(e.key)) {
        openedByArrow = null
      }

      if (['Enter', 'ArrowDown', ' '].includes(e.key)) {
        openedByKeyboard = true
        menu.value = true
      }

      if (['Escape', 'Tab'].includes(e.key)) {
        menu.value = false
      }

      if (props.clearable && e.key === 'Backspace') {
        e.preventDefault()
        model.value = []
        onClear(e)
        return
      }

      if (e.key === 'Home') {
        listRef.value?.focus('first')
      } else if (e.key === 'End') {
        listRef.value?.focus('last')
      }

      // html select hotkeys
      const KEYBOARD_LOOKUP_THRESHOLD = 1000 // milliseconds

      if (!checkPrintable(e)) return

      const now = performance.now()
      if (now - keyboardLookupLastTime > KEYBOARD_LOOKUP_THRESHOLD) {
        keyboardLookupPrefix = ''
        keyboardLookupIndex = 0
      }
      keyboardLookupPrefix += e.key.toLowerCase()
      keyboardLookupLastTime = now

      const items = displayItems.value
      function findItem () {
        let result = findItemBase()
        if (result) return result

        if (keyboardLookupPrefix.at(-1) === keyboardLookupPrefix.at(-2)) {
          // No matches but we have a repeated letter, try the next item with that prefix
          keyboardLookupPrefix = keyboardLookupPrefix.slice(0, -1)
          keyboardLookupIndex++
          result = findItemBase()
          if (result) return result
        }

        // Still nothing, wrap around to the top
        keyboardLookupIndex = 0
        result = findItemBase()
        if (result) return result

        // Still nothing, try just the new letter
        keyboardLookupPrefix = e.key.toLowerCase()
        return findItemBase()
      }
      function findItemBase () {
        for (let i = keyboardLookupIndex; i < items.length; i++) {
          const _item = items[i]
          if (_item.title.toLowerCase().startsWith(keyboardLookupPrefix)) {
            return [_item, i] as const
          }
        }
        return undefined
      }

      const result = findItem()
      if (!result) return

      const [item, index] = result
      keyboardLookupIndex = index
      listRef.value?.focus(index)
      if (!props.multiple) {
        model.value = [item]
      }
    }

    /** @param set - null means toggle */
    function select (item: ListItem, set: boolean | null = true) {
      if (item.props.disabled) return

      if (props.multiple) {
        const index = model.value.findIndex(selection => (props.valueComparator || deepEqual)(selection.value, item.value))
        const add = set == null ? !~index : set

        if (~index) {
          const value = add ? [...model.value, item] : [...model.value]
          value.splice(index, 1)
          model.value = value
        } else if (add) {
          model.value = [...model.value, item]
        }
      } else {
        const add = set !== false
        model.value = add ? [item] : []

        nextTick(() => {
          menu.value = false
        })
      }
    }
    function onBlur (e: FocusEvent) {
      const target = e.target as Element
      if (!vTextFieldRef.value?.$el.contains(target)) {
        menu.value = false
      }
    }
    function getSelectedIndex () {
      return displayItems.value.findIndex(
        item => model.value.some(s => (props.valueComparator || deepEqual)(s.value, item.value))
      )
    }
    function focusSelectedItem (options?: FocusOptions) {
      // Virtual list only mounts a window — numeric index into DOM children is wrong
      const selected = listRef.value?.$el?.querySelector?.('[aria-selected="true"]') as HTMLElement | null
      if (!selected) return false
      selected.focus(options)
      return true
    }
    function onAfterEnter () {
      if (props.eager) {
        vVirtualScrollRef.value?.calculateVisibleItems()
      }
      if (!listRef.value || !isFocused.value) return

      // VMenu re-dispatches ArrowUp/Down after open and already moved focus to next/prev
      if (listRef.value.$el?.contains(document.activeElement)) return

      const opts: FocusOptions = { focusVisible: false, preventScroll: props.noAutoScroll }

      // fallback for VMenu's re-dispatch; fires before the virtual list has scrolled
      if (openedByArrow) {
        listRef.value.focus(openedByArrow, opts)
        return
      }

      if (focusSelectedItem(opts)) return

      if (openedByKeyboard) {
        listRef.value.focus('first', opts)
      }
    }
    function onAfterLeave () {
      search.value = ''

      if (isFocused.value) {
        if (vMenuRef.value?.contentEl?._clickOutside?.lastMousedownWasOutside) {
          isFocused.value = false
        } else {
          vTextFieldRef.value?.focus()
        }
      }
    }
    function onFocusin (e: FocusEvent) {
      isFocused.value = true
    }
    function onFocusout (e: FocusEvent) {
      if (
        !vTextFieldRef.value?.$el.contains(e.relatedTarget as Node) &&
        !(e.currentTarget as HTMLElement).contains(e.relatedTarget as Node)
      ) {
        if (repairOrphanedFocus(e)) return
        isFocused.value = false
      }
    }
    function onModelUpdate (v: any) {
      if (v == null) model.value = []
      else if (matchesSelector(vTextFieldRef.value, ':autofill') || matchesSelector(vTextFieldRef.value, ':-webkit-autofill')) {
        const item = items.value.find(item => item.title === v)
        if (item) {
          select(item)
        }
      } else if (vTextFieldRef.value) {
        vTextFieldRef.value.value = ''
      }
    }

    watch(menu, val => {
      if (!val) {
        openedByKeyboard = false
        openedByArrow = null
      }

      if (!props.hideSelected && menu.value && model.value.length) {
        const index = getSelectedIndex()
        IN_BROWSER && !props.noAutoScroll && window.requestAnimationFrame(() => {
          index >= 0 && vVirtualScrollRef.value?.scrollToIndex(index)
        })
      }
    })

    watch(items, (newVal, oldVal) => {
      if (menu.value) return

      if (isFocused.value && props.hideNoData && !oldVal.length && newVal.length) {
        menu.value = true
      }
    })

    useRender(() => {
      const hasChips = !!(props.chips || slots.chip)
      const hasList = !!(
        (!props.hideNoData || displayItems.value.length) ||
        slots['prepend-item'] ||
        slots['append-item'] ||
        slots['no-data']
      )
      const isDirty = model.value.length > 0
      const textFieldProps = VTextField.filterProps(props)

      const placeholder = isDirty || (
        !isFocused.value &&
        props.label &&
        !props.persistentPlaceholder
      ) ? undefined : props.placeholder

      const menuSlotProps = {
        search,
        filteredItems: filteredItems.value,
      }

      return (
        <VTextField
          ref={ vTextFieldRef }
          { ...textFieldProps }
          modelValue={ model.value.map(v => v.props.title).join(', ') }
          name={ undefined }
          onUpdate:modelValue={ onModelUpdate }
          v-model:focused={ isFocused.value }
          validationValue={ model.externalValue }
          counterValue={ counterValue.value }
          dirty={ isDirty }
          class={[
            'v-select',
            {
              'v-select--active-menu': menu.value,
              'v-select--chips': !!props.chips,
              [`v-select--${props.multiple ? 'multiple' : 'single'}`]: true,
              'v-select--selected': model.value.length,
              'v-select--selection-slot': !!slots.selection,
            },
            props.class,
          ]}
          style={ props.style }
          inputmode="none"
          placeholder={ placeholder }
          onClick:clear={ onClear }
          onMousedown:control={ onMousedownControl }
          onBlur={ onBlur }
          onKeydown={ onKeydown }
          aria-expanded={ ariaExpanded.value }
          aria-controls={ ariaControls.value }
        >
          {{
            ...slots,
            default: ({ id }) => (
              <>
                <select
                  hidden
                  multiple={ props.multiple }
                  name={ autocomplete.fieldName.value }
                >
                  { items.value.map(item => (
                    <option
                      key={ item.value }
                      value={ item.value }
                      selected={ selectedValues.value.includes(item.value) }
                    />
                  ))}
                </select>

                <VMenu
                  id={ menuId.value }
                  ref={ vMenuRef }
                  v-model={ menu.value }
                  activator="parent"
                  captureFocus={ false }
                  disabled={ menuDisabled.value }
                  eager={ props.eager }
                  maxHeight={ 310 }
                  openOnClick={ false }
                  closeOnContentClick={ false }
                  transition={ props.transition }
                  onAfterEnter={ onAfterEnter }
                  onAfterLeave={ onAfterLeave }
                  { ...computedMenuProps.value }
                  contentClass={['v-select__content', computedMenuProps.value.contentClass]}
                >
                  <VSheet
                    elevation={ props.menuElevation }
                    onFocusin={ onFocusin }
                    onFocusout={ onFocusout }
                    onKeydown={ onMenuKeydown }
                  >
                    { slots['menu-header'] && (
                      <header ref={ headerRef }>
                        { slots['menu-header'](menuSlotProps) }
                      </header>
                    )}

                    { hasList && (
                      <VList
                        key="select-list"
                        ref={ listRef }
                        selected={ selectedValues.value }
                        selectStrategy={ props.multiple ? 'independent' : 'single-independent' }
                        tabindex="-1"
                        selectable={ !!displayItems.value.length }
                        aria-live="polite"
                        aria-labelledby={ `${id.value}-label` }
                        aria-multiselectable={ props.multiple }
                        color={ props.itemColor ?? props.color }
                        { ...listEvents }
                        { ...props.listProps }
                      >
                        { slots['prepend-item']?.() }

                        { !displayItems.value.length && !props.hideNoData && (slots['no-data']?.() ?? (
                          <VListItem key="no-data" title={ t(props.noDataText) } />
                        ))}

                        <VVirtualScroll ref={ vVirtualScrollRef } renderless items={ displayItems.value } itemKey="value">
                          { ({ item, index, itemRef }) => {
                            const camelizedProps = camelizeProps(item.props)

                            const itemProps = mergeProps(item.props, {
                              ref: itemRef,
                              key: item.value,
                              onClick: () => select(item, null),
                              'aria-posinset': index + 1,
                              'aria-setsize': displayItems.value.length,
                            })

                            if (item.type === 'divider') {
                              return slots.divider?.({ props: item.raw, index }) ?? (
                                <VDivider { ...item.props } key={ `divider-${index}` } />
                              )
                            }

                            if (item.type === 'subheader') {
                              return slots.subheader?.({ props: item.raw, index }) ?? (
                                <VListSubheader { ...item.props } key={ `subheader-${index}` } />
                              )
                            }

                            return slots.item?.({
                              item: item.raw,
                              internalItem: item,
                              index,
                              props: itemProps,
                            }) ?? (
                              <VListItem { ...itemProps } role="option">
                                {{
                                  prepend: ({ isSelected }) => (
                                    <>
                                      { props.multiple && !props.hideSelected ? (
                                        <VCheckboxBtn
                                          key={ item.value }
                                          modelValue={ isSelected }
                                          ripple={ false }
                                          tabindex="-1"
                                          aria-hidden
                                          onClick={ (event: MouseEvent) => event.preventDefault() }
                                        />
                                      ) : undefined }

                                      { camelizedProps.prependAvatar && (
                                        <VAvatar image={ camelizedProps.prependAvatar } />
                                      )}

                                      { camelizedProps.prependIcon && (
                                        <VIcon icon={ camelizedProps.prependIcon } />
                                      )}
                                    </>
                                  ),
                                  title: () => {
                                    return search.value
                                      ? (
                                        <VHighlight
                                          text={ item.title }
                                          matches={ getMatches(item)?.title }
                                          markClass="v-select__mask"
                                          matchAll
                                          ignoreCase
                                        />
                                      )
                                      : item.title
                                  },
                                }}
                              </VListItem>
                            )
                          }}
                        </VVirtualScroll>

                        { slots['append-item']?.() }
                      </VList>
                    )}

                    { slots['menu-footer'] && (
                      <footer ref={ footerRef }>
                        { slots['menu-footer'](menuSlotProps) }
                      </footer>
                    )}
                  </VSheet>
                </VMenu>

                { model.value.map((item, index) => {
                  function onChipClose (e: Event) {
                    e.stopPropagation()
                    e.preventDefault()

                    select(item, false)
                  }

                  const slotProps = mergeProps(VChip.filterProps(item.props), {
                    'onClick:close': onChipClose,
                    onKeydown (e: KeyboardEvent) {
                      if (e.key !== 'Enter' && e.key !== ' ') return

                      e.preventDefault()
                      e.stopPropagation()

                      onChipClose(e)
                    },
                    onMousedown (e: MouseEvent) {
                      e.preventDefault()
                      e.stopPropagation()
                    },
                    modelValue: true,
                    'onUpdate:modelValue': undefined,
                  })

                  const hasSlot = hasChips ? !!slots.chip : !!slots.selection
                  const slotContent = hasSlot
                    ? ensureValidVNode(
                      hasChips
                        ? slots.chip!({ item: item.raw, internalItem: item, index, props: slotProps })
                        : slots.selection!({ item: item.raw, internalItem: item, index })
                    )
                    : undefined

                  if (hasSlot && !slotContent) return undefined

                  return (
                    <div key={ item.value } class="v-select__selection">
                      { hasChips ? (
                        !slots.chip ? (
                          <VChip
                            key="chip"
                            closable={ closableChips.value }
                            size="small"
                            text={ item.title }
                            disabled={ item.props.disabled }
                            { ...slotProps }
                          />
                        ) : (
                          <VDefaultsProvider
                            key="chip-defaults"
                            defaults={{
                              VChip: {
                                closable: closableChips.value,
                                size: 'small',
                                text: item.title,
                              },
                            }}
                          >
                            { slotContent }
                          </VDefaultsProvider>
                        )
                      ) : (
                        slotContent ?? (
                          <span class="v-select__selection-text">
                            { item.title }
                            { props.multiple && (index < model.value.length - 1) && (
                              <span class="v-select__selection-comma">,</span>
                            )}
                          </span>
                        )
                      )}
                    </div>
                  )
                })}
              </>
            ),
            'append-inner': (...args) => (
              <>
                { slots['append-inner']?.(...args) }
                { props.menuIcon ? (
                  <VIcon
                    class="v-select__menu-icon"
                    color={ vTextFieldRef.value?.fieldIconColor }
                    icon={ props.menuIcon }
                    aria-hidden
                  />
                ) : undefined }
                { props.appendInnerIcon && (
                  <InputIcon
                    key="append-icon"
                    name="appendInner"
                    color={ args[0].iconColor.value }
                  />
                )}
              </>
            ),
          }}
        </VTextField>
      )
    })

    return forwardRefs({
      isFocused,
      menu,
      search,
      filteredItems,
      select,
    }, vTextFieldRef)
  },
})

export type VSelect = InstanceType<typeof VSelect>
