<template>
  <Teleport to="body">
    <Transition
      :name="`drawer-${side}`"
      appear
      @after-leave="isRendered = false"
    >
      <div
        v-if="isRendered && isOpen"
        :id="componentId"
        class="fixed inset-0 z-50 flex"
        :class="side === 'right' ? 'justify-end' : 'justify-start'"
        v-bind="attrs"
      >
        <div
          :class="twMerge('absolute inset-0 bg-black/40', overlayClass)"
          data-test="backdrop"
          @click="onBackdropClick"
        />

        <div
          ref="panelRef"
          :class="twMerge(
            'relative z-50 flex h-full w-full max-w-md flex-col bg-white',
            drawerClass,
          )"
          role="dialog"
          aria-modal="true"
          :aria-labelledby="hasHeader ? headerId : undefined"
          :aria-label="hasHeader ? undefined : (ariaLabel || undefined)"
          tabindex="-1"
          data-test="panel"
          @keydown="onKeydown"
        >
          <div
            v-if="hasHeader || showClose"
            :class="twMerge('flex items-center justify-between gap-4 px-5 py-[25px]', headerClass)"
            data-test="header"
          >
            <div
              :id="headerId"
              class="min-w-0 flex-1"
              data-test="header-content"
            >
              <slot name="header" />
            </div>
            <button
              v-if="showClose"
              type="button"
              aria-label="Fermer"
              class="flex h-8 w-8 shrink-0 cursor-pointer items-center justify-center rounded-full transition-colors hover:bg-m-surface"
              data-test="close-button"
              @click="close"
            >
              <IconifyIcon
                icon="mdi:cancel-bold"
                :width="16"
                :height="16"
              />
            </button>
          </div>
          <div
            :class="twMerge('flex-1 overflow-y-auto px-5', bodyClass)"
            data-test="body"
          >
            <slot />
            <div
              v-if="$slots.footer"
              :class="footerClass"
              data-test="footer"
            >
              <slot name="footer" />
            </div>
          </div>
        </div>
      </div>
    </Transition>
  </Teleport>
</template>

<script setup lang="ts">
import {
  computed,
  nextTick,
  onBeforeUnmount,
  onMounted,
  ref,
  useAttrs,
  useId,
  useSlots,
  watch,
} from 'vue'
import { Icon as IconifyIcon } from '@iconify/vue'
import { twMerge } from 'tailwind-merge'

defineOptions({ name: 'MalioDrawer', inheritAttrs: false })

const props = withDefaults(
  defineProps<{
    id?: string
    modelValue?: boolean
    side?: 'right' | 'left'
    showClose?: boolean
    dismissable?: boolean
    closeOnEscape?: boolean
    ariaLabel?: string
    drawerClass?: string
    overlayClass?: string
    headerClass?: string
    bodyClass?: string
    footerClass?: string
  }>(),
  {
    id: '',
    modelValue: undefined,
    side: 'right',
    showClose: true,
    dismissable: true,
    closeOnEscape: true,
    ariaLabel: '',
    drawerClass: '',
    overlayClass: '',
    headerClass: '',
    bodyClass: '',
    footerClass: '',
  },
)

const emit = defineEmits<{
  (e: 'update:modelValue', value: boolean): void
  (e: 'close'): void
}>()

const attrs = useAttrs()
const generatedId = useId()

const componentId = computed(() => props.id || `malio-drawer-${generatedId}`)

const slots = useSlots()
const headerId = computed(() => `${componentId.value}-header`)
const hasHeader = computed(() => !!slots.header)

const isControlled = computed(() => props.modelValue !== undefined)
const localValue = ref(false)
const isOpen = computed(() =>
  isControlled.value ? props.modelValue! : localValue.value,
)
const isRendered = ref(isOpen.value)

const panelRef = ref<HTMLElement | null>(null)

let previouslyFocused: HTMLElement | null = null

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

function onOpen() {
  previouslyFocused = (document.activeElement as HTMLElement | null) ?? null
  document.body.style.overflow = 'hidden'
  nextTick(() => {
    const panel = panelRef.value
    if (!panel) return
    const focusable = getFocusable(panel)
    ;(focusable[0] ?? panel).focus()
  })
}

function onClose() {
  document.body.style.overflow = ''
  previouslyFocused?.focus?.()
  previouslyFocused = null
}

watch(isOpen, (val) => {
  if (val) {
    isRendered.value = true
    onOpen()
  }
  else {
    onClose()
  }
})

onMounted(() => {
  if (isOpen.value) onOpen()
})

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

function onBackdropClick() {
  if (props.dismissable) close()
}

function onKeydown(e: KeyboardEvent) {
  if (e.key === 'Escape' && props.closeOnEscape) {
    e.stopPropagation()
    close()
    return
  }
  if (e.key !== 'Tab') return

  const panel = panelRef.value
  if (!panel) return
  const focusable = getFocusable(panel)
  if (focusable.length === 0) {
    e.preventDefault()
    panel.focus()
    return
  }
  const first = focusable[0]!
  const last = focusable[focusable.length - 1]!
  if (e.shiftKey && document.activeElement === first) {
    e.preventDefault()
    last.focus()
  }
  else if (!e.shiftKey && document.activeElement === last) {
    e.preventDefault()
    first.focus()
  }
}

function close() {
  if (!isControlled.value) localValue.value = false
  emit('update:modelValue', false)
  emit('close')
}
</script>

<style scoped>
.drawer-right-enter-active,
.drawer-right-leave-active,
.drawer-left-enter-active,
.drawer-left-leave-active {
  transition: opacity 0.2s ease;
}

.drawer-right-enter-active > div:last-child,
.drawer-right-leave-active > div:last-child,
.drawer-left-enter-active > div:last-child,
.drawer-left-leave-active > div:last-child {
  transition: transform 0.3s ease;
}

.drawer-right-enter-from,
.drawer-right-leave-to,
.drawer-left-enter-from,
.drawer-left-leave-to {
  opacity: 0;
}

.drawer-right-enter-from > div:last-child,
.drawer-right-leave-to > div:last-child {
  transform: translateX(100%);
}

.drawer-left-enter-from > div:last-child,
.drawer-left-leave-to > div:last-child {
  transform: translateX(-100%);
}
</style>
