<script lang="ts" setup>
import { useVirtualList } from '@vueuse/core'

export type MultiSelectRawValueType = Array<string | number>

export type RawValueType = string | number | MultiSelectRawValueType

interface ListItem {
  value?: RawValueType
  label?: string
  [key: string]: any
}

/**
 * Props interface for the List component
 */
interface Props {
  /** The currently selected value */
  value: RawValueType
  /** The list of items to display */
  list: ListItem[]
  /**
   * The key to use for accessing the value from a list item
   * @default 'value'
   */
  optionValueKey?: string
  /**
   * The key to use for accessing the label from a list item
   * @default 'label'
   */
  optionLabelKey?: string
  /** Whether the list is open or closed */
  open?: boolean
  /**
   * Whether to close the list after an item is selected
   * @default true
   */
  closeOnSelect?: boolean
  /** Placeholder text for the search input */
  searchInputPlaceholder?: string
  /** Whether to show the currently selected option */
  showSelectedOption?: boolean
  /**
   * The height of each item in the list, used for virtual list rendering.
   * @default 38
   */
  itemHeight?: number
  /** Custom filter function for list items */
  filterOption?: (input: string, option: ListItem, index: Number) => boolean
  /**
   * Indicates whether the component allows multiple selections.
   */
  isMultiSelect?: boolean
  /**
   * The minimum number of items required in the list to enable search functionality.
   */
  minItemsForSearch?: number
}

interface Emits {
  (e: 'update:value', value: RawValueType): void
  (e: 'update:open', open: boolean): void
  (e: 'change', option: ListItem): void
}

const props = withDefaults(defineProps<Props>(), {
  open: false,
  closeOnSelect: true,
  searchInputPlaceholder: '',
  showSelectedOption: true,
  optionValueKey: 'value',
  optionLabelKey: 'label',
  itemHeight: 38,
  isMultiSelect: false,
  minItemsForSearch: 4,
})

const emits = defineEmits<Emits>()

const vModel = useVModel(props, 'value', emits, {
  defaultValue: props.isMultiSelect ? ([] as MultiSelectRawValueType) : undefined,
})

const vOpen = useVModel(props, 'open', emits)

const { optionValueKey, optionLabelKey } = props

const { closeOnSelect, showSelectedOption } = toRefs(props)

const listRef = ref<HTMLDivElement>()

const searchQuery = ref('')

const inputRef = ref()

const activeOptionIndex = ref(-1)

const showHoverEffectOnSelectedOption = ref(true)

const isSearchEnabled = computed(() => props.list.length > props.minItemsForSearch)

const keyDown = ref(false)

/**
 * Computed property that filters the list of options based on the search query.
 * If a custom filter function is provided via props.filterOption, it will be used instead of the default filtering logic.
 *
 * @returns Filtered list of options
 *
 * @typeparam ListItem - The type of items in the list
 */
const list = computed(() => {
  const query = searchQuery.value.toLowerCase()

  return props.list.filter((item, i) => {
    if (props?.filterOption) {
      return props.filterOption(query, item, i)
    } else {
      return item[optionLabelKey]?.toLowerCase()?.includes(query)
    }
  })
})

const {
  list: virtualList,
  containerProps,
  wrapperProps,
  scrollTo,
} = useVirtualList(list, {
  itemHeight: props.itemHeight,
})

/**
 * Compares the given value with the current vModel value.
 * If the component is in multi-select mode, it checks if the value is included in the vModel array.
 * Otherwise, it performs a direct equality check.
 *
 * @param value - The value to compare with the vModel value.
 * @returns {boolean} - True if the value matches the vModel value, false otherwise.
 */
function compareVModel(value: string | number): boolean {
  if (props.isMultiSelect) {
    return (vModel.value as MultiSelectRawValueType).includes(value)
  }

  return vModel.value === value
}

