<template>
  <div class="cpMultiselect" :class="multiselectDynamicClasses">
    <base-input-label
      v-if="label"
      v-bind-once="{ for: multiselectId }"
      :id="labelId"
      :is-invalid
      :required
    >
      {{ label }}
    </base-input-label>
    <auto-complete
      ref="multiselect"
      v-model="selectModel"
      :append-to
      :aria-labelledby="ariaLabelledby"
      auto-option-focus
      :data-key="trackBy"
      :disabled
      :force-selection
      :input-class
      :input-id="multiselectId"
      :invalid="isInvalid"
      :multiple
      :name
      :option-disabled
      :option-label
      :placeholder
      :pt="passThroughConfig"
      :suggestions="dynamicOptions"
      :typeahead
      @blur="handleBlur"
      @click="handleClick"
      @complete="handleSearch"
      @hide="handleOverlayHidden"
      @keydown.arrow-down="handleArrowKeys"
      @keydown.arrow-up="handleArrowKeys"
      @keydown.enter="toggleDropdown"
      @keydown.esc.stop
      @show="handleOverlayShown"
      @update:model-value="handleUpdateModelValue"
      @value-change="handleValueChange"
    >
      <template #empty>
        <slot name="empty">
          <div class="cpMultiselect__empty">{{ emptyMessage }}</div>
        </slot>
      </template>
      <template #chip="{ value, removeCallback }">
        <slot name="tag" :option="value" :remove="removeCallback">
          <cp-badge class="cpMultiselect__tag" is-clearable size="sm" @on-clear="removeCallback">
            <template #leading-icon>
              <slot name="tag-leading-icon" :option="value" />
            </template>
            {{ value.name }}
          </cp-badge>
        </slot>
      </template>
      <template #option="{ option }">
        <slot name="option" :option="option">
          <button class="cpMultiselect__option" type="button">
            {{ option.name }}
          </button>
        </slot>
      </template>
      <template v-if="typeahead" #dropdown>
        <div v-if="hasPrefixSlot" class="cpMultiselect__prefix">
          <slot name="prefix" />
        </div>
        <cp-loader v-if="isLoading" class="cpMultiselect__loader" color="#B2B2BD" />
        <button
          v-else
          class="cpMultiselect__toggle"
          :disabled="disabled"
          tabindex="-1"
          type="button"
          @click.stop="handleDropdownIconClick"
        >
          <cp-icon class="cpMultiselect__dropdownIcon" :class="chevronDynamicClass" type="chevron-down" />
        </button>
        <base-select-clear-button
          v-if="displayClearButton"
          class="cpMultiselect__clear"
          tabindex="-1"
          @click="handleClear"
        />
      </template>
    </auto-complete>
    <cp-transition-expand mode="out-in">
      <p v-if="isInvalid" class="cpMultiselect__error">
        {{ errorMessage }}
      </p>
    </cp-transition-expand>
  </div>
</template>

<script setup lang="ts">
import { absolutePosition, getOuterWidth } from '@primeuix/utils/dom'
import AutoComplete from 'primevue/autocomplete'
import type { Slots } from 'vue'
import { computed, onMounted, ref, useId, useSlots } from 'vue'

import BaseInputLabel from '@/components/BaseInputLabel.vue'
import BaseSelectClearButton from '@/components/BaseSelectClearButton.vue'
import CpTransitionExpand from '@/components/CpTransitionExpand.vue'

import { isEmpty } from '@/helpers/object'

interface Emits {
  (e: 'search', query: string): void
  (e: 'clear'): void
  (e: 'update:modelValue', value: Record<string, unknown> | Record<string, unknown>[] | string[] | null): void
  (e: 'overlayShown'): void
  (e: 'overlayHidden'): void
}

interface Props {
  appendTo?: string | HTMLElement
  disableBlurReset?: boolean
  disabled?: boolean
  emptyMessage?: string
  errorMessage?: string
  forceSelection?: boolean
  isClearable?: boolean
  isInvalid?: boolean
  isLoading?: boolean
  label?: string
  modelValue?: Record<string, unknown>[] | Record<string, unknown> | string[] | null
  multiple?: boolean
  name?: string
  onSearch?: (query: string) => void
  optionDisabled?: string | ((option: unknown) => boolean)
  optionLabel?: string | ((option: unknown) => string)
  options?: unknown[]
  placeholder?: string
  required?: boolean
  size?: 'sm' | 'md'
  trackBy?: string
  withoutTypeahead?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  appendTo: 'body',
  label: '',
  name: '',
  placeholder: '',
  optionLabel: 'name',
  disableBlurReset: false,
  trackBy: undefined,
  emptyMessage: 'No results found',
  errorMessage: '',
  modelValue: null,
  optionDisabled: 'disabled',
  options: () => [],
  size: 'md',
  onSearch: undefined,
})

