<template lang="pug">
slot(name="activator" :is-open="detachableVisible")
slot(v-if="!$slots.activator")
teleport(v-if="detachableDomReady" :to="teleportTarget" :disabled="!teleportTarget")
  transition(:name="transitionName" appear @after-enter="onDetachableAfterEnter" @after-leave="onAfterLeave")
    .w-menu(
      v-if="custom && detachableVisible"
      ref="detachable"
      v-bind="$attrs"
      @click="hideOnMenuClick && close(true)"
      @mouseenter="showOnHover && (hoveringMenu = true)"
      @mouseleave="showOnHover && ((hoveringMenu = false), close())"
      :class="classes"
      :style="styles")
      slot(v-if="$slots.activator")
      slot(v-else name="content")
    w-card.w-menu(
      v-else-if="detachableVisible"
      ref="detachable"
      v-bind="$attrs"
      @click.native="hideOnMenuClick && close(true)"
      @mouseenter.native="showOnHover && (hoveringMenu = true)"
      @mouseleave.native="showOnHover && ((hoveringMenu = false), close())"
      :tile="tile"
      :title-class="titleClasses"
      :content-class="contentClasses"
      :shadow="shadow"
      :no-border="noBorder"
      :class="classes"
      :style="styles")
      template(v-if="$slots.title" #title)
        slot(name="title")
      template(v-if="$slots.actions" #actions)
        slot(name="actions")
      slot(v-if="$slots.activator")
      slot(v-else name="content")
  w-overlay(
    v-if="overlay"
    ref="overlay"
    :model-value="detachableVisible"
    :persistent="persistent"
    :class="overlayClasses"
    v-bind="overlayProps"
    :z-index="(zIndex || 200) - 1"
    @update:model-value="detachableVisible = false")
</template>

<script>
import { objectifyClasses } from '../utils/index'
import DetachableMixin from '../mixins/detachable'
import { focusElement } from '../utils/focus'

export default {
  name: 'w-menu',
  expose: ['focus', 'computeDetachableCoords'],
  mixins: [DetachableMixin],
  inheritAttrs: false, // The attrs are only bound to the button-partial, not the root.

  props: {
    modelValue: {}, // Show or hide.
    showOnHover: { type: Boolean },
    hideOnMenuClick: { type: Boolean },
    color: { type: String },
    bgColor: { type: String },
    shadow: { type: Boolean },
    custom: { type: Boolean }, // Include a w-card or not. It does by default.
    tile: { type: Boolean },
    round: { type: Boolean },
    noBorder: { type: Boolean },
    transition: { type: String },
    menuClass: { type: [String, Object, Array] },
    titleClass: { type: [String, Object, Array] },
    contentClass: { type: [String, Object, Array] },
    arrow: { type: Boolean }, // The small triangle pointing toward the activator.
    minWidth: { type: [Number, String] }, // can be like: `40`, `5em`, `activator`.
    maxWidth: { type: [Number, String] }, // can be like: `40`, `5em`, `activator`.
    overlay: { type: Boolean },
    overlayClass: { type: [String, Object, Array] },
    overlayProps: { type: Object }, // Allow passing down an object of props to the w-overlay component.
    persistent: { type: Boolean },
    delay: { type: Number },
    dark: { type: Boolean },
    light: { type: Boolean }
    // Other props in the detachable mixin:
    // detachTo, appendTo, fixed, top, bottom, left, right, alignTop, alignBottom, alignLeft,
    // alignRight, noPosition, zIndex, activator.
  },

  emits: ['input', 'update:modelValue', 'open', 'close'],

  provide () {
    return {
      // If a detachable is used inside a w-menu without an appendTo, default to the menu element
      // instead of the w-app.
      detachableDefaultRoot: () => this.$refs.detachable?.$el || this.$refs.detachable || null
    }
  },

  data: () => ({
    detachableVisible: false,
    hoveringActivator: false,
    hoveringMenu: false,
    // null until first computeDetachableCoords run, so no inline top/left is set before positioning.
    detachableCoords: {
      top: null,
      left: null
    },
    activatorWidth: 0,
    detachableEl: null,
    timeoutId: null
  }),

  computed: {
    /**
     * Other computed in the detachable mixin:
     * - `appendToTarget`
     * - `detachableParentEl`
     * - `activatorEl`
     * - `position`
     * - `effectiveDetachablePosition`
     * - `alignment`
     **/

    transitionName () {
      return this.transition || 'scale-fade'
    },

    menuMinWidth () {
      if (this.minWidth === 'activator') return this.activatorWidth ? `${this.activatorWidth}px` : 0
      return isNaN(this.minWidth) ? this.minWidth : (this.minWidth ? `${this.minWidth}px` : 0)
    },

    menuMaxWidth () {
      if (this.maxWidth === 'activator') return this.activatorWidth ? `${this.activatorWidth}px` : 0
      return isNaN(this.maxWidth) ? this.maxWidth : (this.maxWidth ? `${this.maxWidth}px` : 0)
    },

    menuClasses () {
      return objectifyClasses(this.menuClass)
    },

    titleClasses () {
      return objectifyClasses(this.titleClass)
    },

    contentClasses () {
      return objectifyClasses(this.contentClass)
    },

    overlayClasses () {
      return {
        ...objectifyClasses(this.overlayClass),
        'w-overlay--no-pointer-event': this.showOnHover
      }
    },

    classes () {
      return {
        [this.color]: this.color,
        [`${this.bgColor}--bg`]: this.bgColor,
        ...this.menuClasses,
        [`w-menu--${this.effectiveDetachablePosition}`]: !this.noPosition,
        [`w-menu--align-${this.alignment}`]: !this.noPosition && this.alignment,
        'w-menu--tile': this.tile,
        'w-menu--card': !this.custom,
        'w-menu--round': this.round,
        'w-menu--arrow': this.arrow,
        'w-menu--shadow': this.shadow,
        'w-menu--fixed': this.fixed,
        'w-menu--dark': this.dark,
        'w-menu--light': this.light
      }
    },

    // The floating menu styles.
    styles () {
      return {
        zIndex: this.zIndex || this.zIndex === 0 || (this.overlay && !this.zIndex && 200) || null,
        top: Number.isFinite(this.detachableCoords.top) ? `${~~this.detachableCoords.top}px` : null,
        left: Number.isFinite(this.detachableCoords.left) ? `${~~this.detachableCoords.left}px` : null,
        minWidth: (this.minWidth && this.menuMinWidth) || null,
        maxWidth: (this.maxWidth && this.menuMaxWidth) || null,
        visibility: this.detachableReady ? null : 'hidden', // Hidden until coords are ready.
        '--w-menu-bg-color': this.arrow ? (this.$waveui.colors[this.bgColor] || 'var(--w-base-bg-color)') : null
      }
    },

    activatorEventHandlers () {
      if (this.disable) return {}

      let handlers = {}

      if (this.showOnHover) {
        handlers = {
          focus: this.toggle,
          blur: this.toggle,
          mouseenter: e => {
            this.hoveringActivator = true
            this.open(e)
          },
          mouseleave: () => {
            this.hoveringActivator = false
            // Wait 10ms, the time to get the hoveringMenu updated on mouseenter on the menu.
            setTimeout(() => {
              if (!this.hoveringMenu) this.close()
            }, 10)
          }
        }
        // Check the window exists: SSR-proof.
        if (typeof window !== 'undefined' && 'ontouchstart' in window) {
          handlers.click = this.toggle
        }
      }
      else handlers = { click: this.toggle }
      return handlers
    }
  },

  methods: {
    focus () {
      if (this.overlay && this.$refs.overlay) this.$refs.overlay.focus()
      else focusElement(this.activatorEl)
    },

    /**
     * Other methods in the `detachable` mixin:
     * - `open`
     * - `getActivatorCoordinates`
     * - `computeDetachableCoords`
     * - `onResize`
     * - `onOutsideMousedown`
     * - `insertInDOM`
     * - `removeFromDOM`
     **/

    // ! \ This function uses the DOM - NO SSR (only trigger from beforeMount and later).
    toggle (e) {
      if (this.disable) return

      let shouldShowMenu = this.detachableVisible
      if (typeof window !== 'undefined' && 'ontouchstart' in window &&
          this.showOnHover && e.type === 'click') {
        shouldShowMenu = !shouldShowMenu
      }
      else if (e.type === 'click' && !this.showOnHover) shouldShowMenu = !shouldShowMenu
      else if (e.type === 'mouseenter' && this.showOnHover) {
        this.hoveringActivator = true
        shouldShowMenu = true
      }
      else if (e.type === 'mouseleave' && this.showOnHover) {
        this.hoveringActivator = false
        shouldShowMenu = false
      }

      this.timeoutId = clearTimeout(this.timeoutId)

      if (shouldShowMenu) this.open(e)
      else this.close()
    },

    /**
     * Closes the menu. Can happen on:
     * - click of activator
     * - hover outside if showOnHover
     * - click inside menu if hideOnMenuClick.
     * / ! \ This function uses the DOM - NO SSR (only trigger from beforeMount and later).
     *
     * @param {Boolean} force when showOnHover is set to true, hovering menu should keep it open.
     *                        But if hideOnMenuClick is also set to true, this should force close
     *                        even while hovering the menu.
     */
    async close (force = false) {
      // The user may open and close the detachable so fast (like when toggling on hover) that it
      // should not show up at all. This cancels the opening timer (if there is a set delay prop).
      this.openTimeout = clearTimeout(this.openTimeout)

      // Might be already closed.
      // E.g. showOnHover & hideOnMenuClick: on click, force hide then mouseleave is also firing.
      if (!this.detachableVisible) return

      if (this.showOnHover && !force) {
        await new Promise(resolve => setTimeout(resolve, 10))
        if (this.showOnHover && (this.hoveringMenu || this.hoveringActivator)) return
      }

      this.$emit('update:modelValue', (this.detachableVisible = false))
      this.$emit('input', false)
      this.$emit('close')
      this.viewportPlacementOverride = null
      // Remove the mousedown listener if the menu got closed without a mousedown outside of the menu.
      if (typeof document !== 'undefined') {
        document.removeEventListener('mousedown', this.onOutsideMousedown)
      }
      if (typeof window !== 'undefined') {
        window.removeEventListener('resize', this.onResize)
      }
    }
  }

  // watch, mounted & beforeDestroy hooks are set in the detachable.js mixin.
}
</script>

<style lang="scss">
.w-menu-wrap {display: none;}

.w-menu {
  position: absolute;
  z-index: 100;
  color: var(--w-menu-color);

  @include themeable;

  &--fixed {position: fixed;z-index: 1000;}
  &--card {background-color: var(--w-menu-bg-color);}
  &--tile {border-radius: 0;}
  &--round {
    border-radius: 99em;
    padding: var(--w-base-increment) calc(var(--w-base-increment) * 2.5);
  }
  &--shadow {box-shadow: var(--w-box-shadow);}

  &--top {margin-top: calc(var(--w-base-increment) * -3);}
  &--bottom {margin-top: calc(var(--w-base-increment) * 3);}
  &--left {margin-left: calc(var(--w-base-increment) * -3);}
  &--right {margin-left: calc(var(--w-base-increment) * 3);}

  &--arrow {
    &.w-menu--top {margin-top: calc(var(--w-base-increment) * -4);}
    &.w-menu--bottom {margin-top: calc(var(--w-base-increment) * 4);}
    &.w-menu--left {margin-left: calc(var(--w-base-increment) * -4);}
    &.w-menu--right {margin-left: calc(var(--w-base-increment) * 4);}

    // Menu without border.
    &.w-menu--no-border {
      @include triangle(var(--w-menu-bg-color), '.w-menu', 9px, 0);
    }

    // Menu with border.
    &:not(.w-menu--no-border) {
      @include triangle(var(--w-menu-bg-color), '.w-menu', 9px);
    }
}
}
</style>
