import * as ReactDOM from "react-dom";
import { MenuContext } from "./menuContext";
import { calculateDimensions, injectCSS } from "./menuHelpers";
import { applyMenuKeyboardNavigation } from "./keyboardNavigation";
import { MenuAutoResizer } from "./MenuAutoResizer";
import { useState, useEffect, useContext } from "react";
import { useMenu } from "../../hooks/useMenu";
import { FEAGlobals } from "../../../common/Globals";
/**
 * Proxy/fake events are needed because when `document.addEventListener` is used inside a component
 * that is rendered inside react portal, `document` will refer to the toolbar's document instead
 * of the childWindow's document. Clicking inside a portal will not therefore capture those events.
 *
 * This code intercepts these events inside the portal and then forwards them to the parent (toolbar).
 */
const setupEventForwarding = (newWindow) => {
    ["mouseDown", "click", "mouseUp"].forEach((eventName) => {
        newWindow.document.addEventListener(eventName, (event) => {
            document.dispatchEvent(new CustomEvent(`portal:${eventName}`, {
                detail: {
                    // Devs may need access to the x and y of the cursor
                    mouseEvent: new MouseEvent(eventName, event),
                    // And possibly the target element
                    target: event.target,
                },
            }));
        });
    });
    /**
     * Proxies keyboard events from the portal up into the parent window.
     * instead of e.key, you'll need to access e.detail.key.
     */
    ["keyup", "keydown", "keypress"].forEach((eventName) => {
        newWindow === null || newWindow === void 0 ? void 0 : newWindow.document.addEventListener(eventName, (event) => {
            document.dispatchEvent(new CustomEvent(`portal:${eventName}`, {
                detail: new KeyboardEvent(event.type, event),
            }));
        });
    });
};
/**
 * The window (opened by window.open) is an empty html document.
 * This function creates a containerDiv for the parent to insert its
 * React DOM. That containerDiv is set through the `setElement()` method
 * which must be passed in.
 *
 * This function also does portal housekeeping such as transferring CSS and window
 * events.
 */
const insertWindowContent = ({ newWindow, isAutoResizable, className, setElement, menuHeight, }) => {
    // Set the child window's <html> tag to class "menu-portal" for scrollbar styling
    newWindow.document.documentElement.className = "menu-portal";
    if (isAutoResizable)
        newWindow.document.body.setAttribute("resizing", "true");
    // Create the element which will hold our react elements and then append it into the child window's body.
    const containerDiv = document.createElement("div");
    newWindow.document.body.appendChild(containerDiv);
    // The className that was passed to MenuShell is added to the child window's <body>
    className === null || className === void 0 ? void 0 : className.split(" ").forEach((name) => newWindow.document.body.classList.add(name));
    // This will update local state which will cause the parent React Functional Component
    // to return a react portal. See comments at the end of this file.
    setElement(containerDiv);
    // Inject css into the child window's <head>
    injectCSS(newWindow);
    // THE FOLLOWING IS LIKELY DEPRECATED BUT KEEPING JUST IN CASE
    // Place height on the portal.
    // inside of the portal, window.outerHeight === 39 (toolbar height).
    // Knowing the height of the window is important because some menus use that
    // information to force overflow on certain sections of the menu.
    newWindow.portalHeight = menuHeight;
};
/**
 * React hook to create a drop down menu. This hook creates a new window using window.open
 * and assigns it as the childWindow to the MenuPortal. It then uses insertWindowContent()
 * to establish the React/DOM connection with the parent (e.g. Toolbar).
 */
