import { computed, defineComponent, h, PropType, provide, ref, watch } from 'vue'

import { CFormControlWrapper } from './../form/CFormControlWrapper'
import { CConditionalTeleport } from '../conditional-teleport'

import { CMultiSelectNativeSelect } from './CMultiSelectNativeSelect'
import { CMultiSelectOptions } from './CMultiSelectOptions'
import { CMultiSelectSelection } from './CMultiSelectSelection'

import { useDropdownWithPopper } from '../../composables'
import { getNextActiveElement, isEqual } from '../../utils'

import {
  createOption,
  filterOptionsList,
  flattenOptionsArray,
  getOptionsList,
  isExternalSearch,
  isGlobalSearch,
  selectOptions,
} from './utils'
import type { Option, OptionsGroup, Search, SelectedOption } from './types'

const CMultiSelect = defineComponent({
  name: 'CMultiSelect',
  props: {
    /**
     * Allow users to create options if they are not in the list of options.
     *
     * @since 4.9.0
     */
    allowCreateOptions: Boolean,
    /**
     * A string that provides an accessible label for the cleaner button. This label is read by screen readers to describe the action associated with the button.
     *
     * @since 5.7.0
     */
    ariaCleanerLabel: {
      type: String,
      default: 'Clear all selections',
    },
    /**
     * Enables selection cleaner element.
     *
     * @default true
     */
    cleaner: {
      type: Boolean,
      default: true,
    },
    /**
     * Appends the dropdown to a specific element. You can pass an HTML element or function that returns a single element.
     *
     * @since 5.7.0
     */
    container: {
      type: [Object, String] as PropType<HTMLElement | (() => HTMLElement) | string>,
      default: 'body',
    },
    /**
     * Clear current search on selecting an item.
     *
     * @since 4.9.0
     */
    clearSearchOnSelect: Boolean,
    /**
     * Toggle the disabled state for the component.
     */
    disabled: Boolean,
    /**
     * Provide valuable, actionable feedback.
     *
     * @since 4.6.0
     */
    feedback: String,
    /**
     * Provide valuable, actionable feedback.
     *
     * @since 4.6.0
     */
    feedbackInvalid: String,
    /**
     * Provide valuable, actionable invalid feedback when using standard HTML form validation which applied two CSS pseudo-classes, `:invalid` and `:valid`.
     *
     * @since 4.6.0
     */
    feedbackValid: String,
    /**
     * Set the id attribute for the native select element.
     *
     * **[Deprecated since v5.3.0]** The name attribute for the native select element is generated based on the `id` property:
     * - `<select name="\{id\}-multi-select" />`
     */
    id: String,
    /**
     * Set component validation state to invalid.
     *
     * @since 4.6.0
     */
    invalid: Boolean,
    /**
     * Add a caption for a component.
     *
     * @since 4.6.0
     */
    label: String,
    /**
     * When set, the options list will have a loading style: loading spinner and reduced opacity.
     *
     * @since 4.9.0
     */
    loading: Boolean,
    /**
     * It specifies that multiple options can be selected at once.
     *
     * @default true
     */
    multiple: {
      type: Boolean,
      default: true,
    },
    /**
     * The name attribute for the select element.
     *
     * @since 5.3.0
     */
    name: String,
    /**
     * List of option elements.
     */
    options: {
      type: Array as PropType<(Option | OptionsGroup)[]>,
      default: () => [],
    },
    /**
     * Sets maxHeight of options list.
     *
     * @default 'auto'
     */
    optionsMaxHeight: {
      type: [Number, String],
      default: 'auto',
    },
    /**
     * Sets option style.
     *
     * @values 'checkbox', 'text'
     * @default 'checkbox'
     */
    optionsStyle: {
      type: String,
      default: 'checkbox',
      validator: (value: string) => {
        return ['checkbox', 'text'].includes(value)
      },
    },
    /**
     * Specifies a short hint that is visible in the search input.
     *
     * @default 'Select...''
     */
    placeholder: {
      type: String,
      default: 'Select...',
    },
    /**
     * When it is present, it indicates that the user must choose a value before submitting the form.
     */
    required: Boolean,
    /**
     * Determines whether the selected options should be cleared when the options list is updated. When set to true, any previously selected options will be reset whenever the options list undergoes a change. This ensures that outdated selections are not retained when new options are provided.
     *
     * @since 5.3.0
     */
    resetSelectionOnOptionsChange: Boolean,
    /**
     * The `search` prop determines how the search input element is enabled and behaves. It accepts multiple types to provide flexibility in configuring search behavior:
     *
     * - `true` : Enables the default search input element with standard behavior.
     * - `'external'`: Enables an external search mechanism, possibly integrating with external APIs or services.
     * - `'global'`: When set, the user can perform searches across the entire component, regardless of where their focus is within the component.
     * - `{ external?: boolean; global?: boolean }`: Allows for granular control over the search behavior by specifying individual properties.  It is useful when you also want to use external and global search.
     */
    search: {
      type: [Boolean, String, Object] as PropType<Search>,
      default: true,
      validator: (value: boolean | object | string) => {
        if (typeof value == 'boolean') {
          return true
        }
        if (typeof value == 'string') {
          return ['external', 'global'].includes(value)
        }
        if (typeof value === 'object' && value !== null) {
          // Ensure that all keys are either 'external' or 'global'
          const validKeys = ['external', 'global']
          const keys = Object.keys(value)
          const allKeysValid = keys.every((key) => validKeys.includes(key))

          if (!allKeysValid) {
            return false
          }

          // Ensure that all values corresponding to the keys are boolean
          const allValuesBoolean = keys.every(
            (key) => typeof value[key as keyof typeof value] === 'boolean'
          )

          return allValuesBoolean
        }
        return false
      },
    },
    /**
     * Sets the label for no results when filtering.
     */
    searchNoResultsLabel: {
      type: String,
      default: 'no items',
    },
    /**
     * Enables select all button.
     *
     * @default true
     */
    selectAll: {
      type: Boolean,
      default: true,
    },
    /**
     * Sets the select all button label.
     *
     * @default 'Select all options'
     */
    selectAllLabel: {
      type: String,
      default: 'Select all options',
    },
    /**
     * Sets the selection style.
     *
     * @values 'counter', 'tags', 'text'
     * @default 'tags'
     */
    selectionType: {
      type: String,
      default: 'tags',
      validator: (value: string) => {
        return ['counter', 'tags', 'text'].includes(value)
      },
    },
    /**
     * Sets the counter selection label.
     *
     * @default 'item(s) selected'
     */
    selectionTypeCounterText: {
      type: String,
      default: 'item(s) selected',
    },
    /**
     * Size the component small or large.
     *
     * @values 'sm', 'lg'
     */
    size: {
      type: String,
      validator: (value: string) => {
        return ['sm', 'lg'].includes(value)
      },
    },
    /**
     * Generates dropdown menu using Teleport.
     *
     * @since 5.7.0
     */
    teleport: {
      type: [Boolean],
      default: false,
    },
    /**
     * Add helper text to the component.
     *
     * @since 4.6.0
     */
    text: String,
    /**
     * Display validation feedback in a styled tooltip.
     *
     * @since 4.6.0
     */
    tooltipFeedback: Boolean,
    /**
     * Set component validation state to valid.
     *
     * @since 4.6.0
     */
    valid: Boolean,
    /**
     * Sets the initially selected values for the multi-select component.
     *
     * @since 5.11.0
     */
    value: [String, Number, Array] as PropType<string | number | (string | number)[]>,
    /**
     * Enable virtual scroller for the options list.
     *
     * @since 4.8.0
     */
    virtualScroller: Boolean,
    /**
     * Toggle the visibility of multi select dropdown.
     *
     * @default false
     */
    visible: Boolean,
    /**
     *
     * Amount of visible items when virtualScroller is set to `true`.
     *
     * @since 4.8.0
     */
    visibleItems: {
      type: Number,
      default: 10,
    },
  },
  emits: [
    /**
     * Execute a function when a user changes the selected option. [docs]
     */
    'change',
    /**
     * Execute a function when the filter value changed.
     *
     * @since 4.7.0
     */
    'filterChange',
    /**
     * The callback is fired when the Multi Select component requests to be hidden.
     */
    'hide',
    /**
     * The callback is fired when the Multi Select component requests to be shown.
     */
    'show',
  ],
  setup(props, { attrs, emit, slots }) {
    const multiSelectRef = ref<HTMLDivElement>()
    const nativeSelectRef = ref<HTMLSelectElement>()
    const searchRef = ref<HTMLInputElement>()

    const searchValue = ref('')
    const selected = ref<SelectedOption[]>([])
    const userOptions = ref<Option[]>([])

    const {
      dropdownMenuElement,
      dropdownRefElement,
      isOpen,
      closeDropdown,
      openDropdown,
      toggleDropdown,
      updatePopper,
    } = useDropdownWithPopper()

    provide('nativeSelectRef', nativeSelectRef)

    const filteredOptions = computed(() =>
      flattenOptionsArray(
        isExternalSearch(props.search)
          ? [...props.options, ...filterOptionsList(searchValue.value, userOptions.value)]
          : filterOptionsList(searchValue.value, [...props.options, ...userOptions.value]),
        true
      )
    )

    const flattenedOptions = computed(() => {
      return flattenOptionsArray(props.options).map((option) => {
        if (props.value && Array.isArray(props.value)) {
          return {
            ...option,
            selected: props.value.includes(option.value),
          }
        }

        if (props.value === option.value) {
          return {
            ...option,
            selected: true,
          }
        }

        return option
      })
    })

    const userOption = computed(() => {
      if (
        props.allowCreateOptions &&
        filteredOptions.value.some(
          (option) => option.label && option.label.toLowerCase() === searchValue.value.toLowerCase()
        )
      ) {
        return false
      }

      return searchRef.value && createOption(String(searchValue.value), flattenedOptions.value)
    })

    watch(
      flattenedOptions,
      () => {
        if (props.resetSelectionOnOptionsChange) {
          selected.value = []
          return
        }

        const _selected = flattenedOptions.value.filter(
          (option: Option | OptionsGroup) => option.selected === true
        )
        const deselected = flattenedOptions.value.filter(
          (option: Option | OptionsGroup) => option.selected === false
        ) as Option[]

        if (_selected.length > 0) {
          const newSelectedValue = selectOptions(
            props.multiple,
            _selected,
            selected.value,
            deselected
          )
          if (!isEqual(newSelectedValue, selected.value)) {
            selected.value = newSelectedValue
          }
        }
      },
      { immediate: true }
    )

    watch(selected, () => {
      nativeSelectRef.value?.dispatchEvent(new Event('change', { bubbles: true }))

      updatePopper()
    })

    watch(
      () => props.visible,
      (visible) => {
        if (visible) {
          openDropdown()
        } else {
          closeDropdown()
        }
      },
      {
        immediate: true,
      }
    )

    watch(isOpen, () => {
      if (isOpen.value) {
        emit('show')

        if (props.teleport && dropdownMenuElement.value && dropdownRefElement.value) {
          dropdownMenuElement.value.style.minWidth = `${(dropdownRefElement.value as HTMLElement).offsetWidth}px`
        }

        searchRef.value?.focus()
        return
      }

      emit('hide')
      searchValue.value = ''
      if (searchRef.value) {
        searchRef.value.value = ''
      }
    })

    const handleSearchChange = (event: InputEvent) => {
      const target = event.target as HTMLInputElement
      searchValue.value = target.value.toLowerCase()

      emit('filterChange', target.value)
    }

    const handleSearchKeyDown = (event: KeyboardEvent) => {
      if (!isOpen.value) {
        openDropdown()
      }

      if (
        event.key === 'ArrowDown' &&
        dropdownMenuElement.value &&
        searchRef.value &&
        searchRef.value.value.length === searchRef.value.selectionStart
      ) {
        event.preventDefault()

        const items = getOptionsList(dropdownMenuElement.value)
        const target = event.target as HTMLDivElement

        getNextActiveElement(
          items,
          target,
          event.key === 'ArrowDown',
          !items.includes(target)
        ).focus()
        return
      }

      if (event.key === 'Enter' && searchValue.value && props.allowCreateOptions) {
        event.preventDefault()

        if (!userOption.value) {
          selected.value = [
            ...selected.value,
            filteredOptions.value.find(
              (option) => String(option.label).toLowerCase() === searchValue.value.toLowerCase()
            ) as Option,
          ]
        }

        if (userOption.value) {
          selected.value = [...selected.value, ...userOption.value]
          userOptions.value = [...userOptions.value, ...userOption.value]
        }

        searchValue.value = ''

        if (searchRef.value) {
          searchRef.value.value = ''
        }

        return
      }

      if (searchValue.value.length > 0) {
        return
      }

      if (event.key === 'Backspace' || event.key === 'Delete') {
        const last = selected.value.filter((option: Option) => !option.disabled).pop()

        if (last) {
          selected.value = selected.value.filter((option: Option) => option.value !== last.value)
        }
      }
    }

    const handleTogglerKeyDown = (event: KeyboardEvent) => {
      if (!isOpen.value && (event.key === 'Enter' || event.key === 'ArrowDown')) {
        event.preventDefault()
        openDropdown()
        return
      }

      if (isOpen && dropdownMenuElement.value && event.key === 'ArrowDown') {
        event.preventDefault()
        const items = getOptionsList(dropdownMenuElement.value)
        const target = event.target as HTMLDivElement

        getNextActiveElement(
          items,
          target,
          event.key === 'ArrowDown',
          !items.includes(target)
        ).focus()
      }
    }

    const handleGlobalSearch = (event: KeyboardEvent) => {
      if (
        isGlobalSearch(props.search) &&
        searchRef.value &&
        (event.key.length === 1 || event.key === 'Backspace' || event.key === 'Delete')
      ) {
        searchRef.value.focus()
      }
    }

    const handleOnOptionClick = (option: Option) => {
      if (!props.multiple) {
        selected.value = [option] as SelectedOption[]
        closeDropdown()

        if (searchRef.value) {
          searchRef.value.value = ''
        }

        return
      }

      if (option.custom && !userOptions.value.some((_option) => _option.value === option.value)) {
        userOptions.value = [...userOptions.value, option]
      }

      if (props.clearSearchOnSelect || option.custom) {
        searchValue.value = ''
        if (searchRef.value) {
          searchRef.value.value = ''
          searchRef.value.focus()
        }
      }

      if (selected.value.some((_option) => _option.value === option.value)) {
        selected.value = selected.value.filter((_option) => _option.value !== option.value)
      } else {
        selected.value = [...selected.value, option] as SelectedOption[]
      }
    }

    const handleSelectAll = () => {
      selected.value = selectOptions(
        props.multiple,
        [
          ...flattenedOptions.value.filter((option: Option | OptionsGroup) => !option.disabled),
          ...userOptions.value,
        ],
        selected.value
      )
    }

    const handleDeselectAll = () => {
      selected.value = selected.value.filter((option) => option.disabled)
    }

    return () => [
      h(CMultiSelectNativeSelect, {
        id: props.id,
        multiple: props.multiple,
        name: props.name,
        options: selected.value,
        required: props.required,
        value: props.multiple
          ? selected.value.map((option: SelectedOption) => option.value.toString())
          : selected.value.map((option: SelectedOption) => option.value)[0],
        onChange: () => emit('change', selected.value),
      }),
      h(
        CFormControlWrapper,
        {
          ...(typeof attrs['aria-describedby'] === 'string' && {
            describedby: attrs['aria-describedby'],
          }),
          feedback: props.feedback,
          feedbackInvalid: props.feedbackInvalid,
          feedbackValid: props.feedbackValid,
          id: props.id,
          invalid: props.invalid,
          label: props.label,
          text: props.text,
          tooltipFeedback: props.tooltipFeedback,
          valid: props.valid,
        },
        {
          default: () =>
            h(
              'div',
              {
                class: [
                  'form-multi-select',
                  {
                    disabled: props.disabled,
                    [`form-multi-select-${props.size}`]: props.size,
                    'is-invalid': props.invalid,
                    'is-valid': props.valid,
                    show: isOpen.value,
                  },
                ],
                onKeydown: handleGlobalSearch,
                'aria-expanded': isOpen.value,
                ref: multiSelectRef,
              },
              {
                default: () => [
                  h(
                    'div',
                    {
                      class: 'form-multi-select-input-group',
                      ...(!props.search && !props.disabled && { tabIndex: 0 }),
                      onClick: () => {
                        if (!props.disabled) {
                          openDropdown()
                        }
                      },
                      onKeydown: handleTogglerKeyDown,
                      ref: dropdownRefElement,
                    },
                    {
                      default: () => [
                        h(
                          CMultiSelectSelection,
                          {
                            disabled: props.disabled,
                            multiple: props.multiple,
                            placeholder: props.placeholder,
                            onRemove: (option: Option) =>
                              !props.disabled && handleOnOptionClick(option),
                            search: props.search,
                            selected: selected.value,
                            selectionType: props.selectionType,
                            selectionTypeCounterText: props.selectionTypeCounterText,
                          },
                          {
                            default: () =>
                              props.search
                                ? h('input', {
                                    type: 'text',
                                    class: 'form-multi-select-search',
                                    disabled: props.disabled,
                                    autocomplete: 'off',
                                    onInput: (event: InputEvent) => handleSearchChange(event),
                                    onKeydown: (event: KeyboardEvent) => handleSearchKeyDown(event),
                                    ...(selected.value.length === 0 && {
                                      placeholder: props.placeholder,
                                    }),
                                    ...(selected.value.length > 0 &&
                                      props.selectionType === 'counter' && {
                                        placeholder: `${selected.value.length} ${props.selectionTypeCounterText}`,
                                      }),
                                    ...(selected.value.length > 0 &&
                                      !props.multiple && {
                                        placeholder: selected.value.map(
                                          (option) => option.label
                                        )[0],
                                      }),
                                    ...(props.multiple &&
                                      selected.value.length > 0 &&
                                      props.selectionType !== 'counter' && {
                                        size: searchValue.value.length + 2,
                                      }),
                                    ref: searchRef,
                                  })
                                : selected.value.length === 0 &&
                                  h(
                                    'span',
                                    {
                                      class: 'form-multi-select-placeholder',
                                    },
                                    {
                                      default: () => props.placeholder,
                                    }
                                  ),
                          }
                        ),
                        h(
                          'div',
                          { class: 'form-multi-select-buttons' },
                          {
                            default: () => [
                              !props.disabled &&
                                props.cleaner &&
                                selected.value.length > 0 &&
                                h('button', {
                                  class: 'form-multi-select-cleaner',
                                  onClick: () => handleDeselectAll(),
                                  type: 'button',
                                  'aria-label': props.ariaCleanerLabel,
                                }),
                              h('button', {
                                class: 'form-multi-select-indicator',
                                onClick: (event: Event) => {
                                  event.preventDefault()
                                  event.stopPropagation()

                                  if (!props.disabled) {
                                    toggleDropdown()
                                  }
                                },
                                type: 'button',
                                ...(props.disabled && { tabIndex: -1 }),
                              }),
                            ],
                          }
                        ),
                      ],
                    }
                  ),
                  h(
                    CConditionalTeleport,
                    {
                      container: props.container,
                      teleport: props.teleport,
                    },
                    {
                      default: () =>
                        h(
                          'div',
                          {
                            class: [
                              'form-multi-select-dropdown',
                              {
                                show: props.teleport && isOpen.value,
                              },
                            ],
                            onKeydown: handleGlobalSearch,
                            role: 'menu',
                            ref: dropdownMenuElement,
                          },
                          {
                            default: () => [
                              props.multiple &&
                                props.selectAll &&
                                h(
                                  'button',
                                  {
                                    class: 'form-multi-select-all',
                                    onClick: () => handleSelectAll(),
                                    type: 'button',
                                  },
                                  props.selectAllLabel
                                ),
                              h(CMultiSelectOptions, {
                                loading: props.loading,
                                onOptionClick: (option: Option) => handleOnOptionClick(option),
                                options:
                                  filteredOptions.value.length === 0 && props.allowCreateOptions
                                    ? userOption.value || []
                                    : filteredOptions.value,
                                optionsMaxHeight: props.optionsMaxHeight,
                                optionsStyle: props.optionsStyle,
                                scopedSlots: slots,
                                searchNoResultsLabel: props.searchNoResultsLabel,
                                selected: selected.value,
                                virtualScroller: props.virtualScroller,
                                visibleItems: props.visibleItems,
                              }),
                            ],
                          }
                        ),
                    }
                  ),
                ],
              }
            ),
        }
      ),
    ]
  },
})

export { CMultiSelect }
