/**
 * This file contains all the startup tasks that are specific to running a window within
 * Finsemble under FEA (as opposed to a freestanding app).
 */
import { System } from "../../common/system";
import { FEAGlobals } from "../../common/Globals";
import { RouterClient } from "../routerClient";
import { getControllerInstance } from "../windowClient";
import { ConfigClient } from "../configClient";
import { Logger } from "../logger";
import SystemManagerClient from "../../common/systemManagerClient";
import { NotificationClient } from "../notificationClient";
import { actions, emptyStore } from "../../platform/services/window/modules/actions";
import { getRemoteStore } from "../../common/redux/getRemoteStore";
import debounce from "lodash/debounce";
import AppsClient from "../appsClient";
import { WINDOWSTATE } from "../../common/constants";
/**
 * This code sends document.title changes (and initial value) off to the window service.
 * The window service may then decide to use this new value for display in the windowTitleBar
 * (e.g. unless overridden in app config)
 *
 * We use a MutationObserver to watch the title tag for changes. If the title tag doesn't exist
 * then we use another MutationObserver to watch the document head until the title does exist.
 *
 * Note that calling document.title does update the title tag. It will also create the tag if one doesn't exist, and that
 * tag will always be in the head.
 *
 * This code will pick up an initial title tag anywhere, including the body (where many apps keep their title) but
 * it will not pick up the case where a tag is dynamically added to the body.
 *
 * Note that Electron's "page-title-updated" event is not reliable. It doesn't update in WebContents (BrowserViews)
 * and is reliant on Chromium's underlying WebObserver which has documented bugs that seem to affect Electron.
 */
const monitorDocumentTitleChanges = () => {
    let previousTitle = null;
    if (!System.isTitlebarWindow()) {
        const store = getRemoteStore({
            server: "@@finsemble/remoteRedux/WindowServiceStore",
            slice: "windowTitleBar",
            emptyStore,
            dispatchOnly: true,
        });
        // Adds the <title> observer, once we know for sure that a title tag exists
        const addTitleObserver = (titleTag) => {
            // Don't update any more frequently than every 250 milliseconds, and only if the title has actually changed
            const observer = new MutationObserver(debounce(() => {
                if (document.title !== previousTitle) {
                    previousTitle = document.title;
                    store.dispatch(actions.setDocumentTitle({
                        title: document.title,
                    }));
                }
            }, 250));
            observer.observe(titleTag, { attributes: false, childList: true, subtree: false });
        };
        // Check to see if the title tag exists, and if so then shoot off an initial value and set up the observer
        const maybeAddTitleObserver = () => {
            const titleTag = document.getElementsByTagName("title")[0];
            if (titleTag) {
                store.dispatch(actions.setDocumentTitle({
                    title: document.title,
                }));
                addTitleObserver(titleTag);
                return true;
            }
            return false;
        };
        // Observes the document.head until a document.title exists. Then it starts up the title observer and
        // shuts itself down
        const waitForTitleTagToExist = () => {
            const observer = new MutationObserver(() => {
                if (document.title) {
                    maybeAddTitleObserver();
                    observer.disconnect();
                }
            });
            observer.observe(document.head, { attributes: false, childList: true, subtree: false });
        };
        if (!maybeAddTitleObserver())
            waitForTitleTagToExist();
    }
};
/**
 * Checks the persistURL config option which indicates whether to save or not the URL
 */
const shouldUpdateURL = () => { var _a, _b, _c, _d, _e, _f; return !!((_f = (_e = (_d = (_c = (_b = (_a = FEAGlobals.finsembleWindow) === null || _a === void 0 ? void 0 : _a.windowOptions) === null || _b === void 0 ? void 0 : _b.customData) === null || _c === void 0 ? void 0 : _c.foreign) === null || _d === void 0 ? void 0 : _d.services) === null || _e === void 0 ? void 0 : _e.workspaceService) === null || _f === void 0 ? void 0 : _f.persistURL); };
/**
 * Listens for changes in the hash and persists the change to the url property, and then saves it.
 */
