<template>
  <div ref="rootRef" class="vm-icon-picker">
    <div ref="triggerRef" class="vm-icon-picker__row">
      <button
        type="button"
        class="vm-icon-picker__preview"
        :class="{ 'is-open': open }"
        :disabled="disabled"
        :title="normalized || crudLabel('pickIcon')"
        @click="toggle"
      >
        <i v-if="normalized" :class="normalized" />
        <i v-else class="ri-apps-fill vm-icon-picker__preview-ph" />
      </button>
      <div class="vm-icon-picker__value">
        <input
          class="vm-icon-picker__input"
          :value="displayName"
          :placeholder="placeholder ?? crudLabel('iconPlaceholder')"
          :disabled="disabled"
          @focus="openPanel"
          @input="onNameInput"
          @keydown.esc.stop="closePanel"
        />
        <i
          v-if="displayName && clearable && !disabled"
          class="ri-close-circle-fill vm-icon-picker__clear"
          @click.stop="clearValue"
        />
      </div>
    </div>

    <Teleport to="body">
      <!-- 嵌套 FocusScope：暂停 Dialog 焦点陷阱，否则 Teleport 内 input 无法聚焦 -->
      <FocusScope
        v-if="open"
        as-child
        loop
        :trapped="true"
        @unmount-auto-focus="onPanelUnmountAutoFocus"
      >
        <div
          ref="panelRef"
          class="vm-icon-picker__panel vm-dialog-float"
          :style="panelStyle"
          @pointerdown.stop
          @mousedown.stop
        >
          <div class="vm-icon-picker__toolbar">
            <div class="vm-icon-picker__search">
              <i class="ri-search-line vm-icon-picker__search-ico" />
              <input
                ref="searchRef"
                class="vm-icon-picker__search-input"
                :value="search"
                :placeholder="crudLabel('iconSearchPh')"
                @input="onSearchInput"
                @keydown.esc.stop="closePanel"
              />
              <i
                v-if="search"
                class="ri-close-circle-fill vm-icon-picker__search-clear"
                @click.stop="clearSearch"
              />
            </div>
            <div class="vm-icon-picker__styles" role="group" :aria-label="crudLabel('iconType')">
              <button
                v-for="opt in STYLE_OPTS"
                :key="opt.value"
                type="button"
                class="vm-icon-picker__style-btn"
                :class="{ 'is-active': styleFilter === opt.value }"
                :title="opt.title"
                @click.stop="setStyleFilter(opt.value)"
              >
                {{ opt.label }}
              </button>
            </div>
          </div>

          <div class="vm-icon-picker__grid">
            <button
              v-for="name in pageIcons"
              :key="name"
              type="button"
              class="vm-icon-picker__cell"
              :class="{ 'is-active': name === normalized }"
              :title="name"
              @click="pick(name)"
            >
              <i :class="name" />
            </button>
            <button
              v-if="showPager"
              type="button"
              class="vm-icon-picker__page-btn"
              :disabled="!canPrev"
              :title="crudLabel('prevPage')"
              :aria-label="crudLabel('prevPage')"
              @click.prevent.stop="prevBatch"
            >
              <i class="ri-arrow-left-s-line" />
            </button>
            <button
              v-if="showPager"
              type="button"
              class="vm-icon-picker__page-btn"
              :disabled="!canNext"
              :title="crudLabel('nextPage')"
              :aria-label="crudLabel('nextPage')"
              @click.prevent.stop="nextBatch"
            >
              <i class="ri-arrow-right-s-line" />
            </button>
            <p v-if="!filtered.length" class="vm-icon-picker__empty">{{ crudLabel('noMatchIcon') }}</p>
          </div>
        </div>
      </FocusScope>
    </Teleport>
  </div>
</template>

<script setup lang="ts">
import { crudLabel } from '../crud/style'
import { ref, computed, watch, nextTick, onMounted, onUnmounted } from 'vue'
import { FocusScope } from 'reka-ui'
import { POPUP_TRIGGER_GAP } from '../lib/dialog-float'
import allIcons from '../data/remixicon-all.json'

defineOptions({ name: 'vm-icon-picker' })

type IconStyle = 'fill' | 'line' | 'other'

