<template>
    <span
        ref="reference"
        @mouseenter="show"
        @mouseleave="hide"
    ><slot></slot><span
        v-if="visible && hasContent"
        ref="floating"
        class="popover"
        :style="floatingStyles"
    ><span
        ref="arrow"
        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></span></span>
</template>

<script lang="ts">
import type { PropType } from 'vue';
import { arrow, autoUpdate, computePosition, flip, offset, shift, size } from '@floating-ui/dom';
import Vue from 'vue';
import type { CSSProperties } from 'vue/types/jsx';
import type { ComputePositionReturn, Placement } from '@floating-ui/dom';

const getDPR = (element: Element): number => {
    if (typeof window === 'undefined') {
        return 1;
    }
    const win = element.ownerDocument.defaultView || window;
    return win.devicePixelRatio || 1;
};

const roundByDPR = (element: Element, value: number): number => {
    const dpr = getDPR(element);
    return Math.round(value * dpr) / dpr;
};

const remToPx = (valueInRem: number): number => {
    return valueInRem * parseFloat(getComputedStyle(document.documentElement).fontSize);
};

interface Data {
    visible: boolean;
    position: ComputePositionReturn | null;
    cleanup: (() => void) | null;
}

interface Refs {
    reference: HTMLSpanElement | undefined;
    floating: HTMLSpanElement | undefined;
    content: HTMLSpanElement | undefined;
    arrow: HTMLSpanElement | undefined;
}

export default Vue.extend({
    props: {
        placement: { default: 'bottom', type: String as PropType<Placement> },
        resize: { type: Boolean },
    },
    data(): Data {
        return {
            visible: false,
            position: null,
            cleanup: null,
        };
    },
    computed: {
        $tRefs(): Refs {
            return this.$refs as unknown as Refs;
        },
        hasContent(): boolean {
            return !!this.$slots.content?.[0];
        },
        floatingStyles(): CSSProperties | undefined {
            if (!this.position || !this.$tRefs.floating) {
                return;
            }
            const xVal = roundByDPR(this.$tRefs.floating, this.position.x);
            const yVal = roundByDPR(this.$tRefs.floating, this.position.y);
            return {
                transform: `translate(${xVal}px, ${yVal}px)`,
                ...getDPR(this.$tRefs.floating) >= 1.5 && {
                    willChange: 'transform',
                },
            };
        },
        arrowStyles(): CSSProperties | undefined {
            if (!this.position) {
                return;
            }

            const arrowData = this.position.middlewareData.arrow;

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

            return {
                left: typeof arrowData?.x === 'number' ? `${arrowData.x}px` : '',
                top: typeof arrowData?.y === 'number' ? `${arrowData.y}px` : '',
                [staticSite]: '-0.25rem',
            };
        },
    },
    beforeDestroy() {
        this.hide();
    },
    methods: {
        async show() {
            this.visible = true;
            // floating element will be rendered on next tick
            await this.$nextTick();
            if (!this.$tRefs.reference || !this.$tRefs.floating) {
                return;
            }
            // remove title from reference element to prevent a double tooltip
            this.$tRefs.reference.removeAttribute('title');
            this.cleanup = autoUpdate(this.$tRefs.reference, this.$tRefs.floating, () => this.update());
        },
        hide() {
            this.visible = false;
            if (this.cleanup) {
                this.cleanup();
                this.cleanup = null;
            }
        },
        async update() {
            if (!this.$tRefs.reference || !this.$tRefs.floating || !this.$tRefs.arrow) {
                return;
            }
            const middleware = [offset(remToPx(0.25)), flip(), shift()];
            if (this.resize) {
                middleware.push(size({
                    apply: ({ availableWidth, availableHeight }) => {
                        if (!this.$tRefs.content) {
                            return;
                        }
                        Object.assign(this.$tRefs.content.style, {
                            maxWidth: `${availableWidth}px`,
                            maxHeight: `${availableHeight}px`,
                        });
                    },
                }));
            }
            middleware.push(arrow({ element: this.$tRefs.arrow }));

            this.position = await computePosition(this.$tRefs.reference, this.$tRefs.floating, {
                middleware,
                placement: this.placement as Placement,
            });
        },
    },
});
</script>

<style lang="scss">
.popover {
    position: absolute;
    top: 0;
    /*! rtl:ignore: popovers are always positioned absolutely from the upper-left corner */
    left: 0;
    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>