const listenForHashChanges = () => {
    var _a;
    // we should only do this in the view or in the window if it has no views
    if (System.isTitlebarWindow()) {
        return;
    }
    const myWindow = (_a = window.top) !== null && _a !== void 0 ? _a : window;
    const maybeUpdateURL = () => {
        if (shouldUpdateURL()) {
            const url = myWindow.location.href;
            FEAGlobals.finsembleWindow.updateOptions({ url });
        }
    };
    // get url on page load.
    maybeUpdateURL();
    // There's no pushState event in the browser. This is a monkey patched solution that allows us to catch hash
    // changes. onhashchange doesn't fire when a site is loaded with a hash (e.g., salesforce).
    (function (history) {
        const { pushState } = history;
        history.pushState = function (state) {
            if (typeof history.onpushstate === "function") {
                history.onpushstate({ state });
            }
            pushState.apply(history, arguments);
            maybeUpdateURL();
        };
        const { replaceState } = history;
        history.replaceState = function (state) {
            if (typeof history.onreplacestate === "function") {
                history.onreplacestate({ state });
            }
            replaceState.apply(history, arguments);
            maybeUpdateURL();
        };
    })(window.history);
    window.addEventListener("hashchange", () => {
        maybeUpdateURL();
    });
};
/**
 * Prevents the browser's default behavior of loading files/images if they're dropped anywhere in the window.
 * If a component has a drop area that _doesn't_ preventDefault, the image/file will still be loaded.
 * This only prevents bad behavior from happening when the user drops an image/file on part of the window that _isn't_ listening for drag/drop events (usually by accident).
 */
const preventUnintendedDropEvents = () => {
    const preventDefault = (e) => {
        e.preventDefault();
    };
    window.addEventListener("dragover", preventDefault, false);
    window.addEventListener("drop", preventDefault, false);
};
/**
 * Checks if a component is resizable
 * @returns {Boolean} resizable setting
 */
const isResizable = async () => new Promise((res) => {
    System.Window.getCurrent().getOptions((opt) => {
        if (opt.resizable !== undefined)
            res(opt.resizable);
        else
            res(true);
    });
});
/**
 * If the user presses windows key + left or right it causes all kinds of abhorrent behavior.
 * This function captures these hotkeys and essentially prevents the behavior.
 */