const STYLE_OPTS = computed(() =>
  (
    [
      { value: 'fill' as const, labelKey: 'iconFill', titleKey: 'iconFillTitle' },
      { value: 'line' as const, labelKey: 'iconLine', titleKey: 'iconLineTitle' },
      { value: 'other' as const, labelKey: 'iconOther', titleKey: 'iconOtherTitle' },
    ] as const
  ).map((o) => ({
    value: o.value,
    label: crudLabel(o.labelKey),
    title: crudLabel(o.titleKey),
  })),
)

const props = withDefaults(
  defineProps<{
    modelValue?: string | null
    placeholder?: string
    disabled?: boolean
    clearable?: boolean
  }>(),
  {
    placeholder: undefined,
    clearable: true,
  },
)

const emit = defineEmits<{
  'update:modelValue': [v: string]
}>()

/** 10 列 × 5 行 = 50 格，末尾 2 格给上一页 / 下一页 */
const PAGE_SIZE = 48
const ALL = allIcons as string[]

const rootRef = ref<HTMLElement | null>(null)
const triggerRef = ref<HTMLElement | null>(null)
const panelRef = ref<HTMLElement | null>(null)
const searchRef = ref<HTMLInputElement | null>(null)
const open = ref(false)
const search = ref('')
const page = ref(0)
const styleFilter = ref<IconStyle>('fill')
const panelStyle = ref<Record<string, string>>({})
/** 关闭后面板卸载会回焦外层 input，避免再次触发 openPanel */
const suppressOpen = ref(false)

/** 仅补齐 ri- 前缀，保留 fill / line / 其它原样 */
function normalizeIcon(raw: string | null | undefined): string {
  if (!raw) return ''
  let n = String(raw).trim()
  if (!n) return ''
  if (!n.startsWith('ri-')) n = `ri-${n}`
  return n
}

function styleOf(name: string): IconStyle {
  if (name.endsWith('-fill')) return 'fill'
  if (name.endsWith('-line')) return 'line'
  return 'other'
}

const normalized = computed(() => normalizeIcon(props.modelValue))

/** 外层输入框展示当前 icon 名称（与 v-model 同步） */
const displayName = computed(() => {
  const raw = props.modelValue
  if (raw == null || raw === '') return ''
  return String(raw)
})

function fuzzyMatch(hay: string, needle: string): boolean {
  const h = hay.toLowerCase()
  const n = needle.toLowerCase().replace(/\s+/g, '')
  if (!n) return true
  if (h.includes(n)) return true
  let j = 0
  for (let i = 0; i < h.length && j < n.length; i++) {
    if (h[i] === n[j]) j++
  }
  return j === n.length
}

function iconSearchKey(name: string) {
  return name
    .replace(/^ri-/, '')
    .replace(/-fill$/, '')
    .replace(/-line$/, '')
    .replace(/-/g, '')
}

const filtered = computed(() => {
  const byStyle = ALL.filter((name) => styleOf(name) === styleFilter.value)
  const q = search.value.trim()
  if (!q) return byStyle
  const qKey = q
    .replace(/^ri-/, '')
    .replace(/-fill$/, '')
    .replace(/-line$/, '')
  return byStyle.filter((name) => {
    if (fuzzyMatch(name, q)) return true
    if (fuzzyMatch(iconSearchKey(name), qKey.replace(/-/g, ''))) return true
    return fuzzyMatch(
      name.replace(/^ri-/, '').replace(/-fill$/, '').replace(/-line$/, ''),
      qKey,
    )
  })
})

const pageCount = computed(() =>
  Math.max(1, Math.ceil(filtered.value.length / PAGE_SIZE)),
)

const pageIcons = computed(() => {
  const start = page.value * PAGE_SIZE
  return filtered.value.slice(start, start + PAGE_SIZE)
})

const showPager = computed(() => filtered.value.length > PAGE_SIZE)
const canPrev = computed(() => page.value > 0)
const canNext = computed(
  () => page.value < pageCount.value - 1 && filtered.value.length > PAGE_SIZE,
)

watch(filtered, () => {
  page.value = 0
})

function setStyleFilter(v: IconStyle) {
  styleFilter.value = v
  page.value = 0
}

