<script setup lang="ts">
import Fuse from 'fuse.js'
import { computed, ref, watch } from 'vue'
import TitleRenderer from '#slidev/title-renderer'
import { useNav } from '../composables/useNav'
import { activeElement, showGotoDialog } from '../state'

const container = ref<HTMLDivElement>()
const input = ref<HTMLInputElement>()
const list = ref<HTMLUListElement>()
const items = ref<HTMLLIElement[]>()
const text = ref('')
const selectedIndex = ref(0)

const { go, slides } = useNav()

function notNull<T>(value: T | null | undefined): value is T {
  return value !== null && value !== undefined
}

const fuse = computed(() => new Fuse(slides.value.map(i => i.meta?.slide).filter(notNull), {
  keys: ['no', 'title'],
  threshold: 0.3,
  shouldSort: true,
  minMatchCharLength: 1,
}))

const path = computed(() => text.value.startsWith('/') ? text.value.substring(1) : text.value)
const result = computed(() => path.value ? fuse.value.search(path.value).map(result => result.item) : [])
const valid = computed(() => !!result.value.length)

function goTo() {
  if (valid.value) {
    const item = result.value.at(selectedIndex.value || 0)
    if (item)
      go(item.no)
  }
  close()
}

function close() {
  text.value = ''
  showGotoDialog.value = false
}

function focusDown(event: Event) {
  event.preventDefault()
  selectedIndex.value++
  if (selectedIndex.value >= result.value.length)
    selectedIndex.value = 0
  scroll()
}

function focusUp(event: Event) {
  event.preventDefault()
  selectedIndex.value--
  if (selectedIndex.value <= -2)
    selectedIndex.value = result.value.length - 1
  scroll()
}

function scroll() {
  const item = items.value?.[selectedIndex.value]
  if (item && list.value) {
    if (item.offsetTop + item.offsetHeight > list.value.offsetHeight + list.value.scrollTop) {
      list.value.scrollTo({
        behavior: 'smooth',
        top: item.offsetTop + item.offsetHeight - list.value.offsetHeight + 1,
      })
    }
    else if (item.offsetTop < list.value.scrollTop) {
      list.value.scrollTo({
        behavior: 'smooth',
        top: item.offsetTop,
      })
    }
  }
}

function updateText(event: Event) {
  selectedIndex.value = 0
  text.value = (event.target as HTMLInputElement).value
}

function select(no: number) {
  go(no)
  close()
}

watch(showGotoDialog, async (show) => {
  if (show) {
    text.value = ''
    selectedIndex.value = 0
    // delay the focus to avoid the g character coming from the key that triggered showGotoDialog
    setTimeout(() => input.value?.focus(), 0)
  }
  else {
    input.value?.blur()
  }
})

watch(activeElement, () => {
  if (!container.value?.contains(activeElement.value as Node))
    close()
})
</script>

<template>
  <div
    id="slidev-goto-dialog"
    ref="container"
    class="fixed right-5 transition-all"
    w-90 max-w-90 min-w-90
    :class="showGotoDialog ? 'top-5' : '-top-20 op0 pointer-events-none'"
  >
    <div
      class="bg-main transform"
      shadow="~"
      p="x-4 y-2"
      border="~ transparent rounded dark:main"
    >
      <input
        id="slidev-goto-input"
        ref="input"
        :value="text"
        type="text"
        :disabled="!showGotoDialog"
        class="outline-none bg-transparent"
        placeholder="Goto..."
        :class="{ 'text-red-400': !valid && text }"
        @keydown.enter="goTo"
        @keydown.escape="close"
        @keydown.down="focusDown"
        @keydown.up="focusUp"
        @input="updateText"
      >
    </div>
    <div
      v-if="showGotoDialog && result.length > 0"
      ref="list"
      class="autocomplete-list"
      shadow="~"
      border="~ transparent rounded dark:main"
    >
      <ul table w-full border-collapse>
        <li
          v-for="(item, index) of result"
          ref="items"
          :key="item.id"
          role="button"
          tabindex="0"
          cursor-pointer
          hover="op100"
          table-row
          items-center
          :border="index === 0 ? undefined : 't main'"
          :class="selectedIndex === index ? 'bg-active op100' : 'op80'"
          @click.stop.prevent="select(item.no)"
        >
          <div text-right op50 text-sm table-cell py-2 pl-4 pr-3 vertical-middle>
            {{ item.no }}
          </div>
          <TitleRenderer table-cell py-2 pr-4 w-full :no="item.no" />
        </li>
      </ul>
    </div>
  </div>
</template>

<style scoped>
.autocomplete-list {
  --uno: bg-main mt-1;
  overflow: auto;
  max-height: calc(100vh - 100px);
}

.autocomplete {
  cursor: pointer;
}
</style>
