<template>
  <div class="antd-icon-picker">
    <a-input-group compact class="antd-icon-picker-row">
      <a-input
        v-model:value="iconName"
        :placeholder="placeholder"
        :maxlength="maxlength"
        allow-clear
        class="antd-icon-picker-input"
      >
        <template v-if="iconName" #prefix>
          <span class="antd-icon-picker-prefix">
            <component v-if="previewRender" :is="previewRender" />
          </span>
        </template>
      </a-input>
      <a-popover
        v-model:open="pickerOpen"
        trigger="click"
        placement="bottomLeft"
        :overlay-style="{ width: '400px' }"
      >
        <template #content>
          <a-form-item-rest>
            <a-input-search
              v-model:value="keyword"
              placeholder="搜索图标名称"
              allow-clear
              class="mb-2"
            />
            <a-tabs v-model:active-key="iconStyleTab" size="small" class="antd-icon-picker-tabs mb-2">
              <a-tab-pane key="outlined" tab="线条" />
              <a-tab-pane key="filled" tab="实底" />
              <a-tab-pane key="twoTone" tab="双色" />
            </a-tabs>
            <!-- 虚拟列表：只挂载可视区域内的行，避免一次性渲染上千个图标组件 -->
            <div
              v-if="filteredNames.length > 0"
              ref="scrollRef"
              class="antd-icon-picker-grid"
              @scroll.passive="onGridScroll"
            >
              <div class="antd-icon-picker-grid-spacer" :style="{ height: `${totalScrollHeight}px` }">
                <div
                  class="antd-icon-picker-grid-inner"
                  :style="{ transform: `translateY(${virtualOffsetY}px)` }"
                >
                  <div
                    v-for="(row, rowIdx) in visibleRows"
                    :key="virtualStartRow + rowIdx"
                    class="antd-icon-picker-grid-row"
                  >
                    <button
                      v-for="name in row"
                      :key="name"
                      type="button"
                      :title="name"
                      class="antd-icon-picker-cell"
                      :class="{ 'is-active': iconName === name }"
                      @click="selectIcon(name)"
                    >
                      <component :is="getIconRenderCached(name)" />
                    </button>
                  </div>
                </div>
              </div>
            </div>
            <div v-else class="text-center text-gray-400 py-4 text-sm">无匹配图标</div>
          </a-form-item-rest>
        </template>
        <a-button type="default" class="antd-icon-picker-trigger">
          <template #icon><AppstoreOutlined /></template>
          选择
        </a-button>
      </a-popover>
    </a-input-group>
  </div>
</template>

<script setup lang="ts">
import { computed, h, nextTick, ref, watch } from 'vue'
import type { VNode } from 'vue'
import * as Icons from '@ant-design/icons-vue'
import { AppstoreOutlined } from '@ant-design/icons-vue'

withDefaults(
  defineProps<{
    placeholder?: string
    maxlength?: number
  }>(),
  {
    placeholder: '请输入或点击选择图标（Ant Design Icons）',
    maxlength: 50,
  }
)

const iconName = defineModel<string>('value', { default: '' })

const keyword = ref('')
const pickerOpen = ref(false)
const scrollRef = ref<HTMLElement | null>(null)
const scrollTop = ref(0)

/** 每行高度（与样式 .antd-icon-picker-grid-row 一致） */
const ROW_HEIGHT = 44
const COLS = 8
const BUFFER_ROWS = 3
const GRID_MAX_HEIGHT = 280

const EXCLUDED_ICON_MODULE_KEYS = new Set([
  'default',
  'createFromIconfontCN',
  'Icon',
  'getTwoToneColor',
  'setTwoToneColor',
])

function isVueIconExport(val: unknown): boolean {
  if (val === null || val === undefined) return false
  const t = typeof val
  return t === 'function' || t === 'object'
}

function buildNamesBySuffix(suffix: 'Outlined' | 'Filled' | 'TwoTone'): string[] {
  return Object.keys(Icons)
    .filter((k) => {
      if (EXCLUDED_ICON_MODULE_KEYS.has(k)) return false
      if (!k.endsWith(suffix)) return false
      return isVueIconExport((Icons as Record<string, unknown>)[k])
    })
    .sort((a, b) => a.localeCompare(b))
}

/** 线条 / 实底 / 双色 三套列表，分页切换减少单次渲染量 */
const OUTLINED_ICON_NAMES = buildNamesBySuffix('Outlined')
const FILLED_ICON_NAMES = buildNamesBySuffix('Filled')
const TWO_TONE_ICON_NAMES = buildNamesBySuffix('TwoTone')

type IconStyleTab = 'outlined' | 'filled' | 'twoTone'

const iconStyleTab = ref<IconStyleTab>('outlined')

const namesByTab = computed(() => {
  switch (iconStyleTab.value) {
    case 'filled':
      return FILLED_ICON_NAMES
    case 'twoTone':
      return TWO_TONE_ICON_NAMES
    default:
      return OUTLINED_ICON_NAMES
  }
})

