<template>
  <div ref="rootEl" class="native-select" :class="{ 'native-select--open': open, 'native-select--disabled': disabled }">
    <select
      class="native-select__native"
      :value="selectedKey"
      :disabled="disabled"
      aria-hidden="true"
      tabindex="-1"
    >
      <option
        v-for="option in options"
        :key="optionKey(option.value)"
        :value="optionKey(option.value)"
        :disabled="option.disabled"
      >
        {{ option.label }}
      </option>
    </select>

    <button
      ref="triggerEl"
      type="button"
      class="native-select__trigger"
      :disabled="disabled"
      :aria-expanded="open"
      aria-haspopup="listbox"
      :aria-controls="listboxId"
      @click="toggleDropdown"
      @keydown="onButtonKeydown"
    >
      <span class="native-select__value">{{ selectedOption?.label ?? placeholder }}</span>
      <span class="native-select__chevron" aria-hidden="true">
        <ChevronDown :size="20" :stroke-width="2.8" />
      </span>
    </button>

    <Teleport to="body">
      <div v-if="open" ref="menuEl" :id="listboxId" class="native-select__menu" :style="menuStyle" role="listbox">
        <template v-for="(option, index) in options" :key="optionKey(option.value)">
          <p v-if="option.group && option.group !== options[index - 1]?.group" class="native-select__group">
            {{ option.group }}
          </p>
          <button
            type="button"
            class="native-select__option"
            :class="{
              'native-select__option--active': index === activeIndex,
              'native-select__option--selected': isSelected(option),
            }"
            :disabled="option.disabled"
            role="option"
            :aria-selected="isSelected(option)"
            @mouseenter="setActiveIndex(index)"
            @click="selectOption(option)"
          >
            <span class="native-select__option-label">{{ option.label }}</span>
            <span v-if="option.meta" class="native-select__option-meta">{{ option.meta }}</span>
            <Check v-if="isSelected(option)" class="native-select__check" :size="18" :stroke-width="3" />
          </button>
        </template>
      </div>
    </Teleport>
  </div>
</template>

<script setup lang="ts">
import { Check, ChevronDown } from '@lucide/vue'
import { computed, onBeforeUnmount, onMounted, ref, watch, type CSSProperties } from 'vue'

type SelectOptionValue = string | number | null

type SelectOption = {
  label: string
  value: SelectOptionValue
  meta?: string
  disabled?: boolean
  group?: string
}

const props = withDefaults(defineProps<{
  modelValue: SelectOptionValue
  options: SelectOption[]
  placeholder?: string
  disabled?: boolean
}>(), {
  placeholder: 'Auswählen',
  disabled: false,
})

const emit = defineEmits<{
  'update:modelValue': [value: SelectOptionValue]
  change: [value: SelectOptionValue, option: SelectOption]
}>()

const rootEl = ref<HTMLElement | null>(null)
const triggerEl = ref<HTMLButtonElement | null>(null)
const menuEl = ref<HTMLElement | null>(null)
const open = ref(false)
const activeIndex = ref(0)
const listboxId = `native-select-${Math.random().toString(36).slice(2)}`
const menuPos = ref({ top: 0, bottom: 'auto' as string | number, left: 0, width: 0, maxHeight: 286 })

const menuStyle = computed<CSSProperties>(() => ({
  position: 'fixed',
  top: menuPos.value.top !== 0 ? `${menuPos.value.top}px` : 'auto',
  bottom: menuPos.value.bottom !== 'auto' ? `${menuPos.value.bottom}px` : 'auto',
  left: `${menuPos.value.left}px`,
  width: `${menuPos.value.width}px`,
  maxHeight: `${menuPos.value.maxHeight}px`,
  zIndex: 9999,
}))