/**
 * Resets the hover effect on the selected option
 * @param clearActiveOption - Whether to clear the active option index
 */
const handleResetHoverEffect = (clearActiveOption = false, newActiveIndex?: number) => {
  if ((clearActiveOption && keyDown.value) || (clearActiveOption && activeOptionIndex.value === newActiveIndex)) {
    return
  }

  if (clearActiveOption && showHoverEffectOnSelectedOption.value) {
    activeOptionIndex.value = -1
  }

  if (newActiveIndex !== undefined) {
    activeOptionIndex.value = newActiveIndex
  }

  if (!showHoverEffectOnSelectedOption.value) return

  showHoverEffectOnSelectedOption.value = false
}

/**
 * Handles the selection of an option from the list
 *
 * @param option - The selected list item
 *
 * This function is responsible for handling the selection of an option from the list.
 * It updates the model value, emits a change event, and optionally closes the dropdown.
 */
const handleSelectOption = (option: ListItem, index?: number) => {
  if (!option?.[optionValueKey]) return

  if (index !== undefined) {
    activeOptionIndex.value = index
  }

  if (props.isMultiSelect) {
    if ((vModel.value as MultiSelectRawValueType).includes(option?.[optionValueKey])) {
      vModel.value = (vModel.value as MultiSelectRawValueType).filter((op) => op !== option?.[optionValueKey])
    } else {
      vModel.value = [...(vModel.value as MultiSelectRawValueType), option?.[optionValueKey]]
    }
  } else {
    vModel.value = option[optionValueKey] as RawValueType
  }

  emits('change', option)
  if (closeOnSelect.value) {
    vOpen.value = false
  }
}

/**
 * Automatically scrolls to the active option in the list
 */
const handleAutoScrollOption = (useDelay = false) => {
  if (activeOptionIndex.value === -1) return

  if (!useDelay) {
    scrollTo(activeOptionIndex.value)
    return
  }

  setTimeout(() => {
    scrollTo(activeOptionIndex.value)
  }, 150)
}

const onArrowDown = () => {
  keyDown.value = true
  handleResetHoverEffect()

  if (activeOptionIndex.value === list.value.length - 1) return

  activeOptionIndex.value = Math.min(activeOptionIndex.value + 1, list.value.length - 1)
  handleAutoScrollOption()

  ncDelay(100).then(() => {
    keyDown.value = false
  })
}

const onArrowUp = () => {
  keyDown.value = true
  handleResetHoverEffect()

  if (activeOptionIndex.value === 0) return

  activeOptionIndex.value = Math.max(activeOptionIndex.value - 1, 0)
  handleAutoScrollOption()

  ncDelay(100).then(() => {
    keyDown.value = false
  })
}

const handleKeydownEnter = () => {
  if (list.value[activeOptionIndex.value]) {
    handleSelectOption(list.value[activeOptionIndex.value])
  } else if (list.value[0]) {
    handleSelectOption(list.value[activeOptionIndex.value])
  }
}

/**
 * Focuses the input box when the list is opened
 */
const focusInputBox = () => {
  if (!vOpen.value) return

  setTimeout(() => {
    inputRef.value?.focus()
  }, 100)
}

/**
 * Focuses the list wrapper when the list is opened
 *
 * This function is called when the list is opened and search is not enabled.
 * It sets a timeout to focus the list wrapper element after a short delay.
 * This allows for proper rendering and improves accessibility.
 */
const focusListWrapper = () => {
  if (!vOpen.value || isSearchEnabled.value) return

  setTimeout(() => {
    listRef.value?.focus()
  }, 100)
}

