import { dispatchInitiatorExecution } from "./dispatchInitiatorExecution.js";
import { MEMORIZE_NATIVE_EVENT_PROPERTY } from "./memorizeNativeEvent.js";
import { isCurrentlyInTransaction } from "../../checker/transaction.js";
import { OPT_IN_CONTENT_BLOCKER_ALL } from "../../events/optInContentBlockerAll.js";

import type { InitiatorNativeEventDetails } from "../../events/initiatorExecution.js";
import type { OptInContentBlockerAllEvent } from "../../events/optInContentBlockerAll.js";

const OVERWRITE_PROPERTY = "rcbNativeEventListener";

/**
 * Overwrite `window.addEventListener('load')` as they can not be triggered by our script blocker.
 * This can also be used for other known events.
 */
function applyNativeEventListenerInitiator(
    element: { addEventListener: Window["addEventListener"] },
    eventName: string,
    {
        onBeforeExecute,
        isLoad,
        definePropertySetter,
    }: {
        onBeforeExecute?: () => void;
        /**
         * Use this if the event behaves like the `load` event and we need to wait until
         * all the page resources are loaded (even `<script async` scripts).
         */
        isLoad?: boolean;
        /**
         * Pass e.g. `onload` for `window.onload =` assignments so the passed callback gets
         * redirect to the native event listener.
         */
        definePropertySetter?: string;
    } = {
        onBeforeExecute: undefined,
        isLoad: false,
    },
) {
    const overwriteProp = `${OVERWRITE_PROPERTY}_${eventName}`;
    const memorizeProp = `${MEMORIZE_NATIVE_EVENT_PROPERTY}_${eventName}`;

    // Only overwrite once
    if (element[overwriteProp]) {
        return;
    }

    const { addEventListener } = element;

    if (definePropertySetter) {
        try {
            Object.defineProperty(element, definePropertySetter, {
                set: function (newValue) {
                    if (typeof newValue === "function") {
                        element.addEventListener(eventName, newValue);
                    }
                },
                enumerable: true,
                configurable: true,
            });
        } catch (e) {
            // Silence is golden
        }
    }

    Object.assign(element, {
        [overwriteProp]: true,
        addEventListener: function (type: any, ...rest: any[]) {
            if (type === eventName) {
                // Redirect to own thread to avoid variable order lacks (e. g. Uncode Gmaps Integration, Contact Form 7
                const executeHandle = () =>
                    setTimeout(() => {
                        const afterExecution = dispatchInitiatorExecution<InitiatorNativeEventDetails>({
                            type: "nativeEvent",
                            eventName,
                        });
                        onBeforeExecute?.();
                        rest[0]?.(
                            new Event(eventName, {
                                bubbles: true,
                                cancelable: true,
                            }),
                        );
                        afterExecution();
                    }, 0);

                if (isCurrentlyInTransaction()) {
                    const memorizeExecutionPromise = element[memorizeProp];
                    document.addEventListener(
                        OPT_IN_CONTENT_BLOCKER_ALL,
                        (({ detail: { load } }: CustomEvent<OptInContentBlockerAllEvent>) => {
                            if (memorizeExecutionPromise) {
                                memorizeExecutionPromise.then(executeHandle);
                            } else if (isLoad) {
                                load.then(executeHandle);
                            } else {
                                executeHandle();
                            }
                        }) as any,
                        {
                            once: true,
                        },
                    );
                } else {
                    executeHandle();
                }
            } else {
                addEventListener.apply(this, [type, ...rest] as any);
            }
        },
    });
}

export { applyNativeEventListenerInitiator };
