<script setup lang="ts">
import type { AcceptableValue } from 'reka-ui';
import { useFilter } from 'reka-ui';
import { computed, ref } from 'vue';
import {
  Popover as ShadcnPopover,
  PopoverAnchor as ShadcnPopoverAnchor,
  PopoverContent as ShadcnPopoverContent,
} from '@/components/core/popover';
import { UiIcon } from '@/components/UiIcon';
import { cn } from '@/lib/utils';
import { provideUiAutocompleteContext } from './context';
import type { UiAutocompleteEmits, UiAutocompleteProps } from './types';

defineOptions({
  name: 'UiAutocomplete',
});

const props = withDefaults(defineProps<UiAutocompleteProps>(), {
  modelValue: undefined,
  defaultValue: undefined,
  placeholder: undefined,
  ariaLabel: undefined,
  inputValue: undefined,
  defaultInputValue: '',
  open: undefined,
  defaultOpen: false,
  multiple: false,
  disabled: false,
  required: false,
  name: undefined,
  dir: 'ltr',
  emptyText: 'No results found.',
  startIcon: undefined,
  showIndicator: true,
});

const emit = defineEmits<UiAutocompleteEmits>();

const inputEl = ref<HTMLInputElement | null>(null);
const itemLabels = ref<Map<string, string>>(new Map());
const highlightedIndex = ref(-1);
const internalOpen = ref(props.defaultOpen);
const internalInputValue = ref(props.defaultInputValue);
const internalValue = ref<string | string[]>(
  props.defaultValue ?? (props.multiple ? [] : ''),
);
const { contains } = useFilter({ sensitivity: 'base' });

const isValueControlled = computed(() => props.modelValue !== undefined);
const isInputControlled = computed(() => props.inputValue !== undefined);
const options = computed(() => props.options ?? []);

const openState = computed({
  get() {
    return props.open ?? internalOpen.value;
  },
  set(value: boolean) {
    internalOpen.value = value;
    emit('update:open', value);
  },
});

const inputText = computed({
  get() {
    return props.inputValue ?? internalInputValue.value;
  },
  set(value: string) {
    if (!isInputControlled.value) {
      internalInputValue.value = value;
    }
    emit('update:inputValue', value);
  },
});

const selectedValues = computed<string[]>(() => {
  const value = isValueControlled.value
    ? props.modelValue
    : internalValue.value;
  if (Array.isArray(value)) return value;
  if (typeof value === 'string' && value.length > 0) return [value];
  return [];
});

const selectedOptions = computed(() =>
  selectedValues.value.map(
    (value) =>
      options.value.find((option) => option.value === value) ?? {
        value,
        label: itemLabels.value.get(value) ?? value,
      },
  ),
);

const displayText = computed(() =>
  selectedOptions.value.map((option) => option.label).join(', '),
);

const hasSelection = computed(() => selectedValues.value.length > 0);

const showSelectionDisplay = computed(
  () => inputText.value.length === 0 && hasSelection.value,
);

const filteredOptions = computed(() => {
  const query = inputText.value.trim();
  if (!query) return options.value;
  return options.value.filter((option) => contains(option.label, query));
});

const hasInternalOptions = computed(() => props.options !== undefined);
const keyboardValues = computed(() =>
  hasInternalOptions.value
    ? filteredOptions.value
        .filter((option) => !option.disabled)
        .map((option) => option.value)
    : Array.from(itemLabels.value.keys()),
);
const highlightedValue = computed(
  () => keyboardValues.value[highlightedIndex.value],
);

const formHiddenValues = computed(() => {
  if (!props.name) return [];
  return props.multiple
    ? selectedValues.value
    : selectedValues.value.slice(0, 1);
});

const inputSlotProps = computed(() => ({
  value: inputText.value,
  placeholder: props.placeholder,
  ariaLabel: props.ariaLabel ?? props.placeholder ?? 'Autocomplete',
  disabled: props.disabled,
  selectedValues: selectedValues.value,
  selectedOptions: selectedOptions.value,
  displayText: displayText.value,
  showSelectionDisplay: showSelectionDisplay.value,
  open: openState.value,
  setInputValue,
  setOpen,
}));

const shellClasses = computed(() =>
  cn(
    'border-input data-[placeholder]:bg-background-input-default [&:not([data-placeholder])]:border-border-surface-focus-ring focus-within:border-border-surface-focus-ring focus-within:ring-ring/50 focus-within:shadow-xs aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive flex h-9 w-full min-w-0 items-center gap-2 rounded-md border bg-accent px-3 py-2 text-sm transition-[color,box-shadow] outline-none focus-within:ring-[3px]',
    props.disabled && 'pointer-events-none cursor-not-allowed opacity-50',
  ),
);

