<template>
  <div
    ref="rootRef"
    class="vm-multi-select"
    :class="{ 'is-open': open, 'is-disabled': disabled }"
  >
    <button
      ref="triggerRef"
      type="button"
      class="vm-multi-select__trigger"
      :disabled="disabled"
      @click="toggle"
    >
      <span class="vm-multi-select__label" :class="{ 'is-placeholder': !selected.length }">
        {{ triggerText }}
      </span>
      <i class="ri-arrow-down-s-line vm-multi-select__caret" />
    </button>

    <Teleport to="body">
      <FocusScope
        v-if="open"
        as-child
        loop
        :trapped="true"
        @unmount-auto-focus="onPanelUnmountAutoFocus"
      >
        <div
          ref="panelRef"
          class="vm-multi-select__panel vm-dialog-float"
          :style="panelStyle"
          @pointerdown.stop
          @mousedown.stop
        >
          <label
            v-for="opt in options"
            :key="String(opt.value)"
            class="vm-multi-select__option"
          >
            <input
              type="checkbox"
              :checked="isChecked(opt.value)"
              @change="toggleValue(opt.value)"
            />
            <span>{{ opt.label }}</span>
          </label>
          <div v-if="!options.length" class="vm-multi-select__empty">{{ crudLabel('noOptions') }}</div>
        </div>
      </FocusScope>
    </Teleport>
  </div>
</template>

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

defineOptions({ name: 'vm-multi-select' })

const props = withDefaults(
  defineProps<{
    /** 多选值；兼容误传 '' / 单值，内部归一成数组 */
    modelValue?: Array<string | number | boolean> | string | number | boolean | null
    options?: Array<{ label: string; value: string | number | boolean }>
    placeholder?: string
    disabled?: boolean
  }>(),
  {
    modelValue: () => [],
    options: () => [],
    placeholder: undefined,
  },
)

const emit = defineEmits<{
  'update:modelValue': [v: Array<string | number | boolean>]
  change: [v: Array<string | number | boolean>]
}>()

const rootRef = ref<HTMLElement | null>(null)
const triggerRef = ref<HTMLElement | null>(null)
const panelRef = ref<HTMLElement | null>(null)
const open = ref(false)
const panelStyle = ref<Record<string, string>>({})
const suppressOpen = ref(false)

function normalizeMulti(
  v: unknown,
): Array<string | number | boolean> {
  if (Array.isArray(v)) return v as Array<string | number | boolean>
  if (v == null || v === '') return []
  return [v as string | number | boolean]
}

const selected = computed(() => normalizeMulti(props.modelValue))

const triggerText = computed(() => {
  if (!selected.value.length) return props.placeholder ?? crudLabel('pleaseSelect')
  const labels = selected.value.map((v) => {
    const hit = props.options.find((o) => String(o.value) === String(v))
    return hit?.label ?? String(v)
  })
  if (labels.length <= 2) return labels.join('、')
  return crudFormat(crudLabel('selectedCount'), labels.length)
})

function isChecked(v: string | number | boolean) {
  return selected.value.some((x) => String(x) === String(v))
}

function toggleValue(v: string | number | boolean) {
  const next = [...selected.value]
  const i = next.findIndex((x) => String(x) === String(v))
  if (i >= 0) next.splice(i, 1)
  else next.push(v)
  emit('update:modelValue', next)
  emit('change', next)
}

function syncPanelPos() {
  const el = triggerRef.value
  if (!el) return
  const r = el.getBoundingClientRect()
  const width = Math.max(r.width, 200)
  panelStyle.value = {
    position: 'fixed',
    top: `${r.bottom + POPUP_TRIGGER_GAP}px`,
    left: `${r.left}px`,
    width: `${width}px`,
    zIndex: '200',
  }
}

function onPanelUnmountAutoFocus(event: Event) {
  event.preventDefault()
}

function closePanel() {
  if (!open.value) return
  suppressOpen.value = true
  open.value = false
  nextTick(() => {
    requestAnimationFrame(() => {
      suppressOpen.value = false
    })
  })
}

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

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

function isInside(el: Node | null) {
  if (!el) return false
  if (rootRef.value?.contains(el)) return true
  if (panelRef.value?.contains(el)) return true
  return el instanceof Element && Boolean(el.closest('.vm-multi-select__panel'))
}

function onDocPointer(e: Event) {
  if (!open.value) return
  const t = e.target as Node | null
  if (!isInside(t)) 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-multi-select {
  position: relative;
  box-sizing: border-box;
  width: 100%;

  &.is-disabled {
    opacity: 0.55;
    pointer-events: none;
  }
}

.vm-multi-select__trigger {
  display: flex;
  box-sizing: border-box;
  width: 100%;
  height: var(--vm-control-height);
  align-items: center;
  justify-content: space-between;
  gap: 6px;
  padding: 0 10px;
  border: 1px solid transparent;
  border-radius: var(--vm-control-radius);
  background: var(--muted);
  color: var(--foreground);
  font-size: var(--vm-control-font-size);
  cursor: pointer;
  outline: none;
}

.vm-multi-select__label {
  overflow: hidden;
  flex: 1 1 auto;
  min-width: 0;
  text-align: left;
  text-overflow: ellipsis;
  white-space: nowrap;

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

.vm-multi-select__caret {
  flex: 0 0 auto;
  color: var(--muted-foreground);
  font-size: 14px;
}

.vm-multi-select__panel {
  max-height: min(220px, 45vh);
  overflow: auto;
  padding: 6px;
  border: 1px solid var(--border);
  border-radius: var(--vm-control-radius);
  background: var(--card);
  box-shadow: var(--shadow-card);
}

.vm-multi-select__option {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 6px 8px;
  border-radius: 8px;
  font-size: 13px;
  cursor: pointer;

  &:hover {
    background: var(--muted);
  }

  input {
    accent-color: var(--brand);
  }
}

.vm-multi-select__empty {
  padding: 10px;
  color: var(--muted-foreground);
  font-size: 12px;
  text-align: center;
}
</style>
