<script setup lang="ts">
import { Icon } from '@iconify/vue';
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch, type CSSProperties } from 'vue';
import { useI18n } from 'vue-i18n';
import { iconCheck, iconChevronDown, iconClose } from '../icons';

export type TagsSelectOption = {
  id: number | string;
  name: string;
  label?: string;
  thumbnailUrl?: string | null;
};

type OptionRow = {
  value: string;
  label: string;
  id: string;
  thumbnailUrl: string | null;
};

type CandidateRow = { type: 'option'; id: string; value: string; label: string } | { type: 'create'; id: string };
type DropdownStrategy = 'absolute' | 'fixed';

const props = withDefaults(
  defineProps<{
    id: string;
    modelValue: string[] | string;
    options: TagsSelectOption[];
    multiple?: boolean;
    max?: number;
    placeholder?: string;
    searchPlaceholder?: string;
    emptyText?: string;
    allowCreate?: boolean;
    creating?: boolean;
    createLabel?: string;
    dropdownStrategy?: DropdownStrategy;
    clearable?: boolean;
  }>(),
  {
    multiple: true,
    max: 0,
    allowCreate: false,
    creating: false,
    clearable: false
  }
);

const emit = defineEmits<{
  'update:modelValue': [value: string[] | string];
  create: [name: string];
}>();

const { t } = useI18n();
const root = ref<HTMLElement | null>(null);
const trigger = ref<HTMLButtonElement | null>(null);
const searchInput = ref<HTMLInputElement | null>(null);
const isOpen = ref(false);
const search = ref('');
const activeIndex = ref(-1);
const dropdownStyle = ref<CSSProperties>({});
const dropdownPlacement = ref<'top' | 'bottom'>('bottom');
const isInsideModal = ref(false);
let positionFrame = 0;

const optionRows = computed(() =>
  props.options.map((option, index) => ({
    value: String(option.id),
    label: option.label ?? option.name,
    id: `${props.id}-option-${index}`,
    thumbnailUrl: option.thumbnailUrl ?? null
  }))
);

const modelValues = computed(() => {
  if (Array.isArray(props.modelValue)) return props.modelValue;
  return props.modelValue ? [props.modelValue] : [];
});
const selectedValues = computed(() => new Set(modelValues.value));
const maxReached = computed(() => props.multiple && props.max > 0 && modelValues.value.length >= props.max);

const selectedRows = computed(() =>
  modelValues.value
    .map((value) => optionRows.value.find((option) => option.value === value))
    .filter((option): option is OptionRow => option !== undefined)
);
const selectedLabel = computed(() => selectedRows.value[0]?.label ?? '');
const selectedThumbnailUrl = computed(() => selectedRows.value[0]?.thumbnailUrl ?? '');

const filteredRows = computed(() => {
  const keyword = search.value.trim().toLowerCase();
  if (!keyword) return optionRows.value;
  return optionRows.value.filter((option) => option.label.toLowerCase().includes(keyword));
});
const createName = computed(() => search.value.trim());
const hasExactMatch = computed(() => {
  const keyword = createName.value.toLowerCase();
  return optionRows.value.some((option) => option.label.toLowerCase() === keyword);
});
const canCreate = computed(() => props.allowCreate && createName.value !== '' && !hasExactMatch.value && !maxReached.value);
const placeholderText = computed(() => props.placeholder ?? t('common.searchOrSelect'));
const searchPlaceholderText = computed(() => props.searchPlaceholder ?? t('common.search'));
const emptyTextValue = computed(() => props.emptyText ?? t('common.noMatches'));
const createText = computed(() => props.createLabel?.replace('{name}', createName.value) ?? t('common.createNamed', { name: createName.value }));
const optionsListId = computed(() => `${props.id}-options`);
const createOptionId = computed(() => `${props.id}-create`);
const candidateRows = computed<CandidateRow[]>(() => {
  const rows: CandidateRow[] = filteredRows.value
    .filter((option) => isSearchSelectable(option.value))
    .map((option) => ({ type: 'option', id: option.id, value: option.value, label: option.label }));

  if (canCreate.value) {
    rows.push({ type: 'create', id: createOptionId.value });
  }

  return rows;
});
const activeCandidate = computed(() => candidateRows.value[activeIndex.value]);
const activeDescendant = computed(() => activeCandidate.value?.id);
const resolvedDropdownStrategy = computed<DropdownStrategy>(() => props.dropdownStrategy ?? (isInsideModal.value ? 'fixed' : 'absolute'));
const usesFixedDropdown = computed(() => resolvedDropdownStrategy.value === 'fixed');

function setDefaultActiveIndex() {
  const keyword = createName.value.toLowerCase();
  const exactIndex = keyword
    ? candidateRows.value.findIndex((candidate) => candidate.type === 'option' && candidate.label.toLowerCase() === keyword)
    : -1;
  activeIndex.value = exactIndex >= 0 ? exactIndex : candidateRows.value.length ? 0 : -1;
}