const maybeRejectWindowsKeyResizes = async () => {
    // Get the 'enableWindowsAeroSnap' variable from the window service config
    const { err, data } = await ConfigClient.get([
        "finsemble",
        "servicesConfig",
        "docking",
        "enableWindowsAeroSnap",
    ]);
    if (err) {
        Logger.system.error("maybeRejectWindowsKeyResizes(): Error fetching config", err);
    }
    // If true, will send router messages to have docking respond to windows aero snap.
    // Otherwise, will restore a window's prior state when those aero events occur.
    const enableWindowsAeroSnap = data !== null && data !== void 0 ? data : false;
    // Responds to key events here in order to send router messages and determine whether a system-bounds-changed event should occur.
    // Essentially, this is catching actions to allow Finsemble to respond to windows aero snap functionality.
    // While true resize will be disabled which will prevent disable Aero Snap from working. This flag will only be true while the windows key is down.
    let preventResize = false;
    const resizable = await isResizable();
    let metaIsPressed = false;
    const releaseMetaKey = () => {
        // When Aero Snap is disabled and the windows key released, windows will be able to be resized.
        // Otherwise users would not be able to manually resize windows.
        if (preventResize && resizable) {
            preventResize = false;
            const win = System.Window.getCurrent();
            const isMaximized = FEAGlobals.finsembleWindow.windowState === WINDOWSTATE.MAXIMIZED;
            win.updateOptions({
                resizable: true,
            }, 
            // restore and maximize is a workaround to prevent a case when a maximized window moves a little bit on set resizable call. (KB52816)
            // The Electron thread about the problem: https://github.com/electron/electron/issues/31233
            () => {
                if (isMaximized) {
                    win.restore(() => {
                        // timeout is required because maximize not always work after restore
                        setTimeout(win.maximize, 20);
                    });
                }
            });
        }
        metaIsPressed = false;
    };
    const onKeyUp = (e) => {
        if (e.key !== "Meta")
            return;
        // aero snap enabled case, windows key released
        if (enableWindowsAeroSnap) {
            // If the key being released is the windows key, send a router message to docking
            RouterClient.transmit("Finsemble.WindowService.WindowsKey", "up");
        }
        else {
            releaseMetaKey();
        }
    };
    const onKeyDown = (e) => {
        if (e.key !== "Meta")
            return;
        // aero snap enabled case, windows key down
        if (enableWindowsAeroSnap) {
            // If the key being pressed is the windows key, send a message to docking
            RouterClient.transmit("Finsemble.WindowService.WindowsKey", "down");
        }
        else {
            metaIsPressed = true;
            // When aero snap is disabled and the windows key is down, disable resize on windows to prevent Aero Snap from working.
            if (resizable) {
                preventResize = true;
                System.Window.getCurrent().updateOptions({
                    resizable: false,
                });
            }
        }
    };
    // For Win + D shortcut only keyDown event is fired without keyUp, cause the window is minimized.
    // Restore the correct window state after Win + D. (fix for KB26881)
    FEAGlobals.finsembleWindow.addEventListener("minimized", () => {
        if (metaIsPressed && !enableWindowsAeroSnap) {
            // restore and focus window to handle keyUp event for it
            System.Window.getCurrent().restore();
            System.Window.getCurrent().focus();
        }
    });
    /**
     * The browser's keyDown isn't capable of capturing keyChords if the first key pressed is the windows key.
     */
    window.addEventListener("keyup", onKeyUp);
    window.addEventListener("keydown", onKeyDown);
    window.addEventListener("blur", () => {
        // A workaround to prevent a case when the Win + Down works with disabled aero snap.
        // It happens because the blur event releases the Meta key before the minimized listeners is invoked.
        setTimeout(releaseMetaKey, 500);
    });
};
/**
 * Intercepts CTRL-R within titlebar windows and then causes both the titlebar window and app contents window
 * (browser view) to reload.
 *
 * When users press CTRL-R, they expect their app content to reload. Without this task, that would not occur -
 * only the titlebar window would reload, appearing as if nothing had happened.
 */
const modifyReload = () => {
    if (System.isTitlebarWindow()) {
        // Chromium's built-in reloading for CTRL+R happens before a keypress or keyup event is received,
        // so we must use keydown.
        System.getManifest((manifest) => {
            // If reload window hotkey is disabled, don't add the keydown listener
            if (manifest["finsemble-electron-adapter"].reloadWindowHotkey === false)
                return;
            document.addEventListener("keydown", (e) => {
                // Refresh should only be done if Ctrl and R are pressed, so skip if alt or shift are pressed
                if (e.altKey || e.shiftKey)
                    return;
                if (e.ctrlKey && e.key.toLowerCase() === "r") {
                    e.preventDefault();
                    e.stopPropagation();
                    FEAGlobals.finsembleWindow.reload();
                }
            });
        });
    }
};
/**
 * Add listeners to the window that checks for standard tab navigation shortcuts, and changes the active
 * tab accordingly.
 *
 * Listeners are only added to browserview windows. Injected titlebars implement keyboard navigation
 * in their own way.
 *
 * Supported tab navigation shortcut pairs:
 * - Ctrl+Tab / Ctrl+Shift+Tab
 * - Ctrl+PgUp / Ctrl+PgDn
 * - Cmd+Opt+Right / Cmd+Opt+Left
 */
