/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

var gSerialDeviceObserver = {
  _activePortCounts: new WeakMap(),

  observe(subject, topic, _data) {
    if (topic != "serial-device-state-changed") {
      return;
    }

    let props = subject.QueryInterface(Ci.nsIPropertyBag2);
    const browserId = props.getPropertyAsUint64("browserId");
    let bc = BrowsingContext.getCurrentTopByBrowserId(browserId);
    if (!bc) {
      console.warn("BrowsingContext not found for browser ID:", browserId);
      return;
    }
    let browser = bc.embedderElement;
    if (!browser) {
      console.warn("No embedder element for BrowsingContext");
      return;
    }

    let connected = props.getPropertyAsBool("connected");
    let count = this._activePortCounts.get(browser) || 0;
    count = connected ? count + 1 : Math.max(0, count - 1);
    this._activePortCounts.set(browser, count);

    if (gBrowser) {
      gBrowser.updateBrowserSharing(browser, {
        serial: count > 0 ? "serial" : null,
      });
    }
  },

  resetBrowserCount(browser) {
    this._activePortCounts.delete(browser);
  },
};

let _resolveDelayedStartup;
var delayedStartupPromise = new Promise(resolve => {
  _resolveDelayedStartup = resolve;
});

var gBrowserInit = {
  delayedStartupFinished: false,
  domContentLoaded: false,

  _tabToAdopt: undefined,
  _firstContentWindowPaintDeferred: Promise.withResolvers(),
  idleTasksFinished: Promise.withResolvers(),
  _reducedProtectionPrefObserver: null,

  /**
   * Handles considerations when the enabled state of the Translations feature
   * changes during runtime, such as being blocked or re-enabled in the AI Settings.
   */
  _translationsEnabledStateObserver: {
    observe(_subject, topic, data) {
      if (topic !== "translations:enabled-state-changed") {
        console.warn(`received unexpected topic: ${topic}`);
        return;
      }

      // Ensures that the Application Menu correctly includes or omits the translate-page menu item.
      XULBrowserWindow._updateElementsForContentType();

      if (data === "enabled") {
        // When the feature is re-enabled, ensure that actor instances exist for all tabs.
        // This loop is synchronous, but should be lightweight to instantiate only the actor objects.
        // Any expensive work is scheduled asynchronously to the event loop by the actor itself after construction.
        for (const tab of gBrowser.tabs) {
          try {
            const windowGlobal =
              tab.linkedBrowser?.browsingContext?.currentWindowGlobal;

            if (
              !windowGlobal ||
              windowGlobal.isClosed ||
              !windowGlobal.isCurrentGlobal
            ) {
              // This tab's windowGlobal is not relevant to create a Translations actor.
              continue;
            }

            // Ensure that a Translations actor instance is (re)created for each open tab that is allowed to have one.
            windowGlobal.getActor("Translations");
          } catch {
            // Not every tab is allowed to have a Translations actor, which is okay.
            // See ActorManagerParent for the official allow list of URLs.
          }
        }
      }
    },
  },

  _setupFirstContentWindowPaintPromise() {
    let lastTransactionId = window.windowUtils.lastTransactionId;
    let layerTreeListener = () => {
      if (this.getTabToAdopt()) {
        // Need to wait until we finish adopting the tab, or we might end
        // up focusing the initial browser and then losing focus when it
        // gets swapped out for the tab to adopt.
        return;
      }
      removeEventListener("MozLayerTreeReady", layerTreeListener);
      let listener = e => {
        if (e.transactionId > lastTransactionId) {
          window.removeEventListener("MozAfterPaint", listener);
          this._firstContentWindowPaintDeferred.resolve();
        }
      };
      addEventListener("MozAfterPaint", listener);
    };
    addEventListener("MozLayerTreeReady", layerTreeListener);
  },

  getTabToAdopt() {
    if (this._tabToAdopt !== undefined) {
      return this._tabToAdopt;
    }

    if (window.arguments && window.XULElement.isInstance(window.arguments[0])) {
      this._tabToAdopt = window.arguments[0];

      // Clear the reference of the tab being adopted from the arguments.
      window.arguments[0] = null;
    } else {
      // There was no tab to adopt in the arguments, set _tabToAdopt to null
      // to avoid checking it again.
      this._tabToAdopt = null;
    }

    return this._tabToAdopt;
  },

  _clearTabToAdopt() {
    this._tabToAdopt = null;
  },

  // Used to check if the new window is still adopting an existing tab as its first tab
  // (e.g. from the WebExtensions internals).
  isAdoptingTab() {
    return !!this.getTabToAdopt();
  },

  onBeforeInitialXULLayout() {
    this._setupFirstContentWindowPaintPromise();

    updateBookmarkToolbarVisibility();

    // Set a sane starting width/height for all resolutions on new profiles.
    if (ChromeUtils.shouldResistFingerprinting("RoundWindowSize", null)) {
      // When the fingerprinting resistance is enabled, making sure that we don't
      // have a maximum window to interfere with generating rounded window dimensions.
      document.documentElement.setAttribute("sizemode", "normal");
    } else if (!document.documentElement.hasAttribute("width")) {
      const TARGET_WIDTH = 1280;
      const TARGET_HEIGHT = 1040;
      let width = Math.min(screen.availWidth * 0.9, TARGET_WIDTH);
      let height = Math.min(screen.availHeight * 0.9, TARGET_HEIGHT);

      document.documentElement.setAttribute("width", width);
      document.documentElement.setAttribute("height", height);

      if (width < TARGET_WIDTH && height < TARGET_HEIGHT) {
        document.documentElement.setAttribute("sizemode", "maximized");
      }
    }
    {
      const toolbarMenubar = document.getElementById("toolbar-menubar");
      const nativeMenubar = Services.appinfo.nativeMenubar;
      toolbarMenubar.collapsed = nativeMenubar;
      if (nativeMenubar) {
        toolbarMenubar.removeAttribute("autohide");
      } else {
        document.l10n.setAttributes(
          toolbarMenubar,
          "toolbar-context-menu-menu-bar-cmd"
        );
        toolbarMenubar.setAttribute("data-l10n-attrs", "toolbarname");
      }
    }
    // If opening a Taskbar Tab or AI window, add an attribute to the top-level element
    // to inform window styling.
    if (window.arguments?.[1] instanceof Ci.nsIPropertyBag2) {
      let extraOptions = window.arguments[1];
      if (extraOptions.hasKey("taskbartab")) {
        let taskbarTabId = extraOptions.getPropertyAsAString("taskbartab");
        let taskbarTabClass = extraOptions.get("taskbartabclass");
        if (taskbarTabClass) {
          window.document.documentElement.setAttribute(
            "windowclass",
            taskbarTabClass
          );
        }
        window.document.documentElement.setAttribute(
          "taskbartab",
          taskbarTabId
        );
        window.document.documentElement.id = "taskbartab-" + taskbarTabId;
      }
      if (extraOptions.hasKey("ai-window")) {
        document.documentElement.setAttribute("ai-window", true);
      }
      if (extraOptions.hasKey("aiwindow-immersive-view")) {
        document.documentElement.setAttribute("aiwindow-immersive-view", true);
      }
      if (extraOptions.hasKey("aiwindow-new-window")) {
        document.documentElement.setAttribute("aiwindow-new-window", true);
      }
    }

    // Run menubar initialization first, to avoid CustomTitlebar code picking
    // up mutations from it and causing a reflow.
    BrowserUtils.callModulesFromCategory(
      {
        categoryName:
          "browser-window-before-initial-xul-layout-document-preparation",
        jsGlobal: globalThis,
      },
      window
    );

    // Update the customtitlebar attribute so the window can be sized
    // correctly.
    window.TabBarVisibility.update();

    if (
      Services.prefs.getBoolPref(
        "toolkit.legacyUserProfileCustomizations.windowIcon",
        false
      )
    ) {
      document.documentElement.setAttribute("icon", "main-window");
    }

    // The following modules would be initialized:
    // CustomTitlebar, LightweightThemeConsumer, and ToolbarIconColor.
    // ToolbarIconColor.init should be called after we set the attributes, since
    // it might change the toolbars' computed text color.
    BrowserUtils.callModulesFromCategory(
      {
        categoryName: "browser-window-before-initial-xul-layout",
        jsGlobal: globalThis,
      },
      window
    );
  },

  onDOMContentLoaded() {
    // All of this needs setting up before we create the first remote browser.
    window.docShell.treeOwner
      .QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIAppWindow).XULBrowserWindow = window.XULBrowserWindow;
    BrowserUtils.callModulesFromCategory(
      { categoryName: "browser-window-domcontentloaded-before-tabbrowser" },
      window
    );

    // This adds gBrowser to the global scope.
    BrowserUtils.callModulesFromCategory(
      {
        categoryName: "browser-window-domcontentloaded-tabbrowser",
        jsGlobal: globalThis,
      },
      window
    );
    gURLBar.addGBrowserListeners();
    if (Services.prefs.getBoolPref("browser.search.widget.new", false)) {
      document.getElementById("searchbar-new")?.addGBrowserListeners();
    }

    BrowserUtils.callModulesFromCategory(
      {
        categoryName: "browser-window-domcontentloaded",
        jsGlobal: globalThis,
      },
      window
    );

    gURLBar.initPlaceHolder();

    // Hack to ensure that the various initial pages favicon is loaded
    // instantaneously, to avoid flickering and improve perceived performance.
    this._callWithURIToLoad(uriToLoad => {
      let url = URL.parse(uriToLoad);
      if (!url) {
        return;
      }
      let nonQuery = url.URI.prePath + url.pathname;
      if (nonQuery in gPageIcons) {
        gBrowser.setIcon(gBrowser.selectedTab, gPageIcons[nonQuery]);
      }
    });

    updateFxaToolbarMenu(gFxaToolbarEnabled, true);

    updatePrintCommands(gPrintEnabled);

    // Setting the focus will cause a style flush, it's preferable to call anything
    // that will modify the DOM from within this function before this call.
    this._setInitialFocus();

    this.domContentLoaded = true;
  },

  onLoad() {
    gBrowser.addEventListener("DOMUpdateBlockedPopups", e =>
      PopupAndRedirectBlockerObserver.handleEvent(e)
    );
    gBrowser.addEventListener("DOMUpdateBlockedRedirect", e =>
      PopupAndRedirectBlockerObserver.handleEvent(e)
    );
    gBrowser.addEventListener(
      "TranslationsParent:LanguageState",
      FullPageTranslationsPanel
    );
    gBrowser.addEventListener(
      "TranslationsParent:OfferTranslation",
      FullPageTranslationsPanel
    );
    gBrowser.tabContainer.addEventListener("TabSelect", () => {
      FullPageTranslationsPanel.onLocationChange(gBrowser.selectedBrowser);
    });
    gBrowser.addTabsProgressListener(FullPageTranslationsPanel);

    window.addEventListener("AppCommand", HandleAppCommandEvent, true);

    if (!gMultiProcessBrowser) {
      // There is a Content:Click message manually sent from content.
      gBrowser.tabpanels.addEventListener("click", contentAreaClick, {
        capture: true,
        mozSystemGroup: true,
      });
    }

    // hook up UI through progress listener
    gBrowser.addProgressListener(window.XULBrowserWindow);
    gBrowser.addTabsProgressListener(window.TabsProgressListener);

    // TODO bug 2038578: audit these consumers and move any that don't need
    // to run before SessionStore's per-window init to 'browser-window-load'.
    BrowserUtils.callModulesFromCategory(
      {
        categoryName: "browser-window-load-before-sessionstore-init",
        jsGlobal: globalThis,
      },
      window
    );

    // Init the SecurityLevelButton
    SecurityLevelButton.init();

    gTorConnectUrlbarButton.init();
    gTorConnectTitlebarStatus.init();

    // Init the OnionAuthPrompt
    OnionAuthPrompt.init();

    // Init the Onion Location pill
    OnionLocationParent.init(document);

    gTorCircuitPanel.init();

    // Certain kinds of automigration rely on this notification to complete
    // their tasks BEFORE the browser window is shown. SessionStore uses it to
    // restore tabs into windows AFTER important parts like gMultiProcessBrowser
    // have been initialized.
    Services.obs.notifyObservers(window, "browser-window-before-show");

    if (
      !window.toolbar.visible ||
      window.document.documentElement.hasAttribute("taskbartab")
    ) {
      // adjust browser UI for popups
      gURLBar.readOnly = true;
    }

    BrowserUtils.callModulesFromCategory(
      { categoryName: "browser-window-load", jsGlobal: globalThis },
      window
    );

    // Update UI if browser is under remote control.
    gRemoteControl.updateVisualCue();

    // If we are given a tab to swap in, take care of it before first paint to
    // avoid an about:blank flash.
    let tabToAdopt = this.getTabToAdopt();
    if (tabToAdopt) {
      let evt = new CustomEvent("before-initial-tab-adopted", {
        bubbles: true,
      });
      gBrowser.tabpanels.dispatchEvent(evt);

      // Stop the about:blank load
      gBrowser.stop();

      // Remove the speculative focus from the urlbar to let the url be formatted.
      gURLBar.removeAttribute("focused");

      let swapBrowsers = () => {
        if (gBrowser.isTabGroupLabel(tabToAdopt)) {
          // TODO bug 1967937: Merge this case with the tab group case below.
          gBrowser.adoptTabGroup(tabToAdopt.group, { elementIndex: 0 });
          gBrowser.removeTab(gBrowser.selectedTab);
        } else if (gBrowser.isTabGroup(tabToAdopt)) {
          // Via gBrowser.replaceGroupWithWindow
          let tempBlankTab = gBrowser.selectedTab;
          gBrowser.adoptTabGroup(tabToAdopt, { tabIndex: 0, selectTab: true });
          gBrowser.removeTab(tempBlankTab);
          Glean.tabgroup.groupInteractions.move_window.add(1);
        } else if (gBrowser.isSplitViewWrapper(tabToAdopt)) {
          let tempBlankTab = gBrowser.selectedTab;
          let splitview = gBrowser.adoptSplitView(tabToAdopt, {
            elementIndex: 0,
            selectTab: true,
          });
          // If tabs are multiselected, add the newly adopted splitview back into the selection
          if (gBrowser.selectedTabs.length > 1) {
            gBrowser.addRangeToMultiSelectedTabs(
              splitview.tabs[0],
              splitview.tabs[splitview.tabs.length - 1]
            );
          }
          gBrowser.removeTab(tempBlankTab);
        } else {
          if (tabToAdopt.group) {
            Glean.tabgroup.tabInteractions.remove_new_window.add();
          }
          gBrowser.swapBrowsersAndCloseOther(gBrowser.selectedTab, tabToAdopt);
        }

        // Clear the reference to the tab once its adoption has been completed.
        this._clearTabToAdopt();
      };
      if (
        gBrowser.isTab(tabToAdopt) &&
        !tabToAdopt.linkedBrowser.isRemoteBrowser
      ) {
        swapBrowsers();
      } else {
        // For remote browsers, wait for the paint event, otherwise the tabs
        // are not yet ready and focus gets confused because the browser swaps
        // out while tabs are switching.
        addEventListener("MozAfterPaint", swapBrowsers, { once: true });
      }
    }

    // Wait until chrome is painted before executing code not critical to making the window visible
    this._boundDelayedStartup = this._delayedStartup.bind(this);
    window.addEventListener("MozAfterPaint", this._boundDelayedStartup);

    if (!PrivateBrowsingUtils.enabled) {
      document.getElementById("Tools:PrivateBrowsing").hidden = true;
      // Setting disabled doesn't disable the shortcut, so we just remove
      // the keybinding.
      document.getElementById("key_privatebrowsing").remove();
    }

    if (BrowserUIUtils.quitShortcutDisabled) {
      document.getElementById("key_quitApplication").remove();
      document.getElementById("menu_FileQuitItem").removeAttribute("key");

      PanelMultiView.getViewNode(
        document,
        "appMenu-quit-button2"
      )?.removeAttribute("key");
    }

    if (window.browsingContext.isDocumentPiP) {
      for (const cmd of ["Browser:AddBookmarkAs", "Browser:Reload"]) {
        document.getElementById(cmd).setAttribute("disabled", "true");
      }
    }

    this._loadHandled = true;
  },

  _cancelDelayedStartup() {
    window.removeEventListener("MozAfterPaint", this._boundDelayedStartup);
    this._boundDelayedStartup = null;
  },

  _delayedStartup() {
    Glean.browserTimings.startupTimeline.delayedStartupStarted.set(
      Services.telemetry.msSinceProcessStart()
    );

    this._cancelDelayedStartup();

    gBrowser.addEventListener(
      "PermissionStateChange",
      function () {
        gIdentityHandler.refreshIdentityBlock();
        gPermissionPanel.updateSharingIndicator();
      },
      true
    );

    this._handleURIToLoad();

    Services.obs.addObserver(gIdentityHandler, "perm-changed");
    Services.obs.addObserver(gRemoteControl, "devtools-socket");
    Services.obs.addObserver(gRemoteControl, "marionette-listening");
    Services.obs.addObserver(gRemoteControl, "remote-listening");
    Services.obs.addObserver(
      gSessionHistoryObserver,
      "browser:purge-session-history"
    );
    Services.obs.addObserver(
      gStoragePressureObserver,
      "QuotaManager::StoragePressure"
    );
    Services.obs.addObserver(gXPInstallObserver, "addon-install-disabled");
    Services.obs.addObserver(gXPInstallObserver, "addon-install-started");
    Services.obs.addObserver(gXPInstallObserver, "addon-install-blocked");
    Services.obs.addObserver(
      gXPInstallObserver,
      "addon-install-fullscreen-blocked"
    );
    Services.obs.addObserver(
      gXPInstallObserver,
      "addon-install-origin-blocked"
    );
    Services.obs.addObserver(
      gXPInstallObserver,
      "addon-install-policy-blocked"
    );
    Services.obs.addObserver(
      gXPInstallObserver,
      "addon-install-webapi-blocked"
    );
    Services.obs.addObserver(gXPInstallObserver, "addon-install-failed");
    Services.obs.addObserver(gXPInstallObserver, "addon-install-confirmation");
    Services.obs.addObserver(gKeywordURIFixup, "keyword-uri-fixup");
    Services.obs.addObserver(gLocaleChangeObserver, "intl:app-locales-changed");
    TranslationsParent.ensurePrefObservers();
    Services.obs.addObserver(
      this._translationsEnabledStateObserver,
      "translations:enabled-state-changed"
    );
    Services.obs.addObserver(
      gSerialDeviceObserver,
      "serial-device-state-changed"
    );

    BrowserOffline.init();

    BrowserUtils.callModulesFromCategory(
      {
        categoryName: "browser-window-delayed-startup",
        profilerMarker: "delayed-startup-task",
      },
      window
    );

    // Initialize the full zoom setting.
    // We do this before the session restore service gets initialized so we can
    // apply full zoom settings to tabs restored by the session restore service.
    FullZoom.init();
    PanelUI.init(shouldSuppressPopupNotifications);

    UpdateUrlbarSearchSplitterState();

    BookmarkingUI.init();
    gURLBar.delayedStartupInit();
    if (Services.prefs.getBoolPref("browser.search.widget.new", false)) {
      document.getElementById("searchbar-new")?.delayedStartupInit();
    }
    gProtectionsHandler.init();
    gTrustPanelHandler.init();

    let safeMode = document.getElementById("helpSafeMode");
    if (Services.appinfo.inSafeMode) {
      document.l10n.setAttributes(safeMode, "menu-help-exit-troubleshoot-mode");
      safeMode.setAttribute(
        "appmenu-data-l10n-id",
        "appmenu-help-exit-troubleshoot-mode"
      );
    }

    // BiDi UI
    gBidiUI = isBidiEnabled();
    if (gBidiUI) {
      document.getElementById("documentDirection-separator").hidden = false;
      document.getElementById("documentDirection-swap").hidden = false;
      document.getElementById("textfieldDirection-separator").hidden = false;
      document.getElementById("textfieldDirection-swap").hidden = false;
    }

    // Setup click-and-hold gestures access to the session history
    // menus if global click-and-hold isn't turned on
    if (!Services.prefs.getBoolPref("ui.click_hold_context_menus", false)) {
      SetClickAndHoldHandlers();
    }

    function initBackForwardButtonTooltip(tooltipId, l10nId, shortcutId) {
      let shortcut = document.getElementById(shortcutId);
      shortcut = ShortcutUtils.prettifyShortcut(shortcut);

      let tooltip = document.getElementById(tooltipId);
      document.l10n.setAttributes(tooltip, l10nId, { shortcut });
    }

    initBackForwardButtonTooltip(
      "back-button-tooltip-description",
      "navbar-tooltip-back-2",
      "goBackKb"
    );

    initBackForwardButtonTooltip(
      "forward-button-tooltip-description",
      "navbar-tooltip-forward-2",
      "goForwardKb"
    );

    PlacesToolbarHelper.init();

    ctrlTab.readPref();
    Services.prefs.addObserver(ctrlTab.prefName, ctrlTab);

    ReducedProtectionNotification.observePref();

    // The object handling the downloads indicator is initialized here in the
    // delayed startup function, but the actual indicator element is not loaded
    // unless there are downloads to be displayed.
    DownloadsButton.initializeIndicator();

    if (AppConstants.platform != "macosx") {
      updateEditUIVisibility();
      let placesContext = document.getElementById("placesContext");
      placesContext.addEventListener("popupshowing", updateEditUIVisibility);
      placesContext.addEventListener("popuphiding", updateEditUIVisibility);
    }

    FullScreen.init();

    if (AppConstants.MOZ_DATA_REPORTING) {
      gDataNotificationInfoBar.init();
    }

    if (!AppConstants.MOZILLA_OFFICIAL) {
      DevelopmentHelpers.init();
    }

    gExtensionsNotifications.init();

    let wasMinimized = window.windowState == window.STATE_MINIMIZED;
    window.addEventListener("sizemodechange", () => {
      let isMinimized = window.windowState == window.STATE_MINIMIZED;
      if (wasMinimized != isMinimized) {
        wasMinimized = isMinimized;
        UpdatePopupNotificationsVisibility();
      }
    });

    window.addEventListener("mousemove", MousePosTracker);
    window.addEventListener("mouseout", MousePosTracker);
    window.addEventListener("dragover", MousePosTracker);

    gNavToolbox.addEventListener("customizationstarting", CustomizationHandler);
    gNavToolbox.addEventListener("aftercustomization", CustomizationHandler);

    SessionStore.promiseInitialized.then(() => {
      // Bail out if the window has been closed in the meantime.
      if (window.closed) {
        return;
      }

      // Enable the Restore Last Session command if needed
      gRestoreLastSessionObserver.init();

      SidebarController.startDelayedLoad();

      PanicButtonNotifier.init();
    });

    if (BrowserHandler.kiosk) {
      // We don't modify popup windows for kiosk mode
      if (!gURLBar.readOnly) {
        window.fullScreen = true;
      }
    }

    if (Services.policies.status === Services.policies.ACTIVE) {
      if (!Services.policies.isAllowed("hideShowMenuBar")) {
        document
          .getElementById("toolbar-menubar")
          .removeAttribute("toolbarname");
      }
      if (!Services.policies.isAllowed("profileImport")) {
        document.documentElement.setAttribute("disableprofileimport", "true");
      }
      if (!Services.policies.isAllowed("filepickers")) {
        let savePageCommand = document.getElementById("Browser:SavePage");
        let openFileCommand = document.getElementById("Browser:OpenFile");

        savePageCommand.setAttribute("disabled", "true");
        openFileCommand.setAttribute("disabled", "true");

        document.addEventListener("FilePickerBlocked", function (event) {
          let browser = event.target;

          let notificationBox = browser
            .getTabBrowser()
            ?.getNotificationBox(browser);

          // Prevent duplicate notifications
          if (
            notificationBox &&
            !notificationBox.getNotificationWithValue("filepicker-blocked")
          ) {
            notificationBox.appendNotification("filepicker-blocked", {
              label: {
                "l10n-id": "filepicker-blocked-infobar",
              },
              priority: notificationBox.PRIORITY_INFO_LOW,
            });
          }
        });
      }
      let policies = Services.policies.getActivePolicies();
      if ("ManagedBookmarks" in policies) {
        let managedBookmarks = policies.ManagedBookmarks;
        let children = managedBookmarks.filter(
          child => !("toplevel_name" in child)
        );
        if (children.length) {
          let managedBookmarksButton =
            document.createXULElement("toolbarbutton");
          managedBookmarksButton.setAttribute("id", "managed-bookmarks");
          managedBookmarksButton.setAttribute("class", "bookmark-item");
          let toplevel = managedBookmarks.find(
            element => "toplevel_name" in element
          );
          if (toplevel) {
            managedBookmarksButton.setAttribute(
              "label",
              toplevel.toplevel_name
            );
          } else {
            document.l10n.setAttributes(
              managedBookmarksButton,
              "managed-bookmarks"
            );
          }
          managedBookmarksButton.setAttribute("context", "placesContext");
          managedBookmarksButton.setAttribute("container", "true");
          managedBookmarksButton.setAttribute("removable", "false");
          managedBookmarksButton.setAttribute("type", "menu");

          let managedBookmarksPopup = document.createXULElement("menupopup");
          managedBookmarksPopup.setAttribute("id", "managed-bookmarks-popup");
          managedBookmarksPopup.addEventListener("command", event =>
            PlacesToolbarHelper.openManagedBookmark(event)
          );
          managedBookmarksPopup.addEventListener(
            "dragover",
            event => (event.dataTransfer.effectAllowed = "none")
          );
          managedBookmarksPopup.addEventListener("dragstart", event =>
            PlacesToolbarHelper.onDragStartManaged(event)
          );
          managedBookmarksPopup.addEventListener("popupshowing", event =>
            PlacesToolbarHelper.populateManagedBookmarks(event.currentTarget)
          );
          managedBookmarksPopup.setAttribute("placespopup", "true");
          managedBookmarksPopup.toggleAttribute("nonnative", true);
          managedBookmarksPopup.setAttribute("is", "places-popup");
          managedBookmarksPopup.classList.add("toolbar-menupopup");
          managedBookmarksButton.appendChild(managedBookmarksPopup);

          gNavToolbox.palette.appendChild(managedBookmarksButton);

          CustomizableUI.ensureWidgetPlacedInWindow(
            "managed-bookmarks",
            window
          );

          // Add button if it doesn't exist
          if (!CustomizableUI.getPlacementOfWidget("managed-bookmarks")) {
            CustomizableUI.addWidgetToArea(
              "managed-bookmarks",
              CustomizableUI.AREA_BOOKMARKS,
              0
            );
          }
        }
      }
    }

    CaptivePortalWatcher.delayedStartup();

    SessionStore.promiseAllWindowsRestored.then(() => {
      this._schedulePerWindowIdleTasks();
      document.documentElement.setAttribute("sessionrestored", "true");
    });

    this.delayedStartupFinished = true;
    _resolveDelayedStartup();
    Services.obs.notifyObservers(window, "browser-delayed-startup-finished");
    Glean.browserTimings.startupTimeline.delayedStartupFinished.set(
      Services.telemetry.msSinceProcessStart()
    );
    // We've announced that delayed startup has finished. Do not add code past this point.
  },

  /**
   * Resolved on the first MozLayerTreeReady and next MozAfterPaint in the
   * parent process.
   */
  get firstContentWindowPaintPromise() {
    return this._firstContentWindowPaintDeferred.promise;
  },

  _setInitialFocus() {
    let initiallyFocusedElement = document.commandDispatcher.focusedElement;

    // To prevent startup flicker, the urlbar has the 'focused' attribute set
    // by default. If we are not sure the urlbar will be focused in this
    // window, we need to remove the attribute before first paint.
    // TODO (bug 1629956): The urlbar having the 'focused' attribute by default
    // isn't a useful optimization anymore since UrlbarInput needs layout
    // information to focus the urlbar properly.
    let shouldRemoveFocusedAttribute = true;

    this._callWithURIToLoad(uriToLoad => {
      if (
        isBlankPageURL(uriToLoad) ||
        uriToLoad == "about:privatebrowsing" ||
        this.getTabToAdopt()?.isEmpty
      ) {
        gURLBar.select();
        shouldRemoveFocusedAttribute = false;
        return;
      }

      // If the initial browser is remote, in order to optimize for first paint,
      // we'll defer switching focus to that browser until it has painted.
      // Otherwise use a regular promise to guarantee that mutationobserver
      // microtasks that could affect focusability have run.
      let promise = gBrowser.selectedBrowser.isRemoteBrowser
        ? this.firstContentWindowPaintPromise
        : Promise.resolve();

      promise.then(() => {
        // If focus didn't move while we were waiting, we're okay to move to
        // the browser.
        if (
          document.commandDispatcher.focusedElement == initiallyFocusedElement
        ) {
          gBrowser.selectedBrowser.focus();
        }
      });
    });

    // Delay removing the attribute using requestAnimationFrame to avoid
    // invalidating styles multiple times in a row if uriToLoadPromise
    // resolves before first paint.
    if (shouldRemoveFocusedAttribute) {
      window.requestAnimationFrame(() => {
        if (shouldRemoveFocusedAttribute) {
          gURLBar.removeAttribute("focused");
        }
      });
    }
  },

  _handleURIToLoad() {
    this._callWithURIToLoad(uriToLoad => {
      if (!uriToLoad) {
        // We don't check whether window.arguments[5] (userContextId) is set
        // because tabbrowser.js takes care of that for the initial tab.
        return;
      }

      // We don't check if uriToLoad is a XULElement because this case has
      // already been handled before first paint, and the argument cleared.
      if (Array.isArray(uriToLoad)) {
        // This function throws for certain malformed URIs, so use exception handling
        // so that we don't disrupt startup
        try {
          gBrowser.loadTabs(uriToLoad, {
            inBackground: false,
            replace: true,
            // See below for the semantics of window.arguments. Only the minimum is supported.
            userContextId: window.arguments[5],
            triggeringPrincipal:
              window.arguments[8] ||
              Services.scriptSecurityManager.getSystemPrincipal(),
            allowInheritPrincipal: window.arguments[9],
            policyContainer: window.arguments[10],
            fromExternal: true,
          });
        } catch (e) {}
      } else if (window.arguments.length >= 3) {
        // window.arguments[1]: extraOptions (nsIPropertyBag)
        //                 [2]: referrerInfo (nsIReferrerInfo)
        //                 [3]: postData (nsIInputStream)
        //                 [4]: allowThirdPartyFixup (bool)
        //                 [5]: userContextId (int)
        //                 [6]: originPrincipal (nsIPrincipal)
        //                 [7]: originStoragePrincipal (nsIPrincipal)
        //                 [8]: triggeringPrincipal (nsIPrincipal)
        //                 [9]: allowInheritPrincipal (bool)
        //                 [10]: policyContainer (nsIPolicyContainer)
        //                 [11]: nsOpenWindowInfo
        let userContextId =
          window.arguments[5] != undefined
            ? window.arguments[5]
            : Ci.nsIScriptSecurityManager.DEFAULT_USER_CONTEXT_ID;

        let hasValidUserGestureActivation = undefined;
        let textDirectiveUserActivation = undefined;
        let fromExternal = undefined;
        let globalHistoryOptions = undefined;
        let triggeringRemoteType = undefined;
        let forceAllowDataURI = false;
        let schemelessInput = Ci.nsILoadInfo.SchemelessInputTypeUnset;
        if (window.arguments[1]) {
          if (!(window.arguments[1] instanceof Ci.nsIPropertyBag2)) {
            throw new Error(
              "window.arguments[1] must be null or Ci.nsIPropertyBag2!"
            );
          }

          let extraOptions = window.arguments[1];
          if (extraOptions.hasKey("hasValidUserGestureActivation")) {
            hasValidUserGestureActivation = extraOptions.getPropertyAsBool(
              "hasValidUserGestureActivation"
            );
          }
          if (extraOptions.hasKey("textDirectiveUserActivation")) {
            textDirectiveUserActivation = extraOptions.getPropertyAsBool(
              "textDirectiveUserActivation"
            );
          }
          if (extraOptions.hasKey("fromExternal")) {
            fromExternal = extraOptions.getPropertyAsBool("fromExternal");
          }
          if (extraOptions.hasKey("triggeringSponsoredURL")) {
            globalHistoryOptions = {
              triggeringSponsoredURL: extraOptions.getPropertyAsACString(
                "triggeringSponsoredURL"
              ),
            };
            if (extraOptions.hasKey("triggeringSponsoredURLVisitTimeMS")) {
              globalHistoryOptions.triggeringSponsoredURLVisitTimeMS =
                extraOptions.getPropertyAsUint64(
                  "triggeringSponsoredURLVisitTimeMS"
                );
            }
            if (extraOptions.hasKey("triggeringSource")) {
              globalHistoryOptions.triggeringSource =
                extraOptions.getPropertyAsACString("triggeringSource");
            }
          }
          if (extraOptions.hasKey("triggeringRemoteType")) {
            triggeringRemoteType = extraOptions.getPropertyAsACString(
              "triggeringRemoteType"
            );
          }
          if (extraOptions.hasKey("forceAllowDataURI")) {
            forceAllowDataURI =
              extraOptions.getPropertyAsBool("forceAllowDataURI");
          }
          if (extraOptions.hasKey("schemelessInput")) {
            schemelessInput =
              extraOptions.getPropertyAsUint32("schemelessInput");
          }
        }

        try {
          openLinkIn(uriToLoad, "current", {
            referrerInfo: window.arguments[2] || null,
            postData: window.arguments[3] || null,
            allowThirdPartyFixup: window.arguments[4] || false,
            userContextId,
            // pass the origin principal (if any) and force its use to create
            // an initial about:blank viewer if present:
            originPrincipal: window.arguments[6],
            originStoragePrincipal: window.arguments[7],
            triggeringPrincipal: window.arguments[8],
            // TODO fix allowInheritPrincipal to default to false.
            // Default to true unless explicitly set to false because of bug 1475201.
            allowInheritPrincipal: window.arguments[9] !== false,
            policyContainer: window.arguments[10],
            forceAboutBlankViewerInCurrent: !!window.arguments[6],
            forceAllowDataURI,
            hasValidUserGestureActivation,
            textDirectiveUserActivation,
            fromExternal,
            globalHistoryOptions,
            triggeringRemoteType,
            schemelessInput,
          });
        } catch (e) {
          console.error(e);
        }

        window.focus();
      } else {
        // Note: loadOneOrMoreURIs *must not* be called if window.arguments.length >= 3.
        // Such callers expect that window.arguments[0] is handled as a single URI.
        loadOneOrMoreURIs(uriToLoad, {
          newWindowLoad: true,
        });
      }
    });
  },

  /**
   * Use this function as an entry point to schedule tasks that
   * need to run once per window after startup, and can be scheduled
   * by using an idle callback.
   *
   * The functions scheduled here will fire from idle callbacks
   * once every window has finished being restored by session
   * restore, and after the equivalent only-once tasks
   * have run (from _scheduleStartupIdleTasks in BrowserGlue.sys.mjs).
   */
  _schedulePerWindowIdleTasks() {
    // Bail out if the window has been closed in the meantime.
    if (window.closed) {
      return;
    }

    function scheduleIdleTask(func, options) {
      requestIdleCallback(function idleTaskRunner() {
        if (!window.closed) {
          func();
        }
      }, options);
    }

    scheduleIdleTask(() => {
      // Initialize the Sync UI
      gSync.init();
    });

    scheduleIdleTask(() => {
      // Read prefers-reduced-motion setting
      let reduceMotionQuery = window.matchMedia(
        "(prefers-reduced-motion: reduce)"
      );
      function readSetting() {
        gReduceMotionSetting = reduceMotionQuery.matches;
      }
      reduceMotionQuery.addListener(readSetting);
      readSetting();
    });

    scheduleIdleTask(() => {
      // setup simple gestures support
      gGestureSupport.init(true);

      // setup history swipe animation
      gHistorySwipeAnimation.init();
    });

    scheduleIdleTask(() => {
      gBrowserThumbnails.init();
    });

    scheduleIdleTask(
      () => {
        // Initialize the download manager some time after the app starts so that
        // auto-resume downloads begin (such as after crashing or quitting with
        // active downloads) and speeds up the first-load of the download manager UI.
        // If the user manually opens the download manager before the timeout, the
        // downloads will start right away, and initializing again won't hurt.
        try {
          DownloadsCommon.initializeAllDataLinks();
          ChromeUtils.importESModule(
            "moz-src:///browser/components/downloads/DownloadsTaskbar.sys.mjs"
          )
            .DownloadsTaskbar.registerIndicator(window)
            .catch(ex => {
              console.error(ex);
            });
          if (AppConstants.platform == "macosx") {
            ChromeUtils.importESModule(
              "moz-src:///browser/components/downloads/DownloadsMacFinderProgress.sys.mjs"
            ).DownloadsMacFinderProgress.register();
          }
        } catch (ex) {
          console.error(ex);
        }
      },
      { timeout: 10000 }
    );

    if (Win7Features) {
      scheduleIdleTask(() => Win7Features.onOpenWindow());
    }

    scheduleIdleTask(async () => {
      NewTabPagePreloading.maybeCreatePreloadedBrowser(window);
    });

    scheduleIdleTask(() => {
      gGfxUtils.init();
    });

    scheduleIdleTask(async () => {
      await gProfiles.init();
    });

    // This should always go last, since the idle tasks (except for the ones with
    // timeouts) should execute in order. Note that this observer notification is
    // not guaranteed to fire, since the window could close before we get here.
    scheduleIdleTask(() => {
      this.idleTasksFinished.resolve();
      Services.obs.notifyObservers(
        window,
        "browser-idle-startup-tasks-finished"
      );
    });
  },

  // Returns the URI(s) to load at startup if it is immediately known, or a
  // promise resolving to the URI to load.
  get uriToLoadPromise() {
    delete this.uriToLoadPromise;
    return (this.uriToLoadPromise = (function () {
      // window.arguments[0]: URI to load (string), or an nsIArray of
      //                      nsISupportsStrings to load, or a xul:tab of
      //                      a tabbrowser, which will be replaced by this
      //                      window (for this case, all other arguments are
      //                      ignored).
      let uri = window.arguments?.[0];
      if (!uri || window.XULElement.isInstance(uri)) {
        return null;
      }

      let defaultArgs = BrowserHandler.defaultArgs;

      // figure out which URI to actually load (or a Promise to get the uri)
      uri = (aUri => {
        // If the given URI is different from the homepage, we want to load it.
        if (aUri != defaultArgs) {
          AboutNewTab.noteNonDefaultStartup();

          if (aUri instanceof Ci.nsIArray) {
            // Transform the nsIArray of nsISupportsString's into a JS Array of
            // JS strings.
            return Array.from(
              aUri.enumerate(Ci.nsISupportsString),
              supportStr => supportStr.data
            );
          } else if (aUri instanceof Ci.nsISupportsString) {
            return aUri.data;
          }
          return aUri;
        }

        // The URI appears to be the the homepage. We want to load it only if
        // session restore isn't about to override the homepage.
        let willOverride = SessionStartup.willOverrideHomepage;
        if (typeof willOverride == "boolean") {
          return willOverride ? null : aUri;
        }
        return willOverride.then(willOverrideHomepage =>
          willOverrideHomepage ? null : aUri
        );
      })(uri);

      // if using TorConnect, convert these uris to redirects
      if (TorConnect.shouldShowTorConnect) {
        return Promise.resolve(uri).then(aUri =>
          TorConnectParent.getURIsToLoad(aUri ?? [])
        );
      }
      return uri;
    })());
  },

  // Calls the given callback with the URI to load at startup.
  // Synchronously if possible, or after uriToLoadPromise resolves otherwise.
  _callWithURIToLoad(callback) {
    let uriToLoad = this.uriToLoadPromise;
    if (uriToLoad && uriToLoad.then) {
      uriToLoad.then(callback);
    } else {
      callback(uriToLoad);
    }
  },

  onUnload() {
    BrowserUtils.callModulesFromCategory(
      { categoryName: "browser-window-unload-begin", jsGlobal: globalThis },
      window
    );

    // In certain scenarios it's possible for unload to be fired before onload,
    // (e.g. if the window is being closed after browser.js loads but before the
    // load completes). In that case, there's nothing to do here.
    if (!this._loadHandled) {
      return;
    }

    gGestureSupport.init(false);

    gHistorySwipeAnimation.uninit();

    FullScreen.uninit();

    gSync.uninit();

    gExtensionsNotifications.uninit();

    try {
      gBrowser.removeProgressListener(window.XULBrowserWindow);
      gBrowser.removeTabsProgressListener(window.TabsProgressListener);
    } catch (ex) {}

    PlacesToolbarHelper.uninit();

    BookmarkingUI.uninit();

    SecurityLevelButton.uninit();

    gTorConnectUrlbarButton.uninit();
    gTorConnectTitlebarStatus.uninit();

    OnionAuthPrompt.uninit();

    gTorCircuitPanel.uninit();

    // LinkPreview.sys.mjs is missing. tor-browser#44045.

    BrowserUtils.callModulesFromCategory(
      { categoryName: "browser-window-unload", jsGlobal: globalThis },
      window
    );

    // Now either cancel delayedStartup, or clean up the services initialized from
    // it.
    if (this._boundDelayedStartup) {
      this._cancelDelayedStartup();
    } else {
      if (Win7Features) {
        Win7Features.onCloseWindow();
      }
      Services.prefs.removeObserver(ctrlTab.prefName, ctrlTab);
      ctrlTab.uninit();
      gBrowserThumbnails.uninit();
      gProtectionsHandler.uninit();
      gTrustPanelHandler.uninit();
      FullZoom.destroy();

      Services.obs.removeObserver(gIdentityHandler, "perm-changed");
      Services.obs.removeObserver(gRemoteControl, "devtools-socket");
      Services.obs.removeObserver(gRemoteControl, "marionette-listening");
      Services.obs.removeObserver(gRemoteControl, "remote-listening");
      Services.obs.removeObserver(
        gSessionHistoryObserver,
        "browser:purge-session-history"
      );
      Services.obs.removeObserver(
        gStoragePressureObserver,
        "QuotaManager::StoragePressure"
      );
      Services.obs.removeObserver(gXPInstallObserver, "addon-install-disabled");
      Services.obs.removeObserver(gXPInstallObserver, "addon-install-started");
      Services.obs.removeObserver(gXPInstallObserver, "addon-install-blocked");
      Services.obs.removeObserver(
        gXPInstallObserver,
        "addon-install-fullscreen-blocked"
      );
      Services.obs.removeObserver(
        gXPInstallObserver,
        "addon-install-origin-blocked"
      );
      Services.obs.removeObserver(
        gXPInstallObserver,
        "addon-install-policy-blocked"
      );
      Services.obs.removeObserver(
        gXPInstallObserver,
        "addon-install-webapi-blocked"
      );
      Services.obs.removeObserver(gXPInstallObserver, "addon-install-failed");
      Services.obs.removeObserver(
        gXPInstallObserver,
        "addon-install-confirmation"
      );
      Services.obs.removeObserver(gKeywordURIFixup, "keyword-uri-fixup");
      Services.obs.removeObserver(
        gLocaleChangeObserver,
        "intl:app-locales-changed"
      );
      Services.obs.removeObserver(
        this._translationsEnabledStateObserver,
        "translations:enabled-state-changed"
      );
      Services.obs.removeObserver(
        gSerialDeviceObserver,
        "serial-device-state-changed"
      );

      BrowserOffline.uninit();
      PanelUI.uninit();
    }

    BrowserUtils.callModulesFromCategory(
      {
        categoryName: "browser-window-unload-tabbrowser",
        jsGlobal: globalThis,
      },
      window
    );
    window.XULBrowserWindow = null;
    window.docShell.treeOwner
      .QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIAppWindow).XULBrowserWindow = null;

    BrowserUtils.callModulesFromCategory(
      { categoryName: "browser-window-final-unload", jsGlobal: globalThis },
      window
    );
  },
};
