<template>
  <div class="cpDialog">
    <dialog
      ref="dialogElement"
      :aria-describedby="ariaDescribedby"
      :aria-labelledby="titleId"
      aria-modal="true"
      class="cpDialog__dialog"
      @keydown.esc.stop.prevent="handleClose"
    >
      <div aria-hidden="true" class="cpDialog__overlay" :class="overlayDynamicClass" @click="handleOverlayClick" />
      <main ref="dialogContainer" class="cpDialog__container" :style="dynamicStyle" @keydown.tab="trapFocus">
        <header class="cpDialog__header">
          <div v-if="hasTitleOrSubtitle" class="cpDialog__headerTexts">
            <div v-if="hasTitle" class="cpDialog__title">
              <slot name="title">
                <h2 :id="titleId">{{ title }}</h2>
              </slot>
            </div>
            <div v-if="hasSubtitle" :id="subtitleId" class="cpDialog__subtitle">
              <slot name="subtitle">
                <p>{{ subtitle }}</p>
              </slot>
            </div>
          </div>
          <button
            v-if="!preventClose"
            aria-label="Close dialog"
            class="cpDialog__close"
            type="button"
            @click="handleClose"
          >
            <cp-icon aria-hidden="true" type="x" />
          </button>
        </header>
        <section class="cpDialog__content">
          <slot />
        </section>
        <footer v-if="hasFooterSlot" class="cpDialog__footer">
          <slot name="footer" />
        </footer>
      </main>
    </dialog>
  </div>
</template>

<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, useId, useSlots } from 'vue'

import { getKeyboardFocusableElements, handleTrapFocus } from '@/helpers/dom'

interface Props {
  isClosableOnClickOutside?: boolean
  maxWidth?: number
  preventClose?: boolean
  subtitle?: string
  title?: string
}

interface Emits {
  (e: 'close'): void
}

const props = withDefaults(defineProps<Props>(), {
  maxWidth: 600,
  title: '',
  subtitle: '',
  isClosableOnClickOutside: false,
})

const emit = defineEmits<Emits>()

const dialogId = useId()
const titleId = computed(() => `${dialogId}-title`)
const subtitleId = computed(() => `${dialogId}-subtitle`)

const ariaDescribedby = computed(() => (hasSubtitle.value ? subtitleId.value : undefined))

const slots = useSlots()

const dialogElement = ref<HTMLDialogElement | null>(null)
const dialogContainer = ref<HTMLElement | null>(null)

const dynamicStyle = computed(() => ({ maxWidth: `${props.maxWidth}px` }))

const overlayDynamicClass = computed(() => ({
  'cpDialog__overlay--isClosableOnClickOutside': props.isClosableOnClickOutside,
}))

const hasTitleSlot = computed(() => !!slots.title)
const hasTitle = computed(() => !!props.title || hasTitleSlot.value)
const hasSubtitleSlot = computed(() => !!slots.subtitle)
const hasSubtitle = computed(() => !!props.subtitle || hasSubtitleSlot.value)
const hasTitleOrSubtitle = computed(() => hasTitle.value || hasSubtitle.value)

const hasFooterSlot = computed(() => !!slots.footer)

const handleClose = () => {
  if (props.preventClose) return
  emit('close')
}

const trapFocus = (event: KeyboardEvent) => handleTrapFocus(event, dialogContainer.value)

const openDialog = () => dialogElement.value?.show()
const closeDialog = () => dialogElement.value?.close()

const focusOnFirstFocusableElement = () => {
  const focusableElements = getKeyboardFocusableElements(dialogContainer.value)

  if (!focusableElements.length) return

  focusableElements[0].focus()
}

const handleOverlayClick = () => {
  if (!props.isClosableOnClickOutside) return
  handleClose()
}

onMounted(() => {
  openDialog()
  nextTick(() => focusOnFirstFocusableElement())
})

onBeforeUnmount(() => closeDialog())
</script>

<style lang="scss">
body:has(.cpDialog) {
  overflow: hidden;
}

$dialog-breakpoint: 550px;

.cpDialog {
  position: fixed;
  z-index: 3;
  width: 100%;
  max-width: 100%;
  height: 100%;
  max-height: 100%;
  inset: 0;
  transition-duration: 300ms;

  @media screen and (min-width: calc(#{$dialog-breakpoint} + 1px)) {
    padding: 10vh var(--cp-dimensions-8);
  }

  &__dialog {
    position: relative;
    display: flex;
    width: 100%;
    height: 100%;
    align-items: flex-start;
    justify-content: center;
    padding: 0;
    border: none;
    background: none;
  }

  &__overlay {
    position: fixed;
    z-index: -1;
    width: 100%;
    height: 100%;
    background: var(--cp-background-overlay);
    inset: 0;
    pointer-events: none;
    transition: opacity 250ms ease;

    &--isClosableOnClickOutside {
      z-index: 0;
      cursor: pointer;
      pointer-events: auto;
    }
  }

  &__container {
    position: relative;
    z-index: 2;
    display: flex;
    overflow: hidden;
    width: 100%;
    max-height: 100%;
    flex-direction: column;
    justify-content: space-between;
    border-radius: var(--cp-radius-lg);
    background-color: var(--cp-background-primary);
    box-shadow: var(--cp-shadows-overlay);
    -webkit-overflow-scrolling: touch;
    transition:
      transform 300ms ease,
      opacity 250ms ease;

    @media screen and (max-width: $dialog-breakpoint) {
      max-width: 100% !important;
      height: 100%;
      border-radius: 0;
    }
  }

  &__header,
  &__content,
  &__footer {
    width: 100%;
  }

  &__header,
  &__footer {
    padding: var(--cp-spacing-xl) var(--cp-spacing-2xl);
  }

  &__header {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--cp-spacing-md);

    &:not(:has(.cpDialog__headerTexts)) {
      padding-bottom: 0;
    }

    &:has(.cpDialog__headerTexts) {
      border-bottom: 1px solid var(--cp-border-soft);
    }
  }

  &__headerText {
    display: flex;
    min-width: 0;
    flex-direction: column;
  }

  &__title > * {
    @extend %u-text-ellipsis;
  }

  &__title,
  &__title > h2 {
    font-size: var(--cp-text-size-xl);
    font-weight: 600;
    line-height: var(--cp-line-height-xl);
  }

  &__subtitle,
  &__subtitle > p {
    color: var(--cp-text-secondary);
    font-size: var(--cp-text-size-sm);
    font-weight: 400;
    line-height: var(--cp-line-height-md);
  }

  &__close {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--cp-spacing-sm);
    border-radius: var(--cp-radius-md);
    margin-left: auto;
    color: var(--cp-foreground-secondary);
    transition: 200ms ease-in-out;
    transition-property: color, background-color;

    &:hover {
      background-color: var(--cp-background-secondary);
      color: var(--cp-foreground-primary);
    }

    &:focus-visible {
      outline: fn.px-to-rem(2) solid var(--cp-focus);
    }
  }

  &__content {
    @extend %u-layout-box-padding;

    overflow: auto;
    min-height: 0;
    flex: 1;
  }

  &__footer {
    display: flex;
    align-items: center;
    justify-content: space-between;

    &:empty {
      display: none;
    }

    &:not(:empty) {
      border-top: 1px solid var(--cp-border-soft);
    }
  }
}

@media (max-width: 650px) {
  .cpDialog__footer {
    padding: var(--cp-spacing-xl);
  }
}
</style>
