<script setup>
import { computed, ref, watch, nextTick, useSlots, onMounted, onBeforeUnmount } from "vue";
import { useNamespace, useFormItem, useId, useZIndex, useLocale, useConfig } from "@yh-ui/hooks";
import { useComponentTheme } from "@yh-ui/theme";
defineOptions({
  name: "YhAutocomplete"
});
const props = defineProps({
  modelValue: { type: String, required: false },
  placeholder: { type: String, required: false },
  disabled: { type: Boolean, required: false, default: false },
  clearable: { type: Boolean, required: false, default: false },
  size: { type: String, required: false, default: void 0 },
  fetchSuggestions: { type: Function, required: false },
  triggerOnFocus: { type: Boolean, required: false, default: true },
  debounce: { type: Number, required: false, default: 300 },
  placement: { type: String, required: false, default: "bottom-start" },
  valueKey: { type: String, required: false, default: "value" },
  popperClass: { type: String, required: false },
  teleported: { type: Boolean, required: false, default: true },
  fitInputWidth: { type: Boolean, required: false, default: true },
  highlightFirstItem: { type: Boolean, required: false, default: false },
  prefixIcon: { type: null, required: false },
  suffixIcon: { type: null, required: false },
  validateEvent: { type: Boolean, required: false, default: true },
  autofocus: { type: Boolean, required: false, default: false },
  name: { type: String, required: false },
  autocomplete: { type: String, required: false, default: "off" },
  themeOverrides: { type: null, required: false }
});
const emit = defineEmits(["update:modelValue", "input", "change", "focus", "blur", "clear", "select"]);
const slots = useSlots();
const ns = useNamespace("autocomplete");
const inputId = useId();
const { t } = useLocale();
const { nextZIndex } = useZIndex();
const { themeStyle } = useComponentTheme(
  "autocomplete",
  computed(() => props.themeOverrides)
);
const { form, formItem, validate: triggerValidate } = useFormItem();
const { globalSize } = useConfig();
const autocompleteSize = computed(
  () => props.size || formItem?.size || form?.size || globalSize.value || "default"
);
const autocompleteDisabled = computed(() => props.disabled || form?.disabled || false);
const inputRef = ref();
const wrapperRef = ref();
const dropdownRef = ref();
const listRef = ref();
const focused = ref(false);
const hovering = ref(false);
const visible = ref(false);
const loading = ref(false);
const suggestions = ref([]);
const highlightedIndex = ref(-1);
const isClickingDropdown = ref(false);
const dropdownZIndex = ref(nextZIndex());
const dropdownStyle = ref({});
let debounceTimer = null;
let currentRequestId = 0;
const showClear = computed(
  () => props.clearable && !autocompleteDisabled.value && !!props.modelValue && (focused.value || hovering.value)
);
const hasPrefix = computed(() => !!slots.prefix || !!props.prefixIcon);
const hasSuffix = computed(() => !!slots.suffix || !!props.suffixIcon || showClear.value);
const hasPrepend = computed(() => !!slots.prepend);
const hasAppend = computed(() => !!slots.append);
const wrapperClasses = computed(() => [
  ns.b(),
  ns.m(autocompleteSize.value),
  ns.is("disabled", autocompleteDisabled.value),
  ns.is("focused", focused.value),
  {
    [ns.b("group")]: hasPrepend.value || hasAppend.value,
    [ns.bem("group", "", "prepend")]: hasPrepend.value,
    [ns.bem("group", "", "append")]: hasAppend.value
  }
]);
const updateDropdownPosition = () => {
  if (typeof window === "undefined" || !wrapperRef.value || !props.teleported) return;
  const rect = wrapperRef.value.getBoundingClientRect();
  const styles = window.getComputedStyle(wrapperRef.value);
  const primary = styles.getPropertyValue("--yh-color-primary").trim();
  const primaryRgb = styles.getPropertyValue("--yh-color-primary-rgb").trim();
  const style = {
    ...themeStyle.value,
    position: "fixed",
    zIndex: String(dropdownZIndex.value),
    minWidth: props.fitInputWidth ? `${rect.width}px` : "auto",
    "--yh-color-primary": primary,
    "--yh-color-primary-rgb": primaryRgb
  };
  if (props.placement.startsWith("top")) {
    style.bottom = `${window.innerHeight - rect.top + 4}px`;
  } else {
    style.top = `${rect.bottom + 4}px`;
  }
  if (props.placement.endsWith("start")) {
    style.left = `${rect.left}px`;
  } else if (props.placement.endsWith("end")) {
    style.right = `${window.innerWidth - rect.right}px`;
  } else {
    style.left = `${rect.left}px`;
  }
  dropdownStyle.value = style;
};
watch(visible, (val) => {
  if (val) {
    dropdownZIndex.value = nextZIndex();
    if (props.teleported) {
      nextTick(updateDropdownPosition);
    }
  }
});
const handleResize = () => {
  if (visible.value) {
    updateDropdownPosition();
  }
};
const handleScroll = () => {
  if (visible.value) {
    updateDropdownPosition();
  }
};
const handleOutsideClick = (e) => {
  const target = e.target;
  if (wrapperRef.value?.contains(target) || dropdownRef.value?.contains(target) || target.closest?.("[data-popper-root]") === dropdownRef.value) {
    return;
  }
  visible.value = false;
};
onMounted(() => {
  if (typeof window !== "undefined") {
    if (props.teleported) {
      window.addEventListener("resize", handleResize);
      window.addEventListener("scroll", handleScroll, true);
    }
    window.addEventListener("click", handleOutsideClick);
  }
});
onBeforeUnmount(() => {
  if (typeof window !== "undefined") {
    if (props.teleported) {
      window.removeEventListener("resize", handleResize);
      window.removeEventListener("scroll", handleScroll, true);
    }
    window.removeEventListener("click", handleOutsideClick);
  }
  if (debounceTimer) {
    clearTimeout(debounceTimer);
  }
});
const fetchSuggestions = (query) => {
  if (!props.fetchSuggestions) {
    suggestions.value = [];
    return;
  }
  loading.value = true;
  if (focused.value) {
    visible.value = true;
  }
  currentRequestId++;
  const requestId = currentRequestId;
  props.fetchSuggestions(query, (results) => {
    if (requestId !== currentRequestId) return;
    loading.value = false;
    suggestions.value = results || [];
    if (props.highlightFirstItem && suggestions.value.length > 0) {
      highlightedIndex.value = 0;
    } else {
      highlightedIndex.value = -1;
    }
    if (focused.value) {
      visible.value = true;
    }
  });
};
const debouncedFetch = (query) => {
  if (debounceTimer) {
    clearTimeout(debounceTimer);
  }
  debounceTimer = setTimeout(() => {
    fetchSuggestions(query);
  }, props.debounce);
};
const handleInput = (event) => {
  const target = event.target;
  const value = target.value;
  emit("update:modelValue", value);
  emit("input", value);
  if (value) {
    debouncedFetch(value);
  } else {
    currentRequestId++;
    suggestions.value = [];
    visible.value = false;
  }
};
const handleChange = (event) => {
  const target = event.target;
  emit("change", target.value);
};
const handleFocus = (event) => {
  focused.value = true;
  emit("focus", event);
  if (props.triggerOnFocus) {
    fetchSuggestions(props.modelValue || "");
  }
};
const handleBlur = (event) => {
  setTimeout(() => {
    if (!isClickingDropdown.value) {
      focused.value = false;
      visible.value = false;
      emit("blur", event);
      if (props.validateEvent) {
        triggerValidate("blur");
      }
    }
  }, 150);
};
const handleMouseEnter = () => {
  hovering.value = true;
};
const handleMouseLeave = () => {
  hovering.value = false;
};
const handleClear = () => {
  currentRequestId++;
  emit("update:modelValue", "");
  emit("change", "");
  emit("clear");
  suggestions.value = [];
  visible.value = false;
  nextTick(() => {
    focus();
  });
};
const handleSelect = (item) => {
  const value = String(item[props.valueKey] || item.value);
  emit("update:modelValue", value);
  emit("select", item);
  emit("change", value);
  visible.value = false;
  highlightedIndex.value = -1;
  if (props.validateEvent) {
    triggerValidate("change");
  }
};
const handleKeydown = (event) => {
  if (autocompleteDisabled.value) return;
  switch (event.key) {
    case "ArrowDown":
      event.preventDefault();
      if (!visible.value) {
        if (props.triggerOnFocus || props.modelValue) {
          visible.value = true;
          fetchSuggestions(props.modelValue || "");
        }
      } else {
        if (suggestions.value.length === 0) return;
        highlightedIndex.value = (highlightedIndex.value + 1) % suggestions.value.length;
        scrollToHighlighted();
      }
      break;
    case "ArrowUp":
      event.preventDefault();
      if (visible.value) {
        if (suggestions.value.length === 0) return;
        highlightedIndex.value = (highlightedIndex.value - 1 + suggestions.value.length) % suggestions.value.length;
        scrollToHighlighted();
      }
      break;
    case "Enter":
      if (visible.value && highlightedIndex.value >= 0) {
        event.preventDefault();
        handleSelect(suggestions.value[highlightedIndex.value]);
      }
      break;
    case "Escape":
      if (visible.value) {
        event.preventDefault();
        visible.value = false;
      }
      break;
    case "Tab":
      visible.value = false;
      break;
  }
};
const scrollToHighlighted = () => {
  nextTick(() => {
    if (listRef.value && highlightedIndex.value >= 0) {
      const items = listRef.value.querySelectorAll(`.${ns.e("suggestion")}`);
      const item = items[highlightedIndex.value];
      if (item) {
        const list = listRef.value;
        const scrollTop = list.scrollTop;
        const offsetTop = item.offsetTop;
        const height = list.offsetHeight;
        const itemHeight = item.offsetHeight;
        if (offsetTop < scrollTop) {
          list.scrollTop = offsetTop;
        } else if (offsetTop + itemHeight > scrollTop + height) {
          list.scrollTop = offsetTop + itemHeight - height;
        }
      }
    }
  });
};
const focus = () => {
  inputRef.value?.focus();
};
const blur = () => {
  inputRef.value?.blur();
};
const close = () => {
  visible.value = false;
};
const highlight = (index) => {
  highlightedIndex.value = Math.max(0, Math.min(index, suggestions.value.length - 1));
};
defineExpose({
  focus,
  blur,
  close,
  highlight,
  inputRef: inputRef.value
});
</script>