function updateMenuRect() {
  if (!triggerEl.value) return
  const r = triggerEl.value.getBoundingClientRect()
  const PADDING = 8
  const MAX_H = 286
  const spaceBelow = window.innerHeight - r.bottom - PADDING
  const spaceAbove = r.top - PADDING

  if (spaceBelow >= Math.min(MAX_H, 160) || spaceBelow >= spaceAbove) {
    menuPos.value = {
      top: r.bottom + PADDING,
      bottom: 'auto',
      left: r.left,
      width: r.width,
      maxHeight: Math.min(MAX_H, spaceBelow),
    }
  } else {
    menuPos.value = {
      top: 0,
      bottom: window.innerHeight - r.top + PADDING,
      left: r.left,
      width: r.width,
      maxHeight: Math.min(MAX_H, spaceAbove),
    }
  }
}

const selectedOption = computed(() =>
  props.options.find((option) => isSameValue(option.value, props.modelValue)) ?? null,
)

const selectedKey = computed(() => optionKey(selectedOption.value?.value ?? null))

watch(
  () => [props.modelValue, props.options] as const,
  () => {
    const selectedIndex = props.options.findIndex((option) => isSameValue(option.value, props.modelValue))
    activeIndex.value = selectedIndex >= 0 ? selectedIndex : firstEnabledIndex()
  },
  { immediate: true },
)

function optionKey(value: SelectOptionValue) {
  return value === null ? 'null:' : `${typeof value}:${value}`
}

function isSameValue(left: SelectOptionValue, right: SelectOptionValue) {
  return optionKey(left) === optionKey(right)
}

function isSelected(option: SelectOption) {
  return isSameValue(option.value, props.modelValue)
}

function firstEnabledIndex() {
  return Math.max(0, props.options.findIndex((option) => !option.disabled))
}

function setActiveIndex(index: number) {
  if (!props.options[index]?.disabled) {
    activeIndex.value = index
  }
}

function toggleDropdown() {
  if (props.disabled || props.options.length === 0) return
  open.value = !open.value
  if (open.value) {
    updateMenuRect()
    if (props.options[activeIndex.value]?.disabled) {
      activeIndex.value = firstEnabledIndex()
    }
  }
}

function closeDropdown() {
  open.value = false
}

function selectOption(option: SelectOption) {
  if (props.disabled || option.disabled) return
  emit('update:modelValue', option.value)
  emit('change', option.value, option)
  closeDropdown()
}

function selectActiveOption() {
  const option = props.options[activeIndex.value]
  if (option) selectOption(option)
}

function moveActive(delta: number) {
  if (!props.options.length || props.disabled) return
  if (!open.value) {
    open.value = true
  }

  let nextIndex = activeIndex.value
  for (let step = 0; step < props.options.length; step += 1) {
    nextIndex = (nextIndex + delta + props.options.length) % props.options.length
    if (!props.options[nextIndex]?.disabled) {
      activeIndex.value = nextIndex
      return
    }
  }
}

function onButtonKeydown(event: KeyboardEvent) {
  if (event.key === 'ArrowDown') {
    event.preventDefault()
    moveActive(1)
    return
  }

  if (event.key === 'ArrowUp') {
    event.preventDefault()
    moveActive(-1)
    return
  }

  if (event.key === 'Enter' || event.key === ' ') {
    event.preventDefault()
    open.value ? selectActiveOption() : toggleDropdown()
    return
  }

  if (event.key === 'Escape') {
    event.preventDefault()
    closeDropdown()
  }
}

function onDocumentPointerDown(event: PointerEvent) {
  const target = event.target as Node
  if (!rootEl.value?.contains(target) && !menuEl.value?.contains(target)) {
    closeDropdown()
  }
}

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

onBeforeUnmount(() => {
  document.removeEventListener('pointerdown', onDocumentPointerDown)
  window.removeEventListener('scroll', updateMenuRect, true)
  window.removeEventListener('resize', updateMenuRect)
})
</script>

<style scoped>
.native-select{
  position:relative;
  width:100%;
  min-width:0;
  z-index:1;
}

.native-select--open{
  z-index:80;
}

.native-select__native{
  position:absolute;
  width:1px;
  height:1px;
  opacity:0;
  pointer-events:none;
}