watch(
  vOpen,
  () => {
    if (!vOpen.value) return

    searchQuery.value = ''

    if (props.isMultiSelect) {
      showHoverEffectOnSelectedOption.value = false
    } else {
      showHoverEffectOnSelectedOption.value = true
    }

    if (vModel.value && !props.isMultiSelect) {
      activeOptionIndex.value = list.value.findIndex((o) => compareVModel(o?.[optionValueKey]))

      nextTick(() => {
        handleAutoScrollOption(true)
      })
    } else {
      activeOptionIndex.value = -1
    }

    if (isSearchEnabled.value) {
      focusInputBox()
    } else {
      focusListWrapper()
    }
  },
  {
    immediate: true,
  },
)
</script>

<template>
  <div
    ref="listRef"
    tabindex="0"
    class="flex flex-col pt-2 w-64 !focus:(shadow-none outline-none)"
    @keydown.arrow-down.prevent="onArrowDown"
    @keydown.arrow-up.prevent="onArrowUp"
    @keydown.enter.prevent="handleSelectOption(list[activeOptionIndex])"
  >
    <template v-if="isSearchEnabled">
      <div class="w-full px-2" @click.stop>
        <a-input
          ref="inputRef"
          v-model:value="searchQuery"
          :placeholder="searchInputPlaceholder"
          class="nc-toolbar-dropdown-search-field-input !pl-2 !pr-1.5"
          allow-clear
          :bordered="false"
          @keydown.enter.stop="handleKeydownEnter"
          @change="handleResetHoverEffect(false, 0)"
        >
          <template #prefix> <GeneralIcon icon="search" class="nc-search-icon h-3.5 w-3.5 mr-1" /> </template
        ></a-input>
      </div>
      <NcDivider />
    </template>

    <slot name="listHeader"></slot>
    <div class="nc-list-wrapper">
      <template v-if="list.length">
        <div class="h-auto !max-h-[247px]">
          <div
            v-bind="containerProps"
            class="nc-list !h-auto w-full nc-scrollbar-thin px-2 pb-2"
            :style="{
              maxHeight: '247px !important',
            }"
          >
            <div v-bind="wrapperProps">
              <div
                v-for="{ data: option, index: idx } in virtualList"
                :key="idx"
                class="flex items-center gap-2 w-full py-2 px-2 hover:bg-gray-100 cursor-pointer rounded-md"
                :class="[
                  `nc-list-option-${idx}`,
                  {
                    'nc-list-option-selected': compareVModel(option[optionValueKey]),
                    'bg-gray-100 ': showHoverEffectOnSelectedOption && compareVModel(option[optionValueKey]),
                    'bg-gray-100 nc-list-option-active': activeOptionIndex === idx,
                  },
                ]"
                @mouseover="handleResetHoverEffect(true, idx)"
                @click="handleSelectOption(option, idx)"
              >
                <slot name="listItem" :option="option" :is-selected="() => compareVModel(option[optionValueKey])" :index="idx">
                  <NcTooltip class="truncate flex-1" show-on-truncate-only>
                    <template #title>
                      {{ option[optionLabelKey] }}
                    </template>
                    {{ option[optionLabelKey] }}
                  </NcTooltip>
                  <GeneralIcon
                    v-if="showSelectedOption && compareVModel(option[optionValueKey])"
                    id="nc-selected-item-icon"
                    icon="check"
                    class="flex-none text-primary w-4 h-4"
                  />
                </slot>
              </div>
            </div>
          </div>
        </div>
      </template>
      <template v-if="!list.length">
        <slot name="emptyState">
          <div class="h-full text-center flex items-center justify-center gap-3 mt-4">
            <a-empty :image="Empty.PRESENTED_IMAGE_SIMPLE" :description="$t('labels.noData')" class="!my-0" />
          </div>
        </slot>
      </template>
    </div>
    <slot name="listFooter"></slot>
  </div>
</template>

<style lang="scss" scoped>
:deep(.nc-toolbar-dropdown-search-field-input) {
  &.ant-input-affix-wrapper-focused {
    .ant-input-prefix svg {
      @apply text-brand-500;
    }
  }
  .ant-input {
    @apply placeholder-gray-500;
  }
}
</style>
