<template>
  <Teleport to="body">
    <div ref="menuElement" :style="getInlineStyles" :class="classNames">
      <slot></slot>
    </div>
  </Teleport>
</template>

<script setup lang="ts">
import {
  onBeforeUnmount,
  onMounted,
  type Ref,
  ref,
  watch,
  computed,
  type ComputedRef,
} from 'vue';

interface ElementXY {
  x: number;
  y: number;
}

const props = withDefaults(
  defineProps<{
    visible: boolean;
    to: HTMLElement | null;
    matchWidth?: boolean;
    x?: string;
    y?: string;
  }>(),
  {
    visible: false,
    matchWidth: false,
    x: 'left',
    y: 'bottom',
  },
);

const elementX: Ref<number> = ref(0);
const elementY: Ref<number> = ref(0);
const menuWidth: Ref<number> = ref(200);
const xPosition: Ref<string> = ref('left');
const yPosition: Ref<string> = ref('bottom');

const classNames: ComputedRef<string> = computed(() => {
  return `ui-menu ${xPosition.value} ${yPosition.value}${
    props.visible && props.to ? ' is-visible' : ''
  }${props.matchWidth ? ' is-matchwidth' : ''}`;
});

const getInlineStyles: ComputedRef<string> = computed(() => {
  return `--ui-menu-x-position: ${elementX.value}px; --ui-menu-y-position: ${
    elementY.value
  }px${props.matchWidth ? `;--ui-menu-width: ${menuWidth.value}px` : ''}`;
});
watch(
  () => props.visible,
  () => {
    if (props.to) {
      bindBounding(props.to);
    }
  },
);
watch(
  () => props.to,
  () => {
    if (props.to) {
      bindBounding(props.to);
    }
  },
);

onMounted(() => {
  if (props.to) {
    bindBounding(props.to);
  }

  window.addEventListener('resize', listener);
  window.addEventListener('scroll', listener, true);
});

onBeforeUnmount(() => {
  window.removeEventListener('resize', listener);
  window.removeEventListener('scroll', listener, true);
});

const bindBounding = (element: HTMLElement): void => {
  if (!element || !(element instanceof Element)) {
    return;
  }
  xPosition.value = props.x;
  yPosition.value = props.y;

  const boundingRect: DOMRect = element.getBoundingClientRect();
  const bounding = getBounding(boundingRect);

  setBounding(bounding);
};

const setBounding = ({ x, y }: ElementXY): void => {
  elementX.value = x;
  elementY.value = y;
};
const getBounding = (boundingRect: DOMRect): ElementXY => {
  const x: number =
    xPosition.value === 'left'
      ? boundingRect.x
      : boundingRect.x + (boundingRect.right - boundingRect.left);
  const y: number =
    yPosition.value === 'bottom'
      ? boundingRect.y + (boundingRect.bottom - boundingRect.top)
      : window.innerHeight - boundingRect.y;

  menuWidth.value = boundingRect.right - boundingRect.left;

  return { x, y };
};
const listener = () => {
  if (props.to) {
    bindBounding(props.to);
  }
};
</script>

<style lang="scss" scoped>
.ui-menu {
  --ui-menu-width: auto;

  min-width: 100px;
  max-height: 400px;
  width: var(--ui-menu-width, auto);
  width: 400px;
  position: fixed;
  z-index: var(--bms-z-index-fixed);
  display: block;
  pointer-events: none;
  opacity: 0;
  transition: opacity 0.1s linear;

  &.is-matchwidth {
    max-width: 100%;
  }

  &.is-visible {
    pointer-events: all;
    opacity: 1;
  }

  &.left {
    left: var(--ui-menu-x-position, 0);
  }

  &.bottom {
    top: var(--ui-menu-y-position, 0);
  }

  &.top {
    bottom: var(--ui-menu-y-position, 0);
  }

  &.right {
    right: calc(100% - var(--ui-menu-x-position, 0));
  }
}
</style>