function clampActiveIndex() {
  if (!candidateRows.value.length) {
    activeIndex.value = -1;
    return;
  }

  if (activeIndex.value < 0) {
    activeIndex.value = 0;
    return;
  }

  activeIndex.value = Math.min(activeIndex.value, candidateRows.value.length - 1);
}

async function openDropdown() {
  isOpen.value = true;
  await nextTick();
  updateDropdownPosition();
  addPositionListeners();
  setDefaultActiveIndex();
  searchInput.value?.focus();
}

function closeDropdown() {
  isOpen.value = false;
  search.value = '';
  activeIndex.value = -1;
  dropdownStyle.value = {};
  removePositionListeners();
}

function toggleDropdown() {
  if (isOpen.value) {
    closeDropdown();
  } else {
    void openDropdown();
  }
}

function updateValue(values: string[]) {
  emit('update:modelValue', props.multiple ? values : (values[0] ?? ''));
}

function selectOption(value: string) {
  if (!props.multiple) {
    if (props.clearable && selectedValues.value.has(value)) {
      updateValue([]);
      closeDropdown();
      return;
    }

    updateValue([value]);
    closeDropdown();
    return;
  }

  if (selectedValues.value.has(value)) {
    updateValue(modelValues.value.filter((item) => item !== value));
    return;
  }

  if (!maxReached.value) {
    updateValue([...modelValues.value, value]);
    search.value = '';
    setDefaultActiveIndex();
    scheduleDropdownPositionUpdate();
  }
}

function remove(value: string) {
  updateValue(modelValues.value.filter((item) => item !== value));
  scheduleDropdownPositionUpdate();
}

function createOption() {
  if (!canCreate.value || props.creating) return;
  emit('create', createName.value);
  search.value = '';
}

function isSearchSelectable(value: string) {
  return !props.multiple || (!selectedValues.value.has(value) && !maxReached.value);
}

function isActiveOption(option: OptionRow) {
  const candidate = activeCandidate.value;
  return candidate?.type === 'option' && candidate.value === option.value;
}

function isCreateActive() {
  return activeCandidate.value?.type === 'create';
}

function moveActive(delta: number) {
  if (!candidateRows.value.length) return;
  const nextIndex = activeIndex.value < 0 ? 0 : activeIndex.value + delta;
  activeIndex.value = (nextIndex + candidateRows.value.length) % candidateRows.value.length;
}

function commitSearch() {
  const candidate = activeCandidate.value;
  if (candidate?.type === 'option') {
    selectOption(candidate.value);
    return;
  }

  if (candidate?.type === 'create') {
    createOption();
  }
}

function onRootKeydown(event: KeyboardEvent) {
  if (event.key === 'Escape' && isOpen.value) {
    event.stopPropagation();
    closeDropdown();
  }
}

function onDocumentPointerDown(event: PointerEvent) {
  if (root.value && !root.value.contains(event.target as Node)) {
    closeDropdown();
  }
}

function scheduleDropdownPositionUpdate() {
  if (!usesFixedDropdown.value || !isOpen.value || positionFrame) {
    return;
  }

  positionFrame = window.requestAnimationFrame(() => {
    positionFrame = 0;
    updateDropdownPosition();
  });
}

function updateDropdownPosition() {
  if (!usesFixedDropdown.value || !isOpen.value || !trigger.value) {
    dropdownStyle.value = {};
    return;
  }

  const viewportPadding = 12;
  const dropdownGap = 6;
  const dropdownChromeHeight = 72;
  const triggerRect = trigger.value.getBoundingClientRect();
  const viewportWidth = window.innerWidth;
  const viewportHeight = window.innerHeight;
  const width = Math.min(triggerRect.width, viewportWidth - viewportPadding * 2);
  const left = Math.min(Math.max(triggerRect.left, viewportPadding), viewportWidth - width - viewportPadding);
  const spaceBelow = viewportHeight - triggerRect.bottom - viewportPadding - dropdownGap;
  const spaceAbove = triggerRect.top - viewportPadding - dropdownGap;
  const placeAbove = spaceBelow < 220 && spaceAbove > spaceBelow;
  const availableSpace = Math.max(144, placeAbove ? spaceAbove : spaceBelow);
  const optionsMaxHeight = Math.max(96, Math.min(240, availableSpace - dropdownChromeHeight));
  const nextStyle = {
    left: `${left}px`,
    width: `${width}px`,
    '--tags-select-options-max-height': `${optionsMaxHeight}px`
  } as CSSProperties;

  if (placeAbove) {
    dropdownPlacement.value = 'top';
    dropdownStyle.value = {
      ...nextStyle,
      bottom: `${viewportHeight - triggerRect.top + dropdownGap}px`
    };
    return;
  }

  dropdownPlacement.value = 'bottom';
  dropdownStyle.value = {
    ...nextStyle,
    top: `${triggerRect.bottom + dropdownGap}px`
  };
}

function addPositionListeners() {
  if (!usesFixedDropdown.value) {
    return;
  }

  window.addEventListener('resize', scheduleDropdownPositionUpdate);
  window.addEventListener('scroll', scheduleDropdownPositionUpdate, true);
}

