<template>
  <transition
    :name="transition"
    v-on="transitionEvent"
    @after-enter="$emit('after-enter')"
    @after-leave="$emit('after-leave')"
    @before-leave="$emit('before-leave')">
    <outside
      v-if="!lazyRender || rendered"
      v-show="rendered && localVisible"
      ref="popper"
      v-bind="outsideOptions"
      :style="realPopperStyle"
      :class="popperClass"
      :x-has-arrow="visibleArrow"
      :x-loading="loading">
      <template v-if="loading">
        <loading size="40px"></loading>
      </template>
      <div v-show="!loading">
        <slot></slot>
      </div>
      <div
        v-if="visibleArrow"
        key="arrow"
        ref="arrow"
        :class="arrowClass"></div>
    </outside>
  </transition>
</template>

<script>
import Popper from "popper.js/dist/umd/popper"
import { getScrollParent } from "popper.js/dist/umd/popper-utils"
import { addListener, removeListener } from "resize-detector/dist"
import merge from "lodash/merge"
import isFunction from "lodash/isFunction"
import { getNodes } from "../../utils/context"
import Loading from "../loading"
import config from "../../managers/config"
import Outside from "../../components/outside"

config.defaults({
  "Popper.transition": "zoom-in-top",
  "Popper.placement": "bottom-start",
  "Popper.visibleArrow": false,
  "Popper.trigger": "click",
  "Popper.offset": 0,
  "Popper.delay": 300,
  "Popper.lazyRender": true,
  "Popper.appendToBody": true,
  "Popper.removeOnDestroy": true,
  "Popper.trackRefWidth": false,
  "Popper.getContainer": () => null,
  "Popper.positionFixed": false,
})

const transformTrigger = trigger => (Array.isArray(trigger) ? trigger : trigger.split(" "))