const handleKeyboardInteractions = () => {
    if (!System.isBrowserView()) {
        return;
    }
    const store = getRemoteStore({
        server: "@@finsemble/remoteRedux/WindowServiceStore",
        slice: "windowTitleBar",
        emptyStore,
        dispatchOnly: true,
    });
    document.addEventListener("keydown", (e) => {
        if (e.ctrlKey) {
            // On Mac or Windows, Ctrl+Tab changes tabs to the right
            // On Windows, Ctrl+PageDown changes tabs to the right
            if ((e.key === "Tab" && !e.shiftKey) || e.key === "PageDown") {
                store.dispatch(actions.shiftPromotedTab(1));
                e.preventDefault();
                // On Mac or Windows, Ctrl+Shift+Tab changes tabs to the left
                // On Windows, Ctrl+PageUp changes tabs to the left
            }
            else if (e.key === "PageUp" || (e.key === "Tab" && e.shiftKey)) {
                store.dispatch(actions.shiftPromotedTab(-1));
                e.preventDefault();
            }
        }
        else if (e.metaKey && e.altKey) {
            // On Mac, Cmd+Opt+Right changes tabs to the right
            if (e.key === "ArrowRight") {
                store.dispatch(actions.shiftPromotedTab(1));
                e.preventDefault();
                // On Mac, Cmd+Opt+Left changes tabs to the left
            }
            else if (e.key === "ArrowLeft") {
                store.dispatch(actions.shiftPromotedTab(-1));
                e.preventDefault();
            }
        }
    });
};
/**
 * Both FinsembleWindow and WindowClient provide a static member that contains system options.
 * These must be initialized on startup.
 *
 * Note: these static members should be deprecated in favor of direct asynchronous calls to FSBL.System.getOptions().
 *
 * @param systemStartupData The initial options fetched from FSBL.System.getOptions()
 *
 */
const setInitialOptions = (systemStartupData) => {
    FEAGlobals.finsembleWindow.windowOptions = systemStartupData !== null && systemStartupData !== void 0 ? systemStartupData : {};
    const controller = getControllerInstance();
    controller.setStartupData(systemStartupData);
};
/**
 * Obtains the initial bounds and then saves them to the workspace
 */
export const cacheInitialBounds = async () => {
    var _a, _b, _c, _d, _e;
    // Don't run this function in the titlebar window because then updateOptions will be called twice
    // for the same overall window
    if (System.isTitlebarWindow())
        return;
    const { err, data: bounds } = await FEAGlobals.finsembleWindow.getBounds();
    if (err || !bounds) {
        console.warn("Get bounds error.", err, "Window may not be registered with the window service");
        return;
    }
    const options = {
        defaultLeft: bounds.left,
        defaultTop: bounds.top,
        defaultWidth: bounds.width,
        defaultHeight: bounds.height,
    };
    // If the url is to be persisted then set it as an option so that the workspace will pick it up for future instantiations
    const { persistWindowState = false } = (_d = (_c = (_b = (_a = FEAGlobals.finsembleWindow.windowOptions) === null || _a === void 0 ? void 0 : _a.customData) === null || _b === void 0 ? void 0 : _b.foreign) === null || _c === void 0 ? void 0 : _c.components) === null || _d === void 0 ? void 0 : _d["Window Manager"];
    const url = persistWindowState ? ((_e = window.top) !== null && _e !== void 0 ? _e : window).location.toString() : undefined;
    if (url && shouldUpdateURL()) {
        options.url = url;
    }
    await FEAGlobals.finsembleWindow.updateOptions({
        options,
    });
};
/**
 * Communicates to the window service when this window is minimized and restored from a minimized state.
 */
const addMinimizedRestoreWatcher = () => {
    FEAGlobals.finsembleWindow.addEventListener("minimized", () => {
        const onMinimizedRestored = () => {
            RouterClient.transmit("DockingService.windowRestored", FEAGlobals.finsembleWindow.name);
            FEAGlobals.finsembleWindow.removeEventListener("restored", onMinimizedRestored);
        };
        RouterClient.query("DockingService.windowMinimized", {
            windowName: FEAGlobals.finsembleWindow.name,
        });
        FEAGlobals.finsembleWindow.addEventListener("restored", onMinimizedRestored);
    });
};
/**
 * The window service expects to receive heartbeats for every window that it spawns.
 */