const useCreateDropdown = ({ setElement, childWindow, setChildWindow, }) => {
    const { thisMenuId, menuWidth, menuHeight, className, maxHeight, minHeight } = useContext(MenuContext);
    const { blurMenu } = useMenu();
    const isAutoResizable = !menuHeight;
    /**
     * Initialize the window. This should only run once though it is possible
     * to run multiple times if MenuContext changes.
     */
    useEffect(() => {
        let menuAutoResizer = null;
        /**
         * Initial size and location of the menu is irrelevant because it is hidden.
         * The menu will be resized and positioned correctly when it is shown.
         */
        const iocdFeatures = `mode=frameless,height=1,width=${menuWidth},left=0,top=0,hidden=true`;
        const finsembleFeatures = `height=1,width=${menuWidth},left=0,top=0,opacity=1`;
        const features = window.glue42gd ? iocdFeatures : finsembleFeatures;
        /**
         * Opens the window which will contain the menu contents. This looks like a browser window open
         * but actually opens an Electron window which FEA intercepts. FEA uses the `childWindowOptions`
         * json configuration to determine how to display the window. `childWindowOptions.show` should
         * be set to `false` otherwise a white window will flash prior to the menu being rendered.
         *
         *
         * The "/component/toolbar/menu.html" is just an empty page. It's necessary to reference a real page
         * rather than about:blank in order that the "origin" is correct for the page. Many CSS ui-assets such
         * as fonts and images are set with relative paths that require a correct origin.
         *
         * Note, we append the menu ID for automated testing. This allows Selenium to
         * uniquely identify each menu.
         */
        const newWindow = window.open(`./menu.html#${thisMenuId}`, `(Menu) ${thisMenuId}`, features);
        setChildWindow(newWindow);
        if (newWindow) {
            const onLoad = () => {
                insertWindowContent({ newWindow, isAutoResizable, className, setElement, menuHeight });
                setupEventForwarding(newWindow);
                /* Close window when a user clicks away.
                    Are you wondering why not just close the window without calling
                    toggle? because we want to update the state in the store first
                    which will cause openMenuId to no longer match the shell id
                    and eventually destroy this component which will cause the window
                    to close (see cleanup function).
                */
                newWindow.addEventListener("blur", blurMenu);
                newWindow.addEventListener("keydown", (e) => {
                    if (e.key === "Escape") {
                        blurMenu();
                        FEAGlobals.finsembleWindow.focus();
                    }
                });
                applyMenuKeyboardNavigation(newWindow);
                // Attach MenuAutoResizer only if the menu doesn't have a fixed height
                if (isAutoResizable)
                    menuAutoResizer = new MenuAutoResizer(newWindow, thisMenuId, menuWidth, maxHeight, minHeight);
            };
            newWindow.addEventListener("load", onLoad, true);
        }
        else {
            FEAGlobals.FSBL.Clients.Logger.system.error(`MenuPortal error: window.open returned null for "./menu.html#${thisMenuId}"`);
        }
        return () => {
            // imesner 20210406 childwindow and keyboardNavigation were both null but hadn't closed the window or released global hotkeys
            const win = childWindow || (menuAutoResizer === null || menuAutoResizer === void 0 ? void 0 : menuAutoResizer.childWindow);
            menuAutoResizer === null || menuAutoResizer === void 0 ? void 0 : menuAutoResizer.disconnect();
            win === null || win === void 0 ? void 0 : win.close();
            setChildWindow(null);
        };
    }, [blurMenu, className, menuWidth, menuHeight, maxHeight, thisMenuId]);
};
const showHideResizeWindowIOCD = async ({ childWindow, show, thisMenuId, menuHeight, maxHeight, minHeight, }) => {
    var _a, _b;
    const finWindow = childWindow.fin.desktop.Window.getCurrent();
    const { name } = childWindow;
    if (show) {
        const iop = window.iop;
        const me = iop.windows.my();
        const menu = iop.windows.find(name);
        const { width = 200 } = await menu.getBounds();
        const selector = `#${thisMenuId}-menu-toggle`;
        const menuToggle = document.querySelector(selector);
        if (!menuToggle) {
            console.error(`showHideResizeWindowIOCD() can't find selector "${selector}"`);
            return;
        }
        const displays = await iop.displays.all();
        const primaryDisplay = displays.find((d) => d.isPrimary);
        const myDisplay = await me.getDisplay();
        const targetBounds = menuToggle.getBoundingClientRect();
        const displayBounds = myDisplay.workArea;
        const toolbarBounds = await me.getBounds();
        const scalingFactor = ((_a = myDisplay === null || myDisplay === void 0 ? void 0 : myDisplay.scaleFactor) !== null && _a !== void 0 ? _a : 1) / ((_b = primaryDisplay === null || primaryDisplay === void 0 ? void 0 : primaryDisplay.scaleFactor) !== null && _b !== void 0 ? _b : 1);
        const spaceAboveToolbar = toolbarBounds.top * scalingFactor - displayBounds.top * scalingFactor;
        const spaceBelowToolbar = displayBounds.height +
            displayBounds.top * scalingFactor -
            toolbarBounds.top * scalingFactor -
            toolbarBounds.height;
        const contentHeight = childWindow.document.documentElement.offsetHeight;
        const CHROMIUM_MIN_HEIGHT = minHeight !== null && minHeight !== void 0 ? minHeight : 0;
        const minimumContentHeight = Math.max(contentHeight, CHROMIUM_MIN_HEIGHT);
        let height = menuHeight ? menuHeight : Math.min(minimumContentHeight, maxHeight);
        // Default to putting menu at bottom of toolbar
        let targetLocation = "bottom";
        // If the menu is too big for below, but there's enough space above, then put it above
        if (height > spaceBelowToolbar && height < spaceAboveToolbar) {
            targetLocation = "top";
        }
        // If the menu is too big for either below or above, then put it wherever there is the most
        // room, and shorten the height to fit that space
        if (height > spaceBelowToolbar && height > spaceAboveToolbar) {
            if (spaceAboveToolbar > spaceBelowToolbar) {
                height = spaceAboveToolbar;
                targetLocation = "top";
            }
            else {
                height = spaceBelowToolbar;
            }
        }
        me.showPopup({
            size: {
                height,
                width,
            },
            targetBounds,
            targetLocation,
            windowId: menu.id,
        });
        // Ensures that the menu has keyboard focus for accessibility
        finWindow.focus();
        // Ensures that the menu is positioned above any other windows that have setAlwaysOnTop flagged
        FEAGlobals.FSBL.FinsembleWindow.getInstance({ name }, (e, fsblWindow) => {
            fsblWindow.setAlwaysOnTop({ alwaysOnTop: true });
        });
    }
    else {
        FEAGlobals.FSBL.FinsembleWindow.getInstance({ name }, (e, fsblWindow) => {
            fsblWindow.setAlwaysOnTop({ alwaysOnTop: false });
        });
        finWindow.hide();
    }
};
/**
 * Sets visibility, size and location of the window. This can be called numerous times
 * during the lifetime of the MenuPortal.
 */
