import fastdom from "fastdom";

import { mapValueSuffixPx } from "./createVarFactory.js";

import type { CreatePureScopedStylesheetCallbackArgs } from "./createStylesheet.js";
import type { VarConsumer } from "./createVarFactory.js";

type ObserveFunction = (element: HTMLElement, dependencies?: HTMLElement[]) => () => void;

type DimsVarConsumer = {
    width: VarConsumer<number, boolean>;
    height: VarConsumer<number, boolean>;
    scrollbar: VarConsumer<boolean, boolean>;
    scrolledTop: VarConsumer<boolean, boolean>;
    scrolledBottom: VarConsumer<boolean, boolean>;
};

const createRenderedDimensionVariables = <
    PassedVarsFn extends CreatePureScopedStylesheetCallbackArgs["vars"] = undefined,
>(
    varName: CreatePureScopedStylesheetCallbackArgs["varName"],
    /**
     * If set, the dimension variables are not directly set to the passed observed element,
     * instead it will be handled like a usual variable for the stylesheet.
     */
    vars?: PassedVarsFn,
): PassedVarsFn extends undefined
    ? [
          ObserveFunction,
          {
              width: string;
              height: string;
          },
          () => void,
      ]
    : [ObserveFunction, DimsVarConsumer, () => void] => {
    let varWidth: string;
    let varHeight: string;
    let variableConsumer: [
        DimsVarConsumer,
        (
            value: Partial<{
                width: number;
                height: number;
                scrollbar: boolean;
                scrolledTop: boolean;
                scrolledBottom: boolean;
            }>,
        ) => string,
        any,
    ];

    if (vars) {
        variableConsumer = vars(
            {
                width: 0,
                height: 0,
                scrollbar: false as boolean,
                scrolledTop: false as boolean,
                scrolledBottom: false as boolean,
            },
            {
                width: mapValueSuffixPx,
                height: mapValueSuffixPx,
            },
        );
    } else {
        varWidth = varName("width");
        varHeight = varName("height");
    }

    const disposeFns: Array<() => void> = [];
    const disconnect = () =>
        disposeFns.forEach((fn, index, arr) => {
            fn();
            arr.splice(index, 1);
        });

    const observe: ObserveFunction = (element: HTMLElement, dependencies = []) => {
        let lastCreatedTask: ReturnType<typeof setTimeout>;
        const recalculate = () => {
            if (!element) {
                return;
            }

            // We do not need to use `fastdom.measure` here because the recalculate function is called on every
            // `animationend` and `ResizeObserver` event. At this time, we can safely assume that the element's
            // bounding client rect is up to date and that this does not cause a reflow / repaint.
            const { width, height } = element.getBoundingClientRect();
            const { clientHeight, scrollHeight, scrollTop, offsetHeight } = element;
            const scrollPuffer = 3;
            const scrollbar = clientHeight < scrollHeight;
            const scrolledBottom = Math.ceil(scrollTop + offsetHeight + scrollPuffer) >= scrollHeight;

            // The element got invisible, do not change anything
            if (width === 0 && height === 0 && !element.offsetParent) {
                return;
            }

            clearTimeout(lastCreatedTask);
            lastCreatedTask = setTimeout(() => {
                if (variableConsumer) {
                    variableConsumer[1]({ width, height, scrollbar, scrolledTop: scrollTop === 0, scrolledBottom });
                } else {
                    fastdom.mutate(() => {
                        element.style.setProperty(varWidth, `${width}px`);
                        element.style.setProperty(varHeight, `${height}px`);
                    });
                }
            }, 0);
        };

        for (const e of [element, ...dependencies]) {
            if (!e) {
                continue;
            }

            // Support animations
            e.addEventListener("animationend", recalculate);
            e.addEventListener("scroll", recalculate);
            const ro = new ResizeObserver(recalculate);
            ro.observe(e);

            disposeFns.push(() => {
                ro.disconnect();
                e.removeEventListener("animationend", recalculate);
                e.removeEventListener("scroll", recalculate);
            });
        }

        return disconnect;
    };

    return (
        vars
            ? [observe, variableConsumer[0], disconnect]
            : [
                  observe,
                  {
                      width: varWidth,
                      height: varHeight,
                  },
                  disconnect,
              ]
    ) as any;
};

export { createRenderedDimensionVariables };