const emit = defineEmits<Emits>()

const slots: Slots = useSlots()

const multiselectId = useId()
const labelId = useId()

const ariaLabelledby = computed(() => (props.label ? labelId : undefined))

const selectModel = computed({
  get() {
    return props.modelValue
  },
  set(value) {
    if (typeof value === 'string') return
    emit('update:modelValue', value)
  },
})

const inputClass = computed(() => ({
  cpMultiselect__input: true,
  'cpMultiselect__input--isSingle': !props.multiple,
}))

const passThroughConfig = computed(() => ({
  root: { class: `cpMultiselect__select cpMultiselect__select--${props.size}` },
  inputmultiple: { class: 'cpMultiselect__tags' },
  dropdown: { class: 'cpMultiselect__toggle' },
  inputchip: { class: 'cpMultiselect__inputWrapper' },
  overlay: { class: `cpMultiselect__overlay ${!typeahead.value ? 'cpMultiselect__overlay--isHidden' : ''}` },
  listcontainer: { class: 'cpMultiselect__listWrapper' },
  list: { class: 'cpMultiselect__list' },
  option: { class: 'cpMultiselect__optionWrapper' },
  loader: { class: 'cpMultiselect__hidden' },
}))

type ExtendedAutoComplete = InstanceType<typeof AutoComplete> & {
  $el: HTMLElement
  alignOverlay: () => void
  hide: () => void
  overlay: HTMLElement
  overlayVisible: boolean
  show: () => void
  writeValue: (value: unknown, event?: Event) => void
}

const multiselect = ref<ExtendedAutoComplete | null>(null)

const searchQuery = ref('')

const typeahead = computed(() => !props.withoutTypeahead)

const isDropdownOpen = computed(() => multiselect.value?.overlayVisible)

const chevronDynamicClass = computed(() => {
  return { 'cpMultiselect__dropdownIcon--isRotated': isDropdownOpen.value }
})

const hasPrefixSlot = computed(() => !!slots.prefix)

const hasValue = computed(() => {
  if (props.multiple) return !!selectModel.value?.length
  return !isEmpty(selectModel.value)
})

const hasError = computed(() => (props.isInvalid ?? false) && !!props.errorMessage)

const multiselectDynamicClasses = computed(() => ({
  'cpMultiselect--hasPrefix': hasPrefixSlot.value,
  'cpMultiselect--isMultiple': props.multiple ?? false,
  'cpMultiselect--hasValue': hasValue.value,
  'cpMultiselect--hasError': hasError.value,
}))

const displayClearButton = computed(() => {
  if (props.multiple) return false
  return props.isClearable && !isEmpty(selectModel.value)
})

const hasRegisteredOnSearch = computed(() => props.onSearch !== undefined)

const handleSearch = (event: { query: string }) => {
  if (hasRegisteredOnSearch.value) {
    return emit('search', event.query)
  }

  // If no onSearch is registered, toggle the dropdown (if hidden) to show the options
  if (!isDropdownOpen.value) {
    toggleDropdown()
  }
}

const dynamicOptions = computed(() => {
  if (hasRegisteredOnSearch.value) return props.options

  return props.options.filter((option: unknown) => {
    const optionLabel =
      typeof option === 'object' && typeof props.optionLabel === 'string'
        ? (option as Record<string, unknown>)[props.optionLabel]
        : option
    return (optionLabel as string).toLowerCase().includes(searchQuery.value.toLowerCase())
  })
})
const handleClear = () => (selectModel.value = null)
const handleOverlayShown = () => emit('overlayShown')
const handleOverlayHidden = () => emit('overlayHidden')

const handleBlur = () => {
  if (props.disableBlurReset) return
  if (props.multiple) {
    const inputElement = getInputElement()
    if (inputElement) inputElement.value = ''
  } else {
    multiselect.value?.writeValue(selectModel.value)
  }
}

const handleValueChange = (newValue: string | object) => {
  if (typeof newValue !== 'string') return
  searchQuery.value = newValue

  // NOTE: because @complete is not triggered when the input is cleared
  if (newValue === '') emit('search', newValue)
}