<template>
  <div
    ref="wrapperRef"
    :class="wrapperClasses"
    :style="themeStyle"
    @mouseenter="handleMouseEnter"
    @mouseleave="handleMouseLeave"
  >
    <!-- 前置元素 -->
    <div v-if="hasPrepend" :class="ns.e('prepend')">
      <slot name="prepend" />
    </div>

    <!-- 输入框包装器 -->
    <div :class="ns.e('wrapper')">
      <!-- 前置图标 -->
      <span v-if="hasPrefix" :class="ns.e('prefix')">
        <slot name="prefix">
          <component
            v-if="prefixIcon && typeof prefixIcon !== 'string'"
            :is="prefixIcon"
            :class="ns.e('icon')"
          />
          <span v-else-if="prefixIcon" :class="ns.e('icon')">{{ prefixIcon }}</span>
        </slot>
      </span>

      <!-- 输入框 -->
      <input
        ref="inputRef"
        :id="inputId"
        :class="ns.e('inner')"
        :value="modelValue"
        :placeholder="placeholder ?? t('autocomplete.placeholder')"
        :disabled="autocompleteDisabled"
        :name="name"
        :autocomplete="autocomplete"
        :autofocus="autofocus"
        role="combobox"
        :aria-expanded="visible"
        :aria-autocomplete="'list'"
        :aria-controls="`${inputId}-listbox`"
        :aria-activedescendant="highlightedIndex >= 0 ? `${inputId}-option-${highlightedIndex}` : void 0"
        @input="handleInput"
        @change="handleChange"
        @focus="handleFocus"
        @blur="handleBlur"
        @keydown="handleKeydown"
      />

      <!-- 后置图标 -->
      <span v-if="hasSuffix" :class="ns.e('suffix')">
        <!-- 清空按钮 -->
        <span
          v-if="showClear"
          :class="[ns.e('icon'), ns.e('clear')]"
          @mousedown.prevent
          @click.stop="handleClear"
        >
          <svg viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em">
            <path
              fill="currentColor"
              d="M512 64a448 448 0 1 1 0 896 448 448 0 0 1 0-896zm0 393.664L407.936 353.6a38.4 38.4 0 1 0-54.336 54.336L457.664 512 353.6 616.064a38.4 38.4 0 1 0 54.336 54.336L512 566.336 616.064 670.4a38.4 38.4 0 1 0 54.336-54.336L566.336 512 670.4 407.936a38.4 38.4 0 1 0-54.336-54.336L512 457.664z"
            />
          </svg>
        </span>

        <!-- 后置图标插槽 -->
        <slot name="suffix">
          <component
            v-if="suffixIcon && typeof suffixIcon !== 'string'"
            :is="suffixIcon"
            :class="ns.e('icon')"
          />
          <span v-else-if="suffixIcon" :class="ns.e('icon')">{{ suffixIcon }}</span>
        </slot>
      </span>
    </div>

    <!-- 后置元素 -->
    <div v-if="hasAppend" :class="ns.e('append')">
      <slot name="append" />
    </div>

    <!-- 下拉建议列表 -->
    <Teleport to="body" :disabled="!teleported">
      <Transition :name="ns.b('dropdown')">
        <div
          v-show="visible && (suggestions.length > 0 || loading || slots.empty)"
          ref="dropdownRef"
          :class="[ns.e('dropdown'), popperClass]"
          :style="teleported ? dropdownStyle : {}"
          data-popper-root
          @mousedown="isClickingDropdown = true"
          @mouseup="isClickingDropdown = false"
        >
          <!-- 加载状态 -->
          <div v-if="loading" :class="ns.e('loading')">
            <slot name="loading">
              <svg
                :class="ns.e('loading-icon')"
                viewBox="0 0 1024 1024"
                xmlns="http://www.w3.org/2000/svg"
              >
                <path
                  fill="currentColor"
                  d="M512 64a32 32 0 0 1 32 32v192a32 32 0 0 1-64 0V96a32 32 0 0 1 32-32zm0 640a32 32 0 0 1 32 32v192a32 32 0 1 1-64 0V736a32 32 0 0 1 32-32zm448-192a32 32 0 0 1-32 32H736a32 32 0 1 1 0-64h192a32 32 0 0 1 32 32zm-640 0a32 32 0 0 1-32 32H96a32 32 0 0 1 0-64h192a32 32 0 0 1 32 32zM195.2 195.2a32 32 0 0 1 45.248 0L376.32 331.008a32 32 0 0 1-45.248 45.248L195.2 240.448a32 32 0 0 1 0-45.248zm452.544 452.544a32 32 0 0 1 45.248 0L828.8 783.552a32 32 0 0 1-45.248 45.248L647.744 692.992a32 32 0 0 1 0-45.248zM828.8 195.264a32 32 0 0 1 0 45.184L692.992 376.32a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0zm-452.544 452.48a32 32 0 0 1 0 45.248L240.448 828.8a32 32 0 0 1-45.248-45.248l135.808-135.808a32 32 0 0 1 45.248 0z"
                />
              </svg>
              <span>{{ t("autocomplete.loading") }}</span>
            </slot>
          </div>

          <!-- 建议列表 -->
          <ul
            v-else-if="suggestions.length > 0"
            ref="listRef"
            :id="`${inputId}-listbox`"
            :class="ns.e('suggestions')"
            role="listbox"
          >
            <li
              v-for="(item, index) in suggestions"
              :key="index"
              :id="`${inputId}-option-${index}`"
              :class="[ns.e('suggestion'), ns.is('highlighted', highlightedIndex === index)]"
              role="option"
              :aria-selected="highlightedIndex === index"
              @mousedown.prevent
              @click="handleSelect(item)"
              @mouseenter="highlightedIndex = index"
            >
              <slot :item="item">
                {{ item[valueKey] || item.value }}
              </slot>
            </li>
          </ul>

          <!-- 无数据 -->
          <div v-else-if="slots.empty" :class="ns.e('empty')">
            <slot name="empty" />
          </div>
        </div>
      </Transition>
    </Teleport>
  </div>
