import { isElementInViewport, parents } from "@devowl-wp/react-utils";

import { createVisualContainer, hideVisualContainerAndMemoizeDisplayState } from "./createVisualContainer.js";
import { findPreviousVisibleDuplicate } from "./findPreviousVisibleDuplicate.js";
import { findVisualParent } from "./findVisualParent.js";
import {
    isPotentialVisualWithinResponsiveContainer,
    probablyResetParentContainerForVisual,
} from "./probablyResetParentContainer.js";
import {
    HTML_ATTRIBUTE_BLOCKER_CONNECTED,
    HTML_ATTRIBUTE_BLOCKER_CONNECTED_PRESERVED,
    HTML_ATTRIBUTE_CONFIRM,
    HTML_ATTRIBUTE_COOKIE_IDS,
    HTML_ATTRIBUTE_HERO_DIALOG_DEFAULT_OPEN,
    HTML_ATTRIBUTE_VISUAL_FORCE,
    HTML_ATTRIBUTE_VISUAL_PAINT_MODE,
} from "../dom/htmlAttributes.js";

/**
 * Save the identifier for the last clicked content blocker so we can
 * dispatch in our event if it is the clicked one. This is useful e.g.
 * to delegate the click event further and start a video.
 */
let lastClickedConnectedCounter: number = undefined;

type VisualThumbnail = {
    embedId?: string; // Only available if thumbnail got downloaded
    fileMd5?: string; // Only available if thumbnail got downloaded
    url: string;
    title?: string;
    width: number;
    height: number;
    hide?: ("title" | "overlay")[];
    /**
     * Defaults to `top`.
     */
    titleType?: "center" | "top";
    forceRatio?: number;
};

type VisualConfiguration = { shouldForceToShowVisual?: boolean; isVisual: boolean; id: number };

type SetVisualAsLastClickedVisual = (event?: { stopPropagation: () => void }) => void;

type BlockerMountPaintMode = "instantInViewport" | "idleCallback" | "instant" | "interaction";

type BlockerMountCallback<ExtendedBlockerDefinition extends VisualConfiguration = VisualConfiguration> = (data: {
    container: HTMLDivElement;
    blocker: ExtendedBlockerDefinition;
    connectedCounter: number;
    onClick: SetVisualAsLastClickedVisual;
    blockedNode: HTMLElement;
    createBefore: HTMLElement;
    paintMode: BlockerMountPaintMode;
    thumbnail?: VisualThumbnail;
}) => void;

let connectedCounterPreserve = 0;

/**
 * Create visual content blocker for a given node.
 *
 * Returns `undefined` when the container cannot be created.
 */