const getInputElement = () => {
  if (!multiselect.value) return null
  return multiselect.value.$el?.querySelector('input') || null
}

const selectInputElement = () => {
  const inputElement = getInputElement()
  if (inputElement) inputElement.select()
}

const focusAndSelectInput = () => {
  const inputElement = getInputElement()
  if (inputElement) {
    inputElement.focus()
    selectInputElement()
  }
}

const handleClick = () => {
  toggleDropdown()
  selectInputElement()
}

const handleDropdownIconClick = () => {
  toggleDropdown()
  focusAndSelectInput()
}

const handleArrowKeys = () => {
  if (isDropdownOpen.value) return
  return toggleDropdown()
}

const toggleDropdown = () => {
  if (isDropdownOpen.value) {
    multiselect.value?.hide()
  } else {
    multiselect.value?.show()
  }
}

const handleUpdateModelValue = (value: Record<string, unknown> | string[] | null) => {
  // Autocomplete will set the model value to the query string if not blocked
  if (!value || typeof value === 'string') return
}

const overrideAlignOverlay = () => {
  if (!multiselect.value) return
  multiselect.value.alignOverlay = alignOverlay
}

const alignOverlay = () => {
  const target = multiselect.value?.$el

  if (!multiselect.value?.overlay || !target) return

  multiselect.value.overlay.style.width = `${getOuterWidth(target)}px`

  absolutePosition(multiselect.value.overlay, target)
}

onMounted(() => overrideAlignOverlay())
</script>