</template>

<style>
@charset "UTF-8";
/**
 * YH-UI Sass Variables
 * 原具体的 CSS 变量输出规则 (:root, html.dark 等) 已移至 root.scss，以防止各组件样式编译时产生重复的全局变量 CSS 冗余。
 */
.yh-autocomplete {
  position: relative;
  display: inline-flex;
  width: 100%;
  font-size: 14px;
  vertical-align: middle;
}
.yh-autocomplete__wrapper {
  display: inline-flex;
  flex-grow: 1;
  align-items: center;
  justify-content: center;
  position: relative;
  padding: 1px 11px;
  background-color: var(--yh-fill-color-blank, #fff);
  border-radius: var(--yh-border-radius-base, 4px);
  border: 1px solid var(--yh-border-color, #dcdfe6);
  box-sizing: border-box;
  transition: all 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
  cursor: text;
}
.yh-autocomplete__wrapper:hover {
  border-color: var(--yh-border-color-hover, #c0c4cc);
}

.yh-autocomplete__inner {
  flex-grow: 1;
  width: 100%;
  height: 30px;
  padding: 0;
  color: var(--yh-text-color-regular, #606266);
  font-size: inherit;
  line-height: 30px;
  background: none;
  border: none;
  outline: none;
  appearance: none;
  box-shadow: none;
}
.yh-autocomplete__inner::placeholder {
  color: var(--yh-text-color-placeholder, #a8abb2);
}
.yh-autocomplete__inner:disabled {
  cursor: not-allowed;
  color: var(--yh-text-color-placeholder, #a8abb2);
}

.yh-autocomplete__prefix {
  display: inline-flex;
  align-items: center;
  margin-right: 8px;
  color: var(--yh-text-color-placeholder, #a8abb2);
  flex-shrink: 0;
}

.yh-autocomplete__suffix {
  display: inline-flex;
  align-items: center;
  margin-left: 8px;
  color: var(--yh-text-color-placeholder, #a8abb2);
  flex-shrink: 0;
}

.yh-autocomplete__icon {
  width: 1em;
  height: 1em;
  display: flex;
  align-items: center;
  justify-content: center;
}

.yh-autocomplete__clear {
  cursor: pointer;
  transition: color 0.2s;
  font-size: 14px;
}
.yh-autocomplete__clear:hover {
  color: var(--yh-text-color-secondary, #909399);
}

.yh-autocomplete__prepend {
  display: inline-flex;
  align-items: center;
  padding: 0 20px;
  background-color: var(--yh-fill-color-light, #f5f7fa);
  border: 1px solid var(--yh-border-color, #dcdfe6);
  border-right: 0;
  border-radius: var(--yh-border-radius-base, 4px) 0 0 var(--yh-border-radius-base, 4px);
  white-space: nowrap;
  color: var(--yh-text-color-secondary, #909399);
}

.yh-autocomplete__append {
  display: inline-flex;
  align-items: center;
  padding: 0 20px;
  background-color: var(--yh-fill-color-light, #f5f7fa);
  border: 1px solid var(--yh-border-color, #dcdfe6);
  border-left: 0;
  border-radius: 0 var(--yh-border-radius-base, 4px) var(--yh-border-radius-base, 4px) 0;
  white-space: nowrap;
  color: var(--yh-text-color-secondary, #909399);
}

.yh-autocomplete__dropdown {
  position: absolute;
  margin-top: 4px;
  background-color: var(--yh-bg-color-overlay, #fff);
  border: 1px solid var(--yh-border-color-light, #e4e7ed);
  border-radius: var(--yh-border-radius-base, 4px);
  box-shadow: var(--yh-box-shadow-light, 0 2px 12px 0 rgba(0, 0, 0, 0.1));
  box-sizing: border-box;
  overflow: hidden;
}

.yh-autocomplete__suggestions {
  margin: 0;
  padding: 6px 0;
  list-style: none;
  max-height: 280px;
  overflow-y: auto;
  box-sizing: border-box;
}

.yh-autocomplete__suggestion {
  padding: 8px 16px;
  min-height: 34px;
  font-size: 14px;
  color: var(--yh-text-color-regular, #606266);
  cursor: pointer;
  transition: background-color 0.2s;
  box-sizing: border-box;
}
.yh-autocomplete__suggestion:hover {
  background-color: var(--yh-fill-color-light, #f5f7fa);
}
.yh-autocomplete__suggestion.is-highlighted {
  color: var(--yh-color-white, #ffffff);
  background-color: var(--yh-color-primary, #409eff);
  /* 确保自定义内容中的所有文字都显示为白色 */
}
.yh-autocomplete__suggestion.is-highlighted *,
.yh-autocomplete__suggestion.is-highlighted span,
.yh-autocomplete__suggestion.is-highlighted div,
.yh-autocomplete__suggestion.is-highlighted p,
.yh-autocomplete__suggestion.is-highlighted a {
  color: #ffffff !important;
}

.yh-autocomplete__loading {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px 0;
  color: var(--yh-text-color-secondary, #909399);
  font-size: 14px;
}

.yh-autocomplete__empty {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 20px 0;
  color: var(--yh-text-color-placeholder, #a8abb2);
  font-size: 14px;
}

.yh-autocomplete__loading-icon {
  width: 16px;
  height: 16px;
  margin-right: 8px;
  animation: yh-autocomplete-loading 1s linear infinite;
}

.yh-autocomplete.is-focused .yh-autocomplete__wrapper {
  border-color: var(--yh-color-primary, #409eff);
  box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2);
}

.yh-autocomplete.is-disabled {
  cursor: not-allowed;
}
.yh-autocomplete.is-disabled .yh-autocomplete__wrapper {
  background-color: var(--yh-fill-color-light, #f5f7fa);
  border-color: var(--yh-border-color-light, #e4e7ed);
  cursor: not-allowed;
}
.yh-autocomplete.is-disabled .yh-autocomplete__wrapper:hover {
  border-color: var(--yh-border-color-light, #e4e7ed);
}

.yh-autocomplete.is-disabled .yh-autocomplete__inner {
  color: var(--yh-text-color-placeholder, #a8abb2);
  -webkit-text-fill-color: var(--yh-text-color-placeholder, #a8abb2);
}

.yh-autocomplete--large {
  font-size: 14px;
}
.yh-autocomplete--large .yh-autocomplete__wrapper {
  padding: 1px 15px;
}

.yh-autocomplete--large .yh-autocomplete__inner {
  height: 38px;
  line-height: 38px;
}

.yh-autocomplete--small {
  font-size: 12px;
}
.yh-autocomplete--small .yh-autocomplete__wrapper {
  padding: 1px 7px;
}

.yh-autocomplete--small .yh-autocomplete__inner {
  height: 22px;
  line-height: 22px;
}

.yh-group__wrapper {
  border-radius: 0;
  z-index: 1;
}
.yh-group__wrapper:focus-within {
  z-index: 2;
}

.yh-autocomplete .yh-group--prepend .yh-group__wrapper {
  border-top-left-radius: 0;
  border-bottom-left-radius: 0;
}

.yh-autocomplete .yh-group--append .yh-group__wrapper {
  border-top-right-radius: 0;
  border-bottom-right-radius: 0;
}

.yh-autocomplete-dropdown-enter-active,
.yh-autocomplete-dropdown-leave-active {
  transition: opacity 0.3s cubic-bezier(0.23, 1, 0.32, 1), transform 0.3s cubic-bezier(0.23, 1, 0.32, 1);
}

.yh-autocomplete-dropdown-enter-from,
.yh-autocomplete-dropdown-leave-to {
  opacity: 0;
  transform: translateY(-10px) scaleY(0);
}

@keyframes yh-autocomplete-loading {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}
</style>