const filteredNames = computed(() => {
  const q = keyword.value.trim().toLowerCase()
  const list = namesByTab.value
  if (!q) return list
  return list.filter((n) => n.toLowerCase().includes(q))
})

const totalRows = computed(() => Math.ceil(filteredNames.value.length / COLS))

const totalScrollHeight = computed(() => Math.max(0, totalRows.value * ROW_HEIGHT))

const virtualStartRow = computed(() => {
  const tr = totalRows.value
  if (tr === 0) return 0
  const top = scrollTop.value
  const start = Math.max(0, Math.floor(top / ROW_HEIGHT) - BUFFER_ROWS)
  const maxStart = Math.max(0, tr - 1)
  return Math.min(start, maxStart)
})

const virtualEndRow = computed(() => {
  const tr = totalRows.value
  if (tr === 0) return 0
  const h = GRID_MAX_HEIGHT
  const top = scrollTop.value
  const end = Math.ceil((top + h) / ROW_HEIGHT) + BUFFER_ROWS
  return Math.min(tr, Math.max(0, end))
})

/** 当前窗口内要渲染的行（每行最多 COLS 个名称） */
const visibleRows = computed(() => {
  const list = filteredNames.value
  const start = virtualStartRow.value
  const end = virtualEndRow.value
  const rows: string[][] = []
  for (let r = start; r < end; r++) {
    const row: string[] = []
    for (let c = 0; c < COLS; c++) {
      const i = r * COLS + c
      if (i < list.length) row.push(list[i]!)
    }
    if (row.length) rows.push(row)
  }
  return rows
})

const virtualOffsetY = computed(() => virtualStartRow.value * ROW_HEIGHT)

function onGridScroll(e: Event) {
  const el = e.target as HTMLElement
  scrollTop.value = el.scrollTop
}

function resetGridScroll() {
  scrollTop.value = 0
  nextTick(() => {
    if (scrollRef.value) scrollRef.value.scrollTop = 0
  })
}

function inferTabFromIconName(name: string): IconStyleTab {
  if (name.endsWith('TwoTone')) return 'twoTone'
  if (name.endsWith('Filled')) return 'filled'
  return 'outlined'
}

const iconRenderCache = new Map<string, () => VNode>()

function getIconRenderCached(name: string) {
  let fn = iconRenderCache.get(name)
  if (!fn) {
    const C = (Icons as Record<string, unknown>)[name]
    if (!C) {
      fn = () => h('span')
    } else {
      const Comp = C as Parameters<typeof h>[0]
      fn = () => h(Comp)
    }
    iconRenderCache.set(name, fn)
  }
  return fn
}

const previewRender = computed(() => {
  const n = iconName.value
  if (!n) return null
  return getIconRenderCached(n)
})

function selectIcon(name: string) {
  iconName.value = name
  pickerOpen.value = false
  keyword.value = ''
}

watch(keyword, () => {
  resetGridScroll()
})

watch(iconStyleTab, () => {
  resetGridScroll()
})

watch(pickerOpen, (open) => {
  if (!open) {
    keyword.value = ''
    return
  }
  iconStyleTab.value = inferTabFromIconName(iconName.value || '')
  resetGridScroll()
})
</script>

<style scoped>
.antd-icon-picker-row {
  display: flex;
  width: 100%;
}

.antd-icon-picker-input {
  flex: 1;
  min-width: 0;
}

.antd-icon-picker-prefix {
  display: inline-flex;
  align-items: center;
  font-size: 16px;
  line-height: 1;
}

.antd-icon-picker-trigger {
  flex-shrink: 0;
}

.antd-icon-picker-tabs :deep(.ant-tabs-nav) {
  margin-bottom: 8px;
}

.antd-icon-picker-tabs :deep(.ant-tabs-tab) {
  padding: 6px 10px;
}

.antd-icon-picker-grid {
  max-height: 280px;
  overflow-x: hidden;
  overflow-y: auto;
}

.antd-icon-picker-grid-spacer {
  position: relative;
  width: 100%;
}

.antd-icon-picker-grid-inner {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
}

.antd-icon-picker-grid-row {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  gap: 4px;
  height: 44px;
  margin-bottom: 0;
  box-sizing: border-box;
}

.antd-icon-picker-cell {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  min-height: 0;
  height: 40px;
  padding: 0;
  margin: 0;
  border: 1px solid var(--ant-color-border-secondary, #f0f0f0);
  border-radius: 6px;
  background: var(--ant-color-bg-container, #fff);
  cursor: pointer;
  font-size: 18px;
  line-height: 1;
  transition: border-color 0.2s, background 0.2s, color 0.2s;
}

.antd-icon-picker-cell:hover {
  border-color: var(--ant-color-primary, #0958d9);
  color: var(--ant-color-primary, #0958d9);
}

.antd-icon-picker-cell.is-active {
  border-color: var(--ant-color-primary, #0958d9);
  background: var(--ant-color-primary-bg, #e6f4ff);
  color: var(--ant-color-primary, #0958d9);
}
</style>
