import { getCurrentScope, type Component, type MaybeRefOrGetter } from "vue";
import { isDefined } from "@/utils/helpers";
import { isClient } from "@/utils/ssr";
import { unrefElement } from "./unrefElement";
import { useDebounce } from "./useDebounce";
import {
    useEventListener,
    type EventListenerOptions,
    type EventTarget,
} from "./useEventListener";

/** Call a function when the scoll reaches the end or the start of an element.
 * This is useful for infinite scroll lists.
 * @param element - The element to listen for scroll events.
 * @param options - Options for the infinite scroll.
 * @param options.onScroll - Function to call on every scroll event.
 * @param options.onEnd - Function to call when the scroll reaches the end.
 * @param options.onStart - Function to call when the scroll reaches the start.
 * @param options.debounce - Debounce time in milliseconds for the function call on scroll events.
 * @returns A function to call to to manually check the scroll position.
 */
export function useScrollEvents(
    element: MaybeRefOrGetter<EventTarget>,
    options: {
        onScroll?: () => void;
        onScrollEnd?: () => void;
        onScrollStart?: () => void;
        debounce?: number;
    },
    listenerOptions?: EventListenerOptions,
): () => void {
    if (!getCurrentScope())
        throw new Error(
            "The 'useScrollEvents' composable should be used inside a current EffectScope.",
        );

    /** debounced checkScroll funciton */
    const debouncedCheckScroll = useDebounce(
        checkScroll,
        options.debounce ?? 100,
    );

    if (isClient)
        useEventListener(
            element,
            "scroll",
            debouncedCheckScroll,
            listenerOptions,
        );

    /** Check if the scroll list inside the dropdown reached the top or it's end. */
    function checkScroll(): void {
        const el = unrefElement(element);
        if (!el) return;
        if (options.onScroll) options.onScroll();
        const { offsetTop, scrollTop, clientHeight, scrollHeight } = el;

        const trashhold = offsetTop;
        if (clientHeight !== scrollHeight) {
            if (
                Math.ceil(scrollTop + clientHeight + trashhold) >= scrollHeight
            ) {
                if (options.onScrollEnd) options.onScrollEnd();
            } else if (scrollTop <= trashhold) {
                if (options.onScrollStart) options.onScrollStart();
            }
        }
    }

    return debouncedCheckScroll;
}

/**
 * Given an element, returns the element who scrolls it.
 */
export function getScrollingParent(target: HTMLElement): HTMLElement | null {
    if (target.style.position === "fixed" || !target)
        return document.documentElement;

    let isScrollingParent = false;
    let nextParent = target.parentElement;

    while (!isScrollingParent && isDefined(nextParent)) {
        if (nextParent === document.documentElement) break;

        const { overflow, overflowY } = getComputedStyle(nextParent);
        const { scrollHeight, clientHeight } = nextParent; // Both rounded by nature

        isScrollingParent =
            /(auto|scroll)/.test(`${overflow}${overflowY}`) &&
            scrollHeight > clientHeight;

        /* ...found it, this one is returned */
        if (isScrollingParent) break;

        /* ...if not check the next one */
        nextParent = nextParent.parentElement;
    }

    return nextParent;
}

/**
 * Ensure a given child element is within the parent's visible scroll area.
 * If the child is not visible, scroll the parent to the child's position.
 */
export function scrollElementInView(
    scrollableParent: MaybeRefOrGetter<HTMLElement | Component | null>,
    childElement: MaybeRefOrGetter<HTMLElement | Component | null>,
): void {
    const parent = unrefElement(scrollableParent);
    const element = unrefElement(childElement);

    if (!parent || !element) return;

    // The 'offsetTop' is the distance from the outer border of the element (including margin)
    // to the top padding edge of the offsetParent, the closest positioned ancestor element.
    // The 'offsetHeight' is the height of an element, including vertical padding and borders, as an integer.
    // The 'scrollTop' is the number of pixels by which an element's content is scrolled from its top edge.
    const { offsetHeight, offsetTop } = element;
    const { offsetHeight: parentOffsetHeight, scrollTop } = parent;

    const isAbove = offsetTop < scrollTop;
    const isBelow = offsetTop + offsetHeight > scrollTop + parentOffsetHeight;

    if (isAbove) {
        parent.scrollTo(0, offsetTop);
    } else if (isBelow) {
        parent.scrollTo(0, offsetTop - parentOffsetHeight + offsetHeight);
    }
}