export default {
  name: "mm-popper",

  placements: Popper.placements,

  components: {
    Outside,
    Loading,
  },

  inheritAttrs: false,
  props: {
    visible: {
      type: Boolean,
      default: false,
    },
    transition: {
      type: String,
      default: () => config.get("Popper.transition"),
    },
    target: {
      type: null,
      required: true,
    },
    placement: {
      type: String,
      default: () => config.get("Popper.placement"),
      validator(value) {
        return Popper.placements.includes(value)
      },
    },
    visibleArrow: {
      type: Boolean,
      default: () => config.get("Popper.visibleArrow"),
    },
    trigger: {
      type: [String, Array],
      default: () => config.get("Popper.trigger"),
    },
    offset: {
      type: null,
      default: () => config.get("Popper.offset"),
    },
    delay: {
      type: Number,
      default: () => config.get("Popper.delay"),
    },
    lazyRender: {
      type: Boolean,
      default: () => config.get("Popper.lazyRender"),
    },
    appendToBody: {
      type: Boolean,
      default: () => config.get("Popper.appendToBody"),
    },
    removeOnDestroy: {
      type: Boolean,
      default: () => config.get("Popper.removeOnDestroy"),
    },
    trackRefWidth: {
      type: Boolean,
      default: () => config.get("Popper.trackRefWidth"),
    },
    transitionEvent: {
      type: Object,
      default: () => ({}),
    },
    popperOptions: {
      type: [Object, Function],
      default: () => ({}),
    },
    getContainer: {
      type: Function,
      default() {
        return config.get("Popper.getContainer").call(this)
      },
    },
    positionFixed: {
      type: Boolean,
      default: () => config.get("Popper.positionFixed"),
    },
    disabledOutside: {
      type: Boolean,
      default: false,
    },
    arrowClass: {
      type: [String, Array, Object],
      default: "popper-arrow",
    },
    loading: {
      type: Boolean,
      default: false,
    },
    popperClass: {
      type: [String, Array, Object],
    },
    popperStyle: {
      type: [Object, String],
      default: () => ({}),
    },
    popperWidth: {
      type: String,
    },
    onVisibleChange: {
      type: Function,
      default: () => {},
    },
    updateOnResize: {
      type: Boolean,
      default: false,
    },
  },
  data() {
    const _this = this
    return {
      localVisible: this.visible,
      rendered: false,
      refWidth: 0,
      get referenceNode() {
        return getNodes(_this.target, _this.$vnode.context)[0]
      },
      get popperNode() {
        return _this.$refs.popper && _this.$refs.popper.$el
      },
    }
  },
  computed: {
    events() {
      return transformTrigger(this.trigger)
    },
    outsideOptions() {
      return {
        disabled: this.disabledOutside,
        handler: this.closeHandler,
        refs: [this.referenceNode],
        trigger: this.trigger,
        delay: this.delay,
      }
    },

    realPopperStyle() {
      const style = {
        ...this.popperStyle,
      }
      if (this.trackRefWidth) {
        style.width = this.refWidth
      }
      if (this.popperWidth) {
        style.width = this.popperWidth
      }
      return style
    },
  },
  watch: {
    loading() {
      this.update()
    },
    visible(value) {
      this.localVisible = value
    },
    localVisible: {
      immediate: true,
      handler(value) {
        if (value) {
          if (!this.rendered) {
            this.rendered = true
            this.$nextTick(() => {
              this.createPopper()
              this.updateVisible(value)
            })
          } else {
            this.$nextTick(() => {
              this.updatePopper()
              this.updateVisible(value)
            })
          }
        } else {
          if (this.lazyRender && this.rendered && this.removeOnDestroy) {
            this.$once("after-leave", () => {
              this.destroyPopper()
              this.rendered = false
              this.updateVisible(value)
            })
          } else {
            this.updatePopper()
            this.updateVisible(value)
          }
        }
      },
    },
  },
  mounted() {
    if (this.trackRefWidth && this.referenceNode) {
      addListener(this.referenceNode, this.resizeHandler)
      this.$nextTick(() => {
        this.resetReferenceWidth()
      })
    }
  },
  beforeDestroy() {
    if (this.trackRefWidth && this.referenceNode) {
      removeListener(this.referenceNode, this.resizeHandler)
    }

    if (this.popperNode && this.updateOnResize) {
      removeListener(this.popperNode, this.update)
    }

    if (this.popperNode && this.popperNode.parentNode) {
      this.popperNode.parentNode.removeChild(this.popperNode)
    }
  },
  destroyed() {
    this.destroyPopper(true)
  },
  methods: {
    updateVisible(value) {
      this.$emit("update:visible", value)
      this.$emit("visible-change", value)
      this.onVisibleChange(value)
    },
    update() {
      this.updatePopper()
    },
    resizeHandler() {
      this.resetReferenceWidth()
    },
    resetReferenceWidth() {
      if (this.referenceNode) {
        this.refWidth = this.referenceNode.getBoundingClientRect().width + "px"
      }
    },
    closeHandler() {
      this.localVisible = false
    },
    createPopper() {
      if (!this.referenceNode || !this.popperNode) {
        return
      }

      if (
        this.appendToBody &&
        (!this.popperNode.parentNode || this.popperNode.parentNode.tagName !== "BODY") || this.getContainer()
      ) {
        (this.getContainer() || document.body).appendChild(this.popperNode)
      }

      const scrollParent = getScrollParent(this.referenceNode)
      const boundariesElement =
        scrollParent === document.body || scrollParent.scrollHeight === scrollParent.clientHeight
          ? "viewport"
          : "scrollParent"

      const popperOptions = {
        placement: this.placement,
        modifiers: {
          arrow: {
            enabled: this.visibleArrow,
            element: this.$refs.arrow,
          },
          offset: {
            offset: this.offset,
          },
          flip: {
            boundariesElement: boundariesElement,
          },
          preventOverflow: {
            padding: 0,
            boundariesElement: boundariesElement, // document.body,
            escapeWithReference: true,
          },
          computeStyle: {
            gpuAcceleration: false,
          },
        },
        onCreate: this.updateTransformOrigin,
        onUpdate: this.updateTransformOrigin,
        positionFixed: this.positionFixed,
      }

      this.popperInstance = new Popper(
        this.referenceNode,
        this.popperNode,
        merge(
          popperOptions,
          isFunction(this.popperOptions) ? this.popperOptions(Popper) : this.popperOptions
        )
      )

      if (this.updateOnResize) {
        addListener(this.popperNode, this.update)
      }
    },
    updatePopper() {
      if (!this.popperInstance) {
        return
      }
      if (this.localVisible) {
        if (this.popperInstance.reference === this.referenceNode) {
          this.popperInstance.enableEventListeners()
          this.popperInstance.update()
        } else {
          this.destroyPopper()
          this.createPopper()
        }
      } else {
        this.popperInstance.disableEventListeners()
      }
    },
    destroyPopper(force = true) {
      if (!this.popperInstance || (this.localVisible && !force)) {
        return
      }
      this.popperInstance.destroy()
      this.popperInstance = null
    },

    updateTransformOrigin(data) {
      const positionMap = {
        top: "bottom",
        bottom: "top",
        left: "right",
        right: "left",
      }
      const variationMap = {
        start: "0%",
        end: "100%",
      }

      const [position, variation] = data.placement.split("-")
      const p = positionMap[position]
      const v = variationMap[variation]

      data.instance.popper.style.transformOrigin =
        typeof this.transformOrigin === "string"
          ? this.transformOrigin
          : ["top", "bottom"].indexOf(p) > -1
            ? `${v} ${p}`
            : `${p} ${v}`
    },
  },
}
</script>