.native-select__trigger{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:14px;
  width:100%;
  min-height:48px;
  padding:8px 9px 8px 15px;
  border:2px solid #e8def8;
  border-radius:16px;
  background:linear-gradient(180deg,#fff 0%,#fbf8ff 100%);
  box-shadow:0 8px 22px rgba(139,108,219,.08), inset 0 1px 0 rgba(255,255,255,.9);
  color:#1f2337;
  cursor:pointer;
  font-family:'Outfit',sans-serif;
  font-size:14px;
  font-weight:750;
  line-height:1.2;
  outline:none;
  text-align:left;
  transition:border-color .18s ease, box-shadow .18s ease, transform .18s ease, background .18s ease;
}

.native-select__trigger:hover,
.native-select__trigger:focus-visible,
.native-select--open .native-select__trigger{
  border-color:#9b7ce4;
  background:#fff;
  transform:translateY(-2px);
  box-shadow:0 12px 30px rgba(139,108,219,.16), 0 0 0 4px rgba(139,108,219,.12);
}

.native-select__trigger:active{
  transform:translateY(0);
}

.native-select__trigger:disabled{
  cursor:not-allowed;
  border-color:#e5e7eb;
  background:#f8fafc;
  color:#94a3b8;
  box-shadow:none;
}

.native-select__value{
  min-width:0;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}

.native-select__chevron{
  display:grid;
  place-items:center;
  flex:none;
  width:32px;
  height:32px;
  border-radius:12px;
  background:#f1eafd;
  color:#7c5bd2;
  box-shadow:inset 0 0 0 1px rgba(139,108,219,.09);
  transition:transform .18s ease, background .18s ease, color .18s ease;
}

.native-select--open .native-select__chevron{
  transform:rotate(180deg);
  background:#835fd8;
  color:#fff;
}

.native-select--disabled .native-select__chevron{
  background:#eef2f7;
  color:#94a3b8;
}

.native-select__menu{
  display:grid;
  gap:4px;
  max-height:286px;
  padding:8px;
  overflow:auto;
  border:1px solid rgba(139,108,219,.18);
  border-radius:18px;
  background:rgba(255,255,255,.98);
  box-shadow:0 24px 54px rgba(63,53,86,.22);
  backdrop-filter:blur(14px);
  -webkit-backdrop-filter:blur(14px);
}

.native-select__group{
  margin:8px 8px 2px;
  color:#7f728f;
  font-size:10px;
  font-weight:800;
  letter-spacing:.16em;
  line-height:1.2;
  text-transform:uppercase;
}

.native-select__group:first-child{
  margin-top:2px;
}

.native-select__option{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:12px;
  width:100%;
  min-height:42px;
  padding:9px 12px;
  border:0;
  border-radius:13px;
  background:transparent;
  color:#514765;
  cursor:pointer;
  font-family:'Outfit',sans-serif;
  font-size:14px;
  font-weight:650;
  line-height:1.2;
  text-align:left;
  transition:background .16s ease, color .16s ease, transform .16s ease;
}

.native-select__option:hover,
.native-select__option--active{
  background:#f6f0ff;
  color:#4f3a8a;
  transform:translateY(-1px);
}

.native-select__option--selected{
  background:linear-gradient(135deg,#efe6ff,#e7dcfb);
  color:#3f2f70;
  font-weight:800;
}

.native-select__option:disabled{
  cursor:not-allowed;
  color:#a8a0b5;
  background:#f8fafc;
}

.native-select__option-label{
  flex:1 1 auto;
  min-width:0;
  overflow:hidden;
  text-overflow:ellipsis;
  white-space:nowrap;
}

.native-select__option-meta{
  flex:none;
  margin-left:auto;
  border-radius:999px;
  background:#f1eafd;
  color:#6d45cc;
  padding:4px 9px;
  font-size:11px;
  font-weight:850;
  line-height:1;
  white-space:nowrap;
}

.native-select__option--selected .native-select__option-meta{
  background:#fff;
  color:#6d45cc;
}

.native-select__check{
  flex:none;
  color:#7c5bd2;
}
</style>
