<template>
  <Combobox as="div" class="relative"
    v-model="comboBox"
    @update:modelValue="handleSelection"
  >
    <ComboboxInput as="template"
      ref="comboInput"
      #default="{open, disabled}"
      @keyup.enter="handleEnter"
      @keyup.esc="handleEscape"
    >
      <AisSearchInput ref="input"
        v-model="query"
        @clear="clearSelection"
        class="focus:shadow"
        :placeholder="placeholder"
        :disabled="disabled"
      />
    </ComboboxInput>

    <ComboboxOptions v-if="hasResults"
        class="absolute z-50 w-full bg-white shadow-md
             m-0 p-0 mt-1.5 border text-base divide-y
             max-h-96 overflow-y-auto"
    >
      <ComboboxOption
          v-for="item in filteredResults"
          :key="item" :value="item"
          #default="{active, selected, disabled}">
        <div :class="{ [props.highlight]: selected, [props.highlight]: active }">
          <slot name="default"
                :item="item" :disabled="disabled"
                :active="active" :selected="selected"
          >
            <div class="p-3 text-lg font-medium">
              {{ searchService.format(item) }}
            </div>
          </slot>
        </div>
      </ComboboxOption>
    </ComboboxOptions>
  </Combobox>
</template>

<script setup>
import { ref, computed, onBeforeMount, onMounted } from 'vue'
import { useVModel, debouncedWatch } from "@vueuse/core";
import {hasLength} from "@/util/util";
import {
  Combobox, ComboboxLabel, ComboboxOption, ComboboxOptions, ComboboxInput, ComboboxButton
} from "@headlessui/vue";
import AisSearchInput from '@c/search-input/AisSearchInput.vue'

const props = defineProps({

  /**
   * The value to identify a specific search result -- typically a database table's
   * primary key (e.g. id) or business key (e.g. hawkId). The search service will call
   * `lookup(value)` on this value to initialize the selection.
   */
  modelValue: { type: [String, Object], required: false },

  /**
   * The lookup specifies the attribute name in a search result object
   * that can be used to identify a specific record. When a record is selected,
   * the value of this attribute will be emitted with the update:ModelValue event.
   * For example, if we want v-model to sync the selected HawkId, then our lookup key
   * would be something like hawkId.
   */
  lookup: {type: String, required: true},

  /**
   * Service object that will transform the search query into search results.
   * The service object must implement the expected API, which is detailed
   * in the next section.
   */
  service: { type: [Object], required: true, default: null },

  /**
   * The placeholder attribute on the search input field
   */
  placeholder: {type: String, required: false, default: "Search..."},

  /**
   * Tailwind CSS background color class used to highlight selected option.
   */
  highlight: { type: String, required: false, default: 'bg-ui-gray-50' },

  /**
   * The search query is debounced, so we're not sending requests with every
   * keystroke. The `lag` prop allows you to customize how often requests are
   * sent. The default value is 250 milliseconds.
   */
  lag: { type: Number, required: false, default: 250 }
})

const emit = defineEmits([
  'update:modelValue',
  'load',
  'change'
])

const input = ref(null)
const comboInput = ref(null)

const model = useVModel(props, 'modelValue', emit)
const comboBox = ref(undefined)
const query = ref('')
const searchService = computed(() => props.service)

const filteredResults = ref([])
const hasResults = computed(() => hasLength(filteredResults))

async function initializeValue() {
  try {
    if(!model.value) return
    comboBox.value = await searchService.value.lookup(model.value)
    query.value = searchService.value.format(comboBox.value)
    emit('load', comboBox.value)
  } catch(e) {
    console.log('Initialize value failed', e)
  }
}

onMounted(() => {
  // HeadlessUI adds tabindex to the div surrounding our input,
  // which makes it focusable. We need to kill this attribute.
  comboInput.value.$el.removeAttribute("tabindex")
})

onBeforeMount(async () => {
  await initializeValue()
})

async function querySearch (newValue, oldValue) {
  try {
    filteredResults.value = await searchService.value.execute(newValue)

  } catch(e) {
    console.log('Query search failed', e)
  }
}

debouncedWatch(query, querySearch, { debounce: props.lag })

const handleSelection = ($event) => {
  let selection = $event
  model.value = selection ? selection[props.lookup] : undefined
  query.value = searchService.value.format(selection)
  emit('change', comboBox.value)
}

const clearSelection = ($event) => {
  query.value = ''
  model.value = undefined
  comboBox.value = undefined
  emit('change', comboBox.value)
}

/**
 * ComboboxInput shadows AisSearchInput's use of enter to clear it's value.
 * We re-build the functionality by responding to ComboboxInput's @keydown.enter
 * event.
 *
 * todo Not sure why we can't focus the search input via template ref.
 */

function handleEnter(evt) {
  const isClearButton = evt.target && evt.target.classList.contains('ibttn')

  if(isClearButton) {
    clearSelection()

    const wrapper = comboInput.value.$el
    const inputElement = wrapper.querySelector('input')
    inputElement.focus()
  }
}

function handleEscape(evt) {
  if(comboBox.value) {
    query.value = searchService.value.format(comboBox.value)
  }
}

const focus = () => input.value.focus()
defineExpose({ focus })
</script>