function syncPanelPos() {
  const el = triggerRef.value
  if (!el) return
  const r = el.getBoundingClientRect()
  const width = Math.min(480, window.innerWidth - 16)
  // 贴触发框左侧 + 底部
  panelStyle.value = {
    position: 'fixed',
    top: `${r.bottom + POPUP_TRIGGER_GAP}px`,
    left: `${r.left}px`,
    width: `${width}px`,
    zIndex: '100',
  }
}

async function openPanel() {
  if (props.disabled || suppressOpen.value) return
  styleFilter.value = 'fill'
  open.value = true
  await nextTick()
  syncPanelPos()
}

/** 禁止卸载时把焦点塞回外层名称框（否则 @focus 会立刻再次打开） */
function onPanelUnmountAutoFocus(event: Event) {
  event.preventDefault()
}

function closePanel() {
  if (!open.value) return
  suppressOpen.value = true
  open.value = false
  search.value = ''
  page.value = 0
  styleFilter.value = 'fill'
  // 等 FocusScope 卸载与可能的回焦完成后再允许打开
  nextTick(() => {
    requestAnimationFrame(() => {
      suppressOpen.value = false
    })
  })
}

async function toggle() {
  if (props.disabled) return
  if (open.value) {
    closePanel()
    return
  }
  await openPanel()
}

/** 外层输入：直接编辑 / 存储 icon 名称 */
function onNameInput(e: Event) {
  const v = (e.target as HTMLInputElement).value.trim()
  emit('update:modelValue', v)
  if (!open.value) openPanel()
}

function clearValue() {
  emit('update:modelValue', '')
}

function onSearchInput(e: Event) {
  search.value = (e.target as HTMLInputElement).value
}

function clearSearch() {
  search.value = ''
  searchRef.value?.focus()
}

function pick(name: string) {
  emit('update:modelValue', name)
  closePanel()
}

function prevBatch() {
  if (!canPrev.value) return
  page.value -= 1
}

function nextBatch() {
  if (!canNext.value) return
  page.value += 1
}

function onDocPointer(e: Event) {
  if (!open.value) return
  const t = e.target as Node | null
  if (!t) return
  if (rootRef.value?.contains(t)) return
  if (panelRef.value?.contains(t)) return
  closePanel()
}

function onWinChange() {
  if (open.value) syncPanelPos()
}

onMounted(() => {
  document.addEventListener('pointerdown', onDocPointer, true)
  window.addEventListener('resize', onWinChange)
  window.addEventListener('scroll', onWinChange, true)
})

onUnmounted(() => {
  document.removeEventListener('pointerdown', onDocPointer, true)
  window.removeEventListener('resize', onWinChange)
  window.removeEventListener('scroll', onWinChange, true)
})
</script>

<style lang="scss" scoped>
.vm-icon-picker {
  width: 100%;
}

.vm-icon-picker__row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
}

.vm-icon-picker__preview {
  flex: 0 0 var(--vm-control-height);
  width: var(--vm-control-height);
  height: var(--vm-control-height);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--border);
  border-radius: var(--vm-control-radius);
  background: var(--muted);
  color: var(--foreground);
  font-size: 16px;
  cursor: pointer;
  transition:
    border-color 0.15s,
    background 0.15s,
    color 0.15s;

  outline: none;

  &:hover:not(:disabled) {
    border-color: var(--brand);
    background: var(--brand-soft);
    color: var(--brand);
  }

  /* 弹层触发：打开态不额外描边 */

  &:disabled {
    opacity: 0.6;
    cursor: not-allowed;
  }
}

.vm-icon-picker__preview-ph {
  opacity: 0.35;
}

.vm-icon-picker__value {
  position: relative;
  flex: 1;
  min-width: 0;
}

.vm-icon-picker__input {
  width: 100%;
  height: var(--vm-control-height);
  padding: 0 26px 0 var(--vm-control-padding-x);
  border: 1px solid var(--border);
  border-radius: var(--vm-control-radius);
  background: var(--muted);
  color: var(--foreground);
  font-size: var(--vm-control-font-size);
  outline: none;
  transition:
    border-color 0.15s,
    box-shadow 0.15s;

  &::placeholder {
    color: var(--muted-foreground);
  }

  /* 弹层触发旁输入：不展示聚焦描边 */

  &:disabled {
    opacity: 0.6;
    cursor: not-allowed;
  }
}