function setOpen(value: boolean) {
  if (props.disabled) return;
  const wasOpen = openState.value;
  openState.value = value;
  if (value && !wasOpen) {
    syncHighlightToSelection();
  }
}

function setInputValue(value: string) {
  inputText.value = value;
  highlightedIndex.value = -1;
  if (value.length > 0) {
    setOpen(true);
  }
}

function focusInput() {
  inputEl.value?.focus();
}

function normalizeSelection(value: AcceptableValue | AcceptableValue[]) {
  if (Array.isArray(value)) {
    return value.filter((item): item is string => typeof item === 'string');
  }
  return typeof value === 'string' && value.length > 0 ? value : '';
}

function updateSelection(value: AcceptableValue | AcceptableValue[]) {
  applySelectionValue(value, true);

  inputText.value = '';
  highlightedIndex.value = -1;
  if (!props.multiple) {
    setOpen(false);
    return;
  }
  focusInput();
}

function isSelected(value: string) {
  return selectedValues.value.includes(value);
}

function highlightOption(value: string) {
  setHighlightedIndex(keyboardValues.value.indexOf(value));
}

function handleInput(event: Event) {
  emit('input', event);
  setInputValue((event.target as HTMLInputElement).value);
}

function handleKeydown(event: KeyboardEvent) {
  emit('keydown', event);
  handleKeyboardNavigation(event);
}

function handleListboxKeydown(event: KeyboardEvent) {
  handleKeyboardNavigation(event);
}

function handleKeyboardNavigation(event: KeyboardEvent) {
  if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
    event.preventDefault();
    setOpen(true);
    moveHighlight(event.key === 'ArrowDown' ? 1 : -1);
  }

  if (event.key === 'Escape') {
    setOpen(false);
  }

  if (event.key === 'Enter' && openState.value) {
    const value =
      highlightedValue.value ??
      (hasInternalOptions.value
        ? filteredOptions.value.find((option) => !option.disabled)?.value
        : keyboardValues.value[0]);

    if (!value) return;

    event.preventDefault();
    selectOption(value);
  }
}

function selectOption(value: string) {
  if (props.multiple) {
    const next = isSelected(value)
      ? selectedValues.value.filter((item) => item !== value)
      : [...selectedValues.value, value];
    updateSelection(next);
    setOpen(true);
    return;
  }

  updateSelection(value);
}

function handleFocus(event: FocusEvent) {
  emit('focus', event);
  setOpen(true);
}

function handleBlur(event: FocusEvent) {
  emit('blur', event);
}

function moveHighlight(delta: 1 | -1) {
  const values = keyboardValues.value;
  if (values.length === 0) return;

  if (highlightedIndex.value === -1) {
    setHighlightedIndex(delta === 1 ? 0 : values.length - 1);
    return;
  }

  setHighlightedIndex(
    (highlightedIndex.value + delta + values.length) % values.length,
  );
}

function syncHighlightToSelection() {
  const values = keyboardValues.value;
  const selectedValue = selectedValues.value.find((value) =>
    values.includes(value),
  );

  highlightedIndex.value = selectedValue ? values.indexOf(selectedValue) : -1;
}

function setHighlightedIndex(index: number) {
  highlightedIndex.value = index;
}

function applySelectionValue(
  value: AcceptableValue | AcceptableValue[],
  shouldEmitSelect: boolean,
) {
  const next = normalizeSelection(value);
  if (!isValueControlled.value) {
    internalValue.value = next;
  }
  emit('update:modelValue', next);
  if (shouldEmitSelect) {
    emit('select', next);
  }
}

provideUiAutocompleteContext({
  multiple: computed(() => props.multiple),
  selectedValues,
  itemLabels,
  highlightedValue,
  isSelected,
  selectOption,
  highlightOption,
});
</script>

