<template>
  <Teleport :to="teleportTo" :disabled="!mounted">
    <Transition :name="maskTransition" appear>
      <div
        v-if="showMask && isOpen"
        :class="e('mask')"
        @click="onMaskClick"
      />
    </Transition>

    <Transition :name="panelTransition" appear>
      <div
        v-show="isOpen"
        ref="panelRef"
        :class="panelClasses"
        :style="panelStyle"
        role="dialog"
        aria-modal="true"
        :aria-label="ariaLabel"
        tabindex="-1"
        @keydown="onKeydown"
      >
        <slot />
      </div>
    </Transition>
  </Teleport>
</template>

<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import { useNamespace } from 'bolt-ui/utils/hooks/use-namespace'

const props = withDefaults(
  defineProps<{
    open: boolean
    side?: 'left' | 'right'
    width?: string | number
    closeOnMask?: boolean
    closeOnEsc?: boolean
    showMask?: boolean
    zIndex?: number
    transitionDuration?: number
    teleportTo?: string
    ariaLabel?: string
  }>(),
  {
    side: 'left',
    width: '80%',
    closeOnMask: true,
    closeOnEsc: true,
    showMask: true,
    zIndex: 200,
    transitionDuration: 280,
    teleportTo: 'body',
    ariaLabel: '抽屉',
  },
)

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

const { b, e, m } = useNamespace('drawer')

const mounted = ref(false)
const panelRef = ref<HTMLElement | null>(null)
const isOpen = ref(props.open)
let prevOverflow = ''
let lastFocusedTrigger: HTMLElement | null = null

const panelClasses = computed(() => [b(), m(props.side)])
const panelStyle = computed(() => ({
  width: typeof props.width === 'number' ? `${props.width}px` : props.width,
  zIndex: String(props.zIndex),
  transitionDuration: `${props.transitionDuration}ms`,
}))
const panelTransition = computed(() => `bo-drawer-panel-${props.side}-fade`)
const maskTransition = 'bo-drawer-mask-fade'

watch(
  () => props.open,
  async (next) => {
    if (typeof window === 'undefined') return
    isOpen.value = next
    if (next) {
      lastFocusedTrigger = (document.activeElement as HTMLElement | null) ?? null
      prevOverflow = document.body.style.overflow
      document.body.style.overflow = 'hidden'
      await nextTick()
      focusFirst()
    } else {
      document.body.style.overflow = prevOverflow
      lastFocusedTrigger?.focus?.()
    }
  },
  { immediate: true },
)

function onMaskClick() {
  if (props.closeOnMask) emit('update:open', false)
}

function onKeydown(e: KeyboardEvent) {
  if (e.key === 'Escape' && props.closeOnEsc) {
    emit('update:open', false)
    e.stopPropagation()
  }
  if (e.key === 'Tab') trapFocus(e)
}

function focusFirst() {
  const root = panelRef.value
  if (!root) return
  const focusable = getFocusable(root)
  if (focusable.length > 0) {
    focusable[0]?.focus?.()
  } else {
    root.focus()
  }
}

function getFocusable(root: HTMLElement): HTMLElement[] {
  const sel = 'a[href],button:not([disabled]),[tabindex]:not([tabindex="-1"]),input:not([disabled]),select:not([disabled]),textarea:not([disabled])'
  return Array.from(root.querySelectorAll<HTMLElement>(sel))
}

function trapFocus(e: KeyboardEvent) {
  const root = panelRef.value
  if (!root) return
  const focusable = getFocusable(root)
  if (focusable.length === 0) {
    e.preventDefault()
    return
  }
  const first = focusable[0]!
  const last = focusable[focusable.length - 1]!
  const active = document.activeElement as HTMLElement | null
  if (e.shiftKey && active === first) {
    e.preventDefault()
    last.focus()
  } else if (!e.shiftKey && active === last) {
    e.preventDefault()
    first.focus()
  }
}

onMounted(() => {
  mounted.value = true
})

onBeforeUnmount(() => {
  document.body.style.overflow = prevOverflow
})

defineOptions({ name: 'BoDrawer' })
</script>
