<template>
  <Teleport
    :to="appendTarget"
    :disabled="appendDisabled"
  >
    <div
      v-if="containerVisible"
      :ref="maskRef"
      :class="maskClass"
      class="vc-dialog-mask"
      @click.stop.self="onMaskClick"
    >
      <transition
        name="vc-dialog"
        appear
        @before-enter="onBeforeEnter"
        @enter="onEnter"
        @after-enter="onEnterCancelled"
        @before-leave="onBeforeLeave"
        @leave="onLeave"
        @after-leave="onAfterLeave"
      >
        <div
          v-if="visible"
          :ref="containerRef"
          :class="dialogClass"
          :aria-labelledby="ariaLabelledById"
          :style="{
            width: modalWidth,
            maxWidth: modalMaxWidth
          }"
          aria-modal="true"
          role="dialog"
        >
          <div class="vc-dialog-header">
            <slot name="header">
              <div
                v-if="header"
                :id="ariaLabelledById"
                class="vc-dialog-title"
              >
                {{ header }}
              </div>
            </slot>
            <div class="vc-dialog-header-icons">
              <button
                v-if="maximizable"
                class="vc-dialog-header-icon vc-dialog-header-maximize vc-link mx-3"
                :aria-label="ariaCloseLabel"
                type="button"
                tabindex="-1"
                @click="maximize"
              >
                <span class="vc-dialog-header-maximize-icon vc-icon-fullscreen" />
              </button>
              <button
                v-if="closable"
                class="vc-dialog-header-icon vc-dialog-header-close vc-link"
                :aria-label="ariaCloseLabel"
                type="button"
                tabindex="-1"
                @click="close"
              >
                <span class="vc-dialog-header-close-icon vc-icon-close" />
              </button>
            </div>
          </div>
          <div
            :class="contentStyleClass"
            :style="contentStyle"
          >
            <slot />
          </div>
          <div
            v-if="footer || $slots.footer"
            class="vc-dialog-footer"
          >
            <slot name="footer">
              {{ footer }}
            </slot>
          </div>
        </div>
      </transition>
    </div>
  </Teleport>
</template>

<script lang="ts">
/* eslint-disable @typescript-eslint/no-explicit-any */
import { defineComponent, PropType } from 'vue'
import { DialogBreakpoints, DialogPositionType } from './VcModal.types'
import UniqueComponentId from '@/utils/UniqueComponentId'
import ZIndexUtils from '@/utils/ZIndexUtils'
import DomHandler from '@/utils/DomHandler'
import { convertToUnit } from '@/utils/helpers'
import appendTo from '@/mixins/common/appendTo.mixin'

const BASE_Z_INDEX = 2000