function removePositionListeners() {
  window.removeEventListener('resize', scheduleDropdownPositionUpdate);
  window.removeEventListener('scroll', scheduleDropdownPositionUpdate, true);

  if (positionFrame) {
    window.cancelAnimationFrame(positionFrame);
    positionFrame = 0;
  }
}

onMounted(() => {
  isInsideModal.value = root.value?.closest('.modal') !== null;
  document.addEventListener('pointerdown', onDocumentPointerDown);
});

onBeforeUnmount(() => {
  document.removeEventListener('pointerdown', onDocumentPointerDown);
  removePositionListeners();
});

watch(search, setDefaultActiveIndex);
watch(candidateRows, clampActiveIndex);
watch(
  () => props.dropdownStrategy,
  () => {
    if (!isOpen.value) return;

    removePositionListeners();
    void nextTick(() => {
      updateDropdownPosition();
      addPositionListeners();
    });
  }
);
</script>

<template>
  <div ref="root" class="tags-select" :class="{ 'tags-select--single': !multiple }" @keydown="onRootKeydown">
    <button
      :id="id"
      ref="trigger"
      type="button"
      class="tags-select__trigger"
      :class="{ open: isOpen }"
      aria-haspopup="listbox"
      :aria-expanded="isOpen"
      @click="toggleDropdown"
    >
      <span v-if="selectedRows.length" class="tags-select__selected">
        <template v-if="multiple">
          <span v-for="option in selectedRows" :key="option.value" class="tags-select__tag">
            <img v-if="option.thumbnailUrl" class="tags-select__thumb tags-select__thumb--tag" :src="option.thumbnailUrl" alt="" loading="lazy" />
            <span>{{ option.label }}</span>
            <span
              class="tags-select__remove"
              role="button"
              tabindex="0"
              :aria-label="t('common.removeNamed', { name: option.label })"
              @click.stop="remove(option.value)"
              @keydown.enter.stop.prevent="remove(option.value)"
              @keydown.space.stop.prevent="remove(option.value)"
            >
              <Icon :icon="iconClose" class="ui-icon" aria-hidden="true" />
            </span>
          </span>
        </template>
        <span v-else class="tags-select__single-value">
          <img v-if="selectedThumbnailUrl" class="tags-select__thumb" :src="selectedThumbnailUrl" alt="" loading="lazy" />
          <span>{{ selectedLabel }}</span>
        </span>
      </span>
      <span v-else class="tags-select__placeholder">{{ placeholderText }}</span>
      <Icon :icon="iconChevronDown" class="tags-select__arrow" aria-hidden="true" />
    </button>

    <div
      v-if="isOpen"
      class="tags-select__dropdown"
      :class="{
        'tags-select__dropdown--fixed': usesFixedDropdown,
        'tags-select__dropdown--top': usesFixedDropdown && dropdownPlacement === 'top'
      }"
      :style="dropdownStyle"
    >
      <input
        ref="searchInput"
        v-model="search"
        class="tags-select__search"
        type="search"
        :placeholder="searchPlaceholderText"
        :aria-activedescendant="activeDescendant"
        :aria-controls="optionsListId"
        aria-autocomplete="list"
        role="combobox"
        @keydown.enter.stop.prevent="commitSearch"
        @keydown.down.stop.prevent="moveActive(1)"
        @keydown.up.stop.prevent="moveActive(-1)"
      />

      <div :id="optionsListId" class="tags-select__options" role="listbox" :aria-multiselectable="multiple ? 'true' : undefined">
        <button
          v-for="option in filteredRows"
          :key="option.value"
          type="button"
          class="tags-select__option"
          :id="option.id"
          :class="{ selected: selectedValues.has(option.value), active: isActiveOption(option) }"
          role="option"
          :aria-selected="selectedValues.has(option.value)"
          :disabled="!selectedValues.has(option.value) && maxReached"
          @click="selectOption(option.value)"
        >
          <span class="tags-select__option-label">
            <img v-if="option.thumbnailUrl" class="tags-select__thumb" :src="option.thumbnailUrl" alt="" loading="lazy" />
            <span>{{ option.label }}</span>
          </span>
          <span v-if="selectedValues.has(option.value)" class="tags-select__state">
            <Icon :icon="iconCheck" class="ui-icon" aria-hidden="true" />
            {{ t('common.selected') }}
          </span>
        </button>
        <button
          v-if="canCreate"
          type="button"
          class="tags-select__option tags-select__create"
          :id="createOptionId"
          :class="{ active: isCreateActive() }"
          :disabled="creating"
          @click="createOption"
        >
          <span>{{ createText }}</span>
          <span v-if="creating" class="tags-select__state">{{ t('common.creating') }}</span>
        </button>
        <p v-if="!filteredRows.length && !canCreate" class="tags-select__empty">{{ emptyTextValue }}</p>
      </div>
    </div>
  </div>
</template>