<template>
  <div class="flex flex-col" :dir="props.dir">
    <ShadcnPopover v-model:open="openState">
      <ShadcnPopoverAnchor as-child>
        <div
          data-slot="autocomplete-trigger"
          :data-placeholder="hasSelection ? undefined : ''"
          :class="shellClasses"
        >
          <UiIcon
            v-if="props.startIcon"
            :name="props.startIcon"
            :size="16"
            class="text-muted-foreground shrink-0"
          />

          <div class="relative flex min-w-0 flex-1 items-center">
            <slot name="input" v-bind="inputSlotProps">
              <span
                v-if="showSelectionDisplay"
                data-slot="autocomplete-value"
                class="pointer-events-none min-w-0 flex-1 truncate"
              >
                {{ displayText }}
              </span>
              <input
                ref="inputEl"
                data-slot="autocomplete-input"
                :value="inputText"
                :placeholder="
                  showSelectionDisplay ? undefined : props.placeholder
                "
                :aria-label="
                  props.ariaLabel ?? props.placeholder ?? 'Autocomplete'
                "
                :disabled="props.disabled"
                :required="props.required && selectedValues.length === 0"
                class="placeholder:text-muted-foreground min-w-0 bg-transparent outline-none disabled:cursor-not-allowed"
                :class="
                  showSelectionDisplay
                    ? 'absolute inset-0 size-full cursor-text opacity-0'
                    : 'flex-1'
                "
                @input="handleInput"
                @focus="handleFocus"
                @blur="handleBlur"
                @keydown="handleKeydown"
              />
            </slot>
          </div>

          <button
            type="button"
            class="text-muted-foreground flex size-5 shrink-0 items-center justify-center rounded-sm outline-none"
            :disabled="props.disabled"
            :aria-expanded="openState"
            aria-haspopup="listbox"
            :aria-label="openState ? 'Close suggestions' : 'Open suggestions'"
            @mousedown.prevent.stop
            @click.stop="
              setOpen(!openState);
              focusInput();
            "
          >
            <UiIcon
              name="chevron-down"
              :size="16"
              class="transition-transform duration-200"
              :class="{ 'rotate-180': openState }"
            />
          </button>
        </div>
      </ShadcnPopoverAnchor>

      <ShadcnPopoverContent
        data-slot="autocomplete-content"
        side="bottom"
        :side-offset="4"
        align="start"
        :force-mount="true"
        class="min-w-(--reka-popper-anchor-width) w-(--reka-popper-anchor-width) max-w-(--reka-popper-available-width) p-1 data-[state=closed]:hidden data-[state=closed]:pointer-events-none"
        @open-auto-focus.prevent
      >
        <div
          role="listbox"
          aria-label="Options"
          :aria-multiselectable="props.multiple"
          data-slot="listbox-content"
          tabindex="-1"
          class="max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto"
          @keydown.capture="handleListboxKeydown"
        >
          <div
            v-for="option in hasInternalOptions ? filteredOptions : []"
            :key="option.value"
            role="option"
            :aria-selected="isSelected(option.value)"
            :aria-disabled="option.disabled || undefined"
            :data-disabled="option.disabled ? '' : undefined"
            :data-autocomplete-value="option.value"
            :data-highlighted="
              highlightedValue === option.value ? '' : undefined
            "
            :data-state="isSelected(option.value) ? 'checked' : 'unchecked'"
            tabindex="-1"
            class="hover-layer hover-layer-default focus:text-foreground data-[highlighted]:bg-background-surface-muted-subtle data-[highlighted]:text-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50"
            :class="{
              'pr-12': props.showIndicator,
              'pr-2': !props.showIndicator,
            }"
            @pointermove="highlightOption(option.value)"
            @click="!option.disabled && selectOption(option.value)"
            @keydown.enter.prevent="
              !option.disabled && selectOption(option.value)
            "
            @keydown.space.prevent="
              !option.disabled && selectOption(option.value)
            "
          >
            <span class="min-w-0 flex-1 truncate">
              <slot
                name="item"
                :option="option"
                :selected="isSelected(option.value)"
                :disabled="option.disabled ?? false"
              >
                {{ option.label }}
              </slot>
            </span>
            <span
              v-if="props.showIndicator"
              class="!absolute right-2 flex items-center justify-center"
              data-slot="autocomplete-item-end"
            >
              <span v-if="isSelected(option.value)">
                <slot name="indicator-icon">
                  <UiIcon name="check" :size="16" />
                </slot>
              </span>
            </span>
          </div>

          <div
            v-if="hasInternalOptions && filteredOptions.length === 0"
            class="text-muted-foreground px-2 py-1.5 text-sm"
          >
            {{ props.emptyText }}
          </div>

          <slot
            v-else-if="!hasInternalOptions"
            :input-value="inputText"
            :selected-values="selectedValues"
            :selected-options="selectedOptions"
            :is-selected="isSelected"
            :set-open="setOpen"
          />
        </div>
      </ShadcnPopoverContent>
    </ShadcnPopover>

    <input
      v-for="value in formHiddenValues"
      :key="value"
      type="hidden"
      :name="props.name"
      :value="value"
    />
  </div>
</template>