function createVisual<BlockerConfiguration extends VisualConfiguration>({
    node,
    blocker,
    visualParentSelectors,
    dependantVisibilityContainers,
    disableDeduplicateExceptions,
    mount,
}: {
    node: HTMLElement;
    blocker: BlockerConfiguration;
    visualParentSelectors?: Parameters<typeof findVisualParent>[1];
    /**
     * Before trying to create a visual content blocker, check if the node is inside a given container and
     * if it is, wait until this container is visible. For example, you are providing a sidebar with blocked
     * content, you need to pass the selector for this sidebar.
     */
    dependantVisibilityContainers?: string[];
    /**
     * A list of selectors for which we disable the deduplication exceptions. When the selector matches the element
     * where we place the visual content blocker before, we do not check for exceptions.
     *
     * See `findPreviousVisibleDuplicate` for more information about exceptions.
     */
    disableDeduplicateExceptions?: string[];
    mount: BlockerMountCallback<BlockerConfiguration>;
}) {
    // Short circuit: We do not know a blocker for this; do nothing
    if (!blocker) {
        return undefined;
    }

    // Apply a unique identifier for this node
    if (!node.hasAttribute(HTML_ATTRIBUTE_BLOCKER_CONNECTED_PRESERVED)) {
        node.setAttribute(HTML_ATTRIBUTE_BLOCKER_CONNECTED_PRESERVED, connectedCounterPreserve.toString());
        connectedCounterPreserve++;
    }
    const connectedCounter = +node.getAttribute(HTML_ATTRIBUTE_BLOCKER_CONNECTED_PRESERVED);

    const { parentElement } = node;
    const isClickGateway = node.hasAttribute(HTML_ATTRIBUTE_CONFIRM);
    const { shouldForceToShowVisual: shouldForceToShowVisualBlocker = false, isVisual, id: blockerId } = blocker;
    const shouldForceToShowVisual = shouldForceToShowVisualBlocker || node.hasAttribute(HTML_ATTRIBUTE_VISUAL_FORCE);

    let position: HTMLElement["style"]["position"] = "initial";
    try {
        const computedStyle = window.getComputedStyle(node);
        ({ position } = computedStyle);
    } catch (e) {
        // Silence is golden.
    }

    // Visual content blockers are only allowed for "real" content
    const isPositioned = ["fixed", "absolute", "sticky"].indexOf(position) > -1;
    const isNotInContent = [document.body, document.head, document.querySelector("html")].indexOf(parentElement) > -1;
    const isAlreadyConnected = node.getAttribute(HTML_ATTRIBUTE_BLOCKER_CONNECTED);
    const [createBefore, visualParentMode, isVisualParentFound] = findVisualParent(node, visualParentSelectors || {});
    // const isInlineScriptOrStyle = createBefore.hasAttribute(HTML_ATTRIBUTE_INLINE) || createBefore.hasAttribute(HTML_ATTRIBUTE_INLINE_STYLE);
    const isVisible = !!createBefore.offsetParent;

    // Always hide elements (again) if no visual blocker is possible (get) and memorize
    // the "!important" state so we can correctly reset it in `probablyResetParentContainerForVisual`
    const fnStrictHide = (reason: "qualified" | "duplicate") => {
        if (["script", "link"].indexOf(node?.tagName.toLowerCase()) === -1 && !isClickGateway) {
            if (reason === "qualified" && visualParentMode === "childrenSelector") {
                return;
            }

            hideVisualContainerAndMemoizeDisplayState(node);
        }
    };

    if (
        isAlreadyConnected ||
        isNotInContent ||
        (isPositioned && !isPotentialVisualWithinResponsiveContainer(node) && !shouldForceToShowVisual) ||
        !isVisual ||
        (!isVisible && !shouldForceToShowVisual)
    ) {
        // Check if this content blocker is inside a known container for which we need to wait first its visibility
        if (!isVisible && dependantVisibilityContainers) {
            const dependantContainers = parents(node, dependantVisibilityContainers.join(","), 1);
            if (dependantContainers.length > 0 && !dependantContainers[0].offsetParent) {
                // Wait for visibility, do nothing with this node...
                return undefined;
            }
        }

        fnStrictHide("qualified");
        return undefined;
    }

    // Avoid duplicate visual blockers and connect them
    if (!node.hasAttribute(HTML_ATTRIBUTE_CONFIRM) && !parents(node, ".rcb-avoid-deduplication", 1).length) {
        const foundDuplicate = findPreviousVisibleDuplicate(
            createBefore,
            node,
            blockerId,
            disableDeduplicateExceptions?.length ? createBefore.matches(disableDeduplicateExceptions.join(",")) : false,
        );
        if (foundDuplicate) {
            node.setAttribute(
                HTML_ATTRIBUTE_BLOCKER_CONNECTED,
                foundDuplicate.getAttribute(HTML_ATTRIBUTE_BLOCKER_CONNECTED),
            );

            probablyResetParentContainerForVisual(createBefore, true);
            fnStrictHide("duplicate");
            return undefined;
        }
    }

    // If there is a parent confirm() visual content blocker, do not render a visual content blocker inside again
    const confirmParents = parents(node, `[${HTML_ATTRIBUTE_CONFIRM}]`, 1);
    if (confirmParents.length && confirmParents.indexOf(node) === -1) {
        fnStrictHide("duplicate");
        return undefined;
    }

    const { container, thumbnail } = createVisualContainer(node, createBefore, visualParentMode, isVisualParentFound);

    const mountVisualContainer = (paintMode: BlockerMountPaintMode) => {
        container.setAttribute(HTML_ATTRIBUTE_VISUAL_PAINT_MODE, paintMode);
        mount({
            container,
            blocker,
            connectedCounter,
            onClick: (event) => {
                // Stop above propagation so click event handlers do not handle this as a click on the
                // mainly blocked element (example: Video inside another container which delegates click event).
                event?.stopPropagation();
                setLastClickedConnectedCounter(connectedCounter);
            },
            blockedNode: node,
            thumbnail,
            paintMode,
            createBefore,
        });
        probablyResetParentContainerForVisual(createBefore, true);
    };

    // Lazy load the visual container (not depending on view port but do not overload the main thread with idle timing)
    // For click gateways we do not need to check for viewport as the visual content blocker can be rendered lazily
    // as the click gateway is rendered on click over the original, blocked content.
    if (!isClickGateway && isElementInViewport(container, true)) {
        mountVisualContainer("instantInViewport");
    } else if (
        !isClickGateway &&
        // If the previous one was in viewport, consider the next one as "instant" as it could lead
        // to unexpected behavior (e.g. ratio class is used and container is in viewport at the bottom screen
        // but isn't technically due to vertical alignment to bottom).
        (document
            .querySelector(
                `.rcb-content-blocker[${HTML_ATTRIBUTE_BLOCKER_CONNECTED}="${
                    connectedCounter - 1
                }"][${HTML_ATTRIBUTE_VISUAL_PAINT_MODE}]`,
            )
            ?.getAttribute(HTML_ATTRIBUTE_VISUAL_PAINT_MODE) as BlockerMountPaintMode) === "instantInViewport"
    ) {
        mountVisualContainer("instant");
    } else if (isClickGateway) {
        node.setAttribute(HTML_ATTRIBUTE_HERO_DIALOG_DEFAULT_OPEN, "true");
        // Capture on document so this runs before bubble-phase delegates (e.g. vs jQuery's `.on(document, "click", ...)`)
        // and still targets the same subtree as listening on createBefore. Detach only after a click inside
        // createBefore — unlike { once: true } on document, we must not consume the first unrelated click.
        const onClickGatewayDocumentCapture = (e: MouseEvent) => {
            const { target } = e;
            if (target === null || !(target instanceof Node)) {
                return;
            }
            if (!createBefore.contains(target) && target !== createBefore) {
                return;
            }
            document.removeEventListener("click", onClickGatewayDocumentCapture, true);
            if (node.hasAttribute(HTML_ATTRIBUTE_COOKIE_IDS)) {
                mountVisualContainer("interaction");
                e.preventDefault();
                e.stopImmediatePropagation();
            }
        };
        document.addEventListener("click", onClickGatewayDocumentCapture, true);
    } else if ((window as any).requestIdleCallback) {
        (window as any).requestIdleCallback(() => mountVisualContainer("idleCallback"));
    } else {
        mountVisualContainer("instant");
    }

    return container;
}

function getLastClickedConnectedCounter() {
    return lastClickedConnectedCounter;
}

function setLastClickedConnectedCounter(connectedCounter: number) {
    lastClickedConnectedCounter = connectedCounter;
}

export {
    createVisual,
    type VisualConfiguration,
    type VisualThumbnail,
    getLastClickedConnectedCounter,
    setLastClickedConnectedCounter,
    type SetVisualAsLastClickedVisual,
    type BlockerMountCallback,
    type BlockerMountPaintMode,
};
