<template>
  <div
    class="vc-popover__wrapper"
    :style="wrapperStyles"
  >
    <div
      :ref="getReference"
      class="reference h-full"
      @mouseover="onMouseOver"
      @mouseleave="onMouseLeave"
    >
      <slot
        name="reference"
        :open="isOpen"
      />
    </div>
    <VcMountedTeleport :to="appendTarget">
      <transition
        :css="false"
        name="popover"
        @before-enter="onBeforeEnter"
        @enter="onEnter"
        @after-leave="onAfterLeave"
      >
        <div
          v-if="isOpen"
          ref="popoverContent"
          :style="popoverStyle"
          :class="popoverClasses"
          @mouseover="onMousePopOver"
          @mouseleave="onMousePopLeave"
        >
          <div
            class="vc-popover-content"
          >
            <slot />
          </div>
          <div class="absolute inset-0 pointer-events-none">
            <div
              ref="arrowNode"
              class="vc-arrow"
              :style="arrowAddStyles"
            />
          </div>
        </div>
      </transition>
    </VcMountedTeleport>
  </div>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue'
import { ArrowPositionType } from '@/types/Dropdown.types'
import ZIndexUtils from '@/utils/ZIndexUtils'
import { convertToUnit } from '@/utils/helpers'
import appendTo from '@/mixins/common/appendTo.mixin'
import VcMountedTeleport from '@/components/VcPopover/VcMountedTeleport.vue'

const POPOVER_CLASS = 'vc-popover'
const ARROW_POSITION_DEFAULT = 'top-center'
const OPPOSITE_POPUP_POSITIONS: { [position: string]: string } = {
  top: 'bottom',
  bottom: 'top',
  left: 'right',
  right: 'left'
}

