<script setup lang="ts">
import type { Place } from '#shared/types'

defineProps<{ label: string, id: string }>()
const model = defineModel<Place | null>({ default: null })

const query = ref('')
const results = ref<Place[]>([])
const open = ref(false)
const active = ref(-1)

let timer: ReturnType<typeof setTimeout> | undefined
/** Empeche une ecriture programmatique de relancer une recherche. */
let skipNext = false

/**
 * Le champ doit refleter le modele meme quand il change de l'exterieur :
 * inversion depart/arrivee, trajet d'exemple, restauration d'etat.
 * Sans cela le texte affiche ment sur ce qui est reellement selectionne.
 */
watch(model, (m) => {
  const label = m?.label ?? ''
  if (label === query.value) return
  skipNext = true
  query.value = label
}, { immediate: true })

watch(query, (q) => {
  if (skipNext) {
    skipNext = false
    return
  }

  // Toute frappe invalide la selection : on ne calcule jamais un trajet
  // vers un lieu que l'utilisateur n'a pas explicitement confirme.
  if (model.value && q !== model.value.label) model.value = null

  clearTimeout(timer)
  if (q.trim().length < 3) {
    results.value = []
    open.value = false
    return
  }

  timer = setTimeout(async () => {
    results.value = await $fetch<Place[]>('/api/geocode', { query: { q } })
    open.value = results.value.length > 0
    active.value = -1
  }, 250)
})

function select(place: Place) {
  model.value = place
  skipNext = place.label !== query.value
  query.value = place.label
  open.value = false
  active.value = -1
}

function onKeydown(e: KeyboardEvent) {
  if (!open.value) return

  if (e.key === 'ArrowDown') {
    e.preventDefault()
    active.value = (active.value + 1) % results.value.length
  }
  else if (e.key === 'ArrowUp') {
    e.preventDefault()
    active.value = active.value <= 0 ? results.value.length - 1 : active.value - 1
  }
  else if (e.key === 'Enter' && active.value >= 0) {
    e.preventDefault()
    select(results.value[active.value]!)
  }
  else if (e.key === 'Escape') {
    open.value = false
  }
}

/** Le delai laisse le mousedown de la liste s'executer avant la fermeture. */
function onBlur() {
  setTimeout(() => { open.value = false }, 150)
}
</script>

<template>
  <div class="relative">
    <label :for="id" class="text-xs font-medium uppercase tracking-widest text-ink-soft">
      {{ label }}
    </label>

    <input
      :id="id"
      v-model="query"
      type="text"
      role="combobox"
      autocomplete="off"
      :aria-expanded="open"
      :aria-controls="`${id}-list`"
      :aria-activedescendant="active >= 0 ? `${id}-opt-${active}` : undefined"
      placeholder="Ville, gare ou adresse"
      class="mt-1 w-full border-b-2 bg-transparent py-2 text-lg outline-none"
      :class="model ? 'border-bike' : 'border-ink'"
      @keydown="onKeydown"
      @focus="open = results.length > 0"
      @blur="onBlur"
    >

    <ul
      v-if="open"
      :id="`${id}-list`"
      role="listbox"
      class="absolute z-20 mt-1 w-full border-2 border-ink bg-paper shadow-[4px_4px_0_0_var(--color-ink)]"
    >
      <li
        v-for="(place, i) in results"
        :id="`${id}-opt-${i}`"
        :key="place.label"
        role="option"
        :aria-selected="i === active"
        class="cursor-pointer px-3 py-2 text-sm"
        :class="i === active ? 'bg-ink text-paper' : 'hover:bg-paper-2'"
        @mousedown.prevent="select(place)"
      >
        {{ place.label }}
      </li>
    </ul>

    <p class="sr-only" aria-live="polite">
      {{ open ? `${results.length} lieux proposés` : '' }}
    </p>
  </div>
</template>