const startHeartbeats = (windowName) => {
    Logger.system.log(/*'bW9udGVyZXlzIGNvYXN0', */ "startHeartbeats called for ", windowName);
    let counter = 0;
    setInterval(() => {
        const payload = {
            type: "component",
            windowName: windowName,
            componentType: "finsemble",
            isBrowserView: System.isBrowserView(),
        };
        // Here, we log every 10th heartbeat for the first 2 minutes to confirm heartbeats are occurring
        if ((++counter) % 10 === 0 && counter <= 120) {
            Logger.system.log(/*'bW9udGVyZXlzIGNvYXN0', */ "sending heartbeat for ", windowName);
            // Adding in "echo" to the payload so that the window service can log receipt of the heartbeat
            payload["echo"] = true;
            if (counter === 120) {
                Logger.system.log(/*'bW9udGVyZXlzIGNvYXN0', */ "stopping heartbeat logs for ", windowName);
            }
        }
        RouterClient.transmit("Finsemble.heartbeat", payload);
    }, 1000);
};
/**
 * This is only needed for deprecated LauncherClient group stuff which will soon be removed
 */
const getWindowGroups = (finsembleWindow) => {
    // @deprecate all of this code
    // Get Group Updates (only if we are not in a service)
    if (finsembleWindow) {
        // Get Groups from Component State on Load
        const subscribeToGroupUpdates = async () => {
            await SystemManagerClient.waitForStartup("windowService");
            RouterClient.subscribe(`Finsemble.LauncherService.updateGroups.${finsembleWindow.name}`, (err, response) => {
                if (!Array.isArray(response.data))
                    return; // don't attempt to save the initial responder state.
                finsembleWindow.setComponentState({
                    field: "finsemble:windowGroups",
                    value: response.data,
                });
            });
        };
        finsembleWindow.getComponentState({ field: "finsemble:windowGroups" }, (err, groups) => {
            if (!err && groups) {
                return AppsClient.addToGroups({
                    groupNames: groups,
                }, subscribeToGroupUpdates);
            }
            subscribeToGroupUpdates();
        });
    }
};
/**
 * Overrides the browser's window.Notification so that notifications are instead sent
 * through Finsemble's notification system.
 */
const overrideBrowserNotify = async (windowName, myConfig, componentType) => {
    var _a, _b, _c;
    const { data: applyNotificationOverrides } = await ConfigClient.get([
        "finsemble",
        "servicesConfig",
        "notifications",
        "applyNotificationOverrides",
    ]);
    if (!applyNotificationOverrides)
        return;
    const source = (_c = (_b = (_a = myConfig === null || myConfig === void 0 ? void 0 : myConfig.component) === null || _a === void 0 ? void 0 : _a.displayName) !== null && _b !== void 0 ? _b : componentType) !== null && _c !== void 0 ? _c : windowName;
    const notifyOverride = (title, opt) => {
        opt.finsembleSource = source;
        NotificationClient.notify(JSON.parse(JSON.stringify({ title, options: opt })));
    };
    const OldNotify = window.Notification;
    notifyOverride.requestPermission = OldNotify.requestPermission.bind(OldNotify);
    Object.defineProperty(notifyOverride, "permission", {
        get: function () {
            return OldNotify.permission;
        },
    });
    window.Notification = notifyOverride;
};
/**
 * Runs all local client side code necessary to participate in Finsemble's desktop.
 * @todo, remove clients from BaseClient and convert to singletons
 * @todo, move command channel stuff into WindowTitleBar - deprecate
 */
export const windowStartup = async ({ windowName, componentType, finsembleWindow, systemStartupData, }) => {
    Logger.system.log(/*'bW9udGVyZXlzIGNvYXN0', */ "windowStartup called for ", windowName);
    startHeartbeats(windowName);
    getWindowGroups(finsembleWindow);
    monitorDocumentTitleChanges();
    listenForHashChanges();
    preventUnintendedDropEvents();
    maybeRejectWindowsKeyResizes();
    modifyReload();
    setInitialOptions(systemStartupData);
    addMinimizedRestoreWatcher();
    handleKeyboardInteractions();
    overrideBrowserNotify(windowName, systemStartupData.customData, componentType);
    await cacheInitialBounds();
};
//# sourceMappingURL=windowStartup.js.map