const helperCreateResolvablePromise = () => {
    let resolve: () => void;
    const promise = new Promise<void>((r) => (resolve = r));
    return [false, promise, resolve] as [boolean, Promise<void>, () => void];
};

const memorizedReadyStatePromises: Record<DocumentReadyState, ReturnType<typeof helperCreateResolvablePromise>> = {
    loading: helperCreateResolvablePromise(),
    complete: helperCreateResolvablePromise(),
    interactive: helperCreateResolvablePromise(),
};

/**
 * Instead of checking within our events itself at the time of the event, we memorize the state of resolved and reached
 * ready-states. Why? Some WordPress plugins are modifying the JavaScript execution waterfall (e.g. Delay JS in WP Rocket).
 *
 * @see https://github.com/wp-media/wp-rocket/search?q=readystatechange&type=issues
 * @see https://app.clickup.com/t/2v12m07
 */
const memorizeReadyState = () => {
    const { readyState } = document;
    const [reached, , resolve] = memorizedReadyStatePromises[readyState];
    if (!reached) {
        memorizedReadyStatePromises[readyState][0] = true;
        resolve();

        // Edge case: Already `complete`, but `interactive` was never fulfilled
        const [interactiveReached, , interactiveResolve] = memorizedReadyStatePromises.interactive;
        if (readyState === "complete" && !interactiveReached) {
            memorizedReadyStatePromises.interactive[0] = true;
            interactiveResolve();
        }
    }
};

/**
 * Listen to multiple events. Why? See same reason as in `memorizedReadyStatePromises()`.
 */
const LISTEN_TO_READY_EVENTS = [
    "readystatechange",
    "rocket-readystatechange",
    "DOMContentLoaded",
    "rocket-DOMContentLoaded",
    "rocket-allScriptsLoaded",
];

/**
 * Wait DOM is ready depending on a state.
 *
 * - `interactive`: Defer scripts are executing, but DOM is ready
 * - `complete`: All scripts including deferred ones are executed and DOM is ready
 *
 * Attention: The passed callback as first argument is called synchronously, so when using
 * this directly in your entrypoint, keep in mind that this could lead to a long JavaScript task (Profiling).
 * That means, if you have expensive tasks in your callback which are allowed to run asynchronously, we recommend
 * to use `ready().then(() => {})`.
 *
 * @see https://stackoverflow.com/a/65430734/5506547
 * @see https://stackoverflow.com/a/51980980/5506547
 */
const ready = (fn?: () => void, state: DocumentReadyState = "complete") => {
    return new Promise<void>((resolve) => {
        let executed = false;
        const cb = () => {
            memorizeReadyState();

            if (
                // Only execute once
                executed ||
                // Ready-state not yet reached
                !memorizedReadyStatePromises[state][0]
            ) {
                return;
            }
            executed = true;

            fn?.();

            // We need to make this promise `non-blocking`. E.g. on Safari this
            // could lead to issues as the `readystatechange` event listener
            // can be nested (once `interactive`, then waiting `complete`)
            // Imagine, you are using an `await ready` within a `ready(fn)` statement.
            setTimeout(resolve, 0);
        };

        cb();
        for (const event of LISTEN_TO_READY_EVENTS) {
            document.addEventListener(event, cb);
        }

        memorizedReadyStatePromises[state][1].then(cb);
    });
};

export { ready, LISTEN_TO_READY_EVENTS };