export default defineComponent({
  name: 'VcPopover',
  components: {
    VcMountedTeleport
  },
  mixins: [appendTo],
  props: {
    arrowPosition: {
      type: String as PropType<ArrowPositionType>,
      default: ARROW_POSITION_DEFAULT
    },
    trigger: {
      type: String as PropType<'hover' | 'manual'>,
      default: 'hover'
    },
    contentInUse: {
      type: Boolean,
      default: false
    },
    open: {
      type: Boolean,
      default: false
    },
    maxWidth: {
      type: String,
      default: '350'
    },
    popoverWidth: {
      type: String,
      default: '100%'
    },
    wrapperExternalStyles: {
      type: Object,
      default: () => ({})
    },
    clickOutsideHandler: {
      type: Function,
      default: undefined
    },
    popoverClass: {
      type: String,
      default: ''
    },
    delayClose: {
      type: Number,
      default: 200
    }
  },
  emits: ['popover-open'],
  data () {
    return {
      POPOVER_CLASS,
      hovered: false,
      mouseHovered: false,
      customArrowPosition: this.arrowPosition as string,
      popoverContentHeight: 0,
      popoverContentWidth: 0,
      reference: null as HTMLElement | null,
      baseZIndex: 1000,
      observer: null as null | ResizeObserver,
      onTooltip: false,
      popoverStyle: {
        left: '0px',
        top: '0px'
      }
    }
  },
  computed: {
    hasClickOutsideHandler (): boolean {
      return !!this.clickOutsideHandler && typeof this.clickOutsideHandler === 'function'
    },
    wrapperStyles (): {[key:string]: string | undefined} {
      return { maxWidth: convertToUnit(this.maxWidth), ...this.wrapperExternalStyles }
    },
    popoverClasses (): string[] {
      return [
        POPOVER_CLASS,
        `${POPOVER_CLASS}--${this.customArrowPosition}`,
        (!this.open && this.trigger === 'manual') || (!this.hovered && this.trigger === 'hover') ? 'hidden' : '',
        this.popoverClass
      ]
    },
    isOpen (): boolean {
      if (this.trigger === 'manual') {
        return this.open
      }
      return this.hovered
    },
    arrowAddStyles () {
      const position = this.customArrowPosition.split('-')
      if (position.length !== 2) {
        return ''
      }
      if (position && (position[0] === 'top' || position[0] === 'bottom')) {
        if (this.referenceWidth < 60) {
          const _diffIn = this.referenceWidth / 2 - 8
          const diff = _diffIn <= 4 ? 4 : _diffIn
          const key = position[1]
          return { [key]: diff + 'px' }
        }
      } else if (position[0] === 'right' || position[0] === 'left') {
        if (this.referenceHeight < 60) {
          const _diffIn = this.referenceHeight / 2 - 8
          const diff = _diffIn <= 4 ? 4 : _diffIn
          const key = position[1]
          return { [key]: diff + 'px' }
        }
      }
      return ''
    },
    referenceWidth () {
      return this.getReferenceWidth()
    },
    referenceHeight () {
      return this.getReferenceHeight()
    }
  },
  watch: {
    open (val) {
      this.triggerScrollEvent(val)
    },
    hovered (val) {
      this.triggerScrollEvent(val)
    },
    contentInUse (val) {
      if (!val && this.isOpen && !this.mouseHovered) {
        this.onTooltip = false
        this.onMouseLeave()
      }
    },
    customArrowPosition () {
      this.setPopoverStyle()
    },
    isOpen (val) {
      this.$emit('popover-open', val)
    }
  },
  beforeUnmount () {
    if (this.hasClickOutsideHandler) {
      document.removeEventListener('click', this.handleDocumentClick)
    }
  },
  methods: {
    handleDocumentClick (event: MouseEvent): void {
      const node = event.target as Node
      const popoverContent = this.$refs.popoverContent as Element

      if (!this.$el ||
        !this.reference ||
        this.$el.contains(node) ||
        this.reference.contains(node) ||
        !popoverContent ||
        popoverContent.contains(node)) return

      if (!!this.clickOutsideHandler && typeof this.clickOutsideHandler === 'function') {
        this.clickOutsideHandler()
      }
    },
    onBeforeEnter (el: HTMLElement): void {
      this.customArrowPosition = this.arrowPosition
      ZIndexUtils.set('popover', el as HTMLElement, this.baseZIndex)

      if (this.hasClickOutsideHandler) {
        document.addEventListener('click', this.handleDocumentClick)
      }
      this.popoverStyle.left = '0px'
      this.popoverStyle.top = '0px'
    },
    onEnter (el: HTMLElement): void {
      el.style.width = this.popoverWidth === '100%' ? `${this.getReferenceWidth()}px` : convertToUnit(this.popoverWidth) || '0px'
      this.$nextTick(() => {
        this.popoverContentWidth = el.clientWidth
        this.popoverContentHeight = el.clientHeight
        this.setPopoverStyle()
        this.observer = new ResizeObserver(this.setPopupPosition)
        this.observer.observe(this.$refs.popoverContent as Element)
      })
    },
    setPopoverStyle () {
      const { left, top } = this.getContentPosition()
      this.popoverStyle.left = `${left}px`
      this.popoverStyle.top = `${top}px`
    },
    onAfterLeave (el: HTMLElement): void {
      ZIndexUtils.clear(el as HTMLElement)

      if (this.hasClickOutsideHandler) {
        document.removeEventListener('click', this.handleDocumentClick)
      }
      if (this.observer) {
        this.observer.disconnect()
      }
      this.popoverStyle.left = '0px'
      this.popoverStyle.top = '0px'
    },
    getReference (el: HTMLElement): string {
      this.reference = el
      return 'reference'
    },
    triggerScrollEvent (scroll:boolean): void {
      if (scroll) {
        window.addEventListener('scroll', this.handleScroll)
      } else if (!scroll) {
        window.removeEventListener('scroll', this.handleScroll)
      }
    },
    onMouseOver (): void {
      if (this.trigger === 'hover') {
        this.hovered = true
      }
    },
    onMousePopOver (): void {
      this.mouseHovered = true
      if (this.trigger === 'hover') {
        this.onTooltip = true
      }
    },
    onMousePopLeave (): void {
      this.mouseHovered = false
      if (this.trigger === 'hover' && !this.contentInUse) {
        this.onTooltip = false
        this.onMouseLeave()
      }
    },
    onMouseLeave (): void {
      if (this.trigger === 'hover' && !this.contentInUse) {
        setTimeout(() => {
          if (!this.onTooltip) {
            this.hovered = false
          }
        }, this.delayClose)
      }
    },
    handleScroll (): void {
      this.setPopupPosition()
    },
    setPopupPosition (): void {
      this.customArrowPosition = this.arrowPosition
      const changePosition = this.changePopoverPosition()
      if (changePosition && (this.open || this.hovered)) {
        const positions = this.customArrowPosition.split('-')
        const position: string = OPPOSITE_POPUP_POSITIONS[positions[0]]
        this.customArrowPosition = `${position}-${positions[1]}`
      }
    },
    changePopoverPosition (): boolean {
      const positions = this.arrowPosition.split('-')
      const popoverContent = this.$refs.popoverContent as Element
      if (!popoverContent) {
        return false
      }
      this.popoverContentHeight = popoverContent.clientHeight
      this.popoverContentWidth = popoverContent.clientWidth
      const referenceCoordinates = (this.reference as HTMLElement).getBoundingClientRect()
      const windowHeight = (window.innerHeight || document.documentElement.clientHeight)
      const windowWidth = (window.innerWidth || document.documentElement.clientWidth)
      let changePosition
      if (positions[0] === 'top') {
        const visible = (referenceCoordinates.top > 0 || referenceCoordinates.bottom > 0) &&
          referenceCoordinates.bottom + this.popoverContentHeight < windowHeight
        changePosition = !visible && referenceCoordinates.bottom > (windowHeight - referenceCoordinates.bottom)
      } else if (positions[0] === 'bottom') {
        const visible = (referenceCoordinates.top > 0 || referenceCoordinates.bottom > 0) &&
          referenceCoordinates.top > this.popoverContentHeight
        changePosition = !visible && referenceCoordinates.bottom < (windowHeight - referenceCoordinates.bottom)
      } else if (positions[0] === 'left') {
        const visible = (referenceCoordinates.right > 0 || referenceCoordinates.left > 0) &&
          this.popoverContentWidth < (windowWidth - referenceCoordinates.right)
        changePosition = !visible && referenceCoordinates.left > (windowWidth - referenceCoordinates.right)
      } else if (positions[0] === 'right') {
        const visible = (referenceCoordinates.right > 0 || referenceCoordinates.left > 0) &&
          referenceCoordinates.left > this.popoverContentWidth
        changePosition = !visible && referenceCoordinates.left < (windowWidth - referenceCoordinates.right)
      }
      return !!changePosition
    },
    getTop (position: string): number {
      let top = 0
      const targetOptions = this.getTargetOptions()
      const scroll = targetOptions ? 0 : window.scrollY
      if (this.reference) {
        const referenceOptions = this.reference.getBoundingClientRect()
        const options = {
          top: referenceOptions.top - (targetOptions ? targetOptions.top : 0)
        }
        switch (position) {
          case 'top':
            top = options.top + this.reference.offsetHeight + scroll
            break
          case 'bottom':
            top = options.top - this.popoverContentHeight - 13 + scroll
            break
          case 'center':
            top = options.top - (this.popoverContentHeight / 2 - this.reference.offsetHeight / 2) + scroll
            break
          case 'left':
          case 'right':
            top = options.top + scroll
            break
        }
      }
      return top
    },
    getContentPosition (): {[key:string]: number} {
      const [firstArrowPosition, lastArrowPosition] = this.customArrowPosition.split('-')
      const res = {
        top: 0,
        left: 0
      }
      if (!this.reference) {
        return res
      }
      if (firstArrowPosition === 'top' || firstArrowPosition === 'bottom') {
        res.top = this.getTop(firstArrowPosition)
        res.left = this.getLeft(lastArrowPosition)
      } else if (firstArrowPosition === 'left' || firstArrowPosition === 'right') {
        if (lastArrowPosition === 'top') {
          res.top = this.getTop(firstArrowPosition)
        } else if (lastArrowPosition === 'bottom') {
          res.top = this.getTop('top') - this.popoverContentHeight
        } else if (lastArrowPosition === 'center') {
          res.top = this.getTop('center')
        }
        res.left = firstArrowPosition === 'right' ? this.getLeft('left') - this.popoverContentWidth - 13 : this.getLeft('left') + this.getReferenceWidth()
      }
      return res
    },
    getLeft (position: string): number {
      let left = 0
      const targetOptions = this.getTargetOptions()
      if (this.reference) {
        const referenceOptions = this.reference.getBoundingClientRect()
        const options = {
          left: referenceOptions.left - (targetOptions ? targetOptions.left : 0)
        }
        const refWidth = this.getReferenceWidth()
        switch (position) {
          case 'center':
            left = options.left - (this.popoverContentWidth / 2) + (refWidth / 2)
            break
          case 'right':
            left = options.left - this.popoverContentWidth + refWidth
            break
          default:
            left = options.left
            break
        }
      }
      return left
    },
    getTargetOptions () {
      let targetOptions = null as unknown as DOMRect
      if (typeof this.appendTarget === 'string' && this.appendTarget) {
        const appendTarget = document.querySelector(this.appendTarget)
        if (appendTarget) {
          targetOptions = appendTarget.getBoundingClientRect()
        }
      }
      return targetOptions
    },
    getReferenceHeight (): number {
      return (this.reference as HTMLElement).offsetHeight
    },
    getReferenceWidth (): number {
      return (this.reference as HTMLElement).offsetWidth
    }
  }
})
</script>

<style lang="scss" scoped>
@import "VcPopover";
</style>