<style lang="scss">
.cpMultiselect {
  &:has(.cpMultiselect__input:disabled) {
    cursor: not-allowed;
  }

  &__prefix {
    display: flex;
    flex-shrink: 0;
    align-items: center;
    order: -1;

    &:empty {
      display: none;
    }
  }

  &--hasPrefix#{&}--isMultiple#{&}--hasValue .cpMultiselect__select {
    gap: var(--cp-spacing-sm);
  }

  &--hasError .cpMultiselect__select {
    margin-bottom: var(--cp-spacing-md);
  }

  &__select {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: space-between;
    padding: var(--cp-spacing-md);
    border-radius: var(--cp-radius-md);
    background-color: var(--cp-background-primary);
    box-shadow:
      var(--cp-shadows-3xs-inset),
      0 0 0 var(--cp-dimensions-0_25) var(--cp-border-soft);

    &--sm {
      padding: var(--cp-spacing-sm);

      .cpMultiselect__clear {
        right: var(--cp-dimensions-0_5);
      }
    }

    &:has(input:disabled) {
      background: var(--cp-background-disabled);
      box-shadow: 0 0 0 var(--cp-dimensions-0_25) var(--cp-border-disabled);
      color: var(--cp-text-disabled);
      pointer-events: none;

      input,
      input::placeholder {
        color: var(--cp-text-disabled);
      }

      .cpMultiselect__toggle {
        display: none;
      }

      .cpIcon {
        color: var(--cp-foreground-disabled);
      }
    }

    &:has(input:hover):not(:has(input:disabled)) {
      outline: var(--cp-dimensions-0_25) solid var(--cp-border-strong-hover);
    }

    &:has(input:focus-visible):not(:has(input:disabled)) {
      outline: var(--cp-dimensions-0_5) solid var(--cp-border-accent-solid);
    }

    &:is([data-p*='invalid'], .p-invalid, :has([data-p*='invalid']), :has([aria-invalid='true'])) {
      box-shadow: 0 0 0 var(--cp-dimensions-0_25) var(--cp-border-error-solid) !important;
      outline: var(--cp-dimensions-0_25) solid var(--cp-border-error-solid) !important;

      &:is(:hover, :has(input:hover), :has(input:placeholder-shown):has(input:hover)) {
        box-shadow: 0 0 0 var(--cp-dimensions-0_25) var(--cp-border-error-solid) !important;
        outline: var(--cp-dimensions-0_25) solid var(--cp-border-error-solid) !important;
      }

      &:is(:focus-within, :has(input:focus-visible), :has(input:placeholder-shown):has(input:focus-visible)) {
        box-shadow: 0 0 0 var(--cp-dimensions-0_25) var(--cp-border-error-solid) !important;
        outline: var(--cp-dimensions-0_5) solid var(--cp-border-error-solid) !important;
      }
    }
  }

  &__tags {
    display: flex;
    flex: 1;
    flex-wrap: wrap;
    gap: var(--cp-spacing-md);

    li {
      display: flex;
      align-items: center;
    }
  }

  &__tag.cpBadge.cpBadge--sm {
    padding-top: calc(var(--cp-dimensions-0_5) * 1.5);
    padding-bottom: calc(var(--cp-dimensions-0_5) * 1.5);
  }

  &__toggle {
    @extend %u-focus-outline;

    display: flex;
    align-items: center;
    justify-content: center;
    width: fn.px-to-rem(24);
    height: fn.px-to-rem(24);
    padding-right: var(--cp-spacing-sm);

    &:disabled {
      cursor: not-allowed;
    }
  }

  &__inputWrapper {
    display: flex;
    flex: 1;
    align-items: center;
  }

  &__input {
    width: 100%;
    padding: 0;
    background-color: transparent;
    font-size: var(--cp-text-size-sm);
    line-height: var(--cp-line-height-md);
    padding-inline: var(--cp-spacing-md);

    &::placeholder {
      color: var(--cp-text-placeholder);
    }

    &:disabled {
      color: var(--cp-foreground-disabled);
      cursor: not-allowed;

      &::placeholder {
        color: var(--cp-text-disabled);
      }
    }

    &--isSingle {
      width: 100%;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
  }

  &__dropdownIcon {
    width: var(--cp-dimensions-4);
    height: var(--cp-dimensions-4);
    color: var(--cp-foreground-secondary);
    transition: rotate 200ms ease;

    &--isRotated {
      color: var(--cp-foreground-primary);
      rotate: 180deg;
    }
  }

  &__loader {
    width: var(--cp-dimensions-4);
    height: var(--cp-dimensions-4);
  }

  &__overlay {
    left: 0;
    overflow: hidden;
    border-radius: var(--cp-radius-md);
    background: var(--cp-background-primary);
    box-shadow: var(--cp-shadows-overlay);
    margin-block: var(--cp-spacing-md);
    padding-block: var(--cp-spacing-sm);

    &--isHidden {
      display: none;
    }
  }

  &__listWrapper {
    overflow-y: auto;
  }

  &__hidden {
    display: none;
  }

  &__list {
    display: flex;
    flex-direction: column;
    gap: var(--cp-spacing-xs);
    padding-inline: var(--cp-spacing-sm);
  }

  &__optionWrapper {
    border-radius: var(--cp-radius-sm-md);

    &:hover,
    &[data-p-focused='true'],
    &[data-p-selected='true'] {
      background: var(--cp-background-primary-hover);
      color: var(--cp-foreground-primary);
    }

    &[data-p-selected='true']:not([data-p-focused='true']) {
      background: var(--cp-background-accent-primary);
    }

    &[data-p-selected='true'] .cpMultiselect__option {
      color: var(--cp-foreground-accent-primary);
      font-weight: 500;
    }

    &[data-p-disabled='true'] {
      cursor: not-allowed;
      opacity: 0.5;
      pointer-events: none;
    }
  }

  &__optionWrapper > *,
  &__option {
    width: 100%;
    padding: var(--cp-dimensions-2_5);
    border-radius: var(--cp-radius-sm-md);
    font-size: var(--cp-text-size-sm);
    line-height: var(--cp-line-height-sm);
    text-align: left;
  }

  &__option {
    @extend %u-text-ellipsis;
  }

  &__empty {
    padding: var(--cp-spacing-md);
    color: var(--cp-text-secondary);
    font-size: var(--cp-text-size-sm);
  }

  &__error {
    color: var(--cp-text-error-primary);
    font-size: var(--cp-text-size-sm);
    line-height: var(--cp-line-height-md);

    &::first-letter {
      text-transform: capitalize;
    }
  }

  &__clear {
    position: absolute;
    top: 50%;
    right: var(--cp-dimensions-2);
    background-color: var(--cp-background-primary);
    transform: translateY(-50%);
  }

  @include mx.media-query-pointer-device-only {
    &__clear {
      display: none;
    }
  }
}

@include mx.media-query-pointer-device-only {
  .cpMultiselect:has(.cpMultiselect__clear):is(:hover, :focus-within) {
    .cpMultiselect__clear {
      display: flex;
    }

    .cpMultiselect__toggle {
      visibility: hidden;
      pointer-events: none;
    }
  }
}
</style>
