<template>
  <div ref="menu" class="form-control w-full">
    <label class="label">
      <span class="label-text">{{ label }}</span>
    </label>
    <div class="dropdown dropdown-top sm:dropdown-end">
      <div class="relative">
        <input
          v-model="internalSearch"
          tabindex="0"
          class="input w-full items-center flex flex-wrap border border-gray-400 rounded-lg"
          @keyup.enter="selectFirst"
        />
        <button
          v-if="!!modelValue && Object.keys(modelValue).length !== 0"
          style="transform: translateY(-50%)"
          class="top-1/2 absolute right-2 btn btn-xs btn-circle no-animation"
          @click="clear"
        >
          x
        </button>
      </div>
      <ul
        tabindex="0"
        style="display: inline"
        class="dropdown-content mb-1 menu shadow border border-gray-400 rounded bg-base-100 w-full z-[9999] max-h-60 overflow-y-scroll"
      >
        <li v-for="(obj, idx) in filtered" :key="idx">
          <div type="button" @click="select(obj)">
            <slot name="display" v-bind="{ item: obj }">
              {{ extractor(obj, itemText) }}
            </slot>
          </div>
        </li>
        <li class="hidden first:flex">
          <button disabled>
            {{ noResultsText }}
          </button>
        </li>
      </ul>
    </div>
  </div>
</template>

<script lang="ts" setup>
  type ItemsObject = {
    text?: string;
    value?: string;
    [key: string]: unknown;
  };

  interface Props {
    label: string;
    modelValue: string | ItemsObject | null | undefined;
    items: ItemsObject[] | string[];
    itemText?: keyof ItemsObject;
    itemSearch?: keyof ItemsObject | null;
    itemValue?: keyof ItemsObject;
    search?: string;
    noResultsText?: string;
  }

  const emit = defineEmits(["update:modelValue", "update:search"]);
  const props = withDefaults(defineProps<Props>(), {
    label: "",
    modelValue: "",
    items: () => [],
    itemText: "text",
    search: "",
    itemSearch: null,
    itemValue: "value",
    noResultsText: "No Results Found",
  });

  const searchKey = computed(() => props.itemSearch || props.itemText);

  function clear() {
    select(value.value);
  }

  const internalSearch = ref("");

  watch(
    () => props.search,
    val => {
      internalSearch.value = val;
    }
  );

  watch(
    () => internalSearch.value,
    val => {
      emit("update:search", val);
    }
  );

  function extractor(obj: string | ItemsObject, key: string | number): string {
    if (typeof obj === "string") {
      return obj;
    }

    return obj[key] as string;
  }

  const value = useVModel(props, "modelValue", emit);

  const usingObjects = computed(() => {
    return props.items.length > 0 && typeof props.items[0] === "object";
  });

  /**
   * isStrings is a type guard function to check if the items are an array of string
   */
  function isStrings(_arr: string[] | ItemsObject[]): _arr is string[] {
    return !usingObjects.value;
  }

  function selectFirst() {
    if (filtered.value.length > 0) {
      select(filtered.value[0]);
    }
  }

  watch(
    value,
    () => {
      if (value.value) {
        if (typeof value.value === "string") {
          internalSearch.value = value.value;
        } else {
          internalSearch.value = value.value[searchKey.value] as string;
        }
      }
    },
    {
      immediate: true,
    }
  );

  function select(obj: Props["modelValue"]) {
    if (isStrings(props.items)) {
      if (obj === value.value) {
        value.value = "";
        return;
      }
      // @ts-ignore
      value.value = obj;
    } else {
      if (obj === value.value) {
        value.value = {};
        return;
      }

      // @ts-ignore
      value.value = obj;
    }
  }

  const filtered = computed(() => {
    if (!internalSearch.value || internalSearch.value === "") {
      return props.items;
    }

    if (isStrings(props.items)) {
      return props.items.filter(item => item.toLowerCase().includes(internalSearch.value.toLowerCase()));
    } else {
      return props.items.filter(item => {
        if (searchKey.value && searchKey.value in item) {
          return (item[searchKey.value] as string).toLowerCase().includes(internalSearch.value.toLowerCase());
        }
        return false;
      });
    }
  });
</script>