/* 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/. */

/* eslint-disable no-shadow */

import { nsZenThemePicker } from "resource:///modules/zen/ZenGradientGenerator.mjs";
import { ZenSpacesSwipe } from "resource:///modules/zen/ZenSpacesSwipe.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  ZenSessionStore: "resource:///modules/zen/ZenSessionManager.sys.mjs",
});

ChromeUtils.defineLazyGetter(lazy, "browserBackgroundElement", () => {
  return document.getElementById("zen-browser-background");
});

ChromeUtils.defineLazyGetter(lazy, "toolbarBackgroundElement", () => {
  return document.getElementById("zen-toolbar-background");
});

/**
 * Zen Spaces manager. This class is mainly responsible for the UI
 * and user interactions but it also contains some logic to manage
 * the workspaces and their tabs.
 *
 * For window sync, please @see ZenWindowSync
 */
class nsZenWorkspaces {
  /**
   * Stores workspace IDs and their last selected tabs.
   */
  lastSelectedWorkspaceTabs = {};
  #inChangingWorkspace = false;
  draggedElement = null;

  #hasInitialized = false;

  #canDebug = Services.prefs.getBoolPref("zen.workspaces.debug", false);
  #activeWorkspace = "";

  _workspaceCache = [];

  #lastScrollTime = 0;
  #currentSpaceSwitchContext = {
    promise: null,
    animations: [],
  };

  bookmarkMenus = [
    "PlacesToolbar",
    "bookmarks-menu-button",
    "BMB_bookmarksToolbar",
    "BMB_unsortedBookmarks",
    "BMB_mobileBookmarks",
  ];

  promisePinnedInitialized = new Promise(resolve => {
    this._resolvePinnedInitialized = resolve;
  });

  promiseInitialized = new Promise(resolve => {
    this._resolveInitialized = resolve;
  });

  async #waitForPromises() {
    if (this.privateWindowOrDisabled) {
      return;
    }
    await Promise.all([
      this.promisePinnedInitialized,
      SessionStore.promiseAllWindowsRestored,
    ]);
  }

  async init() {
    // Initialize tab selection state
    this._tabSelectionState = {
      inProgress: false,
      lastSelectionTime: 0,
      debounceTime: 100, // ms to wait between tab selections
    };

    // Initialize workspace change mutex
    this._workspaceChangeInProgress = false;

    if (!this.shouldHaveWorkspaces) {
      this._resolveInitialized();
      console.warn(
        "gZenWorkspaces: !!! gZenWorkspaces is disabled in hidden windows !!!"
      );
      return; // We are in a hidden window, don't initialize gZenWorkspaces
    }

    this.ownerWindow = window;
    XPCOMUtils.defineLazyPreferenceGetter(
      this,
      "activationMethod",
      "zen.workspaces.scroll-modifier-key",
      "ctrl"
    );
    XPCOMUtils.defineLazyPreferenceGetter(
      this,
      "naturalScroll",
      "zen.workspaces.natural-scroll",
      true
    );
    XPCOMUtils.defineLazyPreferenceGetter(
      this,
      "shouldWrapAroundNavigation",
      "zen.workspaces.wrap-around-navigation",
      true
    );
    XPCOMUtils.defineLazyPreferenceGetter(
      this,
      "shouldForceContainerTabsToWorkspace",
      "zen.workspaces.force-container-workspace",
      true
    );
    XPCOMUtils.defineLazyPreferenceGetter(
      this,
      "shouldOpenNewTabIfLastUnpinnedTabIsClosed",
      "zen.workspaces.open-new-tab-if-last-unpinned-tab-is-closed",
      false
    );
    this.containerSpecificEssentials = Services.prefs.getBoolPref(
      "zen.workspaces.separate-essentials",
      false
    );
    ChromeUtils.defineLazyGetter(this, "tabContainer", () =>
      document.getElementById("tabbrowser-tabs")
    );
    ChromeUtils.defineLazyGetter(this, "workspaceIcons", () =>
      document.getElementById("zen-workspaces-button")
    );
    this.#activeWorkspace ||= Services.prefs.getStringPref(
      "zen.workspaces.active",
      ""
    );

    if (this.isPrivateWindow) {
      document.documentElement.setAttribute("zen-private-window", "true");
    }

    this.popupOpenHandler = this._popupOpenHandler.bind(this);

    window.addEventListener("resize", this.onWindowResize.bind(this));
    this.addPopupListeners();

    if (this.privateWindowOrDisabled) {
      await this.#waitForPromises();
      await this.restoreWorkspacesFromSessionStore({});
    }

    if (!this.privateWindowOrDisabled) {
      const observerFunction = async () => {
        delete this._workspaceBookmarksCache;
        await this.workspaceBookmarks();
        this._invalidateBookmarkContainers();
      };
      Services.obs.addObserver(observerFunction, "workspace-bookmarks-updated");
      window.addEventListener("unload", () => {
        Services.obs.removeObserver(
          observerFunction,
          "workspace-bookmarks-updated"
        );
      });
    }
  }

  log(...args) {
    if (this.#canDebug) {
      /* eslint-disable no-console */
      console.debug(`[gZenWorkspaces]:`, ...args);
    }
  }

  #afterLoadInit() {
    const onResize = (...args) => {
      requestAnimationFrame(() => {
        this.onPinnedTabsResize(...args);
      });
    };
    this._pinnedTabsResizeObserver = new ResizeObserver(onResize);
    this.registerPinnedResizeObserver();
    this.#initializeWorkspaceTabContextMenus();

    // Non UI related initializations
    if (
      Services.prefs.getBoolPref("zen.workspaces.swipe-actions", false) &&
      this.workspaceEnabled &&
      !this.isPrivateWindow
    ) {
      this._swipeManager = new ZenSpacesSwipe();
      this.initializeWorkspaceNavigation();
    }
  }

  // Validate browser state before tab operations
  _validateBrowserState() {
    // Check if browser window is still open
    if (window.closed) {
      return false;
    }

    // Check if gBrowser is available
    if (!gBrowser || !gBrowser.tabContainer) {
      return false;
    }

    // Check if URL bar is available
    if (!gURLBar) {
      return false;
    }

    return true;
  }

  // Safely select a tab with debouncing to prevent race conditions
  async _safelySelectTab(tab) {
    if (!tab || tab.closing || !tab.ownerGlobal || tab.ownerGlobal.closed) {
      return false;
    }

    // Check if we need to debounce
    const now = Date.now();
    const timeSinceLastSelection =
      now - this._tabSelectionState.lastSelectionTime;

    if (timeSinceLastSelection < this._tabSelectionState.debounceTime) {
      await new Promise(resolve =>
        setTimeout(
          resolve,
          this._tabSelectionState.debounceTime - timeSinceLastSelection
        )
      );
    }

    // Mark selection as in progress
    this._tabSelectionState.inProgress = true;

    try {
      gBrowser.selectedTab = tab;
      this._tabSelectionState.lastSelectionTime = Date.now();
      return true;
    } catch (e) {
      console.error("Error selecting tab:", e);
      return false;
    } finally {
      this._tabSelectionState.inProgress = false;
    }
  }

  async selectEmptyTab(newTabTarget = null) {
    // Validate browser state first
    if (!this._validateBrowserState()) {
      console.warn("Browser state invalid for empty tab selection");
      return null;
    }

    if (gZenUIManager.testingEnabled) {
      return null;
    }

    try {
      // Check if we have a valid empty tab and can replace new tab
      if (
        this._emptyTab &&
        !this._emptyTab.closing &&
        this._emptyTab.ownerGlobal &&
        !this._emptyTab.ownerGlobal.closed &&
        gZenVerticalTabsManager._canReplaceNewTab
      ) {
        // Safely switch to the empty tab using our debounced method
        const success = await this._safelySelectTab(this._emptyTab);
        if (!success) {
          throw new Error("Failed to select empty tab");
        }

        return this._emptyTab;
      }

      // Fall back to creating a new tab
      const newTabUrl =
        newTabTarget ||
        Services.prefs.getStringPref("browser.startup.homepage");
      let tab = gZenUIManager.openAndChangeToTab(newTabUrl);

      // Set workspace ID if available
      if (window.uuid) {
        tab.setAttribute("zen-workspace-id", this.activeWorkspace);
      }
      return tab;
    } catch (e) {
      console.error("Error in selectEmptyTab:", e);

      // Create a fallback tab as a last resort, with proper validation
      try {
        if (this._validateBrowserState()) {
          return gBrowser.addTrustedTab("about:blank");
        }
      } catch (fallbackError) {
        console.error("Critical error creating fallback tab:", fallbackError);
      }
      return null;
    }
  }

  #initializeEmptyTab() {
    for (const tab of gBrowser.tabs) {
      // Check if session store has an empty tab
      if (tab.hasAttribute("zen-empty-tab") && !tab.pinned) {
        this.log("Found existing empty tab from session store!");
        this._emptyTab = tab;
        return;
      }
    }
    this._emptyTab = gBrowser.addTrustedTab("about:blank", {
      inBackground: true,
      userContextId: 0,
      _forZenEmptyTab: true,
    });
  }

  registerPinnedResizeObserver() {
    if (!this._hasInitializedTabsStrip || !this._pinnedTabsResizeObserver) {
      return;
    }
    this._pinnedTabsResizeObserver.disconnect();
    for (let element of document.getElementById("zen-essentials").children) {
      if (element.classList.contains("tabbrowser-tab")) {
        continue;
      }
      this._pinnedTabsResizeObserver.observe(element, { box: "border-box" });
    }
  }

  get activeWorkspaceStrip() {
    if (!this._hasInitializedTabsStrip) {
      return gBrowser.tabContainer.arrowScrollbox;
    }
    return this.activeWorkspaceElement?.tabsContainer;
  }

  get pinnedTabsContainer() {
    if (!this.workspaceEnabled || !this._hasInitializedTabsStrip) {
      return document.getElementById("pinned-tabs-container");
    }
    return this.activeWorkspaceElement?.pinnedTabsContainer;
  }

  get activeWorkspaceIndicator() {
    return this.activeWorkspaceElement?.indicator;
  }

  get activeScrollbox() {
    return (
      this.activeWorkspaceElement?.scrollbox ??
      gBrowser.tabContainer.arrowScrollbox
    );
  }

  get tabboxChildren() {
    return Array.from(this.activeWorkspaceStrip?.children || []);
  }

  get tabboxChildrenWithoutEmpty() {
    return this.tabboxChildren.filter(
      child => !child.hasAttribute("zen-empty-tab")
    );
  }

  get shouldAnimateEssentials() {
    return (
      this.containerSpecificEssentials ||
      document.documentElement.hasAttribute("zen-creating-workspace")
    );
  }

  get activeWorkspaceElement() {
    return this.workspaceElement(this.activeWorkspace);
  }

  workspaceElement(workspaceId) {
    if (typeof workspaceId !== "string") {
      workspaceId = workspaceId?.uuid;
    }
    return document.getElementById(workspaceId);
  }

  #initializeTabsStripSections() {
    const perifery = document.getElementById(
      "tabbrowser-arrowscrollbox-periphery"
    );
    perifery.setAttribute("hidden", "true");
    const tabs = gBrowser.tabContainer.allTabs;
    const workspaces = this.getWorkspaces();
    for (const workspace of workspaces) {
      this.#createWorkspaceTabsSection(workspace, tabs);
    }
    if (tabs.length) {
      const defaultSelectedContainer = this.workspaceElement(
        this.activeWorkspace
      )?.querySelector(".zen-workspace-normal-tabs-section");
      const pinnedContainer = this.workspaceElement(
        this.activeWorkspace
      )?.querySelector(".zen-workspace-pinned-tabs-section");
      // New profile with no workspaces does not have a default selected container
      if (defaultSelectedContainer) {
        for (const tab of tabs) {
          if (tab.hasAttribute("zen-essential")) {
            this.getEssentialsSection(tab).appendChild(tab);
            continue;
          } else if (tab.pinned) {
            pinnedContainer.insertBefore(tab, pinnedContainer.lastChild);
            continue;
          }
          // before to the last child (perifery)
          defaultSelectedContainer.insertBefore(
            tab,
            defaultSelectedContainer.lastChild
          );
        }
      }
      gBrowser.tabContainer._invalidateCachedTabs();
    }
    perifery.setAttribute("hidden", "true");
    this._hasInitializedTabsStrip = true;
    this._fixIndicatorsNames(workspaces);
  }

  getEssentialsSection(container = 0) {
    if (typeof container !== "number") {
      container = container?.getAttribute("usercontextid");
    }
    container ??= 0;
    if (!this.containerSpecificEssentials) {
      container = 0;
    }
    let essentialsContainer = document.querySelector(
      `.zen-essentials-container[container="${container}"]:not([cloned])`
    );
    if (!essentialsContainer) {
      essentialsContainer = document.createXULElement("hbox");
      essentialsContainer.className =
        "zen-essentials-container zen-workspace-tabs-section";
      essentialsContainer.setAttribute("flex", "1");
      essentialsContainer.setAttribute("container", container);
      document
        .getElementById("zen-essentials")
        .appendChild(essentialsContainer);

      // Set an initial hidden state if the essentials section is not supposed
      // to be shown on the current workspace
      if (
        this.containerSpecificEssentials &&
        this.getActiveWorkspaceFromCache()?.containerTabId != container
      ) {
        essentialsContainer.setAttribute("hidden", "true");
      }
    }
    return essentialsContainer;
  }

  getCurrentSpaceContainerId() {
    const currentWorkspace = this.getActiveWorkspaceFromCache();
    return typeof currentWorkspace?.containerTabId === "number"
      ? currentWorkspace.containerTabId
      : 0;
  }

  getCurrentEssentialsContainer() {
    return this.getEssentialsSection(this.getCurrentSpaceContainerId());
  }

  #createWorkspaceTabsSection(workspace, tabs = []) {
    const workspaceWrapper = document.createXULElement("zen-workspace");
    const container = document.getElementById("tabbrowser-arrowscrollbox");
    workspaceWrapper.id = workspace.uuid;
    if (this.activeWorkspace === workspace.uuid) {
      workspaceWrapper.active = true;
    }

    if (document.documentElement.hasAttribute("zen-creating-workspace")) {
      workspaceWrapper.hidden = true; // Hide workspace while creating it
    }
    container.appendChild(workspaceWrapper);
    this.#organizeTabsToWorkspaceSections(workspace, workspaceWrapper, tabs);
    workspaceWrapper.checkPinsExistence();
  }

  #organizeTabsToWorkspaceSections(workspace, spaceElement, tabs) {
    let section = spaceElement.tabsContainer;
    let pinnedSection = spaceElement.pinnedTabsContainer;
    const workspaceTabs = Array.from(tabs).filter(
      tab =>
        tab.getAttribute("zen-workspace-id") === workspace.uuid &&
        !tab.hasAttribute("zen-essential")
    );
    let folders = new Set();
    const getFolderRoot = tab => {
      let root = tab?.group;
      while (root?.group) {
        root = root?.group;
      }
      return root || tab;
    };
    for (let i = workspaceTabs.length - 1; i >= 0; i--) {
      let tab = workspaceTabs[i];
      if (tab.hasAttribute("zen-essential")) {
        continue;
      } // Ignore essentials as they need to be in their own section
      // remove tab from list
      tabs.splice(tabs.indexOf(tab), 1);
      tab = tab.group ?? tab;

      if (gBrowser.isTabGroup(tab)) {
        let rootGroup = getFolderRoot(tab);

        if (folders.has(rootGroup)) {
          continue;
        }

        folders.add(rootGroup);
        tab = rootGroup;
      }

      if (tab.pinned) {
        pinnedSection.insertBefore(tab, pinnedSection.firstChild);
      } else {
        section.insertBefore(tab, section.firstChild);
      }
    }
  }

  initializeWorkspaceNavigation() {
    this.#setupAppCommandHandlers();
    this.#setupSidebarHandlers();
  }

  #setupAppCommandHandlers() {
    // Remove existing handler temporarily - this is needed so that _handleAppCommand is called before the original
    window.removeEventListener("AppCommand", HandleAppCommandEvent, true);

    // Add our handler first
    window.addEventListener(
      "AppCommand",
      this._handleAppCommand.bind(this),
      true
    );

    // Re-add original handler
    window.addEventListener("AppCommand", HandleAppCommandEvent, true);
  }

  get _hoveringSidebar() {
    return gNavToolbox.hasAttribute("zen-has-implicit-hover");
  }

  _handleAppCommand(event) {
    // note: Dont use this._hoveringSidebar as it's not as reliable as checking for :hover
    if (!this.workspaceEnabled || !gNavToolbox.matches(":hover")) {
      return;
    }

    const direction = this.naturalScroll ? -1 : 1;
    // event is forward or back
    switch (event.command) {
      case "Forward":
        this.changeWorkspaceShortcut(1 * direction);
        event.stopImmediatePropagation();
        event.preventDefault();
        break;
      case "Back":
        this.changeWorkspaceShortcut(-1 * direction);
        event.stopImmediatePropagation();
        event.preventDefault();
        break;
    }
    requestAnimationFrame(() => {
      requestAnimationFrame(() => {
        gNavToolbox.setAttribute("zen-has-hover", "true");
      });
    });
  }

  #setupSidebarHandlers() {
    const toolbox = gNavToolbox;

    const scrollCooldown = 200; // Milliseconds to wait before allowing another scroll
    const scrollThreshold = 1; // Minimum scroll delta to trigger workspace change

    toolbox.addEventListener(
      "wheel",
      event => {
        if (this.privateWindowOrDisabled) {
          return;
        }

        // Only process non-gesture scrolls
        if (event.deltaMode !== 1) {
          return;
        }

        const isVerticalScroll = event.deltaY && !event.deltaX;

        //if the scroll is vertical this checks that a modifier key is used before proceeding
        if (isVerticalScroll) {
          const activationKeyMap = {
            ctrl: event.ctrlKey,
            alt: event.altKey,
            shift: event.shiftKey,
            meta: event.metaKey,
          };

          if (
            this.activationMethod in activationKeyMap &&
            !activationKeyMap[this.activationMethod]
          ) {
            return;
          }
        }

        let currentTime = Date.now();
        if (currentTime - this.#lastScrollTime < scrollCooldown) {
          return;
        }

        //this decides which delta to use
        const delta = isVerticalScroll ? event.deltaY : event.deltaX;
        if (Math.abs(delta) < scrollThreshold) {
          return;
        }

        // Determine scroll direction
        let rawDirection = delta > 0 ? 1 : -1;

        let direction = this.naturalScroll ? -1 : 1;
        this.changeWorkspaceShortcut(rawDirection * direction);

        this.#lastScrollTime = currentTime;
      },
      { passive: true, capture: true }
    );
  }

  _popupOpenHandler() {
    // If a popup is opened, we should stop the swipe gesture
    if (this._swipeManager?.isGestureActive) {
      document.documentElement.removeAttribute("swipe-gesture");
      gZenUIManager.tabsWrapper.style.removeProperty("scrollbar-width");
      this.updateTabsContainers();
      this._cancelSwipeAnimation();
    }
  }

  get activeWorkspace() {
    return this.#activeWorkspace;
  }

  set activeWorkspace(value) {
    const spaces = this.getWorkspaces();
    if (!spaces.some(ws => ws.uuid === value)) {
      value = spaces[0]?.uuid || "";
    }
    if (value === this.#activeWorkspace) {
      return;
    }
    this.#activeWorkspace = value;
    if (this.privateWindowOrDisabled) {
      return;
    }
    Services.prefs.setStringPref("zen.workspaces.active", value);
  }

  get isChangingWorkspace() {
    return this.#inChangingWorkspace;
  }

  get shouldHaveWorkspaces() {
    if (typeof this._shouldHaveWorkspaces === "undefined") {
      let chromeFlags = window.docShell.treeOwner
        .QueryInterface(Ci.nsIInterfaceRequestor)
        .getInterface(Ci.nsIAppWindow).chromeFlags;
      this._shouldHaveWorkspaces =
        chromeFlags & Ci.nsIWebBrowserChrome.CHROME_TOOLBAR ||
        chromeFlags & Ci.nsIWebBrowserChrome.CHROME_MENUBAR;
    }
    return (
      this._shouldHaveWorkspaces &&
      !document.documentElement.hasAttribute("taskbartab")
    );
  }

  get isPrivateWindow() {
    return PrivateBrowsingUtils.isWindowPrivate(window);
  }

  get currentWindowIsSyncing() {
    return (
      !document.documentElement.hasAttribute("zen-unsynced-window") &&
      window._zenStartupSyncFlag !== "unsynced" &&
      !this.isPrivateWindow
    );
  }

  get privateWindowOrDisabled() {
    return !this.shouldHaveWorkspaces || !this.currentWindowIsSyncing;
  }

  get workspaceEnabled() {
    return this.shouldHaveWorkspaces && !window.closed;
  }

  getActiveWorkspaceFromCache() {
    return this.getWorkspaceFromId(this.activeWorkspace);
  }

  getWorkspaceFromId(id) {
    try {
      return this.getWorkspaces().find(workspace => workspace.uuid === id);
    } catch {
      return null;
    }
  }

  getWorkspaces(lieToMe = false) {
    if (lieToMe) {
      const { ZenSessionStore } = ChromeUtils.importESModule(
        "resource:///modules/zen/ZenSessionManager.sys.mjs"
      );
      return ZenSessionStore.getClonedSpaces();
    }
    return [...this._workspaceCache];
  }

  getWorkspacesForSessionStore() {
    const spaces = this.getWorkspaces();
    let spacesForSS = [];
    for (const space of spaces) {
      let newSpace = { ...space };
      const element = this.workspaceElement(space.uuid);
      if (element) {
        newSpace.hasCollapsedPinnedTabs = element.hasCollapsedPinnedTabs;
      }
      spacesForSS.push(newSpace);
    }
    return spacesForSS;
  }

  async workspaceBookmarks() {
    if (this.privateWindowOrDisabled) {
      this._workspaceBookmarksCache = {
        bookmarks: [],
        lastChangeTimestamp: 0,
      };
      return this._workspaceBookmarksCache;
    }

    if (this._workspaceBookmarksCache) {
      return this._workspaceBookmarksCache;
    }

    const [bookmarks, lastChangeTimestamp] = await Promise.all([
      ZenWorkspaceBookmarksStorage.getBookmarkGuidsByWorkspace(),
      ZenWorkspaceBookmarksStorage.getLastChangeTimestamp(),
    ]);

    this._workspaceBookmarksCache = { bookmarks, lastChangeTimestamp };

    return this._workspaceCache;
  }

  restoreWorkspacesFromSessionStore(aWinData = {}) {
    if (this.#hasInitialized || !this.workspaceEnabled) {
      return Promise.resolve();
    }
    const spacesFromStore = aWinData.spaces || [];
    if (
      !this.privateWindowOrDisabled &&
      spacesFromStore.length === 0 &&
      lazy.ZenSessionStore._migrationData?.spaces
    ) {
      spacesFromStore.push(...lazy.ZenSessionStore._migrationData.spaces);
    }
    this._workspaceCache = spacesFromStore.length
      ? [...spacesFromStore]
      : [this.#createWorkspaceData("Space", undefined)];
    this.activeWorkspace =
      aWinData.activeZenSpace || this._workspaceCache[0].uuid;
    let promise = this.#initializeWorkspaces();
    for (const workspace of spacesFromStore) {
      const element = this.workspaceElement(workspace.uuid);
      let wasCollapsed = workspace.hasCollapsedPinnedTabs || false;
      if (element) {
        // A bit of a hacky soltuion to ensure that the height
        // when collapsing is calculated correctly after restoring from session store
        setTimeout(() => {
          setTimeout(() => {
            element.collapsiblePins.collapsed = wasCollapsed;
          }, 0);
        }, 0);
      }
    }
    for (const workspace of this._workspaceCache) {
      // We don't want to depend on this by mistake
      delete workspace.hasCollapsedPinnedTabs;
    }
    promise.finally(() => {
      this.#hasInitialized = true;
    });
    return promise;
  }

  #initializeWorkspaces() {
    let activeWorkspace = this.getActiveWorkspace();
    this.activeWorkspace = activeWorkspace?.uuid;
    try {
      if (activeWorkspace) {
        window.gZenThemePicker = new nsZenThemePicker();
        gZenThemePicker.onWorkspaceChange(activeWorkspace);
      }
    } catch (e) {
      console.error("gZenWorkspaces: Error initializing theme picker", e);
    }
    this.#initializeTabsStripSections();
    this.#initializeEmptyTab();
    return (async () => {
      await this.#waitForPromises();
      this.#afterLoadInit();
      await this.workspaceBookmarks();
      await this.changeWorkspace(activeWorkspace, { onInit: true });
      this.#fixTabPositions();
      this.onWindowResize();
      this._resolveInitialized();
      this.#clearAnyZombieTabs(); // Dont call with await
      delete this._resolveInitialized;

      const tabUpdateListener = this.updateTabsContainers.bind(this);
      window.addEventListener("TabOpen", tabUpdateListener);
      window.addEventListener("TabClose", tabUpdateListener);
      window.addEventListener("TabAddedToEssentials", tabUpdateListener);
      window.addEventListener("TabRemovedFromEssentials", tabUpdateListener);
      window.addEventListener("TabPinned", tabUpdateListener);
      window.addEventListener("TabUnpinned", tabUpdateListener);
      window.addEventListener("aftercustomization", tabUpdateListener);
      window.addEventListener("TabSelect", this.onLocationChange.bind(this));
      window.addEventListener(
        "TabBrowserInserted",
        this.onTabBrowserInserted.bind(this)
      );

      this.updateWorkspacesChangeContextMenu();
    })();
  }

  async selectStartPage() {
    if (!this.workspaceEnabled || gZenUIManager.testingEnabled) {
      return;
    }
    await this.promiseInitialized;
    let shownEmptyTab = false;
    let resolveSelectPromise;
    let selectPromise = new Promise(resolve => {
      resolveSelectPromise = resolve;
    });

    const cleanup = () => {
      delete this._tabToSelect;
      delete this._tabToRemoveForEmpty;
      delete this._shouldOverrideTabs;
      delete this._keepSelectedTab;
      resolveSelectPromise();
    };

    let removedEmptyTab = false;
    let initialTabWasEmpty = false;
    if (
      this._initialTab &&
      !(this._initialTab._shouldRemove && this._initialTab._veryPossiblyEmpty)
    ) {
      initialTabWasEmpty = !!this._initialTab._veryPossiblyEmpty;
      gBrowser.selectedTab = this._initialTab;
      this.moveTabToWorkspace(this._initialTab, this.activeWorkspace);
      gBrowser.moveTabTo(this._initialTab, {
        forceUngrouped: true,
        tabIndex: 0,
      });
      removedEmptyTab = true;
      delete this._initialTab;
    }

    if (
      this._tabToRemoveForEmpty &&
      !removedEmptyTab &&
      !this._shouldOverrideTabs
    ) {
      const tabs = gBrowser.tabs.filter(tab => !tab.collapsed);
      if (
        typeof this._tabToSelect === "number" &&
        this._tabToSelect >= 0 &&
        tabs[this._tabToSelect] &&
        (await this.#shouldShowTabInCurrentWorkspace(
          tabs[this._tabToSelect]
        )) &&
        tabs[this._tabToSelect] !== this._tabToRemoveForEmpty
      ) {
        this.log(`Found tab to select: ${this._tabToSelect}, ${tabs.length}`);
        setTimeout(() => {
          let tabToUse = gZenGlanceManager.getTabOrGlanceParent(
            tabs[this._tabToSelect + 1] || this._emptyTab
          );
          gBrowser.selectedTab = tabToUse;
          this._removedByStartupPage = true;
          gBrowser.removeTab(this._tabToRemoveForEmpty, {
            skipSessionStore: true,
          });
          cleanup();
        }, 0);
      } else {
        if (!this._keepSelectedTab) {
          this.selectEmptyTab();
          shownEmptyTab = true;
        }
        setTimeout(() => {
          this._removedByStartupPage = true;
          gBrowser.removeTab(this._tabToRemoveForEmpty, {
            skipSessionStore: true,
            animate: false,
          });
          cleanup();
        }, 0);
      }
    } else {
      setTimeout(() => {
        cleanup();
      }, 0);
    }

    await selectPromise;
    if (this._initialTab) {
      this._removedByStartupPage = true;
      gBrowser.removeTab(this._initialTab, {
        skipSessionStore: true,
      });
      delete this._initialTab;
    }

    const openOnStartup = Services.prefs.getBoolPref(
      "zen.urlbar.open-on-startup",
      true
    );
    shownEmptyTab &&= openOnStartup;
    initialTabWasEmpty &&= openOnStartup;

    // Wait for the next event loop to ensure that the startup focus logic by
    // firefox has finished doing it's thing.
    setTimeout(() => {
      setTimeout(() => {
        if (gZenVerticalTabsManager._canReplaceNewTab && shownEmptyTab) {
          BrowserCommands.openTab();
        } else if (shownEmptyTab || initialTabWasEmpty) {
          openLocation();
        } else {
          gBrowser.selectedBrowser.focus();
        }
      });
    });

    if (
      !gZenVerticalTabsManager._canReplaceNewTab &&
      !Services.prefs.getBoolPref("zen.workspaces.continue-where-left-off")
    ) {
      // Go through each tab and see if there's another tab with the same startup URL.
      // If we do find one, remove it.
      const newTabUrl = Services.prefs.getStringPref(
        "browser.startup.homepage"
      );
      const tabs = gBrowser.tabs.filter(
        tab =>
          !tab.collapsed && !tab.hasAttribute("zen-empty-tab") && !tab.pinned
      );
      for (const tab of tabs) {
        if (tab._originalUrl === newTabUrl && tab !== gBrowser.selectedTab) {
          gBrowser.removeTab(tab, {
            skipSessionStore: true,
          });
        }
      }
    }

    window.dispatchEvent(
      new CustomEvent("AfterWorkspacesSessionRestore", { bubbles: true })
    );
  }

  handleInitialTab(tab, isEmpty) {
    if (gZenUIManager.testingEnabled || !this.workspaceEnabled) {
      return;
    }
    // note: We cant access `gZenVerticalTabsManager._canReplaceNewTab` this early
    if (
      isEmpty &&
      Services.prefs.getBoolPref("zen.urlbar.replace-newtab", true)
    ) {
      tab._markedForReplacement = true;
      this._tabToRemoveForEmpty = tab;
    } else {
      this._initialTab = tab;
      this._initialTab._veryPossiblyEmpty = isEmpty;
    }
  }

  changeWorkspaceIcon() {
    let anchor = this.activeWorkspaceIndicator?.querySelector(
      ".zen-current-workspace-indicator-icon"
    );
    if (this.#contextMenuData?.workspaceId) {
      anchor = this.#contextMenuData.originalTarget;
    }
    const workspaceId =
      this.#contextMenuData?.workspaceId || this.activeWorkspace;
    if (!anchor) {
      return;
    }
    const hasNoIcon = anchor.hasAttribute("no-icon");
    anchor.removeAttribute("no-icon");
    if (hasNoIcon) {
      anchor.textContent = "";
    }
    gZenEmojiPicker.open(anchor, {
      closeOnSelect: false,
      allowNone: hasNoIcon,
      onSelect: async icon => {
        const workspace = this.getWorkspaceFromId(workspaceId);
        if (!workspace) {
          console.warn("No active workspace found to change icon");
          return;
        }
        workspace.icon = icon;
        await this.saveWorkspace(workspace);
      },
    });
  }

  shouldCloseWindow() {
    return (
      !window.toolbar.visible ||
      Services.prefs.getBoolPref("browser.tabs.closeWindowWithLastTab") ||
      (this.privateWindowOrDisabled && !this.isPrivateWindow)
    );
  }

  async #clearAnyZombieTabs() {
    const tabs = this.allStoredTabs;
    const workspaces = this.getWorkspaces();
    for (let tab of tabs) {
      const workspaceID = tab.getAttribute("zen-workspace-id");
      if (
        (workspaceID &&
          !tab.hasAttribute("zen-essential") &&
          !workspaces.find(workspace => workspace.uuid === workspaceID)) ||
        // Also remove empty tabs that are supposed to be from parent folders but
        // they dont exist anymore
        (tab.pinned && tab.hasAttribute("zen-empty-tab") && !tab.group)
      ) {
        // Remove any tabs where their workspace doesn't exist anymore
        this.log("Removed zombie tab from non-existing workspace", tab);
        gBrowser.unpinTab(tab);
        gBrowser.removeTab(tab, {
          skipSessionStore: true,
          closeWindowWithLastTab: false,
        });
      }
    }
  }

  handleTabBeforeClose(tab, closeWindowWithLastTab) {
    delete this._isClosingWindow;
    if (
      !this.workspaceEnabled ||
      this.__contextIsDelete ||
      this._removedByStartupPage
    ) {
      return null;
    }

    let workspaceID = tab.getAttribute("zen-workspace-id");
    if (!workspaceID) {
      return null;
    }

    let tabs = gBrowser.visibleTabs;
    let tabsPinned = tabs.filter(
      t => !this.shouldOpenNewTabIfLastUnpinnedTabIsClosed || !t.pinned
    );
    const shouldCloseWindow =
      closeWindowWithLastTab != null
        ? closeWindowWithLastTab
        : this.shouldCloseWindow();
    if (tabs.length === 1 && tabs[0] === tab) {
      if (shouldCloseWindow) {
        // We've already called beforeunload on all the relevant tabs if we get here,
        // so avoid calling it again:
        window.skipNextCanClose = true;

        // Closing the tab and replacing it with a blank one is notably slower
        // than closing the window right away. If the caller opts in, take
        // the fast path.
        if (!gBrowser._removingTabs.size) {
          // This call actually closes the window, unless the user
          // cancels the operation.  We are finished here in both cases.
          this._isClosingWindow = true;
        }
        return null;
      }
    } else if (tabsPinned.length === 1 && tabsPinned[0] === tab) {
      return this.selectEmptyTab();
    }

    return null;
  }

  handleTabBeforeRemove() {
    // We run this AFTER the beforeunload event check, so we can
    // be sure that if we get here, the tab is actually going to be removed,
    // and beforeunload won't be called again. See gh-12922 for an example.
    if (!this.workspaceEnabled || !this._isClosingWindow) {
      return;
    }
    // Inside a setTimeout to avoid reentrancy issues.
    setTimeout(() => {
      document.getElementById("cmd_closeWindow").doCommand();
    }, 100);
  }

  addPopupListeners() {
    const workspaceActions = document.getElementById("zenWorkspaceMoreActions");
    workspaceActions.addEventListener(
      "popupshowing",
      this.updateWorkspaceActionsMenu.bind(this)
    );
    workspaceActions.addEventListener("popuphidden", () => {
      setTimeout(() => {
        setTimeout(() => {
          this.#contextMenuData = null;
        }, 0);
      }, 0); // Delay to ensure the context menu data is cleared after the popup is hidden
    });

    const contextChangeContainerTabMenu = document.getElementById(
      "context_zenWorkspacesOpenInContainerTab"
    );
    contextChangeContainerTabMenu.addEventListener(
      "popupshowing",
      this.updateWorkspaceActionsMenuContainer.bind(this)
    );
    contextChangeContainerTabMenu.addEventListener(
      "command",
      this.contextChangeContainerTab.bind(this)
    );
  }

  generateMenuItemForWorkspace(workspace, disableCurrent = false) {
    const item = document.createXULElement("menuitem");
    item.className = "zen-workspace-context-menu-item";
    item.setAttribute("zen-workspace-id", workspace.uuid);
    if (!disableCurrent) {
      item.setAttribute("type", "radio");
    }
    if (workspace.uuid === this.activeWorkspace) {
      item.setAttribute(disableCurrent ? "disabled" : "checked", true);
    }
    let name = workspace.name;
    const iconIsSvg = workspace.icon && workspace.icon.endsWith(".svg");
    if (workspace.icon && workspace.icon !== "" && !iconIsSvg) {
      name = `${workspace.icon}  ${name}`;
    }
    item.setAttribute("label", name);
    if (iconIsSvg) {
      item.setAttribute("image", workspace.icon);
      item.classList.add("zen-workspace-context-icon");
    }
    return item;
  }

  #contextMenuData = null;
  updateWorkspaceActionsMenu(event) {
    if (event.target.id !== "zenWorkspaceMoreActions") {
      return;
    }
    const openInContainerMenuItem = document.getElementById(
      "context_zenWorkspacesOpenInContainerTab"
    );
    if (this.shouldShowContainers) {
      openInContainerMenuItem.removeAttribute("hidden");
    } else {
      openInContainerMenuItem.setAttribute("hidden", "true");
    }
    // Call parent node as on windows, the text can be double clicked
    let target;
    try {
      target = event.explicitOriginalTarget?.closest("toolbarbutton");
    } catch (e) {
      console.error("Error getting explicitOriginalTarget in context menu:", e);
    }
    this.#contextMenuData = {
      workspaceId: target?.getAttribute("zen-workspace-id"),
      originalTarget: target,
    };
    const workspaceName = document.getElementById("context_zenEditWorkspace");
    const themePicker = document.getElementById(
      "context_zenChangeWorkspaceTheme"
    );
    /* We can't show the rename input properly in collapsed state,
    so hide the workspace edit input */
    const isCollapsed = !Services.prefs.getBoolPref(
      "zen.view.sidebar-expanded"
    );
    workspaceName.hidden =
      isCollapsed ||
      (this.#contextMenuData.workspaceId &&
        this.#contextMenuData.workspaceId !== this.activeWorkspace);
    themePicker.hidden =
      this.#contextMenuData.workspaceId &&
      this.#contextMenuData.workspaceId !== this.activeWorkspace;
    const separator = document.getElementById("context_zenWorkspacesSeparator");
    for (const item of event.target.querySelectorAll(
      ".zen-workspace-context-menu-item"
    )) {
      item.remove();
    }
    if (!this.#contextMenuData.workspaceId) {
      separator.hidden = false;
      for (const workspace of this.getWorkspaces().reverse()) {
        const item = this.generateMenuItemForWorkspace(workspace);
        item.addEventListener("command", e => {
          this.changeWorkspaceWithID(
            e.target.closest("menuitem").getAttribute("zen-workspace-id")
          );
        });
        separator.after(item);
      }
    } else {
      separator.hidden = true;
    }
    event.target.addEventListener(
      "popuphidden",
      () => {
        this.#contextMenuData = null;
      },
      { once: true }
    );
  }

  updateWorkspaceActionsMenuContainer(event) {
    let workspace;
    if (this.#contextMenuData?.workspaceId) {
      workspace = this.getWorkspaceFromId(this.#contextMenuData.workspaceId);
    } else {
      workspace = this.getActiveWorkspaceFromCache();
    }
    let containerTabId = workspace.containerTabId;
    return window.createUserContextMenu(event, {
      isContextMenu: true,
      excludeUserContextId: containerTabId,
      showDefaultTab: true,
    });
  }

  saveWorkspace(workspaceData) {
    if (this.privateWindowOrDisabled) {
      return;
    }
    const workspacesData = this._workspaceCache;
    const index = workspacesData.findIndex(
      ws => ws.uuid === workspaceData.uuid
    );
    if (index !== -1) {
      workspacesData[index] = workspaceData;
    } else {
      workspacesData.push(workspaceData);
    }
    this.#propagateWorkspaceData();
  }

  removeWorkspace(windowID) {
    let { promise, resolve } = Promise.withResolvers();
    this.#deleteWorkspaceOwnedTabs(windowID);
    let workspacesData = this.getWorkspaces();
    // Remove the workspace from the cache
    workspacesData = workspacesData.filter(
      workspace => workspace.uuid !== windowID
    );
    window.addEventListener(
      "ZenWorkspacesUIUpdate",
      () => {
        resolve();
      },
      { once: true }
    );
    this.#propagateWorkspaceData(workspacesData);
    gBrowser.tabContainer._invalidateCachedVisibleTabs();
    return promise;
  }

  isWorkspaceActive(workspace) {
    return workspace.uuid === this.activeWorkspace;
  }

  getActiveWorkspace() {
    return this.getActiveWorkspaceFromCache();
  }

  workspaceHasIcon(workspace) {
    return workspace.icon && workspace.icon !== "";
  }

  getWorkspaceIcon(workspace) {
    if (this.workspaceHasIcon(workspace)) {
      return workspace.icon;
    }
    try {
      return new Intl.Segmenter()
        .segment(workspace.name)
        .containing()
        .segment.toUpperCase();
    } catch {
      return Array.from(workspace.name)[0]?.toUpperCase();
    }
  }

  get shouldShowContainers() {
    return (
      Services.prefs.getBoolPref("privacy.userContext.ui.enabled") &&
      !!ContextualIdentityService.getPublicIdentities().length
    );
  }

  #propagateWorkspaceData(aSpaceData = null) {
    if (!this.#hasInitialized || this.privateWindowOrDisabled) {
      return;
    }
    window.dispatchEvent(new CustomEvent("ZenWorkspaceDataChanged"), {
      bubbles: true,
    });
    window.gZenWindowSync.propagateWorkspacesToAllWindows(
      aSpaceData ?? this._workspaceCache
    );
  }

  propagateWorkspaces(aWorkspaces) {
    const previousWorkspaces = this._workspaceCache || [];
    let promises = [];
    let hasChanged = false;
    // Remove any workspace elements here that no longer exist
    for (const previousWorkspace of previousWorkspaces) {
      if (
        this.workspaceElement(previousWorkspace.uuid) &&
        !aWorkspaces.find(w => w.uuid === previousWorkspace.uuid)
      ) {
        let promise = Promise.resolve();
        if (this.isWorkspaceActive(previousWorkspace)) {
          // If the removed workspace was active, switch to another one
          const newActiveWorkspace =
            aWorkspaces.find(w => w.uuid !== previousWorkspace.uuid) || null;
          promise = this.changeWorkspace(newActiveWorkspace);
        }
        promise = promise.then(() => {
          this.workspaceElement(previousWorkspace.uuid)?.remove();
          delete this.lastSelectedWorkspaceTabs[previousWorkspace.uuid];
        });
        promises.push(promise);
        hasChanged = true;
      }
    }
    // Add any new workspace elements here
    for (const workspace of aWorkspaces) {
      if (!this.workspaceElement(workspace.uuid)) {
        this.#createWorkspaceTabsSection(workspace);
        hasChanged = true;
      }
    }
    // Order the workspace elements correctly
    let previousElement = null;
    const arrowScrollbox = document.getElementById("tabbrowser-arrowscrollbox");
    for (const workspace of aWorkspaces) {
      const workspaceElement = this.workspaceElement(workspace.uuid);
      if (workspaceElement) {
        if (previousElement === null) {
          arrowScrollbox.moveBefore(
            workspaceElement,
            arrowScrollbox.firstChild
          );
          hasChanged = true;
        } else if (previousElement.nextSibling !== workspaceElement) {
          arrowScrollbox.moveBefore(
            workspaceElement,
            previousElement.nextSibling
          );
          hasChanged = true;
        }
        previousElement = workspaceElement;
      }
    }
    return Promise.all(promises).then(() => {
      this._workspaceCache = aWorkspaces;
      if (hasChanged) {
        this.#fireSpaceUIUpdate();
      }
      this._organizeWorkspaceStripLocations(this.getActiveWorkspaceFromCache());
      this.updateTabsContainers();
      this.updateWorkspacesChangeContextMenu();
    });
  }

  async reorderWorkspace(id, newPosition) {
    if (this.privateWindowOrDisabled) {
      return;
    }
    const workspaces = this._workspaceCache;
    const workspace = workspaces.find(w => w.uuid === id);
    if (!workspace) {
      console.warn(`Workspace with ID ${id} not found for reordering.`);
      return;
    }
    // Remove the workspace from its current position
    const currentIndex = workspaces.indexOf(workspace);
    if (currentIndex === -1) {
      console.warn(`Workspace with ID ${id} not found in the list.`);
      return;
    }
    workspaces.splice(currentIndex, 1);
    // Insert the workspace at the new position
    if (newPosition < 0 || newPosition > workspaces.length) {
      console.warn(
        `Invalid position ${newPosition} for reordering workspace with ID ${id}.`
      );
      return;
    }
    workspaces.splice(newPosition, 0, workspace);
    // Propagate the changes if the order has changed
    if (currentIndex !== newPosition) {
      this.#propagateWorkspaceData();
    }
  }

  async openWorkspaceCreation() {
    let createForm;
    const previousWorkspace = this.getActiveWorkspace();
    document.documentElement.setAttribute("zen-creating-workspace", "true");
    await this.createAndSaveWorkspace("Space", undefined, false, 0, {
      beforeChangeCallback: async workspace => {
        createForm = document.createXULElement("zen-workspace-creation");
        createForm.setAttribute("workspace-id", workspace.uuid);
        createForm.setAttribute(
          "previous-workspace-id",
          previousWorkspace?.uuid || ""
        );
        gBrowser.tabContainer.after(createForm);
        await createForm.promiseInitialized;
      },
    });
    createForm.finishSetup();
  }

  #unpinnedTabsInWorkspace(workspaceID) {
    return Array.from(this.allStoredTabs).filter(
      tab =>
        tab.getAttribute("zen-workspace-id") === workspaceID &&
        tab.visible &&
        !tab.pinned
    );
  }

  #getClosableTabs(tabs) {
    const remainingTabs = tabs.filter(tab => {
      const attributes = [
        "selected",
        "multiselected",
        "pictureinpicture",
        "soundplaying",
      ];
      for (const attr of attributes) {
        if (tab.hasAttribute(attr)) {
          return false;
        }
      }
      const browser = tab.linkedBrowser;
      if (
        window.webrtcUI.browserHasStreams(browser) ||
        browser?.browsingContext?.currentWindowGlobal?.hasActivePeerConnections()
      ) {
        return false;
      }
      return true;
    });
    if (remainingTabs.length === 0) {
      return tabs; // If no tabs are safe to close, return all to force close
    }
    return remainingTabs;
  }

  #deleteAllUnpinnedTabsInWorkspace(tabs) {
    gBrowser.removeTabs(tabs, {
      closeWindowWithLastTab: false,
    });
  }

  #deleteWorkspaceOwnedTabs(workspaceID) {
    const tabs = this.allStoredTabs.filter(
      tab =>
        tab.getAttribute("zen-workspace-id") === workspaceID &&
        !tab.hasAttribute("zen-essential") &&
        !(tab.hasAttribute("zen-empty-tab") && !tab.group)
    );
    gBrowser.removeTabs(tabs, {
      closeWindowWithLastTab: false,
    });
  }

  async unloadWorkspace() {
    const workspaceId =
      this.#contextMenuData?.workspaceId || this.activeWorkspace;

    const tabsToUnload = this.allStoredTabs.filter(
      tab =>
        tab.getAttribute("zen-workspace-id") === workspaceId &&
        !tab.hasAttribute("zen-empty-tab") &&
        !tab.hasAttribute("zen-essential") &&
        !tab.hasAttribute("pending")
    );

    if (tabsToUnload.length === 0) {
      return;
    }

    this.log("Unloading workspace", workspaceId);

    await gBrowser.explicitUnloadTabs(tabsToUnload); // TODO: unit test this
  }

  async unloadAllOtherWorkspaces() {
    const workspaceId =
      this.#contextMenuData?.workspaceId || this.activeWorkspace;

    const tabsToUnload = this.allStoredTabs.filter(
      tab =>
        tab.getAttribute("zen-workspace-id") !== workspaceId &&
        !tab.hasAttribute("zen-empty-tab") &&
        !tab.hasAttribute("zen-essential") &&
        !tab.hasAttribute("pending")
    );

    await gBrowser.explicitUnloadTabs(tabsToUnload);
  }

  moveTabToWorkspace(tab, workspaceID) {
    return this.moveTabsToWorkspace([tab], workspaceID);
  }

  moveTabsToWorkspace(tabs, workspaceID) {
    for (let tab of tabs) {
      const workspaceContainer = this.workspaceElement(workspaceID);
      const container = tab.pinned
        ? workspaceContainer?.pinnedTabsContainer
        : workspaceContainer?.tabsContainer;
      if (container?.contains(tab)) {
        continue;
      }

      if (tab.hasAttribute("zen-essential")) {
        continue;
      }

      tab.owner = null;
      if (container) {
        if (tab.group?.hasAttribute("split-view-group")) {
          gBrowser.zenHandleTabMove(tab.group, () => {
            for (const subTab of tab.group.tabs) {
              subTab.setAttribute("zen-workspace-id", workspaceID);
            }
            container.insertBefore(tab.group, container.lastChild);
          });
          continue;
        }
        gBrowser.zenHandleTabMove(tab, () => {
          tab.setAttribute("zen-workspace-id", workspaceID);
          container.insertBefore(tab, container.lastChild);
        });
      }
      // also change glance tab if it's the same tab
      const glanceTab = tab.querySelector(".tabbrowser-tab[zen-glance-tab]");
      if (glanceTab) {
        glanceTab.setAttribute("zen-workspace-id", workspaceID);
      }
    }
    gBrowser.tabContainer._invalidateCachedTabs();
    return true;
  }

  #prepareNewWorkspace(space) {
    let tabCount = 0;
    for (let tab of gBrowser.tabs) {
      const isEssential = tab.getAttribute("zen-essential") === "true";
      if (
        !tab.hasAttribute("zen-workspace-id") &&
        !tab.pinned &&
        !isEssential
      ) {
        this.moveTabToWorkspace(tab, space.uuid);
        tabCount++;
      }
    }
    if (tabCount === 0) {
      this.selectEmptyTab();
    }
  }

  addChangeListeners(
    func,
    opts = {
      once: false,
    }
  ) {
    if (!this._changeListeners) {
      this._changeListeners = [];
    }
    this._changeListeners.push({ func, opts });
  }

  removeChangeListeners(func) {
    if (!this._changeListeners) {
      return;
    }
    this._changeListeners = this._changeListeners.filter(
      listener => listener.func !== func
    );
  }

  async changeWorkspaceWithID(workspaceID, ...args) {
    const workspace = this.getWorkspaceFromId(workspaceID);
    return await this.changeWorkspace(workspace, ...args);
  }

  async changeWorkspace(workspace, ...args) {
    if (!this.workspaceEnabled) {
      return workspace;
    }
    this.#currentSpaceSwitchContext.animations.forEach(animation => {
      animation.complete();
    });
    await this.#currentSpaceSwitchContext.promise;
    let { resolve, promise } = Promise.withResolvers();
    this.#currentSpaceSwitchContext = {
      promise,
      animations: [],
    };
    this.#inChangingWorkspace = true;
    try {
      this.log("Changing workspace to", workspace?.uuid);
      await this.#performWorkspaceChange(workspace, ...args);
    } catch (e) {
      console.error("gZenWorkspaces: Error changing workspace", e);
    }
    this.#inChangingWorkspace = false;
    resolve();
    return workspace;
  }

  _cancelSwipeAnimation() {
    this.#animateTabs(this.getActiveWorkspaceFromCache(), true);
  }

  async #performWorkspaceChange(
    workspace,
    { onInit = false, alwaysChange = false, whileScrolling = false } = {}
  ) {
    const previousWorkspace = this.getActiveWorkspace();
    alwaysChange = alwaysChange || onInit;
    this.activeWorkspace = workspace.uuid;
    if (
      previousWorkspace &&
      previousWorkspace.uuid === workspace.uuid &&
      !alwaysChange
    ) {
      this._cancelSwipeAnimation();
      return;
    }

    const workspaces = this.getWorkspaces();
    gZenFolders.cancelPopupTimer();

    // Refresh tab cache
    for (const otherWorkspace of workspaces) {
      const container = this.workspaceElement(otherWorkspace.uuid);
      container.active = otherWorkspace.uuid === workspace.uuid;
    }
    // note: We are calling this even though it is also called in `updateTabsContainers`. This is mostly
    // due to a race condition where the workspace strip is not updated before the tabs are moved.
    this.makeSureEmptyTabIsFirst();
    gBrowser.pinnedTabsContainer =
      this.pinnedTabsContainer || gBrowser.pinnedTabsContainer;
    gBrowser.tabContainer.pinnedTabsContainer =
      this.pinnedTabsContainer || gBrowser.tabContainer.pinnedTabsContainer;

    this.tabContainer._invalidateCachedTabs();
    if (!whileScrolling) {
      this._organizeWorkspaceStripLocations(previousWorkspace);
    }

    // Second pass: Handle tab selection
    this.tabContainer._invalidateCachedTabs();
    const tabToSelect = await this._handleTabSelection(
      workspace,
      onInit,
      previousWorkspace.uuid
    );
    if (tabToSelect?.linkedBrowser) {
      gBrowser.warmupTab(tabToSelect);
    }

    // Update UI and state
    const previousWorkspaceIndex = workspaces.findIndex(
      w => w.uuid === previousWorkspace.uuid
    );
    await this.#updateWorkspaceState(workspace, onInit, tabToSelect, {
      previousWorkspaceIndex,
      previousWorkspace,
    });
  }

  makeSureEmptyTabIsFirst() {
    const emptyTab = this._emptyTab;
    if (emptyTab) {
      emptyTab.setAttribute("zen-workspace-id", this.activeWorkspace);
      if (emptyTab.linkedBrowser) {
        gBrowser.TabStateFlusher.flush(emptyTab.linkedBrowser);
      }
      const container = this.activeWorkspaceStrip;
      if (container) {
        container.insertBefore(emptyTab, container.firstChild);
      }
    }
    this.#fixTabPositions();
  }

  #fixTabPositions() {
    // See issue https://github.com/zen-browser/desktop/issues/10157
    if (this.tabContainer) {
      this.tabContainer._invalidateCachedTabs();
    }
    // Fix tabs _tPos values relative to the actual order
    const tabs = gBrowser.tabs;
    const usedGroups = new Set();
    let tPos = 0; // _tPos is used for the session store, not needed for folders
    let pPos = 0; // _pPos is used for the pinned tabs manager
    const recurseFolder = tab => {
      if (tab.group) {
        recurseFolder(tab.group);
        if (!usedGroups.has(tab.group.id)) {
          usedGroups.add(tab.group.id);
          tab.group._pPos = pPos++;
        }
      }
    };
    for (const tab of tabs) {
      recurseFolder(tab);
      tab._tPos = tPos++;
      if (!tab.hasAttribute("zen-empty-tab")) {
        tab._pPos = pPos++;
      }
    }
  }

  #updatePaddingTopOnTabs(
    workspaceElement,
    essentialContainer,
    forAnimation = false
  ) {
    if (
      workspaceElement &&
      !(
        this.#inChangingWorkspace &&
        !forAnimation &&
        !this._alwaysAnimatePaddingTop
      )
    ) {
      delete this._alwaysAnimatePaddingTop;
      const essentialsHeight =
        window.windowUtils.getBoundsWithoutFlushing(essentialContainer).height;
      requestAnimationFrame(() => {
        workspaceElement.style.paddingTop = essentialsHeight + "px";
      });
    }
  }

  _organizeWorkspaceStripLocations(
    workspace,
    justMove = false,
    offsetPixels = 0
  ) {
    if (document.documentElement.hasAttribute("zen-creating-workspace")) {
      // If we are creating a workspace, we don't want to animate the strip
      return;
    }
    this._organizingWorkspaceStrip = true;
    const workspaces = this.getWorkspaces();
    let workspaceIndex = workspaces.findIndex(w => w.uuid === workspace.uuid);
    if (!justMove) {
      this._fixIndicatorsNames(workspaces);
    }
    const otherContainersEssentials = document.querySelectorAll(
      `#zen-essentials .zen-workspace-tabs-section`
    );
    const workspaceContextId = workspace.containerTabId;
    const nextWorkspaceContextId =
      workspaces[workspaceIndex + (offsetPixels > 0 ? -1 : 1)]?.containerTabId;
    for (const otherWorkspace of workspaces) {
      const element = this.workspaceElement(otherWorkspace.uuid);
      const newTransform =
        -(workspaceIndex - workspaces.indexOf(otherWorkspace)) * 100;
      element.style.transform = `translateX(${newTransform + offsetPixels / 2}%)`;
    }
    // Hide other essentials with different containerTabId
    for (const container of otherContainersEssentials) {
      // Get the next workspace contextId, if it's the same, dont apply offsetPixels
      // if it's not we do apply it
      if (
        container.getAttribute("container") != workspace.containerTabId &&
        this.shouldAnimateEssentials
      ) {
        container.setAttribute("hidden", "true");
      } else {
        container.removeAttribute("hidden");
      }
      if (
        nextWorkspaceContextId !== workspaceContextId &&
        offsetPixels &&
        this.shouldAnimateEssentials &&
        (container.getAttribute("container") == nextWorkspaceContextId ||
          container.getAttribute("container") == workspaceContextId)
      ) {
        container.removeAttribute("hidden");
        // Animate from the currently selected workspace
        if (container.getAttribute("container") == workspaceContextId) {
          container.style.transform = `translateX(${offsetPixels / 2}%)`;
        } else {
          // Animate from the next workspace, transitioning towards the current one
          container.style.transform = `translateX(${offsetPixels / 2 + (offsetPixels > 0 ? -100 : 100)}%)`;
        }
      }
    }
    if (offsetPixels) {
      // Find the next workspace we are scrolling to
      const nextWorkspace =
        workspaces[workspaceIndex + (offsetPixels > 0 ? -1 : 1)];
      if (nextWorkspace) {
        const {
          gradient: nextGradient,
          grain: nextGrain,
          toolbarGradient: nextToolbarGradient,
        } = gZenThemePicker.getGradientForWorkspace(nextWorkspace);
        const existingGrain =
          gZenThemePicker.getGradientForWorkspace(workspace).grain;
        const percentage = Math.abs(offsetPixels) / 200;
        [lazy.browserBackgroundElement, lazy.toolbarBackgroundElement].forEach(
          element => {
            element.style.setProperty(
              "--zen-background-opacity",
              1 - percentage
            );
          }
        );
        if (!this._hasAnimatedBackgrounds) {
          this._hasAnimatedBackgrounds = true;
          lazy.browserBackgroundElement.style.setProperty(
            "--zen-main-browser-background-old",
            nextGradient
          );
          lazy.toolbarBackgroundElement.style.setProperty(
            "--zen-main-browser-background-toolbar-old",
            nextToolbarGradient
          );
          document.documentElement.setAttribute("animating-background", "true");
        }
        // Fit the offsetPixels into the grain limits. Both ends may be nextGrain and existingGrain,
        // so we need to use the min and max of both. For example, existing may be 0.2 and next may be 0.5,
        // meaning we should convert the offset to a percentage between 0.2 and 0.5. BUT if existingGrain
        // is 0.5 and nextGrain is 0.2, we should still convert the offset to a percentage between 0.2 and 0.5.
        const minGrain = Math.min(existingGrain, nextGrain);
        const maxGrain = Math.max(existingGrain, nextGrain);
        const grainValue =
          minGrain +
          (maxGrain - minGrain) *
            (existingGrain > nextGrain ? 1 - percentage : percentage);
        if (!this.#inChangingWorkspace) {
          gZenThemePicker.updateNoise(grainValue);
        }
      }
    } else {
      delete this._hasAnimatedBackgrounds;
    }
    delete this._organizingWorkspaceStrip;
  }

  updateWorkspaceIndicator(currentWorkspace, workspaceIndicator) {
    if (!workspaceIndicator) {
      return;
    }
    const indicatorName = workspaceIndicator.querySelector(
      ".zen-current-workspace-indicator-name"
    );
    const indicatorIcon = workspaceIndicator.querySelector(
      ".zen-current-workspace-indicator-icon"
    );
    const iconStack = workspaceIndicator.querySelector(
      ".zen-current-workspace-indicator-stack"
    );

    if (this.workspaceHasIcon(currentWorkspace)) {
      indicatorIcon.removeAttribute("no-icon");
      iconStack.removeAttribute("no-icon");
    } else {
      indicatorIcon.setAttribute("no-icon", "true");
      iconStack.setAttribute("no-icon", "true");
    }
    const icon = this.getWorkspaceIcon(currentWorkspace);
    indicatorIcon.innerHTML = "";
    if (icon?.endsWith(".svg")) {
      const img = document.createElement("img");
      img.src = icon;
      indicatorIcon.appendChild(img);
    } else {
      indicatorIcon.textContent = icon;
    }
    indicatorName.textContent = currentWorkspace.name;
  }

  _fixIndicatorsNames(workspaces) {
    for (const workspace of workspaces) {
      const workspaceIndicator = this.workspaceElement(
        workspace.uuid
      )?.indicator;
      this.updateWorkspaceIndicator(workspace, workspaceIndicator);
    }
  }

  /* eslint-disable complexity */
  async #animateTabs(
    newWorkspace,
    shouldAnimate,
    tabToSelect = null,
    {
      previousWorkspaceIndex = null,
      previousWorkspace = null,
      onInit = false,
    } = {}
  ) {
    gZenUIManager.tabsWrapper.style.scrollbarWidth = "none";
    const kGlobalAnimationDuration = 0.2;
    this._animatingChange = true;
    const animations = [];
    const workspaces = this.getWorkspaces();
    const newWorkspaceIndex = workspaces.findIndex(
      w => w.uuid === newWorkspace.uuid
    );
    const isGoingLeft = newWorkspaceIndex <= previousWorkspaceIndex;
    const clonedEssentials = [];
    if (shouldAnimate && this.shouldAnimateEssentials && previousWorkspace) {
      for (const workspace of workspaces) {
        const essentialsContainer = this.getEssentialsSection(
          workspace.containerTabId
        );
        let lastCloned = clonedEssentials[clonedEssentials.length - 1];
        if (lastCloned && lastCloned.contextId == workspace.containerTabId) {
          lastCloned.repeat++;
          lastCloned.workspaces.push(workspace);
          continue;
        }
        essentialsContainer.setAttribute("hidden", "true");
        const essentialsClone = essentialsContainer.cloneNode(true);
        essentialsClone.removeAttribute("hidden");
        essentialsClone.setAttribute("cloned", "true");
        clonedEssentials.push({
          container: essentialsClone,
          workspaces: [workspace],
          contextId: workspace.containerTabId,
          originalContainer: essentialsContainer,
          repeat: 0,
        });
      }
    }
    document.documentElement.setAttribute("animating-background", "true");
    if (shouldAnimate && previousWorkspace) {
      let previousBackgroundOpacity =
        lazy.browserBackgroundElement.style.getPropertyValue(
          "--zen-background-opacity"
        );
      try {
        // Prevent NaN from being set
        if (previousBackgroundOpacity) {
          previousBackgroundOpacity = parseFloat(previousBackgroundOpacity);
        }
      } catch {
        previousBackgroundOpacity = 1;
      }
      if (previousBackgroundOpacity == 1 || !previousBackgroundOpacity) {
        previousBackgroundOpacity = 0;
      } else {
        previousBackgroundOpacity = 1 - previousBackgroundOpacity;
      }
      gZenThemePicker.previousBackgroundOpacity = previousBackgroundOpacity;
      let elements = [
        lazy.browserBackgroundElement,
        lazy.toolbarBackgroundElement,
      ];
      elements.forEach(element => {
        element.style.setProperty(
          "--zen-background-opacity",
          previousBackgroundOpacity
        );
      });
      animations.push(
        gZenUIManager.motion.animate(
          elements,
          {
            "--zen-background-opacity": [previousBackgroundOpacity, 1],
          },
          {
            type: "spring",
            bounce: 0,
            duration: kGlobalAnimationDuration,
          }
        )
      );
    }
    for (const element of document.querySelectorAll("zen-workspace")) {
      if (element.classList.contains("zen-essentials-container")) {
        continue;
      }
      const existingTransform = element.style.transform;
      const elementWorkspaceId = element.id;
      const elementWorkspaceIndex = workspaces.findIndex(
        w => w.uuid === elementWorkspaceId
      );
      const offset = -(newWorkspaceIndex - elementWorkspaceIndex) * 100;
      const newTransform = `translateX(${offset}%)`;
      if (shouldAnimate) {
        const existingPaddingTop = element.style.paddingTop;
        animations.push(
          gZenUIManager.motion.animate(
            element,
            {
              transform: existingTransform
                ? [existingTransform, newTransform]
                : newTransform,
              paddingTop: existingTransform
                ? [existingPaddingTop, existingPaddingTop]
                : existingPaddingTop,
            },
            {
              type: "spring",
              bounce: 0,
              duration: kGlobalAnimationDuration,
            }
          )
        );
      }
      element.active = offset === 0;
      if (offset === 0) {
        if (tabToSelect != gBrowser.selectedTab && !onInit) {
          gBrowser.selectedTab = tabToSelect;
        }
      }
    }
    if (this.shouldAnimateEssentials && previousWorkspace) {
      // Animate essentials
      const newWorkspaceEssentialsContainer = clonedEssentials.find(cloned =>
        cloned.workspaces.some(w => w.uuid === newWorkspace.uuid)
      );
      // Get a list of essentials containers that are in between the first and last workspace
      const essentialsContainersInBetween = clonedEssentials.filter(cloned => {
        const essentialsWorkspaces = cloned.workspaces;
        const firstIndex = workspaces.findIndex(
          w => w.uuid === essentialsWorkspaces[0].uuid
        );
        const lastIndex = workspaces.findIndex(
          w =>
            w.uuid ===
            essentialsWorkspaces[essentialsWorkspaces.length - 1].uuid
        );

        const [start, end] = [
          Math.min(previousWorkspaceIndex, newWorkspaceIndex),
          Math.max(previousWorkspaceIndex, newWorkspaceIndex),
        ];

        // Check if any part of the container overlaps with the movement range
        return firstIndex <= end && lastIndex >= start;
      });
      for (const cloned of clonedEssentials) {
        const container = cloned.container;
        const essentialsWorkspaces = cloned.workspaces;
        const repeats = cloned.repeat;
        // Animate like the workspaces above expect essentials are a bit more
        // complicated because they are not based on workspaces but on containers
        // So, if we have the following arangement:
        //  | [workspace1] [workspace2] [workspace3] [workspace4]
        //  | [container1] [container1] [container2] [container1]
        // And if we are changing from workspace 1 to workspace 4,
        // we should be doing the following:
        // First container (repeat 2 times) will stay in place until
        // we reach container 3, then animate to the left and container 2
        // also move to the left after that while container 1 in workspace 4
        // will slide in from the right

        // Get the index from first and last workspace
        const firstWorkspaceIndex = workspaces.findIndex(
          w => w.uuid === essentialsWorkspaces[0].uuid
        );
        const lastWorkspaceIndex = workspaces.findIndex(
          w =>
            w.uuid ===
            essentialsWorkspaces[essentialsWorkspaces.length - 1].uuid
        );
        cloned.originalContainer.style.removeProperty("transform");
        // Check if the container is even going to appear on the screen, to save on animation
        if (
          // We also need to check if the container is even going to appear on the screen.
          // In order to do this, we need to check if the container is between the first and last workspace.
          // Note that essential containers can have multiple workspaces,
          // so we need to check if any of the workspaces in the container are between the
          // first and last workspace.
          !essentialsContainersInBetween.find(
            ce =>
              ce.workspaces.some(
                w => w.uuid === essentialsWorkspaces[0].uuid
              ) &&
              ce.workspaces.some(
                w =>
                  w.uuid ===
                  essentialsWorkspaces[essentialsWorkspaces.length - 1].uuid
              )
          )
        ) {
          continue;
        }
        cloned.originalContainer.parentNode.appendChild(container);
        let stepsInBetween =
          Math.abs(
            newWorkspaceIndex -
              (isGoingLeft ? firstWorkspaceIndex : lastWorkspaceIndex)
          ) + 1;
        const usingSameContainer =
          newWorkspaceEssentialsContainer?.workspaces.some(
            w => w.uuid === newWorkspace.uuid
          ) &&
          newWorkspaceEssentialsContainer?.workspaces.some(
            w => w.uuid === previousWorkspace.uuid
          );
        let newOffset =
          -(
            newWorkspaceIndex -
            (isGoingLeft ? firstWorkspaceIndex : lastWorkspaceIndex) +
            (!isGoingLeft ? repeats - 1 : -repeats + 1)
          ) * 100;

        let existingOffset =
          -(
            newWorkspaceIndex -
            (isGoingLeft ? lastWorkspaceIndex : firstWorkspaceIndex) +
            (isGoingLeft ? repeats - 1 : -repeats + 1)
          ) * 100;

        // If we are on the same container and both new and old workspace are in the same "essentialsWorkspaces"
        // we can simply not animate the essentials
        if (
          usingSameContainer &&
          essentialsWorkspaces.some(w => w.uuid === newWorkspace.uuid) &&
          essentialsWorkspaces.some(w => w.uuid === previousWorkspace.uuid)
        ) {
          newOffset = 0;
          existingOffset = 0;
        }

        const needsOffsetAdjustment =
          stepsInBetween > essentialsWorkspaces.length || usingSameContainer;

        if (repeats > 0 && needsOffsetAdjustment) {
          if (!isGoingLeft) {
            if (existingOffset !== 0) {
              existingOffset += 100;
            }
            if (newOffset !== 0) {
              newOffset += 100;
            }
          } else {
            if (existingOffset !== 0) {
              existingOffset -= 100;
            }
            if (newOffset !== 0) {
              newOffset -= 100;
            }
          }
        }

        // Special case: going forward from single reused container to a new one
        if (
          !usingSameContainer &&
          !isGoingLeft &&
          lastWorkspaceIndex === newWorkspaceIndex - 1
        ) {
          existingOffset = 0;
          newOffset = -100;
          stepsInBetween = 1;
        }
        if (
          !usingSameContainer &&
          isGoingLeft &&
          firstWorkspaceIndex === newWorkspaceIndex + 1
        ) {
          existingOffset = 0;
          newOffset = 100;
          stepsInBetween = 1;
        }
        if (
          !usingSameContainer &&
          isGoingLeft &&
          (firstWorkspaceIndex === newWorkspaceIndex - 1 ||
            firstWorkspaceIndex === newWorkspaceIndex)
        ) {
          existingOffset = -100;
          newOffset = 0;
          stepsInBetween = 1;
        }
        if (
          !usingSameContainer &&
          !isGoingLeft &&
          firstWorkspaceIndex === newWorkspaceIndex
        ) {
          existingOffset = 100;
          newOffset = 0;
          stepsInBetween = 1;
        }

        const newTransform = `translateX(${newOffset}%)`;
        let existingTransform = `translateX(${existingOffset}%)`;
        if (container.style.transform && container.style.transform !== "none") {
          existingTransform = container.style.transform;
        }
        if (shouldAnimate) {
          container.style.transform = existingTransform;
          animations.push(
            gZenUIManager.motion.animate(
              container,
              {
                transform: [
                  existingTransform,
                  new Array(stepsInBetween).fill(newTransform).join(","),
                ],
              },
              {
                type: "spring",
                bounce: 0,
                duration: kGlobalAnimationDuration,
              }
            )
          );
        }
      }
    }
    if (shouldAnimate) {
      gZenUIManager._preventToolbarRebuild = true;
      gZenUIManager.updateTabsToolbar();
    }
    let promiseTimeout = new Promise(resolve =>
      setTimeout(resolve, kGlobalAnimationDuration * 1000 + 50)
    );
    this.#currentSpaceSwitchContext.animations = animations;
    // See issue https://github.com/zen-browser/desktop/issues/9334, we need to add
    // some sort of timeout to the animation promise, just in case it gets stuck.
    // We are doing a race between the timeout and the animations finishing.
    await Promise.race([Promise.all(animations), promiseTimeout]).catch(
      console.error
    );
    this.#currentSpaceSwitchContext.animations = [];
    document.documentElement.removeAttribute("animating-background");
    if (shouldAnimate) {
      for (const cloned of clonedEssentials) {
        cloned.container.remove();
      }
      this._alwaysAnimatePaddingTop = true;
      this.updateTabsContainers();
    }
    const essentialsContainer = this.getEssentialsSection(
      newWorkspace.containerTabId
    );
    essentialsContainer.removeAttribute("hidden");
    essentialsContainer.style.transform = "none";
    gBrowser.tabContainer._invalidateCachedTabs();
    gZenUIManager.tabsWrapper.style.removeProperty("scrollbar-width");
    this._animatingChange = false;
  }

  _shouldChangeToTab(aTab) {
    return !(aTab?.pinned && aTab?.hasAttribute("pending")) && !aTab?.closing;
  }

  async #shouldShowTabInCurrentWorkspace(tab) {
    const currentWorkspace = this.getActiveWorkspaceFromCache();
    return this._shouldShowTab(
      tab,
      currentWorkspace.uuid,
      currentWorkspace.containerTabId,
      this.getWorkspaces()
    );
  }

  _shouldShowTab(tab, workspaceUuid, containerId, workspaces) {
    const isEssential = tab.getAttribute("zen-essential") === "true";
    const tabWorkspaceId = tab.getAttribute("zen-workspace-id");
    const tabContextId = tab.getAttribute("usercontextid") ?? "0";

    if (tab.hasAttribute("zen-glance-tab")) {
      return true; // Always show glance tabs
    }

    // See https://github.com/zen-browser/desktop/issues/10666, we should never
    // show closing tabs and consider them as not part of any workspace. This will
    // invalidate the `lastSelectedTab[previousWorkspaceId]` logic in `_handleTabSelection`
    if (tab.closing) {
      return false; // Never show closing tabs
    }

    // Handle essential tabs
    if (isEssential) {
      if (!this.containerSpecificEssentials) {
        return true; // Show all essential tabs when containerSpecificEssentials is false
      }

      if (containerId) {
        // In workspaces with default container: Show essentials that match the container
        return tabContextId == containerId;
      }
      // In workspaces without a default container: Show essentials that aren't in container-specific workspaces
      // or have usercontextid="0" or no usercontextid
      return (
        !tabContextId ||
        tabContextId === "0" ||
        !workspaces.some(
          workspace => workspace.containerTabId === parseInt(tabContextId, 10)
        )
      );
    }

    // For non-essential tabs (both normal and pinned)
    if (!tabWorkspaceId) {
      // Assign workspace ID to tabs without one
      this.moveTabToWorkspace(tab, workspaceUuid);
      return true;
    }

    // Show if tab belongs to current workspace
    return tabWorkspaceId === workspaceUuid;
  }

  async _handleTabSelection(workspace, onInit, previousWorkspaceId) {
    const currentSelectedTab = gBrowser.selectedTab;
    const oldWorkspaceId = previousWorkspaceId;
    const lastSelectedTab = this.lastSelectedWorkspaceTabs[workspace.uuid];

    const containerId = workspace.containerTabId?.toString();
    const workspaces = this.getWorkspaces();

    // Save current tab as last selected for old workspace if it shouldn't be visible in new workspace
    if (oldWorkspaceId && oldWorkspaceId !== workspace.uuid) {
      this.lastSelectedWorkspaceTabs[oldWorkspaceId] =
        gZenGlanceManager.getTabOrGlanceParent(currentSelectedTab);
    }

    let tabToSelect = null;
    // Try last selected tab if it is visible
    if (
      lastSelectedTab &&
      this._shouldShowTab(
        lastSelectedTab,
        workspace.uuid,
        containerId,
        workspaces
      )
    ) {
      tabToSelect = lastSelectedTab;
    }
    // Find first suitable tab
    else {
      tabToSelect = gBrowser.visibleTabs.find(tab => !tab.pinned);
      if (!tabToSelect && gBrowser.visibleTabs.length) {
        tabToSelect = gBrowser.visibleTabs[gBrowser.visibleTabs.length - 1];
      }
      if (!tabToSelect || !this._shouldChangeToTab(tabToSelect)) {
        // Never select an essential tab
        tabToSelect = null;
      }
    }

    // If we found a tab to select, select it
    if (!onInit && !tabToSelect) {
      // Create new tab if needed and no suitable tab was found
      const newTab = this.selectEmptyTab();
      tabToSelect = newTab;
    }
    if (tabToSelect && !onInit) {
      tabToSelect._visuallySelected = true;
    }

    // Always make sure we always unselect the tab from the old workspace
    if (currentSelectedTab && currentSelectedTab !== tabToSelect) {
      currentSelectedTab._selected = false;
    }
    return tabToSelect;
  }

  async #updateWorkspaceState(
    workspace,
    onInit,
    tabToSelect,
    { previousWorkspaceIndex, previousWorkspace } = {}
  ) {
    // Recalculate new tab observers
    gBrowser.tabContainer.observe(
      null,
      "nsPref:changed",
      "privacy.userContext.enabled"
    );

    gBrowser.tabContainer.arrowScrollbox = this.activeScrollbox;

    // Update workspace UI
    requestAnimationFrame(() => {
      gZenThemePicker.onWorkspaceChange(workspace);
    });

    gZenUIManager.tabsWrapper.scrollbarWidth = "none";
    this.workspaceIcons.activeIndex = workspace.uuid;
    await this.#animateTabs(
      workspace,
      !onInit && !this._animatingChange,
      tabToSelect,
      {
        previousWorkspaceIndex,
        previousWorkspace,
        onInit,
      }
    );
    this._organizeWorkspaceStripLocations(workspace, true);
    gZenUIManager.tabsWrapper.style.scrollbarWidth = "";

    // Notify listeners
    if (this._changeListeners?.length) {
      for (const listener of this._changeListeners) {
        const { func, opts } = listener;
        await func({ workspace, onInit });
        if (opts.once) {
          this.removeChangeListeners(func);
        }
      }
    }

    // Avoid forcing a startup toolbar rebuild when there are no
    // workspace-specific bookmark assignments to apply.
    const hasWorkspaceBookmarks = !!Object.keys(
      this._workspaceBookmarksCache?.bookmarks || {}
    ).length;
    if (!onInit || hasWorkspaceBookmarks) {
      this._invalidateBookmarkContainers();
    }

    // Update workspace indicator
    await this.updateWorkspaceIndicator(workspace, this.workspaceIndicator);

    // Fix ctrl+tab behavior. Note, we dont call it with "await" because we dont want to wait for it
    this.#fixCtrlTabBehavior();

    // Bug: When updating from previous versions, we used to hide the tabs not used in the new workspace
    //  we now need to show them again.
    // TODO: Remove this on future versions
    if (onInit) {
      for (const tab of this.allStoredTabs) {
        gBrowser.showTab(tab);
      }
      for (const tab of gBrowser.tabs) {
        if (
          !tab.hasAttribute("zen-workspace-id") &&
          !tab.hasAttribute("zen-workspace-id")
        ) {
          tab.setAttribute("zen-workspace-id", workspace.uuid);
        }
      }
      this.#fireSpaceUIUpdate();
    }
  }

  #fireSpaceUIUpdate() {
    window.dispatchEvent(
      new CustomEvent("ZenWorkspacesUIUpdate", {
        bubbles: true,
        detail: { activeIndex: this.activeWorkspace },
      })
    );
  }

  // Intentionally keep it as async!
  async #fixCtrlTabBehavior() {
    ctrlTab.uninit();
    ctrlTab.readPref();
  }

  _invalidateBookmarkContainers() {
    for (let i = 0, len = this.bookmarkMenus.length; i < len; i++) {
      const element = document.getElementById(this.bookmarkMenus[i]);
      if (element && element._placesView) {
        const placesView = element._placesView;
        placesView.invalidateContainer(placesView._resultNode);
      }
    }
    BookmarkingUI.updateEmptyToolbarMessage();
  }

  updateWorkspacesChangeContextMenu() {
    if (gZenWorkspaces.privateWindowOrDisabled) {
      return;
    }
    const workspaces = this.getWorkspaces();
    const ctxCommand = document.getElementById("cmd_zenCtxDeleteWorkspace");
    if (workspaces.length <= 1) {
      ctxCommand.setAttribute("disabled", "true");
    } else {
      ctxCommand.removeAttribute("disabled");
    }

    let menuPopupID = "moveTabOptionsMenu";
    let menuPopup = document.getElementById(menuPopupID);
    let menubar = document.getElementById("zen-spaces-menubar");
    if (!menuPopup || !menubar) {
      return;
    }
    let itemsToFill = [menubar.querySelector("menupopup"), menuPopup];
    for (const popup of itemsToFill) {
      let isMoveTabPopup = popup.id === menuPopupID;
      for (const item of popup.querySelectorAll(
        ".zen-workspace-context-menu-item"
      )) {
        item.remove();
      }
      const separator = document.createXULElement("menuseparator");
      separator.classList.add("zen-workspace-context-menu-item");
      if (isMoveTabPopup) {
        popup.prepend(separator);
      } else {
        popup.appendChild(separator);
      }
      let i = 0;
      for (let workspace of isMoveTabPopup
        ? workspaces.reverse()
        : workspaces) {
        const menuItem = this.generateMenuItemForWorkspace(
          workspace,
          /* disableCurrent = */ isMoveTabPopup
        );
        if (isMoveTabPopup) {
          popup.prepend(menuItem);
          menuItem.setAttribute("command", "cmd_zenChangeWorkspaceTab");
        } else {
          if (i < 10) {
            menuItem.setAttribute("key", `zen-workspace-switch-${i + 1}`);
          }
          menuItem.addEventListener("command", () => {
            this.changeWorkspace(workspace);
          });
          popup.appendChild(menuItem);
        }
        i++;
      }
    }
  }

  #createWorkspaceData(name, icon, containerTabId = 0) {
    if (!this.currentWindowIsSyncing) {
      containerTabId =
        parseInt(gBrowser.selectedTab.getAttribute("usercontextid")) || 0;
      let label =
        ContextualIdentityService.getUserContextLabel(containerTabId) ||
        "Default";
      name = this.isPrivateWindow ? "Incognito" : label;
      if (this.isPrivateWindow) {
        icon = gZenEmojiPicker.getSVGURL("eye.svg");
      }
    }
    let workspace = {
      uuid: gZenUIManager.generateUuidv4(),
      icon,
      name,
      theme: nsZenThemePicker.getTheme([]),
      containerTabId,
    };
    return workspace;
  }

  async createAndSaveWorkspace(
    name = "Space",
    icon = undefined,
    dontChange = false,
    containerTabId = 0,
    { beforeChangeCallback } = { beforeChangeCallback: null } // Callback to run before changing workspace
  ) {
    if (!this.workspaceEnabled) {
      return null;
    }
    // get extra tabs remaning (e.g. on new profiles) and just move them to the new workspace
    const extraTabs = Array.from(
      gBrowser.tabContainer.arrowScrollbox.children
    ).filter(
      child =>
        gBrowser.isTab(child) &&
        !child.hasAttribute("zen-workspace-id") &&
        !child.hasAttribute("zen-empty-tab") &&
        !child.hasAttribute("zen-essential")
    );
    let workspaceData = this.#createWorkspaceData(name, icon, containerTabId);
    if (!dontChange) {
      this.#prepareNewWorkspace(workspaceData);
      this.#createWorkspaceTabsSection(workspaceData, extraTabs);
      this._organizeWorkspaceStripLocations(workspaceData);
    }
    this.saveWorkspace(workspaceData);
    if (!dontChange) {
      if (beforeChangeCallback) {
        try {
          await beforeChangeCallback(workspaceData);
        } catch (e) {
          console.error("Error in beforeChangeCallback:", e);
        }
      }
      this.registerPinnedResizeObserver();
      this.updateTabsContainers({
        target: this.workspaceElement(workspaceData.uuid).pinnedTabsContainer,
      });
      let changed = !!extraTabs.length;
      if (changed) {
        gBrowser.tabContainer._invalidateCachedTabs();
        gBrowser.selectedTab = extraTabs[0];
      }
      await this.changeWorkspace(workspaceData);
    }
    this.onWindowResize();
    return workspaceData;
  }

  updateTabsContainers(target = undefined, forAnimation = false) {
    this.makeSureEmptyTabIsFirst();
    if (target && !target.target?.parentNode) {
      target = null;
    }
    this.onPinnedTabsResize(
      // This is what happens when we join a resize observer, an event listener
      // while using it as a method.
      [
        {
          target:
            (target?.target ? target.target : target) ??
            this.pinnedTabsContainer,
        },
      ],
      forAnimation
    );
  }

  updateShouldHideSeparator(
    arrowScrollbox,
    pinnedContainer,
    fromTabSelection = false
  ) {
    const visibleTabsFound = () => {
      let count = 0;
      for (const child of arrowScrollbox.children) {
        if (
          !child.hasAttribute("hidden") &&
          !child.closing &&
          !child.hasAttribute("zen-empty-tab")
        ) {
          count++;
          if (count > 1) {
            // Early return
            return true;
          }
        }
      }
      return false;
    };

    // <= 2 because we have the empty tab and the new tab button
    const shouldHideSeparator = fromTabSelection
      ? pinnedContainer.hasAttribute("hide-separator")
      : !visibleTabsFound();
    if (shouldHideSeparator) {
      pinnedContainer.setAttribute("hide-separator", "true");
    } else {
      const workspaceID = pinnedContainer.getAttribute("zen-workspace-id");
      const tabs = this.#unpinnedTabsInWorkspace(workspaceID);
      const closableTabs = this.#getClosableTabs(tabs);
      const button = pinnedContainer.querySelector(
        ".zen-workspace-close-unpinned-tabs-button"
      );
      if (tabs.length === closableTabs.length) {
        button.setAttribute("can-close", "true");
      } else {
        button.removeAttribute("can-close");
      }
      pinnedContainer.removeAttribute("hide-separator");
    }
  }

  onPinnedTabsResize(entries, forAnimation = false) {
    if (
      document.documentElement.hasAttribute("inDOMFullscreen") ||
      !this._hasInitializedTabsStrip ||
      (this._organizingWorkspaceStrip && !forAnimation) ||
      document.documentElement.hasAttribute("zen-creating-workspace") ||
      document.documentElement.hasAttribute("customizing")
    ) {
      return;
    }
    // forAnimation may be of type "ResizeObserver" if it's not a boolean, just ignore it
    if (typeof forAnimation !== "boolean") {
      forAnimation = false;
    }
    for (const entry of entries) {
      let originalWorkspaceId = entry.target.getAttribute("zen-workspace-id");
      if (!originalWorkspaceId) {
        originalWorkspaceId =
          entry.target.closest("zen-workspace")?.id || this.activeWorkspace;
      }
      const workspacesIds = [];
      if (entry.target.closest("#zen-essentials")) {
        // Get all workspaces that have the same userContextId
        const userContextId = parseInt(
          entry.target.getAttribute("container") || "0"
        );
        const workspaces = this.getWorkspaces().filter(
          w => w.containerTabId === userContextId
        );
        workspacesIds.push(...workspaces.map(w => w.uuid));
      } else {
        workspacesIds.push(originalWorkspaceId);
      }
      for (const workspaceId of workspacesIds) {
        const workspaceElement = this.workspaceElement(workspaceId);
        const workspaceObject = this.getWorkspaceFromId(workspaceId);
        if (!workspaceElement || !workspaceObject) {
          console.warn(
            "Workspace element or object not found for id",
            workspaceId
          );
          continue;
        }
        const arrowScrollbox = workspaceElement.tabsContainer;
        const pinnedContainer = workspaceElement.pinnedTabsContainer;
        const essentialContainer = this.getEssentialsSection(
          workspaceObject.containerTabId
        );
        const essentialNumChildren = essentialContainer.children.length;
        let essentialHackType = 0;
        if (essentialNumChildren === 6 || essentialNumChildren === 9) {
          essentialHackType = 1;
        } else if (essentialNumChildren % 2 === 0 && essentialNumChildren < 8) {
          essentialHackType = 2;
        } else if (essentialNumChildren === 5) {
          essentialHackType = 3;
        }
        if (essentialHackType > 0) {
          essentialContainer.setAttribute("data-hack-type", essentialHackType);
        } else {
          essentialContainer.removeAttribute("data-hack-type");
        }
        this.#updatePaddingTopOnTabs(
          workspaceElement,
          essentialContainer,
          forAnimation
        );
        this.updateShouldHideSeparator(arrowScrollbox, pinnedContainer);
      }
    }
  }

  async onTabBrowserInserted(event) {
    let tab = event.originalTarget;
    const isEssential = tab.getAttribute("zen-essential") === "true";
    const workspaceID = tab.getAttribute("zen-workspace-id");

    if (!this.workspaceEnabled || isEssential) {
      return;
    }

    if (workspaceID) {
      if (
        tab.hasAttribute("change-workspace") &&
        this.moveTabToWorkspace(tab, workspaceID)
      ) {
        this.lastSelectedWorkspaceTabs[workspaceID] =
          gZenGlanceManager.getTabOrGlanceParent(tab);
        tab.removeAttribute("change-workspace");
        const workspace = this.getWorkspaceFromId(workspaceID);
        setTimeout(() => {
          this.changeWorkspace(workspace);
        }, 0);
      }
      return;
    }

    let activeWorkspace = this.getActiveWorkspace();
    if (!activeWorkspace) {
      return;
    }
    if (tab.hasAttribute("zen-workspace-id")) {
      const tabWorkspaceId = tab.getAttribute("zen-workspace-id");
      this.moveTabToWorkspace(tab, tabWorkspaceId);
      await this.changeWorkspaceWithID(tabWorkspaceId);
    } else {
      tab.setAttribute("zen-workspace-id", activeWorkspace.uuid);
    }
  }

  #changeToEmptyTab() {
    const isEmpty = gBrowser.selectedTab.hasAttribute("zen-empty-tab");
    gZenCompactModeManager.sidebar.toggleAttribute(
      "zen-has-empty-tab",
      isEmpty
    );
    document.documentElement.setAttribute("zen-has-empty-tab", isEmpty);
  }

  async onLocationChange(event) {
    let tab = event.target;
    this.#changeToEmptyTab();
    if (
      !this.workspaceEnabled ||
      this.#inChangingWorkspace ||
      this._isClosingWindow
    ) {
      return;
    }

    if (tab.hasAttribute("zen-glance-tab")) {
      // Extract from parent node so we are not selecting the wrong (current) tab
      tab = tab.parentNode.closest(".tabbrowser-tab");
      console.assert(tab, "Tab not found for zen-glance-tab");
    }
    const workspaceID = tab.getAttribute("zen-workspace-id");
    const isEssential = tab.getAttribute("zen-essential") === "true";
    if (tab.hasAttribute("zen-empty-tab")) {
      return;
    }

    if (!isEssential) {
      const activeWorkspace = this.getActiveWorkspace();
      if (!activeWorkspace) {
        return;
      }

      // Only update last selected tab for non-essential tabs in their workspace
      if (workspaceID === activeWorkspace.uuid) {
        this.lastSelectedWorkspaceTabs[workspaceID] =
          gZenGlanceManager.getTabOrGlanceParent(tab);
      }

      // Switch workspace if needed
      if (
        workspaceID &&
        workspaceID !== activeWorkspace.uuid &&
        this._hasInitializedTabsStrip
      ) {
        const workspaceToChange = this.getWorkspaceFromId(workspaceID);
        if (!workspaceToChange) {
          return;
        }
        await this.changeWorkspace(workspaceToChange);
      }
    }
  }

  // Context menu management
  async contextChangeContainerTab(event) {
    this._organizingWorkspaceStrip = true;
    let workspaces = this.getWorkspaces();
    let workspace = workspaces.find(
      w =>
        w.uuid === (this.#contextMenuData?.workspaceId || this.activeWorkspace)
    );
    let userContextId = parseInt(
      event.target.getAttribute("data-usercontextid")
    );
    workspace.containerTabId = userContextId + 0; // +0 to convert to number
    this.saveWorkspace(workspace);
  }

  async closeAllUnpinnedTabs() {
    const workspaceId =
      this.#contextMenuData?.workspaceId || this.activeWorkspace;
    const unpinnedTabs = this.#unpinnedTabsInWorkspace(workspaceId);
    const closableTabs = this.#getClosableTabs(unpinnedTabs);

    if (!closableTabs.length) {
      return;
    }
    this.#deleteAllUnpinnedTabsInWorkspace(closableTabs);

    const restoreClosedTabsShortcut =
      gZenKeyboardShortcutsManager.getShortcutDisplayFromCommand(
        "History:RestoreLastClosedTabOrWindowOrSession"
      );

    gZenUIManager.showToast("zen-workspaces-close-all-unpinned-tabs-toast", {
      l10nArgs: {
        shortcut: restoreClosedTabsShortcut,
      },
    });
  }

  async contextDeleteWorkspace() {
    const workspaceId =
      this.#contextMenuData?.workspaceId || this.activeWorkspace;
    const [title, body] = await document.l10n.formatValues([
      { id: "zen-workspaces-delete-workspace-title" },
      {
        id: "zen-workspaces-delete-workspace-body",
        args: { name: this.getWorkspaceFromId(workspaceId).name },
      },
    ]);
    if (Services.prompt.confirm(null, title, body)) {
      this.removeWorkspace(workspaceId);
    }
  }

  findTabToBlur(tab) {
    if ((!this._shouldChangeToTab(tab) || !tab) && this._emptyTab) {
      return this._emptyTab;
    }
    return tab;
  }

  async changeWorkspaceShortcut(
    offset = 1,
    whileScrolling = false,
    disableWrap = false
  ) {
    // Cycle through workspaces
    let workspaces = this.getWorkspaces();
    let activeWorkspace = this.getActiveWorkspace();
    let workspaceIndex = workspaces.indexOf(activeWorkspace);

    // note: offset can be negative
    let targetIndex = workspaceIndex + offset;
    if (this.shouldWrapAroundNavigation && !disableWrap) {
      // Add length to handle negative indices and loop
      targetIndex = (targetIndex + workspaces.length) % workspaces.length;
    } else {
      // Clamp within bounds to disable looping
      targetIndex = Math.max(0, Math.min(workspaces.length - 1, targetIndex));
    }

    let nextWorkspace = workspaces[targetIndex];
    return await this.changeWorkspace(nextWorkspace, { whileScrolling });
  }

  #initializeWorkspaceTabContextMenus() {
    if (this.privateWindowOrDisabled) {
      const commandsToDisable = [
        "cmd_zenOpenFolderCreation",
        "cmd_zenOpenWorkspaceCreation",
        "zen-context-menu-new-folder-toolbar",
      ];
      commandsToDisable.forEach(cmd => {
        const element = document.getElementById(cmd);
        if (element) {
          element.setAttribute("disabled", true);
        }
      });
    }
  }

  async changeTabWorkspace(workspaceID) {
    const tabs = TabContextMenu.contextTab.multiselected
      ? gBrowser.selectedTabs
      : [TabContextMenu.contextTab];
    document.getElementById("tabContextMenu").hidePopup();
    for (let tab of tabs) {
      const previousWorkspaceID = tab.getAttribute("zen-workspace-id");
      this.moveTabToWorkspace(tab, workspaceID);
      if (this.lastSelectedWorkspaceTabs[previousWorkspaceID] === tab) {
        // This tab is no longer the last selected tab in the previous workspace because it's being moved to
        // the current workspace
        delete this.lastSelectedWorkspaceTabs[previousWorkspaceID];
      }
    }
    // Make sure we select the last tab in the new workspace
    this.lastSelectedWorkspaceTabs[workspaceID] =
      gZenGlanceManager.getTabOrGlanceParent(tabs[tabs.length - 1]);
    const workspaces = this.getWorkspaces();
    await this.changeWorkspace(
      workspaces.find(workspace => workspace.uuid === workspaceID)
    );
  }

  // Tab browser utilities

  getContextIdIfNeeded(userContextId, fromExternal) {
    if (!this.workspaceEnabled) {
      return [userContextId, false, undefined];
    }

    if (
      this.shouldForceContainerTabsToWorkspace &&
      typeof userContextId !== "undefined" &&
      this._workspaceCache &&
      !fromExternal
    ) {
      // Find all workspaces that match the given userContextId
      const matchingWorkspaces = this._workspaceCache.filter(
        workspace => workspace.containerTabId === userContextId
      );

      // Check if exactly one workspace matches
      if (matchingWorkspaces.length === 1) {
        const workspace = matchingWorkspaces[0];
        if (workspace.uuid !== this.getActiveWorkspaceFromCache().uuid) {
          return [userContextId, true, workspace.uuid];
        }
      }
    }

    const activeWorkspace = this.getActiveWorkspaceFromCache();
    const activeWorkspaceUserContextId = activeWorkspace?.containerTabId;

    if (
      fromExternal !== true &&
      typeof userContextId !== "undefined" &&
      userContextId !== activeWorkspaceUserContextId
    ) {
      return [userContextId, false, undefined];
    }
    return [activeWorkspaceUserContextId, true, undefined];
  }

  getTabsToExclude(aTab) {
    const tabWorkspaceId = aTab.getAttribute("zen-workspace-id");
    const containerId = aTab.getAttribute("usercontextid") ?? "0";
    // Return all tabs that are not on the same workspace
    return gBrowser.tabs.filter(
      tab =>
        !this._shouldShowTab(
          tab,
          tabWorkspaceId,
          containerId,
          this._workspaceCache
        ) && !tab.hasAttribute("zen-empty-tab")
    );
  }

  async shortcutSwitchTo(index) {
    const workspaces = this.getWorkspaces();
    // The index may be out of bounds, if it doesnt exist, don't do anything
    if (index >= workspaces.length || index < 0) {
      return;
    }
    const workspaceToSwitch = workspaces[index];
    await this.changeWorkspace(workspaceToSwitch);
  }

  isBookmarkInAnotherWorkspace(bookmark) {
    if (!this._workspaceBookmarksCache?.bookmarks) {
      return false;
    }
    const bookmarkGuid = bookmark.bookmarkGuid;
    const activeWorkspaceUuid = this.activeWorkspace;
    let isInActiveWorkspace = false;
    let isInOtherWorkspace = false;

    for (const [workspaceUuid, bookmarkGuids] of Object.entries(
      this._workspaceBookmarksCache.bookmarks
    )) {
      if (bookmarkGuids.includes(bookmarkGuid)) {
        if (workspaceUuid === activeWorkspaceUuid) {
          isInActiveWorkspace = true;
        } else {
          isInOtherWorkspace = true;
        }
      }
    }

    // Return true only if the bookmark is in another workspace and not in the active one
    return isInOtherWorkspace && !isInActiveWorkspace;
  }

  // Session restore functions
  get allStoredTabs() {
    if (this._allStoredTabs) {
      return this._allStoredTabs;
    }

    const tabs = [];
    // we need to go through each tab in each container
    const essentialsContainer = document.querySelectorAll(
      "#zen-essentials .zen-workspace-tabs-section"
    );
    let pinnedContainers = [];
    let normalContainers = [];
    if (!this._hasInitializedTabsStrip) {
      pinnedContainers = [document.getElementById("pinned-tabs-container")];
      normalContainers = [this.activeWorkspaceStrip];
    } else {
      let workspaces = Array.from(this._workspaceCache || []);
      // Make the active workspace first
      workspaces = workspaces.sort((a, b) =>
        /* eslint-disable no-nested-ternary */
        a.uuid === this.activeWorkspace
          ? -1
          : b.uuid === this.activeWorkspace
            ? 1
            : 0
      );
      for (const workspace of workspaces) {
        const container = this.workspaceElement(workspace.uuid);
        if (container) {
          pinnedContainers.push(container.pinnedTabsContainer);
          normalContainers.push(container.tabsContainer);
        }
      }
    }
    const containers = [
      ...essentialsContainer,
      ...pinnedContainers,
      ...normalContainers,
    ];
    for (const container of containers) {
      if (container.hasAttribute("cloned")) {
        continue;
      }
      for (const tab of container.children) {
        if (gBrowser.isTab(tab)) {
          tabs.push(tab);
          const glance = tab.querySelector(".tabbrowser-tab[glance-id]");
          if (glance) {
            tabs.push(glance);
          }
        } else if (gBrowser.isTabGroup(tab)) {
          for (const groupTab of tab.tabs) {
            tabs.push(groupTab);
            const glance = groupTab.querySelector(".tabbrowser-tab[glance-id]");
            if (glance) {
              tabs.push(glance);
            }
          }
        }
      }
    }
    return (this._allStoredTabs = tabs);
  }

  get allTabGroups() {
    if (!this._hasInitializedTabsStrip) {
      let children = this.tabboxChildren;
      return children.filter(node => gBrowser.isTabGroup(node));
    }
    const pinnedContainers = [];
    const normalContainers = [];
    for (const workspace of this._workspaceCache) {
      const container = this.workspaceElement(workspace.uuid);
      if (container) {
        pinnedContainers.push(container.pinnedTabsContainer);
        normalContainers.push(container.tabsContainer);
      }
    }
    const containers = [...pinnedContainers, ...normalContainers];
    const tabGroups = [];
    for (const container of containers) {
      for (const tabGroup of container.querySelectorAll("tab-group")) {
        tabGroups.push(tabGroup);
      }
      for (const tabGroup of container.querySelectorAll("zen-folder")) {
        tabGroups.push(tabGroup);
      }
    }
    return tabGroups;
  }

  get allUsedBrowsers() {
    if (!this._hasInitializedTabsStrip) {
      return gBrowser.browsers;
    }
    const browsers = [];
    for (const tab of this.allStoredTabs) {
      const browser = tab.linkedBrowser;
      if (browser) {
        browsers.push(browser);
      }
    }
    return browsers;
  }

  get pinnedTabCount() {
    return this.pinnedTabsContainer.children.length - 1;
  }

  reorganizeTabsAfterWelcome() {
    const children = gBrowser.tabContainer.arrowScrollbox.children;
    const remainingTabs = Array.from(children).filter(child =>
      gBrowser.isTab(child)
    );
    for (const tab of remainingTabs) {
      this.moveTabToWorkspace(tab, this.activeWorkspace);
    }
  }

  async switchIfNeeded(browser) {
    const tab = gBrowser.getTabForBrowser(browser);
    await this.switchTabIfNeeded(tab);
  }

  async switchTabIfNeeded(tab) {
    // Validate browser state first
    if (!this._validateBrowserState()) {
      console.warn("Browser state invalid for tab switching");
      return;
    }

    if (!tab) {
      console.warn("switchTabIfNeeded called with null tab");
      return;
    }

    // Validate tab state
    if (
      tab.closing ||
      !tab.ownerGlobal ||
      tab.ownerGlobal.closed ||
      !tab.linkedBrowser
    ) {
      console.warn("Tab is no longer valid, cannot select it");
      return;
    }

    try {
      const currentWorkspace = this.getActiveWorkspaceFromCache();
      // Check if we need to change workspace
      if (
        (tab.getAttribute("zen-workspace-id") !== this.activeWorkspace &&
          !tab.hasAttribute("zen-essential")) ||
        (currentWorkspace.containerTabId !==
          parseInt(tab.parentNode.getAttribute("container")) &&
          this.containerSpecificEssentials)
      ) {
        // Use a mutex-like approach to prevent concurrent workspace changes
        if (this._workspaceChangeInProgress) {
          console.warn(
            "Workspace change already in progress, deferring tab switch"
          );
          return;
        }

        let workspaceToSwitch = undefined;
        if (tab.hasAttribute("zen-essential")) {
          // Find first workspace with the same container
          const containerTabId = parseInt(
            tab.parentNode.getAttribute("container")
          );
          // +0 to convert to number
          workspaceToSwitch = this._workspaceCache.find(
            workspace => workspace.containerTabId + 0 === containerTabId
          );
        } else {
          workspaceToSwitch = this.getWorkspaceFromId(
            tab.getAttribute("zen-workspace-id")
          );
        }
        if (!workspaceToSwitch) {
          console.error("No workspace found for tab, cannot switch");
          await this._safelySelectTab(tab);
          return;
        }

        this._workspaceChangeInProgress = true;
        try {
          this.lastSelectedWorkspaceTabs[workspaceToSwitch.uuid] =
            gZenGlanceManager.getTabOrGlanceParent(tab);
          await this.changeWorkspace(workspaceToSwitch);
        } finally {
          this._workspaceChangeInProgress = false;
        }
      }

      // Safely switch to the tab using our debounced method
      await this._safelySelectTab(tab);
    } catch (e) {
      console.error("Error in switchTabIfNeeded:", e);
    }
  }

  getDefaultContainer() {
    if (!this.workspaceEnabled) {
      return 0;
    }
    const workspaces = this._workspaceCache;
    if (!workspaces) {
      return 0;
    }
    const activeWorkspace = this.activeWorkspace;
    const workspace = workspaces.find(w => w.uuid === activeWorkspace);
    return workspace.containerTabId;
  }

  onWindowResize(event = undefined) {
    if (!(!event || event.target === window)) {
      return;
    }
    gZenUIManager.updateTabsToolbar();
    // Check if workspace icons overflow the parent container
    let parent = this.workspaceIcons;
    if (!parent || this._processingResize) {
      return;
    }
    if (!gZenPinnedTabManager.expandedSidebarMode) {
      for (const icon of parent.children) {
        if (icon.tagName === "toolbarbutton") {
          icon.style.width = ""; // Reset to default size when in expanded mode
        }
      }
      parent.removeAttribute("icons-overflow");
      return;
    }
    const maxButtonSize = 32; // IMPORTANT: This should match the CSS size of the icons
    const minButtonSize = maxButtonSize / 2; // Minimum size for icons when space is limited
    const separation = 3; // Space between icons

    // Calculate the total width needed for all icons
    const totalWidth = Array.from(parent.children).reduce((width, icon) => {
      if (icon.tagName === "toolbarbutton") {
        return width + minButtonSize + separation;
      }
      return width;
    }, 0);

    // Check if the total width exceeds the parent's width
    if (totalWidth > parent.clientWidth) {
      parent.setAttribute("icons-overflow", "true");
    } else {
      parent.removeAttribute("icons-overflow");
    }

    // Set the width of each icon to the maximum size they can fit on
    const widthPerButton = Math.max(
      (parent.clientWidth - separation * (parent.children.length - 1)) /
        parent.children.length,
      minButtonSize
    );
    for (const icon of parent.children) {
      if (icon.tagName === "toolbarbutton") {
        icon.style.width = `${Math.min(widthPerButton, maxButtonSize)}px`;
      }
    }
  }

  fixTabInsertLocation(tab) {
    if (tab.hasAttribute("zen-essential")) {
      // Essential tabs should always be inserted at the end of the essentials section
      const essentialsSection = this.getEssentialsSection(tab);
      if (essentialsSection) {
        essentialsSection.appendChild(tab);
      }
    } else if (tab.pinned) {
      // Pinned tabs should always be inserted at the end of the pinned tabs container
      const pinnedContainer = this.pinnedTabsContainer;
      if (pinnedContainer) {
        pinnedContainer.insertBefore(tab, pinnedContainer.lastChild);
      }
    }
  }

  updateOverflowingTabs() {
    if (!this._hasInitializedTabsStrip) {
      return;
    }
    const currentWorkspaceStrip = this.workspaceElement(this.activeWorkspace);
    if (!currentWorkspaceStrip) {
      return;
    }
    if (currentWorkspaceStrip.overflows) {
      gBrowser.tabContainer.setAttribute("overflow", "true");
    } else {
      gBrowser.tabContainer.removeAttribute("overflow");
    }
  }

  handleTabCloseWindow() {
    if (Services.prefs.getBoolPref("zen.tabs.close-window-with-empty")) {
      document.getElementById("cmd_closeWindow").doCommand();
    }
  }
}

window.gZenWorkspaces = new nsZenWorkspaces();
