export const $$ = {
    on(root, eventName, selector, fn) {
        root.removeEventListener(eventName, fn);
        root.addEventListener(eventName, (event) => {
            const target = event.target.closest(selector);
            if (root.contains(target)) {
                fn.call(target, event);
            }
        });
    },
    /**
     * This is a helper function to attach a handler for a `djdt.panel.render`
     * event of a specific panel.
     *
     * root: The container element that the listener should be attached to.
     * panelId: The Id of the panel.
     * fn: A function to execute when the event is triggered.
     */
    onPanelRender(root, panelId, fn) {
        root.addEventListener("djdt.panel.render", (event) => {
            if (event.detail.panelId === panelId) {
                fn.call(event);
            }
        });
    },
    show(element) {
        element.classList.remove("djdt-hidden");
    },
    hide(element) {
        element.classList.add("djdt-hidden");
    },
    toggle(element, value) {
        if (value) {
            $$.show(element);
        } else {
            $$.hide(element);
        }
    },
    visible(element) {
        return !element.classList.contains("djdt-hidden");
    },
    executeScripts(scripts) {
        for (const script of scripts) {
            const el = document.createElement("script");
            el.type = "module";
            el.src = script;
            el.async = true;
            document.head.appendChild(el);
        }
    },
    /**
     * Given a container element, apply styles set via data-djdt-styles attribute.
     * The format is data-djdt-styles="styleName1:value;styleName2:value2"
     * The style names should use the CSSStyleDeclaration camel cased names.
     */
    applyStyles(container) {
        for (const element of container.querySelectorAll(
            "[data-djdt-styles]"
        )) {
            const styles = element.dataset.djdtStyles || "";
            for (const styleText of styles.split(";")) {
                const styleKeyPair = styleText.split(":");
                if (styleKeyPair.length === 2) {
                    const name = styleKeyPair[0].trim();
                    const value = styleKeyPair[1].trim();
                    element.style[name] = value;
                }
            }
        }
    },
};

/**
 * Fetch the debug element from the DOM.
 *
 * This is used to avoid writing the element's id everywhere the element
 * is being selected. A fixed reference to the element should be avoided
 * because the entire DOM could be reloaded such as via HTMX boosting.
 */
export function getDebugElement() {
    let root = document.getElementById("djDebugRoot");
    if (root.shadowRoot) {
        root = root.shadowRoot;
    }
    return root.querySelector("#djDebug");
}

export async function ajax(url, init) {
    try {
        const response = await fetch(url, {
            credentials: "same-origin",
            ...init,
        });
        if (response.ok) {
            try {
                return response.json();
            } catch (error) {
                throw new Error(
                    `The response is a invalid Json object : ${error}`
                );
            }
        }
        throw new Error(`${response.status}: ${response.statusText}`);
    } catch (error) {
        const win = getDebugElement().querySelector("#djDebugWindow");
        win.innerHTML = `<div class="djDebugPanelTitle"><h3>${error.message}</h3><button type="button" class="djDebugClose">»</button></div>`;
        $$.show(win);
        throw error;
    }
}

export function ajaxForm(element) {
    const form = element.closest("form");
    const url = new URL(form.action);
    const formData = new FormData(form);
    for (const [name, value] of formData.entries()) {
        url.searchParams.append(name, value);
    }
    const ajaxData = {
        method: form.method.toUpperCase(),
    };
    return ajax(url, ajaxData);
}

export function replaceToolbarState(newRequestId, data) {
    const djDebug = getDebugElement();
    djDebug.setAttribute("data-request-id", newRequestId);
    // Check if response is empty, it could be due to an expired requestId.
    for (const panelId of Object.keys(data)) {
        const panel = djDebug.querySelector(`#${panelId}`);
        if (panel) {
            panel.outerHTML = data[panelId].content;
            djDebug.querySelector(`#djdt-${panelId}`).outerHTML =
                data[panelId].button;
        }
    }
}

/**
 * Return function that delays invoking `func` until after `timeout` elapsed.
 *
 * Previous calls will be dismissed if the timeout hasn't elapsed.
 *
 * @param {Function} func - Function to be executed.
 * @param {number} timeout - Time to wait before executing function in milliseconds.
 * @returns {Function} - Debounced function.
 */
export function debounce(func, timeout) {
    let timer;
    return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => func(...args), timeout);
    };
}