const showHideResizeWindow = async ({ childWindow, show, thisMenuId, menuHeight, maxHeight, minHeight, }) => {
    const finWindow = childWindow.fin.desktop.Window.getCurrent();
    if (show) {
        const { left, top, height } = await calculateDimensions(childWindow, thisMenuId, menuHeight, maxHeight, minHeight);
        const params = {
            position: "absolute",
            left: left,
            top: top,
            height: height,
            forceOntoMonitor: "monitorRect",
        };
        const { name } = childWindow;
        /**
         * Use Finsemble to show the window. We might be tempted simply to use childWindow.moveTo() or childWindow.resizeTo()
         * but those commands limit the child window to a single monitor. We want to position the window relative to the
         * monitor on which the toolbar currently resides. showWindow() is monitor aware, so by using monitor="mine" we can
         * ensure that the menu will always open on the same monitor as the toolbar. Since our toolbar has built in logic
         * to avoid straddling, we can be sure that the menu will never have to guess whether it exists on the left or right
         * side of the toolbar. Finally, showWindow() forces windows to open entirely on a monitor, so it will slide the
         * menu to the left or right as necessary to prevent it from straddling.
         */
        await FEAGlobals.FSBL.Clients.LauncherClient.showWindow({ windowName: name }, params);
        // Ensures that the menu has keyboard focus for accessibility
        finWindow.focus();
        // Ensures that the menu is positioned above any other windows that have setAlwaysOnTop flagged
        FEAGlobals.FSBL.FinsembleWindow.getInstance({ name: name }, (e, fsblWindow) => {
            fsblWindow.setAlwaysOnTop({ alwaysOnTop: true });
        });
    }
    else {
        const { name } = childWindow;
        FEAGlobals.FSBL.FinsembleWindow.getInstance({ name: name }, (e, fsblWindow) => {
            fsblWindow.setAlwaysOnTop({ alwaysOnTop: false });
        });
        finWindow.hide();
    }
};
/**
 * Closes the menu portal if the menu button is removed from the toolbar
 *
 * @param {Node} element - HTML element to observe
 *
 */
const closePortal = (element) => {
    const { closeMenu } = useMenu();
    const options = {
        childList: true,
        subtree: true,
    };
    /**
     * MutationObserver interface provides the ability to watch for
     * changes to the DOM tree using the observe method
     */
    const mutationObserver = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
            var _a, _b;
            if (((_a = mutation.removedNodes) === null || _a === void 0 ? void 0 : _a.length) &&
                // prevents menu closing if the favorite apps have been changed
                ((_b = mutation.target) === null || _b === void 0 ? void 0 : _b.id) !== "favoritesContainer") {
                closeMenu();
            }
            // stops the MutationObserver from receiving notification from observer
            mutationObserver.disconnect();
        });
    });
    // watches for DOM changes
    mutationObserver.observe(element, options);
};
export const MenuPortal = ({ show, children }) => {
    const { thisMenuId, menuHeight, maxHeight, minHeight } = useContext(MenuContext);
    // Container div for React portal
    const [element, setElement] = useState();
    const [childWindow, setChildWindow] = useState(null);
    /**
     * Show and hide window. This effect will run whenever the `show` prop changes,
     * generally initiated by a click to <MenuToggle>.
     */
    useEffect(() => {
        // If child window isn't yet initialized then no-op
        if (!childWindow)
            return;
        if (window.glue42gd) {
            showHideResizeWindowIOCD({ childWindow, show, thisMenuId, menuHeight, maxHeight, minHeight });
        }
        else {
            showHideResizeWindow({ childWindow, show, thisMenuId, menuHeight, maxHeight, minHeight });
        }
        return () => { };
    }, [show]);
    // Create the dropdown. (if MenuContext changes then the dropdown will be removed and recreated).
    useCreateDropdown({ childWindow, setElement, setChildWindow });
    const toolbar = document.getElementById("Toolbar-tsx");
    if (toolbar)
        closePortal(toolbar);
    // We should only return a react portal when window is ready
    // and container div appended to its body
    // See Dan abramov comment about this at:
    // https://github.com/facebook/react/issues/12355#issuecomment-410996235
    return element ? ReactDOM.createPortal(children, element) : null;
};
//# sourceMappingURL=MenuPortal.js.map