export default defineComponent({
  name: 'VcModal',
  mixins: [appendTo],
  inheritAttrs: false,
  props: {
    maximizable: {
      type: Boolean,
      default: false
    },
    visible: {
      type: Boolean,
      default: false
    },
    position: {
      type: String as PropType<DialogPositionType>,
      default: 'center'
    },
    header: {
      type: String,
      default: ''
    },
    footer: {
      type: String,
      default: ''
    },
    ariaCloseLabel: {
      type: String,
      default: 'close'
    },
    closable: {
      type: Boolean,
      default: true
    },
    closeOnEscape: {
      type: Boolean,
      default: true
    },
    contentClass: {
      type: String,
      default: ''
    },
    contentStyle: {
      type: [String, Object],
      default: null
    },
    baseZIndex: {
      type: Number,
      default: 0
    },
    dismissableMask: {
      type: Boolean,
      default: true
    },
    breakpoints: {
      type: Object as PropType<DialogBreakpoints> | undefined,
      default: undefined
    },
    width: {
      type: String,
      default: '60vw'
    },
    maxWidth: {
      type: String,
      default: '100%'
    }
  },
  emits: ['show', 'hide', 'unmaximize', 'maximize', 'close'],
  data () {
    return {
      containerVisible: this.visible,
      maximized: false,
      mask: null as HTMLElement | null,
      container: null as HTMLElement | null,
      styleElement: null as HTMLStyleElement | null,
      documentKeydownListener: null as any,
      autoZIndex: true
    }
  },
  computed: {
    maskClass (): string[] {
      return [
        'vc-component-overlay vc-component-overlay-enter',
        this.getPositionClass()
      ]
    },
    dialogClass () {
      return ['vc-dialog vc-dialog-component', {
        'vc-dialog-maximized': this.maximizable && this.maximized
      }]
    },
    ariaLabelledById (): string {
      return this.header != null ? this.ariaId + '_header' : 'ariaLabelledId'
    },
    ariaId (): string {
      return UniqueComponentId()
    },
    contentStyleClass () {
      return ['vc-dialog-content', this.contentClass, { 'vc-dialog-no-footer': !this.footer && !this.$slots.footer }]
    },
    attributeSelector (): string {
      return UniqueComponentId()
    },
    modalWidth (): string | undefined {
      return convertToUnit(this.width)
    },
    modalMaxWidth (): string | undefined {
      return convertToUnit(this.maxWidth)
    }
  },
  beforeUnmount (): void {
    this.unbindGlobalListeners() // keydown
    this.destroyStyle()
    this.mask = null
    if (this.container && this.autoZIndex) {
      ZIndexUtils.clear(this.container)
    }
    this.container = null
  },
  updated (): void {
    if (this.visible) {
      this.containerVisible = this.visible
    }
  },
  mounted (): void {
    if (this.breakpoints) {
      this.createStyle()
    }
  },
  methods: {
    destroyStyle (): void {
      if (this.styleElement) {
        document.head.removeChild(this.styleElement)
        this.styleElement = null
      }
    },
    /**
     * Create responsive width (for breakpoints prop)
     */
    createStyle (): void {
      if (!this.styleElement && this.breakpoints) {
        this.styleElement = document.createElement('style')
        this.styleElement.type = 'text/css'
        document.head.appendChild(this.styleElement)
        let innerHTML = ''
        for (const breakpoint in this.breakpoints) {
          innerHTML += `
                        @media screen and (max-width: ${breakpoint}) {
                            .vc-dialog[${this.attributeSelector}] {
                                width: ${this.breakpoints[breakpoint]} !important;
                            }
                        }
                    `
        }
        this.styleElement.innerHTML = innerHTML
      }
    },
    onMaskClick (event: Event): void {
      if (this.dismissableMask && this.closable && this.mask === event.target) {
        this.close()
      }
    },
    getPositionClass (): string {
      const positions = ['left', 'right', 'top', 'top-left', 'top-right', 'bottom', 'bottom-left', 'bottom-right']
      const pos = positions.find(item => item === this.position)
      return pos ? `vc-dialog-${pos}` : ''
    },
    onBeforeEnter (el: HTMLElement | Element): void {
      if (this.autoZIndex) {
        ZIndexUtils.set('modal', el as HTMLElement, this.baseZIndex + BASE_Z_INDEX)
      }
      el.setAttribute(this.attributeSelector, '')
      ZIndexUtils.setModal(this.ariaId)
    },
    onEnter (): void {
      if (!this.mask || !this.container) {
        return
      }
      this.mask.style.overflowY = 'hidden'
      this.mask.style.zIndex = String(parseInt(this.container.style.zIndex, 10) - 1)
      this.$emit('show')
      this.focus()
      this.enableDocumentSettings()
      this.bindGlobalListeners()
    },
    onEnterCancelled () {
      this.$nextTick(() => {
        if (!this.mask || !this.container) {
          return
        }
        this.mask.style.overflowY = 'auto'
      })
    },
    bindGlobalListeners (): void {
      if (this.closable && this.closeOnEscape) {
        this.bindDocumentKeyDownListener()
      }
    },
    onKeyDown (event: KeyboardEvent): void {
      const lastModal = ZIndexUtils.lastModal()
      if (event.code === 'Escape' && lastModal === this.ariaId) {
        this.close()
      }
    },
    bindDocumentKeyDownListener (): void {
      if (!this.documentKeydownListener) {
        this.documentKeydownListener = this.onKeyDown.bind(this)
        document.addEventListener('keydown', this.documentKeydownListener)
      }
    },
    enableDocumentSettings (): void {
      const size = ZIndexUtils.countModals()
      if (size > 1) {
        return
      }
      DomHandler.addClass(document.body, 'overflow-hidden')
    },
    onBeforeLeave (): void {
      if (this.mask) {
        this.mask.style.overflowY = 'hidden'
      }
      DomHandler.addClass(this.mask, 'vc-component-overlay-leave')
    },
    onLeave (): void {
      this.$emit('hide')
    },
    onAfterLeave (el: HTMLElement | Element): void {
      if (this.autoZIndex) {
        ZIndexUtils.clear(el as HTMLElement)
      }
      ZIndexUtils.removeModal(this.ariaId)
      this.containerVisible = false
      this.maximized = false
      this.unbindDocumentState()
      this.unbindGlobalListeners()
    },
    unbindGlobalListeners (): void {
      this.unbindDocumentKeyDownListener()
    },
    unbindDocumentKeyDownListener (): void {
      if (this.documentKeydownListener) {
        document.removeEventListener('keydown', this.documentKeydownListener)
        this.documentKeydownListener = null
      }
    },
    unbindDocumentState (): void {
      const size = ZIndexUtils.countModals()
      if (size > 0) {
        return
      }
      DomHandler.removeClass(document.body, 'overflow-hidden')
    },
    containerRef (el: HTMLElement): string {
      this.container = el
      return 'containerRef'
    },
    focus () {
      if (!this.container) {
        return
      }
      const focusTarget = this.container.querySelector('[autofocus]') as HTMLElement
      if (focusTarget) {
        focusTarget.focus()
      }
    },
    maskRef (el: HTMLElement): string {
      this.mask = el
      return 'maskRef'
    },
    close (): void {
      this.$emit('close', false)
    },
    maximize (event: Event): void {
      if (this.maximized) {
        this.maximized = false
        this.$emit('unmaximize', event)
      } else {
        this.maximized = true
        this.$emit('maximize', event)
      }
    }
  }
})
</script>

<style lang="scss">
@import "VcModal";
</style>
