<script lang="ts" setup>
import type { ElScrollbar } from "element-plus"
import type { RouteRecordName, RouteRecordRaw } from "vue-router"
import { useDevice } from "@/composables/useDevice"
import { usePermissionStore } from "@/store/modules/permission"
import { isExternal } from "@/utils/validate"
import { ElMessage } from "element-plus"
import { cloneDeep, debounce } from "lodash-es"
import { computed, ref, shallowRef } from "vue"
import { useRouter } from "vue-router"
import SearchFooter from "./SearchFooter.vue"
import SearchResult from "./SearchResult.vue"

/** 控制 modal 显隐 */
const modelValue = defineModel<boolean>({ required: true })

const router = useRouter()
const { isMobile } = useDevice()

const inputRef = ref<HTMLInputElement | null>(null)
const scrollbarRef = ref<InstanceType<typeof ElScrollbar> | null>(null)
const searchResultRef = ref<InstanceType<typeof SearchResult> | null>(null)

const keyword = ref<string>("")
const resultList = shallowRef<RouteRecordRaw[]>([])
const activeRouteName = ref<RouteRecordName | undefined>(undefined)
/** 是否按下了上键或下键（用于解决和 mouseenter 事件的冲突） */
const isPressUpOrDown = ref<boolean>(false)

/** 控制搜索对话框宽度 */
const modalWidth = computed(() => (isMobile.value ? "80vw" : "40vw"))
/** 树形菜单 */
const menusData = computed(() => cloneDeep(usePermissionStore().routes))

/** 搜索（防抖） */
const handleSearch = debounce(() => {
  const flatMenusData = flatTree(menusData.value)
  resultList.value = flatMenusData.filter(menu =>
    keyword.value ? menu.meta?.title?.toLocaleLowerCase().includes(keyword.value.toLocaleLowerCase().trim()) : false
  )
  // 默认选中搜索结果的第一项
  const length = resultList.value?.length
  activeRouteName.value = length > 0 ? resultList.value[0].name : undefined
}, 500)

/** 将树形菜单扁平化为一维数组，用于菜单搜索 */
function flatTree(arr: RouteRecordRaw[], result: RouteRecordRaw[] = []) {
  arr.forEach((item) => {
    result.push(item)
    item.children && flatTree(item.children, result)
  })
  return result
}

/** 关闭搜索对话框 */
function handleClose() {
  modelValue.value = false
  // 延时处理防止用户看到重置数据的操作
  setTimeout(() => {
    keyword.value = ""
    resultList.value = []
  }, 200)
}

/** 根据下标位置进行滚动 */
function scrollTo(index: number) {
  if (!searchResultRef.value) return
  const scrollTop = searchResultRef.value.getScrollTop(index)
  // 手动控制 el-scrollbar 滚动条滚动，设置滚动条到顶部的距离
  scrollbarRef.value?.setScrollTop(scrollTop)
}

/** 键盘上键 */
function handleUp() {
  isPressUpOrDown.value = true
  const { length } = resultList.value
  if (length === 0) return
  // 获取该 name 在菜单中第一次出现的位置
  const index = resultList.value.findIndex(item => item.name === activeRouteName.value)
  // 如果已处在顶部
  if (index === 0) {
    const bottomName = resultList.value[length - 1].name
    // 如果顶部和底部的 bottomName 相同，且长度大于 1，就再跳一个位置（可解决遇到首尾两个相同 name 导致的上键不能生效的问题）
    if (activeRouteName.value === bottomName && length > 1) {
      activeRouteName.value = resultList.value[length - 2].name
      scrollTo(length - 2)
    } else {
      // 跳转到底部
      activeRouteName.value = bottomName
      scrollTo(length - 1)
    }
  } else {
    activeRouteName.value = resultList.value[index - 1].name
    scrollTo(index - 1)
  }
}

/** 键盘下键 */
function handleDown() {
  isPressUpOrDown.value = true
  const { length } = resultList.value
  if (length === 0) return
  // 获取该 name 在菜单中最后一次出现的位置（可解决遇到连续两个相同 name 导致的下键不能生效的问题）
  const index = resultList.value.map(item => item.name).lastIndexOf(activeRouteName.value)
  // 如果已处在底部
  if (index === length - 1) {
    const topName = resultList.value[0].name
    // 如果底部和顶部的 topName 相同，且长度大于 1，就再跳一个位置（可解决遇到首尾两个相同 name 导致的下键不能生效的问题）
    if (activeRouteName.value === topName && length > 1) {
      activeRouteName.value = resultList.value[1].name
      scrollTo(1)
    } else {
      // 跳转到顶部
      activeRouteName.value = topName
      scrollTo(0)
    }
  } else {
    activeRouteName.value = resultList.value[index + 1].name
    scrollTo(index + 1)
  }
}

/** 键盘回车键 */
function handleEnter() {
  const { length } = resultList.value
  if (length === 0) return
  const name = activeRouteName.value
  const path = resultList.value.find(item => item.name === name)?.path
  if (path && isExternal(path)) {
    window.open(path, "_blank", "noopener, noreferrer")
    return
  }
  if (!name) {
    ElMessage.warning("无法通过搜索进入该菜单，请为对应的路由设置唯一的 Name")
    return
  }
  try {
    router.push({ name })
  } catch {
    ElMessage.error("该菜单有必填的动态参数，无法通过搜索进入")
    return
  }
  handleClose()
}

/** 释放上键或下键 */
function handleReleaseUpOrDown() {
  isPressUpOrDown.value = false
}
</script>

<template>
  <el-dialog
    v-model="modelValue"
    :before-close="handleClose"
    :width="modalWidth"
    top="5vh"
    class="search-modal__private"
    append-to-body
    @opened="inputRef?.focus()"
    @closed="inputRef?.blur()"
    @keydown.up="handleUp"
    @keydown.down="handleDown"
    @keydown.enter="handleEnter"
    @keyup.up.down="handleReleaseUpOrDown"
  >
    <el-input ref="inputRef" v-model="keyword" placeholder="搜索菜单" size="large" clearable @input="handleSearch">
      <template #prefix>
        <SvgIcon name="search" />
      </template>
    </el-input>
    <el-empty v-if="resultList.length === 0" description="暂无搜索结果" :image-size="100" />
    <template v-else>
      <p>搜索结果</p>
      <el-scrollbar ref="scrollbarRef" max-height="40vh" always>
        <SearchResult
          ref="searchResultRef"
          v-model="activeRouteName"
          :list="resultList"
          :is-press-up-or-down="isPressUpOrDown"
          @click="handleEnter"
        />
      </el-scrollbar>
    </template>
    <template #footer>
      <SearchFooter :total="resultList.length" />
    </template>
  </el-dialog>
</template>

<style lang="scss">
.search-modal__private {
  .svg-icon {
    font-size: 18px;
  }
  .el-dialog__header {
    display: none;
  }
  .el-dialog__footer {
    border-top: 1px solid var(--el-border-color);
    padding-top: var(--el-dialog-padding-primary);
  }
}
</style>