.vm-icon-picker__clear {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 14px;
  color: var(--muted-foreground);
  cursor: pointer;

  &:hover {
    color: var(--foreground);
  }
}
</style>

<style lang="scss">
/* Teleport 到 body，不可 scoped */
.vm-icon-picker__panel {
  /* Dialog modal 会给 body 设 pointer-events:none，Teleport 面板必须显式恢复 */
  pointer-events: auto;
  padding: 10px;
  border: 1px solid var(--border);
  border-radius: 12px;
  background: #fff;
  box-shadow: var(--shadow-soft);
}

.vm-icon-picker__toolbar {
  display: flex;
  align-items: center;
  gap: 8px;
  margin-bottom: 8px;
}

.vm-icon-picker__search {
  position: relative;
  flex: 1;
  min-width: 0;
}

.vm-icon-picker__search-ico {
  position: absolute;
  left: 10px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 14px;
  color: var(--muted-foreground);
  pointer-events: none;
}

.vm-icon-picker__search-input {
  width: 100%;
  height: var(--vm-control-height);
  padding: 0 26px 0 28px;
  border: 1px solid var(--border);
  border-radius: var(--vm-control-radius);
  background: var(--muted);
  color: var(--foreground);
  font-size: var(--vm-control-font-size);
  outline: none;

  &::placeholder {
    color: var(--muted-foreground);
  }

  &:focus {
    border-color: var(--brand);
    background: #fff;
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--brand) 18%, transparent);
  }
}

.vm-icon-picker__search-clear {
  position: absolute;
  right: 8px;
  top: 50%;
  transform: translateY(-50%);
  font-size: 14px;
  color: var(--muted-foreground);
  cursor: pointer;

  &:hover {
    color: var(--foreground);
  }
}

.vm-icon-picker__styles {
  display: inline-flex;
  flex-shrink: 0;
  gap: 4px;
}

.vm-icon-picker__style-btn {
  height: var(--vm-control-height);
  padding: 0 var(--vm-control-padding-x);
  border: 1px solid var(--border);
  border-radius: var(--vm-control-radius);
  background: var(--muted);
  color: var(--muted-foreground);
  font-size: 12px;
  font-weight: 500;
  white-space: nowrap;
  cursor: pointer;
  transition:
    background 0.12s,
    color 0.12s,
    border-color 0.12s;

  &:hover {
    border-color: color-mix(in srgb, var(--brand) 40%, var(--border));
    color: var(--brand);
  }

  &.is-active {
    border-color: var(--brand);
    background: var(--brand-soft);
    color: var(--brand);
  }
}

.vm-icon-picker__grid {
  display: grid;
  grid-template-columns: repeat(10, minmax(0, 1fr));
  gap: 4px;
}

.vm-icon-picker__cell {
  aspect-ratio: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  border-radius: 8px;
  background: transparent;
  color: #50566b;
  font-size: 18px;
  cursor: pointer;
  transition:
    background 0.12s,
    color 0.12s;

  &:hover {
    background: var(--brand-soft);
    color: var(--brand);
  }

  &.is-active {
    background: var(--brand-soft);
    color: var(--brand);
  }
}

.vm-icon-picker__page-btn {
  aspect-ratio: 1;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: 1px dashed color-mix(in srgb, var(--brand) 35%, var(--border));
  border-radius: 8px;
  background: var(--brand-soft);
  color: var(--brand);
  font-size: 18px;
  line-height: 1;
  cursor: pointer;
  transition:
    background 0.12s,
    opacity 0.12s;

  &:hover:not(:disabled) {
    background: color-mix(in srgb, var(--brand) 16%, #fff);
  }

  &:disabled {
    opacity: 0.35;
    cursor: not-allowed;
  }
}

.vm-icon-picker__empty {
  grid-column: 1 / -1;
  margin: 12px 0;
  text-align: center;
  font-size: 12px;
  color: var(--muted-foreground);
}
</style>
