<script setup lang="ts">
import { arrow, autoUpdate, flip, shift, size, useFloating } from '@floating-ui/vue';
import type { Placement } from '@floating-ui/vue';
import type { CSSProperties } from 'vue';

const props = withDefaults(defineProps<{
    tag?: string;
    placement?: Placement;
    resize?: boolean;
}>(), {
    tag: 'span',
    placement: 'bottom',
});

const reference = useTemplateRef<HTMLSpanElement>('reference');
const floating = useTemplateRef<HTMLSpanElement>('floating');
const floatingArrow = useTemplateRef<HTMLSpanElement>('floatingArrow');
const content = useTemplateRef<HTMLDivElement>('content');

const middleware = computed(() => {
    const middleware = [flip(), shift()];
    if (props.resize) {
        middleware.push(size({
            apply: ({ availableWidth, availableHeight }) => {
                if (!content.value) {
                    return;
                }
                Object.assign(content.value.style, {
                    maxWidth: `${availableWidth}px`,
                    maxHeight: `${availableHeight}px`,
                });
            },
        }));
    }
    middleware.push(arrow({ element: floatingArrow }));
    return middleware;
});

const { floatingStyles, placement: calculatedPlacement, middlewareData } = useFloating(reference, floating, {
    middleware,
    placement: props.placement,
    whileElementsMounted: autoUpdate,
});

const visible = ref(false);

const slots = defineSlots<{
    default: () => VNode;
    content?: () => VNode[];
}>();
const hasContent = computed((): boolean => {
    return !!slots.content?.()[0];
});

const arrowStyles = computed((): CSSProperties | undefined => {
    const arrowData = middlewareData.value.arrow;

    const staticSite = {
        top: 'bottom',
        right: 'left',
        bottom: 'top',
        left: 'right',
    }[calculatedPlacement.value.split('-')[0]]!;

    return {
        left: typeof arrowData?.x === 'number' ? `${arrowData.x}px` : '',
        top: typeof arrowData?.y === 'number' ? `${arrowData.y}px` : '',
        [staticSite]: 0,
    };
});

const hoverHandle = ref<ReturnType<typeof setTimeout> | null>(null);

const onEnter = () => {
    visible.value = true;
    if (hoverHandle.value) {
        clearTimeout(hoverHandle.value);
        hoverHandle.value = null;
    }
};
const onLeave = () => {
    hoverHandle.value = setTimeout(() => {
        visible.value = false;
        hoverHandle.value = null;
    }, 50);
};

onMounted(() => {
    // remove title from reference element to prevent a double tooltip
    reference.value?.removeAttribute('title');
});
</script>

<template>
    <component
        :is="tag"
        v-bind="$attrs"
        ref="reference"
        @mouseenter="onEnter()"
        @mouseleave="onLeave()"
    >
        <slot></slot>
    </component>
    <Teleport to="#teleports">
        <div
            v-if="visible && hasContent"
            ref="floating"
            class="popover p-1"
            :style="floatingStyles"
            @mouseenter="onEnter()"
            @mouseleave="onLeave()"
        >
            <span
                ref="floatingArrow"
                class="popover-arrow bg-dark text-white"
                :style="arrowStyles"
            ></span>
            <div
                ref="content"
                class="overflow-auto bg-dark text-white px-2 py-1 rounded"
            >
                <slot name="content"></slot>
            </div>
        </div>
    </Teleport>
</template>

<style lang="scss">
.popover {
    font-weight: normal;
    font-style: normal;
    font-size: .85rem;
    z-index: 999;
}
.popover-arrow {
    position: absolute;
    width: 0.5rem;
    height: 0.5rem;
    transform: rotate(45deg);
}
</style>
