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

"use strict";

// protocol.js uses objects as exceptions in order to define
// error packets.
/* eslint-disable no-throw-literal */

/*
 * WindowGlobalTargetActor is an abstract class used by target actors that hold
 * documents, such as frames, chrome windows, etc.
 *
 * This class is extended by ParentProcessTargetActor, itself being extented by WebExtensionTargetActor.
 *
 * See devtools/docs/backend/actor-hierarchy.md for more details.
 *
 * For performance matters, this file should only be loaded in the targeted context's
 * process. For example, it shouldn't be evaluated in the parent process until we try to
 * debug a document living in the parent process.
 */

var {
  ActorRegistry,
} = require("resource://devtools/server/actors/utils/actor-registry.js");
var DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
var { assert } = DevToolsUtils;
var {
  SourcesManager,
} = require("resource://devtools/server/actors/utils/sources-manager.js");
var makeDebugger = require("resource://devtools/server/actors/utils/make-debugger.js");
const Targets = require("resource://devtools/server/actors/targets/index.js");
const { TargetActorRegistry } = ChromeUtils.importESModule(
  "resource://devtools/server/actors/targets/target-actor-registry.sys.mjs",
  {
    loadInDevToolsLoader: false,
  }
);
const { PrivateBrowsingUtils } = ChromeUtils.importESModule(
  "resource://gre/modules/PrivateBrowsingUtils.sys.mjs"
);

const EXTENSION_CONTENT_SYS_MJS =
  "resource://gre/modules/ExtensionContent.sys.mjs";

const { Pool } = require("resource://devtools/shared/protocol.js");
const {
  LazyPool,
  createExtraActors,
} = require("resource://devtools/shared/protocol/lazy-pool.js");
const {
  windowGlobalTargetSpec,
} = require("resource://devtools/shared/specs/targets/window-global.js");
const Resources = require("resource://devtools/server/actors/resources/index.js");
const {
  BaseTargetActor,
} = require("resource://devtools/server/actors/targets/base-target-actor.js");

loader.lazyRequireGetter(
  this,
  ["ThreadActor", "unwrapDebuggerObjectGlobal"],
  "resource://devtools/server/actors/thread.js",
  true
);
loader.lazyRequireGetter(
  this,
  "WorkerDescriptorActorList",
  "resource://devtools/server/actors/worker/worker-descriptor-actor-list.js",
  true
);
loader.lazyRequireGetter(
  this,
  "StyleSheetsManager",
  "resource://devtools/server/actors/utils/stylesheets-manager.js",
  true
);
const lazy = {};
loader.lazyGetter(lazy, "ExtensionContent", () => {
  return ChromeUtils.importESModule(EXTENSION_CONTENT_SYS_MJS, {
    // ExtensionContent.sys.mjs is a singleton and must be loaded through the
    // main loader. Note that the user of lazy.ExtensionContent elsewhere in
    // this file (at webextensionsContentScriptGlobals) looks up the module
    // via Cu.isESModuleLoaded, which also uses the main loader as desired.
    loadInDevToolsLoader: false,
  }).ExtensionContent;
});

loader.lazyRequireGetter(
  this,
  "TouchSimulator",
  "resource://devtools/server/actors/emulation/touch-simulator.js",
  true
);

function getWindowID(window) {
  return window.windowGlobalChild.innerWindowId;
}

function getDocShellChromeEventHandler(docShell) {
  let handler = docShell.chromeEventHandler;
  if (!handler) {
    try {
      // Toplevel xul window's docshell doesn't have chromeEventHandler
      // attribute. The chrome event handler is just the global window object.
      handler = docShell.domWindow;
    } catch (e) {
      // ignore
    }
  }
  return handler;
}

/**
 * Helper to retrieve all children docshells of a given docshell.
 *
 * Given that docshell interfaces can only be used within the same process,
 * this only returns docshells for children documents that runs in the same process
 * as the given docshell.
 */
function getChildDocShells(parentDocShell) {
  return parentDocShell.browsingContext
    .getAllBrowsingContextsInSubtree()
    .filter(browsingContext => {
      // Filter out browsingContext which don't expose any docshell (e.g. remote frame)
      return browsingContext.docShell;
    })
    .map(browsingContext => {
      // Map BrowsingContext to DocShell
      return browsingContext.docShell;
    });
}

exports.getChildDocShells = getChildDocShells;

/**
 * Browser-specific actors.
 */

function getInnerId(window) {
  return window.windowGlobalChild.innerWindowId;
}

class WindowGlobalTargetActor extends BaseTargetActor {
  /**
   * WindowGlobalTargetActor is the target actor to debug (HTML) documents.
   *
   * WindowGlobal's are the Gecko representation for a given document's window object.
   * It relates to a given nsGlobalWindowInner instance.
   *
   * The main goal of this class is to expose the target-scoped actors being registered
   * via `ActorRegistry.registerModule` and manage their lifetimes. In addition, this
   * class also tracks the lifetime of the targeted window global.
   *
   * ### Main requests:
   *
   * `detach`:
   *  Stop document watching and cleanup everything that the target and its children actors created.
   *  It ultimately lead to destroy the target actor.
   * `switchToFrame`:
   *  Change the targeted document of the whole actor, and its child target-scoped actors
   *  to an iframe or back to its original document.
   *
   * Most properties (like `chromeEventHandler` or `docShells`) are meant to be
   * used by the various child target actors.
   *
   * ### RDP events:
   *
   *  - `tabNavigated`:
   *    Sent when the window global is about to navigate or has just navigated
   *    to a different document.
   *    This event contains the following attributes:
   *     * url (string)
   *       The new URI being loaded.
   *     * state (string)
   *       `start` if we just start requesting the new URL
   *       `stop`  if the new URL is done loading
   *     * isFrameSwitching (boolean)
   *       Indicates the event is dispatched when switching the actor context to a
   *       different frame. When we switch to an iframe, there is no document
   *       load. The targeted document is most likely going to be already done
   *       loading.
   *     * title (string)
   *       The document title being loaded. (sent only on state=stop)
   *
   *  - `frameUpdate`:
   *    Sent when there was a change in the child frames contained in the document
   *    or when the actor's context was switched to another frame.
   *    This event can have four different forms depending on the type of change:
   *    * One or many frames are updated:
   *      { frames: [{ id, url, title, parentID }, ...] }
   *    * One frame got destroyed:
   *      { frames: [{ id, destroy: true }]}
   *    * All frames got destroyed:
   *      { destroyAll: true }
   *    * We switched the context of the actor to a specific frame:
   *      { selected: #id }
   *
   * ### Internal, non-rdp events:
   *
   * Various events are also dispatched on the actor itself without being sent to
   * the client. They all relate to the documents tracked by this target actor
   * (its main targeted document, but also any of its iframes):
   *  - will-navigate
   *    This event fires once navigation starts. All pending user prompts are
   *    dealt with, but it is fired before the first request starts.
   *  - navigate
   *    This event is fired once the document's readyState is "complete".
   *  - window-ready
   *    This event is fired in various distinct scenarios:
   *     * When a new Window object is crafted, equivalent of `DOMWindowCreated`.
   *       It is dispatched before any page script is executed.
   *     * We will have already received a window-ready event for this window
   *       when it was created, but we received a window-destroyed event when
   *       it was frozen into the bfcache, and now the user navigated back to
   *       this page, so it's now live again and we should resume handling it.
   *     * For each existing document, when an `attach` request is received.
   *       At this point scripts in the page will be already loaded.
   *     * When `swapFrameLoaders` is used, such as with moving window globals
   *       between windows or toggling Responsive Design Mode.
   *  - window-destroyed
   *    This event is fired in two cases:
   *     * When the window object is destroyed, i.e. when the related document
   *       is garbage collected. This can happen when the window global is
   *       closed or the iframe is removed from the DOM.
   *       It is equivalent of `inner-window-destroyed` event.
   *     * When the page goes into the bfcache and gets frozen.
   *       The equivalent of `pagehide`.
   *  - changed-toplevel-document
   *    This event fires when we switch the actor's targeted document
   *    to one of its iframes, or back to its original top document.
   *    It is dispatched between window-destroyed and window-ready.
   *
   * Note that *all* these events are dispatched in the following order
   * when we switch the context of the actor to a given iframe:
   *  - will-navigate
   *  - window-destroyed
   *  - changed-toplevel-document
   *  - window-ready
   *  - navigate
   *
   * This class is subclassed by ParentProcessTargetActor and others.
   * Subclasses are expected to implement a getter for the docShell property.
   *
   * @param conn DevToolsServerConnection
   *        The conection to the client.
   * @param options Object
   *        Object with following attributes:
   *        - docShell nsIDocShell
   *          The |docShell| for the debugged frame.
   *        - followWindowGlobalLifeCycle Boolean
   *          If true, the target actor will only inspect the current WindowGlobal (and its children windows).
   *          But won't inspect next document loaded in the same BrowsingContext.
   *          The actor will behave more like a WindowGlobalTarget rather than a BrowsingContextTarget.
   *          This is always true for Tab debugging, but not yet for parent process/web extension.
   *        - isTopLevelTarget Boolean
   *          Should be set to true for all top-level targets. A top level target
   *          is the topmost target of a DevTools "session". For instance for a local
   *          tab toolbox, the WindowGlobalTargetActor for the content page is the top level target.
   *          For the Multiprocess Browser Toolbox, the parent process target is the top level
   *          target.
   *          At the moment this only impacts the WindowGlobalTarget `reconfigure`
   *          implementation. But for server-side target switching this flag will be exposed
   *          to the client and should be available for all target actor classes. It will be
   *          used to detect target switching. (Bug 1644397)
   *        - ignoreSubFrames Boolean
   *          If true, the actor will only focus on the passed docShell and not on the whole
   *          docShell tree. This should be enabled when we have targets for all documents.
   *        - sessionContext Object
   *          The Session Context to help know what is debugged.
   *          See devtools/server/actors/watcher/session-context.js
   */
  constructor(
    conn,
    {
      docShell,
      followWindowGlobalLifeCycle,
      isTopLevelTarget,
      ignoreSubFrames,
      sessionContext,
      customSpec = windowGlobalTargetSpec,
    }
  ) {
    super(conn, Targets.TYPES.FRAME, customSpec);

    this.followWindowGlobalLifeCycle = followWindowGlobalLifeCycle;
    this.isTopLevelTarget = !!isTopLevelTarget;
    this.ignoreSubFrames = ignoreSubFrames;
    this.sessionContext = sessionContext;

    // A map of actor names to actor instances provided by extensions.
    this._extraActors = {};
    this._sourcesManager = null;

    this._shouldAddNewGlobalAsDebuggee =
      this._shouldAddNewGlobalAsDebuggee.bind(this);

    this.makeDebugger = makeDebugger.bind(null, {
      findDebuggees: () => {
        const result = [];
        const inspectUAWidgets = Services.prefs.getBoolPref(
          "devtools.inspector.showAllAnonymousContent",
          false
        );
        for (const win of this.windows) {
          result.push(win);
          // Only expose User Agent internal (like <video controls>) when the
          // related pref is set.
          if (inspectUAWidgets) {
            const principal = win.document.nodePrincipal;
            // We don't use UA widgets for the system principal.
            if (!principal.isSystemPrincipal) {
              result.push(Cu.getUAWidgetScope(principal));
            }
          }
        }
        return result.concat(this.webextensionsContentScriptGlobals);
      },
      shouldAddNewGlobalAsDebuggee: this._shouldAddNewGlobalAsDebuggee,
    });

    // Flag eventually overloaded by sub classes in order to watch new docshells
    // Used by the ParentProcessTargetActor to list all frames in the Browser Toolbox
    this.watchNewDocShells = false;

    this._workerDescriptorActorList = null;
    this._workerDescriptorActorPool = null;
    this._onWorkerDescriptorActorListChanged =
      this._onWorkerDescriptorActorListChanged.bind(this);

    this._onConsoleApiProfilerEvent =
      this._onConsoleApiProfilerEvent.bind(this);
    Services.obs.addObserver(
      this._onConsoleApiProfilerEvent,
      "console-api-profiler"
    );

    // Start observing navigations as well as sub documents.
    // (This is probably meant to disappear once EFT is the only supported codepath)
    this._progressListener = new DebuggerProgressListener(this);

    TargetActorRegistry.registerTargetActor(this);

    if (docShell) {
      this.setDocShell(docShell);
    }
  }

  /**
   * Define the initial docshell.
   *
   * This is called from the constructor for WindowGlobalTargetActor,
   * or from sub class constructors: WebExtensionTargetActor and ParentProcessTargetActor.
   *
   * This is to circumvent the fact that sub classes need to call inner method
   * to compute the initial docshell and we can't call inner methods before calling
   * the base class constructor...
   */
  setDocShell(docShell) {
    Object.defineProperty(this, "docShell", {
      value: docShell,
      configurable: true,
      writable: true,
    });

    // Save references to the original document we attached to
    this._originalWindow = this.window;

    // Update isPrivate as window is based on docShell
    this.isPrivate = PrivateBrowsingUtils.isContentWindowPrivate(this.window);

    // Instantiate the Thread Actor immediately.
    // This is the only one actor instantiated right away by the target actor.
    // All the others are instantiated lazily on first request made the client,
    // via LazyPool API.
    this._createThreadActor();

    // Ensure notifying about the target actor first
    // before notifying about new docshells.
    // Otherwise we would miss these RDP event as the client hasn't
    // yet received the target actor's form.
    // (This is also probably meant to disappear once EFT is the only supported codepath)
    this._docShellsObserved = false;
    DevToolsUtils.executeSoon(() => this._watchDocshells());
  }

  get docShell() {
    throw new Error(
      "A docShell should be provided as constructor argument of WindowGlobalTargetActor, or redefined by the subclass"
    );
  }

  // Optional console API listener options (e.g. used by the WebExtensionActor to
  // filter console messages by addonID), set to an empty (no options) object by default.
  consoleAPIListenerOptions = {};

  /*
   * Return a Debugger instance or create one if there is none yet
   */
  get dbg() {
    if (!this._dbg) {
      this._dbg = this.makeDebugger();
    }
    return this._dbg;
  }

  /**
   * Try to locate the console actor if it exists.
   */
  get _consoleActor() {
    if (this.isDestroyed()) {
      return null;
    }
    const form = this.form();
    return this.conn._getOrCreateActor(form.consoleActor);
  }

  get _memoryActor() {
    if (this.isDestroyed()) {
      return null;
    }
    const form = this.form();
    return this.conn._getOrCreateActor(form.memoryActor);
  }

  _targetScopedActorPool = null;

  /**
   * An object on which listen for DOMWindowCreated and pageshow events.
   */
  get chromeEventHandler() {
    return getDocShellChromeEventHandler(this.docShell);
  }

  /**
   * Getter for the nsIMessageManager associated to the window global.
   */
  get messageManager() {
    try {
      return this.docShell.messageManager;
    } catch (e) {
      // In some cases we can't get a docshell.  We just have no message manager
      // then,
      return null;
    }
  }

  /**
   * Getter for the list of all `docShell`s in the window global.
   * @return {Array}
   */
  get docShells() {
    if (this.ignoreSubFrames) {
      return [this.docShell];
    }

    return getChildDocShells(this.docShell);
  }

  /**
   * Getter for the window global's current DOM window.
   */
  get window() {
    return this.docShell && !this.docShell.isBeingDestroyed()
      ? this.docShell.domWindow
      : null;
  }

  get outerWindowID() {
    if (this.docShell) {
      return this.docShell.outerWindowID;
    }
    return null;
  }

  get browsingContext() {
    return this.docShell?.browsingContext;
  }

  get browsingContextID() {
    return this.browsingContext?.id;
  }

  get browserId() {
    return this.browsingContext?.browserId;
  }

  get openerBrowserId() {
    return this.browsingContext?.opener?.browserId;
  }

  /**
   * Getter for the WebExtensions ContentScript globals related to the
   * window global's current DOM window.
   */
  get webextensionsContentScriptGlobals() {
    // Only retrieve the content scripts globals if the ExtensionContent JSM module
    // has been already loaded (which is true if the WebExtensions internals have already
    // been loaded in the same content process).
    if (Cu.isESModuleLoaded(EXTENSION_CONTENT_SYS_MJS)) {
      return lazy.ExtensionContent.getContentScriptGlobals(this.window);
    }

    return [];
  }

  /**
   * Getter for the list of all content DOM windows in the window global.
   * @return {Array}
   */
  get windows() {
    return this.docShells.map(docShell => {
      return docShell.domWindow;
    });
  }

  /**
   * Getter for the original docShell this actor got attached to in the first
   * place.
   * Note that your actor should normally *not* rely on this top level docShell
   * if you want it to show information relative to the iframe that's currently
   * being inspected in the toolbox.
   */
  get originalDocShell() {
    if (!this._originalWindow) {
      return this.docShell;
    }

    return this._originalWindow.docShell;
  }

  /**
   * Getter for the original window this actor got attached to in the first
   * place.
   * Note that your actor should normally *not* rely on this top level window if
   * you want it to show information relative to the iframe that's currently
   * being inspected in the toolbox.
   */
  get originalWindow() {
    return this._originalWindow || this.window;
  }

  /**
   * Getter for the nsIWebProgress for watching this window.
   */
  get webProgress() {
    return this.docShell
      .QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIWebProgress);
  }

  /**
   * Getter for the nsIWebNavigation for the target.
   */
  get webNavigation() {
    return this.docShell.QueryInterface(Ci.nsIWebNavigation);
  }

  /**
   * Getter for the window global's document.
   */
  get contentDocument() {
    return this.webNavigation.document;
  }

  /**
   * Getter for the window global's title.
   */
  get title() {
    return this.contentDocument.title;
  }

  /**
   * Getter for the window global's URL.
   */
  get url() {
    if (this.webNavigation.currentURI) {
      return this.webNavigation.currentURI.spec;
    }
    // Abrupt closing of the browser window may leave callbacks without a
    // currentURI.
    return null;
  }

  get sourcesManager() {
    if (!this._sourcesManager) {
      this._sourcesManager = new SourcesManager(this.threadActor);
    }
    return this._sourcesManager;
  }

  getStyleSheetsManager() {
    if (!this._styleSheetsManager) {
      this._styleSheetsManager = new StyleSheetsManager(this);
    }
    return this._styleSheetsManager;
  }

  _createExtraActors() {
    // Always use the same Pool, so existing actor instances
    // (created in createExtraActors) are not lost.
    if (!this._targetScopedActorPool) {
      this._targetScopedActorPool = new LazyPool(this.conn);
    }

    // Walk over target-scoped actor factories and make sure they are all
    // instantiated and added into the Pool.
    return createExtraActors(
      ActorRegistry.targetScopedActorFactories,
      this._targetScopedActorPool,
      this
    );
  }

  form() {
    assert(
      !this.isDestroyed(),
      "form() shouldn't be called on destroyed browser actor."
    );
    assert(this.actorID, "Actor should have an actorID.");

    // Note that we don't want the iframe dropdown to change our BrowsingContext.id/innerWindowId
    // We only want to refer to the topmost original window we attached to
    // as that's the one top document this target actor really represent.
    // The iframe dropdown is just a hack that temporarily focus the scope
    // of the target actor to a children iframe document.
    //
    // Also, for WebExtension, we want the target to represent the <browser> element
    // created by DevTools, which always exists and help better connect resources to the target
    // in the frontend. Otherwise all other <browser> element of webext may be reloaded or go away
    // and then we would have troubles matching targets for resources.
    const originalBrowsingContext = this
      .devtoolsSpawnedBrowsingContextForWebExtension
      ? this.devtoolsSpawnedBrowsingContextForWebExtension
      : this.originalDocShell.browsingContext;
    const browsingContextID = originalBrowsingContext.id;
    const innerWindowId =
      originalBrowsingContext.currentWindowContext.innerWindowId;
    const parentInnerWindowId =
      originalBrowsingContext.parent?.currentWindowContext.innerWindowId;
    // Doesn't only check `!!opener` as some iframe might have an opener
    // if their location was loaded via `window.open(url, "iframe-name")`.
    // So also ensure that the document is opened in a distinct tab.
    const isPopup =
      !!originalBrowsingContext.opener &&
      originalBrowsingContext.browserId !=
        originalBrowsingContext.opener.browserId;

    const response = {
      actor: this.actorID,
      browsingContextID,
      processID: Services.appinfo.processID,
      // True for targets created by JSWindowActors, see constructor JSDoc.
      followWindowGlobalLifeCycle: this.followWindowGlobalLifeCycle,
      innerWindowId,
      parentInnerWindowId,
      topInnerWindowId: this.browsingContext.topWindowContext.innerWindowId,
      isTopLevelTarget: this.isTopLevelTarget,
      ignoreSubFrames: this.ignoreSubFrames,
      isPopup,
      isPrivate: this.isPrivate,
      traits: {
        // @backward-compat { version 64 } Exposes a new trait to help identify
        // BrowsingContextActor's inherited actors from the client side.
        isBrowsingContext: true,
        // Browsing context targets can compute the isTopLevelTarget flag on the
        // server. But other target actors don't support this yet. See Bug 1709314.
        supportsTopLevelTargetFlag: true,
        // Supports frame listing via `listFrames` request and `frameUpdate` events
        // as well as frame switching via `switchToFrame` request
        frames: true,
        // Supports the logInPage request.
        logInPage: true,
        // Supports watchpoints in the server. We need to keep this trait because target
        // actors that don't extend WindowGlobalTargetActor (Worker, ContentProcess, …)
        // might not support watchpoints.
        watchpoints: true,
        // Supports back and forward navigation
        navigation: true,
      },
    };

    // We may try to access window while the document is closing, then accessing window
    // throws.
    if (!this.docShell.isBeingDestroyed()) {
      response.title = this.title;
      response.url = this.url;
      response.outerWindowID = this.outerWindowID;
    }

    const actors = this._createExtraActors();
    Object.assign(response, actors);

    // The thread actor is the only actor manually created by the target actor.
    // It is not registered in targetScopedActorFactories and therefore needs
    // to be added here manually.
    if (this.threadActor) {
      Object.assign(response, {
        threadActor: this.threadActor.actorID,
      });
    }

    return response;
  }

  /**
   * Called when the actor is removed from the connection.
   *
   * @params {Object} options
   * @params {Boolean} options.isTargetSwitching: Set to true when this is called during
   *         a target switch.
   * @params {Boolean} options.isModeSwitching: Set to true true when this is called as the
   *         result of a change to the devtools.browsertoolbox.scope pref.
   */
  destroy({ isTargetSwitching = false, isModeSwitching = false } = {}) {
    // Avoid reentrancy. We will destroy the Transport when emitting "destroyed",
    // which will force destroying all actors.
    if (this.destroying) {
      return;
    }
    this.destroying = true;

    // Tell the thread actor that the window global is closed, so that it may terminate
    // instead of resuming the debuggee script.
    // TODO: Bug 997119: Remove this coupling with thread actor
    if (this.threadActor) {
      this.threadActor._parentClosed = true;
    }

    if (this._touchSimulator) {
      this._touchSimulator.stop();
      this._touchSimulator = null;
    }

    // Check for `docShell` availability, as it can be already gone during
    // Firefox shutdown.
    if (this.docShell) {
      this._unwatchDocShell(this.docShell);

      // If this target is being destroyed as part of a target switch or a mode switch,
      // we don't need to restore the configuration (this might cause the content page to
      // be focused again, causing issues in tests and disturbing the user when switching modes).
      if (!isTargetSwitching && !isModeSwitching) {
        this._restoreTargetConfiguration();
      }
    }
    this._unwatchDocshells();

    this._destroyThreadActor();

    if (this._styleSheetsManager) {
      this._styleSheetsManager.destroy();
      this._styleSheetsManager = null;
    }

    // Shut down actors that belong to this target's pool.
    if (this._targetScopedActorPool) {
      this._targetScopedActorPool.destroy();
      this._targetScopedActorPool = null;
    }

    // Make sure that no more workerListChanged notifications are sent.
    if (this._workerDescriptorActorList !== null) {
      this._workerDescriptorActorList.destroy();
      this._workerDescriptorActorList = null;
    }

    if (this._workerDescriptorActorPool !== null) {
      this._workerDescriptorActorPool.destroy();
      this._workerDescriptorActorPool = null;
    }

    if (this._dbg) {
      this._dbg.disable();
      this._dbg = null;
    }

    // Emit a last event before calling Actor.destroy
    // which will destroy the EventEmitter API
    this.emit("destroyed", { isTargetSwitching, isModeSwitching });

    // Destroy BaseTargetActor before nullifying docShell in case any child actor queries the window/docShell.
    super.destroy();

    this.docShell = null;
    this._extraActors = null;

    Services.obs.removeObserver(
      this._onConsoleApiProfilerEvent,
      "console-api-profiler"
    );

    TargetActorRegistry.unregisterTargetActor(this);
    Resources.unwatchAllResources(this);
  }

  /**
   * Return true if the given global is associated with this window global and should
   * be added as a debuggee, false otherwise.
   */
  _shouldAddNewGlobalAsDebuggee(wrappedGlobal) {
    // Otherwise, check if it is a WebExtension content script sandbox
    const global = unwrapDebuggerObjectGlobal(wrappedGlobal);
    if (!global) {
      return false;
    }

    // Check if the global is a sdk page-mod sandbox.
    let metadata = {};
    let id = "";
    try {
      id = getInnerId(this.window);
      metadata = Cu.getSandboxMetadata(global);
    } catch (e) {
      // ignore
    }
    if (metadata?.["inner-window-id"] && metadata["inner-window-id"] == id) {
      return true;
    }

    return false;
  }

  _watchDocshells() {
    // If for some unexpected reason, the actor is immediately destroyed,
    // avoid registering leaking observer listener.
    if (this.isDestroyed()) {
      return;
    }

    // In child processes, we watch all docshells living in the process.
    Services.obs.addObserver(this, "webnavigation-create");
    Services.obs.addObserver(this, "webnavigation-destroy");
    this._docShellsObserved = true;

    // We watch for all child docshells under the current document,
    this._progressListener.watch(this.docShell);

    // And list all already existing ones.
    this._updateChildDocShells();
  }

  _unwatchDocshells() {
    if (this._progressListener) {
      this._progressListener.destroy();
      this._progressListener = null;
      this._originalWindow = null;
    }

    // Removes the observers being set in _watchDocshells, but only
    // if _watchDocshells has been called. The target actor may be immediately destroyed
    // and doesn't have time to register them.
    // (Calling removeObserver without having called addObserver throws)
    if (this._docShellsObserved) {
      Services.obs.removeObserver(this, "webnavigation-create");
      Services.obs.removeObserver(this, "webnavigation-destroy");
      this._docShellsObserved = false;
    }
  }

  _unwatchDocShell(docShell) {
    if (this._progressListener) {
      this._progressListener.unwatch(docShell);
    }
  }

  switchToFrame(request) {
    const windowId = request.windowId;
    let win;

    try {
      win = Services.wm.getOuterWindowWithId(windowId);
    } catch (e) {
      // ignore
    }
    if (!win) {
      throw {
        error: "noWindow",
        message: "The related docshell is destroyed or not found",
      };
    } else if (win == this.window) {
      return {};
    }

    // Reply first before changing the document
    DevToolsUtils.executeSoon(() => this._changeTopLevelDocument(win));

    return {};
  }

  listFrames(request) {
    const windows = this._docShellsToWindows(this.docShells);
    return { frames: windows };
  }

  ensureWorkerDescriptorActorList() {
    if (this._workerDescriptorActorList === null) {
      this._workerDescriptorActorList = new WorkerDescriptorActorList(
        this.conn,
        {
          type: Ci.nsIWorkerDebugger.TYPE_DEDICATED,
          window: this.window,
        }
      );
    }
    return this._workerDescriptorActorList;
  }

  pauseWorkersUntilAttach(shouldPause) {
    this.ensureWorkerDescriptorActorList().workerPauser.setPauseMatching(
      shouldPause
    );
  }

  listWorkers(request) {
    return this.ensureWorkerDescriptorActorList()
      .getList()
      .then(actors => {
        const pool = new Pool(this.conn, "worker-targets");
        for (const actor of actors) {
          pool.manage(actor);
        }

        // Do not destroy the pool before transfering ownership to the newly created
        // pool, so that we do not accidently destroy actors that are still in use.
        if (this._workerDescriptorActorPool) {
          this._workerDescriptorActorPool.destroy();
        }

        this._workerDescriptorActorPool = pool;
        this._workerDescriptorActorList.onListChanged =
          this._onWorkerDescriptorActorListChanged;

        return {
          workers: actors,
        };
      });
  }

  logInPage(request) {
    const { text, category, flags } = request;
    const scriptErrorClass = Cc["@mozilla.org/scripterror;1"];
    const scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError);
    scriptError.initWithWindowID(
      text,
      null,
      null,
      0,
      0,
      flags,
      category,
      getInnerId(this.window)
    );
    Services.console.logMessage(scriptError);
    return {};
  }

  _onWorkerDescriptorActorListChanged() {
    this._workerDescriptorActorList.onListChanged = null;
    this.emit("workerListChanged");
  }

  _onConsoleApiProfilerEvent(subject, topic, data) {
    // TODO: We will receive console-api-profiler events for any browser running
    // in the same process as this target. We should filter irrelevant events,
    // but console-api-profiler currently doesn't emit any information to identify
    // the origin of the event. See Bug 1731033.

    // The new performance panel is not compatible with console.profile().
    const warningFlag = 1;
    this.logInPage({
      text:
        "console.profile is not compatible with the new Performance recorder. " +
        "See https://bugzilla.mozilla.org/show_bug.cgi?id=1730896",
      category: "console.profile unavailable",
      flags: warningFlag,
    });
  }

  observe(subject, topic, data) {
    // Ignore any event that comes before/after the actor is attached.
    // That typically happens during Firefox shutdown.
    if (this.isDestroyed()) {
      return;
    }

    subject.QueryInterface(Ci.nsIDocShell);

    if (topic == "webnavigation-create") {
      this._onDocShellCreated(subject);
    } else if (topic == "webnavigation-destroy") {
      this._onDocShellDestroy(subject);
    }
  }

  _onDocShellCreated(docShell) {
    // (chrome-)webnavigation-create is fired very early during docshell
    // construction. In new root docshells within child processes, involving
    // BrowserChild, this event is from within this call:
    //   https://hg.mozilla.org/mozilla-central/annotate/74d7fb43bb44/dom/ipc/TabChild.cpp#l912
    // whereas the chromeEventHandler (and most likely other stuff) is set
    // later:
    //   https://hg.mozilla.org/mozilla-central/annotate/74d7fb43bb44/dom/ipc/TabChild.cpp#l944
    // So wait a tick before watching it:
    DevToolsUtils.executeSoon(() => {
      // Bug 1142752: sometimes, the docshell appears to be immediately
      // destroyed, bailout early to prevent random exceptions.
      if (docShell.isBeingDestroyed()) {
        return;
      }

      // In child processes, we have new root docshells,
      // let's watch them and all their child docshells.
      if (this._isRootDocShell(docShell) && this.watchNewDocShells) {
        this._progressListener.watch(docShell);
      }
      this._notifyDocShellsUpdate([docShell]);
    });
  }

  _onDocShellDestroy(docShell) {
    // Stop watching this docshell (the unwatch() method will check if we
    // started watching it before).
    this._unwatchDocShell(docShell);

    const webProgress = docShell
      .QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIWebProgress);
    this._notifyDocShellDestroy(webProgress);

    if (webProgress.DOMWindow == this._originalWindow) {
      // If the original top level document we connected to is removed,
      // we try to switch to any other top level document
      const rootDocShells = this.docShells.filter(d => {
        // Ignore docshells without a working DOM Window.
        // When we close firefox we have a chrome://extensions/content/dummy.xhtml
        // which is in process of being destroyed and we might try to fallback to it.
        // Unfortunately docshell.isBeingDestroyed() doesn't return true...
        return d != this.docShell && this._isRootDocShell(d) && d.DOMWindow;
      });
      if (rootDocShells.length) {
        const newRoot = rootDocShells[0];
        this._originalWindow = newRoot.DOMWindow;
        this._changeTopLevelDocument(this._originalWindow);
      } else {
        // If for some reason (typically during Firefox shutdown), the original
        // document is destroyed, and there is no other top level docshell,
        // we detach the actor to unregister all listeners and prevent any
        // exception.
        this.destroy();
      }
      return;
    }

    // If the currently targeted window global is destroyed, and we aren't on
    // the top-level document, we have to switch to the top-level one.
    if (
      webProgress.DOMWindow == this.window &&
      this.window != this._originalWindow
    ) {
      this._changeTopLevelDocument(this._originalWindow);
    }
  }

  _isRootDocShell(docShell) {
    // Should report as root docshell:
    //  - New top level window's docshells, when using ParentProcessTargetActor against a
    // process. It allows tracking iframes of the newly opened windows
    // like Browser console or new browser windows.
    //  - MozActivities or window.open frames on B2G, where a new root docshell
    // is spawn in the child process of the app.
    return !docShell.parent;
  }

  _docShellToWindow(docShell) {
    const webProgress = docShell
      .QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIWebProgress);
    const window = webProgress.DOMWindow;
    const id = docShell.outerWindowID;
    let parentID = undefined;
    // Ignore the parent of the original document on non-e10s firefox,
    // as we get the xul window as parent and don't care about it.
    // Furthermore, ignore setting parentID when parent window is same as
    // current window in order to deal with front end. e.g. toolbox will be fall
    // into infinite loop due to recursive search with by using parent id.
    if (
      window.parent &&
      window.parent != window &&
      window != this._originalWindow
    ) {
      parentID = window.parent.docShell.outerWindowID;
    }

    return {
      id,
      parentID,
      isTopLevel: window == this.originalWindow && this.isTopLevelTarget,
      url: window.location.href,
      title: window.document.title,
    };
  }

  // Convert docShell list to windows objects list being sent to the client
  _docShellsToWindows(docshells) {
    return docshells
      .filter(docShell => {
        // Ensure docShell.document is available.
        docShell.QueryInterface(Ci.nsIWebNavigation);

        // don't include transient about:blank documents
        if (docShell.document.isInitialDocument) {
          return false;
        }

        return true;
      })
      .map(docShell => this._docShellToWindow(docShell));
  }

  _notifyDocShellsUpdate(docshells) {
    // Only top level target uses frameUpdate in order to update the iframe dropdown.
    // This may eventually be replaced by Target listening and target switching.
    if (!this.isTopLevelTarget) {
      return;
    }

    const windows = this._docShellsToWindows(docshells);

    // Do not send the `frameUpdate` event if the windows array is empty.
    if (!windows.length) {
      return;
    }

    this.emit("frameUpdate", {
      frames: windows,
    });
  }

  _updateChildDocShells() {
    this._notifyDocShellsUpdate(this.docShells);
  }

  _notifyDocShellDestroy(webProgress) {
    // Only top level target uses frameUpdate in order to update the iframe dropdown.
    // This may eventually be replaced by Target listening and target switching.
    if (!this.isTopLevelTarget) {
      return;
    }

    webProgress = webProgress.QueryInterface(Ci.nsIWebProgress);
    const id = webProgress.DOMWindow.docShell.outerWindowID;
    this.emit("frameUpdate", {
      frames: [
        {
          id,
          destroy: true,
        },
      ],
    });
  }

  /**
   * Creates and manages the thread actor as part of the Browsing Context Target pool.
   * This sets up the content window for being debugged
   */
  _createThreadActor() {
    this.threadActor = new ThreadActor(this, this.window);
    this.manage(this.threadActor);
  }

  /**
   * Exits the current thread actor and removes it from the Browsing Context Target pool.
   * The content window is no longer being debugged after this call.
   */
  _destroyThreadActor() {
    if (this.threadActor) {
      this.threadActor.destroy();
      this.threadActor = null;
    }

    if (this._sourcesManager) {
      this._sourcesManager.destroy();
      this._sourcesManager = null;
    }
  }

  // Protocol Request Handlers

  detach(request) {
    // Destroy the actor in the next event loop in order
    // to ensure responding to the `detach` request.
    DevToolsUtils.executeSoon(() => {
      this.destroy();
    });

    return {};
  }

  /**
   * Bring the window global's window to front.
   */
  focus() {
    if (this.window) {
      this.window.focus();
    }
    return {};
  }

  goForward() {
    // Wait a tick so that the response packet can be dispatched before the
    // subsequent navigation event packet.
    Services.tm.dispatchToMainThread(
      DevToolsUtils.makeInfallible(() => {
        // This won't work while the browser is shutting down and we don't really
        // care.
        if (Services.startup.shuttingDown) {
          return;
        }

        this.webNavigation.goForward();
      }, "WindowGlobalTargetActor.prototype.goForward's delayed body")
    );

    return {};
  }

  goBack() {
    // Wait a tick so that the response packet can be dispatched before the
    // subsequent navigation event packet.
    Services.tm.dispatchToMainThread(
      DevToolsUtils.makeInfallible(() => {
        // This won't work while the browser is shutting down and we don't really
        // care.
        if (Services.startup.shuttingDown) {
          return;
        }

        this.webNavigation.goBack();
      }, "WindowGlobalTargetActor.prototype.goBack's delayed body")
    );

    return {};
  }

  /**
   * Reload the page in this window global.
   *
   * @backward-compat { legacy }
   *                  reload is preserved for third party tools. See Bug 1717837.
   *                  DevTools should use Descriptor::reloadDescriptor instead.
   */
  reload(request) {
    const force = request?.options?.force;
    // Wait a tick so that the response packet can be dispatched before the
    // subsequent navigation event packet.
    Services.tm.dispatchToMainThread(
      DevToolsUtils.makeInfallible(() => {
        // This won't work while the browser is shutting down and we don't really
        // care.
        if (Services.startup.shuttingDown) {
          return;
        }

        this.webNavigation.reload(
          force
            ? Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE
            : Ci.nsIWebNavigation.LOAD_FLAGS_NONE
        );
      }, "WindowGlobalTargetActor.prototype.reload's delayed body")
    );
    return {};
  }

  /**
   * Navigate this window global to a new location
   */
  navigateTo(request) {
    // Wait a tick so that the response packet can be dispatched before the
    // subsequent navigation event packet.
    Services.tm.dispatchToMainThread(
      DevToolsUtils.makeInfallible(() => {
        this.window.location = request.url;
      }, "WindowGlobalTargetActor.prototype.navigateTo's delayed body:" + request.url)
    );
    return {};
  }

  /**
   * For browsing-context targets which can't use the watcher configuration
   * actor (eg webextension targets), the client directly calls `reconfigure`.
   * Once all targets support the watcher, this method can be removed.
   */
  reconfigure(request) {
    const options = request.options || {};
    return this.updateTargetConfiguration(options);
  }

  /**
   * Apply target-specific options.
   *
   * This will be called by the watcher when the DevTools target-configuration
   * is updated, or when a target is created via JSWindowActors.
   */
  updateTargetConfiguration(options = {}, calledFromDocumentCreation = false) {
    if (!this.docShell) {
      // The window global is already closed.
      return;
    }

    let reload = false;
    if (typeof options.touchEventsOverride !== "undefined") {
      const enableTouchSimulator = options.touchEventsOverride === "enabled";

      this.docShell.metaViewportOverride = enableTouchSimulator
        ? Ci.nsIDocShell.META_VIEWPORT_OVERRIDE_ENABLED
        : Ci.nsIDocShell.META_VIEWPORT_OVERRIDE_NONE;

      // We want to reload the document if it's an "existing" top level target on which
      // the touch simulator will be toggled and the user has turned the
      // "reload on touch simulation" setting on.
      if (
        enableTouchSimulator !== this.touchSimulator.enabled &&
        options.reloadOnTouchSimulationToggle === true &&
        this.isTopLevelTarget &&
        !calledFromDocumentCreation
      ) {
        reload = true;
      }

      if (enableTouchSimulator) {
        this.touchSimulator.start();
      } else {
        this.touchSimulator.stop();
      }
    }

    if (typeof options.customFormatters !== "undefined") {
      this.customFormatters = options.customFormatters;
    }

    if (typeof options.useSimpleHighlightersForReducedMotion == "boolean") {
      this._useSimpleHighlightersForReducedMotion =
        options.useSimpleHighlightersForReducedMotion;
      this.emit("use-simple-highlighters-updated");
    }

    if (!this.isTopLevelTarget) {
      // Following DevTools target options should only apply to the top target and be
      // propagated through the window global tree via the platform.
      return;
    }
    if (typeof options.restoreFocus == "boolean") {
      this._restoreFocus = options.restoreFocus;
    }
    if (typeof options.recordAllocations == "object") {
      const actor = this._memoryActor;
      if (options.recordAllocations == null) {
        actor.stopRecordingAllocations();
      } else {
        actor.attach();
        actor.startRecordingAllocations(options.recordAllocations);
      }
    }

    if (reload) {
      this.webNavigation.reload(Ci.nsIWebNavigation.LOAD_FLAGS_NONE);
    }
  }

  get touchSimulator() {
    if (!this._touchSimulator) {
      this._touchSimulator = new TouchSimulator(this.chromeEventHandler);
    }

    return this._touchSimulator;
  }

  /**
   * Opposite of the updateTargetConfiguration method, that resets document
   * state when closing the toolbox.
   */
  _restoreTargetConfiguration() {
    if (this._restoreFocus && this.browsingContext?.isActive) {
      this.window.focus();
    }
  }

  _changeTopLevelDocument(window) {
    // In case of WebExtension, still using one WindowGlobalTarget instance for many document,
    // when reloading the add-on we might not destroy the previous target and wait for the next
    // one to come and destroy it.
    if (this.window) {
      // Fake a will-navigate on the previous document
      // to let a chance to unregister it
      this._willNavigate({
        window: this.window,
        newURI: window.location.href,
        request: null,
        isFrameSwitching: true,
        navigationStart: Date.now(),
      });

      this._windowDestroyed(this.window, {
        isFrozen: true,
        isFrameSwitching: true,
      });
    }

    // Immediately change the window as this window, if in process of unload
    // may already be non working on the next cycle and start throwing
    this._setWindow(window);

    DevToolsUtils.executeSoon(() => {
      // No need to do anything more if the actor is destroyed.
      // e.g. the client has been closed and the actors destroyed in the meantime.
      if (this.isDestroyed()) {
        return;
      }

      // Then fake window-ready and navigate on the given document
      this._windowReady(window, { isFrameSwitching: true });
      DevToolsUtils.executeSoon(() => {
        this._navigate(window, true);
      });
    });
  }

  _setWindow(window) {
    // Here is the very important call where we switch the currently targeted
    // window global (it will indirectly update this.window and many other
    // attributes defined from docShell).
    this.docShell = window.docShell;
    this.emit("changed-toplevel-document");
    this.emit("frameUpdate", {
      selected: this.outerWindowID,
    });
  }

  /**
   * Handle location changes, by clearing the previous debuggees and enabling
   * debugging, which may have been disabled temporarily by the
   * DebuggerProgressListener.
   */
  _windowReady(window, { isFrameSwitching, isBFCache } = {}) {
    if (this.ignoreSubFrames) {
      return;
    }
    const isTopLevel = window == this.window;

    // We just reset iframe list on WillNavigate, so we now list all existing
    // frames when we load a new document in the original window
    if (window == this._originalWindow && !isFrameSwitching) {
      this._updateChildDocShells();
    }

    // If this follows WindowGlobal lifecycle, a new Target actor will be spawn for the top level
    // target document. Only notify about in-process iframes.
    // Note that OOP iframes won't emit window-ready and will also have their dedicated target.
    // Also, we allow window-ready to be fired for iframe switching of top level documents,
    // otherwise the iframe dropdown no longer works with server side targets.
    if (this.followWindowGlobalLifeCycle && isTopLevel && !isFrameSwitching) {
      return;
    }

    this.emit("window-ready", {
      window,
      isTopLevel,
      isBFCache,
      id: getWindowID(window),
      isFrameSwitching,
    });
  }

  _windowDestroyed(
    window,
    { id = null, isFrozen = false, isFrameSwitching = false }
  ) {
    if (this.ignoreSubFrames) {
      return;
    }
    const isTopLevel = window == this.window;

    // If this follows WindowGlobal lifecycle, this target will be destroyed, alongside its top level document.
    // Only notify about in-process iframes.
    // Note that OOP iframes won't emit window-ready and will also have their dedicated target.
    // Also, we allow window-destroyed to be fired for iframe switching of top level documents,
    // otherwise the iframe dropdown no longer works with server side targets.
    if (this.followWindowGlobalLifeCycle && isTopLevel && !isFrameSwitching) {
      return;
    }

    this.emit("window-destroyed", {
      window,
      isTopLevel,
      id: id || getWindowID(window),
      isFrozen,
    });
  }

  /**
   * Start notifying server and client about a new document being loaded in the
   * currently targeted window global.
   */
  _willNavigate({
    window,
    newURI,
    request,
    isFrameSwitching = false,
    navigationStart,
  }) {
    if (this.ignoreSubFrames) {
      return;
    }
    let isTopLevel = window == this.window;

    let reset = false;
    if (window == this._originalWindow && !isFrameSwitching) {
      // If the top level document changes and we are targeting an iframe, we
      // need to reset to the upcoming new top level document. But for this
      // will-navigate event, we will dispatch on the old window. (The inspector
      // codebase expect to receive will-navigate for the currently displayed
      // document in order to cleanup the markup view)
      if (this.window != this._originalWindow) {
        reset = true;
        window = this.window;
        isTopLevel = true;
      }
    }

    // will-navigate event needs to be dispatched synchronously, by calling the
    // listeners in the order or registration. This event fires once navigation
    // starts, (all pending user prompts are dealt with), but before the first
    // request starts.
    this.emit("will-navigate", {
      window,
      isTopLevel,
      newURI,
      request,
      navigationStart,
      isFrameSwitching,
    });

    // We don't do anything for inner frames here.
    // (we will only update thread actor on window-ready)
    if (!isTopLevel) {
      return;
    }

    // When the actor acts as a WindowGlobalTarget, will-navigate won't fired.
    // Instead we will receive a new top level target with isTargetSwitching=true.
    if (!this.followWindowGlobalLifeCycle) {
      this.emit("tabNavigated", {
        url: newURI,
        state: "start",
        isFrameSwitching,
      });
    }

    if (reset) {
      this._setWindow(this._originalWindow);
    }
  }

  /**
   * Notify server and client about a new document done loading in the current
   * targeted window global.
   */
  _navigate(window, isFrameSwitching = false) {
    if (this.ignoreSubFrames) {
      return;
    }
    const isTopLevel = window == this.window;

    // navigate event needs to be dispatched synchronously,
    // by calling the listeners in the order or registration.
    // This event is fired once the document is loaded,
    // after the load event, it's document ready-state is 'complete'.
    this.emit("navigate", {
      window,
      isTopLevel,
    });

    // We don't do anything for inner frames here.
    // (we will only update thread actor on window-ready)
    if (!isTopLevel) {
      return;
    }

    // We may still significate when the document is done loading, via navigate.
    // But as we no longer fire the "will-navigate", may be it is better to find
    // other ways to get to our means.
    // Listening to "navigate" is misleading as the document may already be loaded
    // if we just opened the DevTools. So it is better to use "watch" pattern
    // and instead have the actor either emit immediately resources as they are
    // already available, or later on as the load progresses.
    if (this.followWindowGlobalLifeCycle) {
      return;
    }

    this.emit("tabNavigated", {
      url: this.url,
      title: this.title,
      state: "stop",
      isFrameSwitching,
    });
  }

  removeActorByName(name) {
    if (name in this._extraActors) {
      const actor = this._extraActors[name];
      if (this._targetScopedActorPool.has(actor)) {
        this._targetScopedActorPool.removeActor(actor);
      }
      delete this._extraActors[name];
    }
  }
}

exports.WindowGlobalTargetActor = WindowGlobalTargetActor;

class DebuggerProgressListener {
  /**
   * The DebuggerProgressListener class is an nsIWebProgressListener which
   * handles onStateChange events for the targeted window global. If the user
   * tries to navigate away from a paused page, the listener makes sure that the
   * debuggee is resumed before the navigation begins.
   *
   * @param WindowGlobalTargetActor targetActor
   *        The window global target actor associated with this listener.
   */
  constructor(targetActor) {
    this._targetActor = targetActor;
    this._onWindowCreated = this.onWindowCreated.bind(this);
    this._onWindowHidden = this.onWindowHidden.bind(this);

    // Watch for windows destroyed (global observer that will need filtering)
    Services.obs.addObserver(this, "inner-window-destroyed");

    // XXX: for now we maintain the list of windows we know about in this instance
    // so that we can discriminate windows we care about when observing
    // inner-window-destroyed events. Bug 1016952 would remove the need for this.
    this._knownWindowIDs = new Map();

    this._watchedDocShells = new WeakSet();
  }

  QueryInterface = ChromeUtils.generateQI([
    "nsIWebProgressListener",
    "nsISupportsWeakReference",
  ]);

  destroy() {
    Services.obs.removeObserver(this, "inner-window-destroyed");
    this._knownWindowIDs.clear();
    this._knownWindowIDs = null;
  }

  watch(docShell) {
    // Add the docshell to the watched set. We're actually adding the window,
    // because docShell objects are not wrappercached and would be rejected
    // by the WeakSet.
    const docShellWindow = docShell.domWindow;
    this._watchedDocShells.add(docShellWindow);

    const webProgress = docShell
      .QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIWebProgress);
    webProgress.addProgressListener(
      this,
      Ci.nsIWebProgress.NOTIFY_STATE_WINDOW |
        Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT
    );

    const handler = getDocShellChromeEventHandler(docShell);
    handler.addEventListener("DOMWindowCreated", this._onWindowCreated, true);
    handler.addEventListener("pageshow", this._onWindowCreated, true);
    handler.addEventListener("pagehide", this._onWindowHidden, true);

    // Dispatch the _windowReady event on the targetActor for pre-existing windows
    const windows = this._targetActor.ignoreSubFrames
      ? [docShellWindow]
      : this._getWindowsInDocShell(docShell);
    for (const win of windows) {
      this._targetActor._windowReady(win);
      this._knownWindowIDs.set(getWindowID(win), win);
    }

    // The `watchedByDevTools` enables gecko behavior tied to this flag, such as:
    //  - reporting the contents of HTML loaded in the docshells,
    //  - or capturing stacks for the network monitor.
    //
    // This flag is also set in frame-helper but in the case of the browser toolbox, we
    // don't have the watcher enabled by default yet, and as a result we need to set it
    // here for the parent process window global.
    // This should be removed as part of Bug 1709529.
    if (this._targetActor.typeName === "parentProcessTarget") {
      docShell.browsingContext.watchedByDevTools = true;
    }
    // Immediately enable CSS error reports on new top level docshells, if this was already enabled.
    // This is specific to MBT and WebExtension targets (so the isRootActor check).
    if (
      this._targetActor.isRootActor &&
      this._targetActor.docShell.cssErrorReportingEnabled
    ) {
      docShell.cssErrorReportingEnabled = true;
    }
  }

  unwatch(docShell) {
    const docShellWindow = docShell.domWindow;
    if (!this._watchedDocShells.has(docShellWindow)) {
      return;
    }
    this._watchedDocShells.delete(docShellWindow);

    const webProgress = docShell
      .QueryInterface(Ci.nsIInterfaceRequestor)
      .getInterface(Ci.nsIWebProgress);
    // During process shutdown, the docshell may already be cleaned up and throw
    try {
      webProgress.removeProgressListener(this);
    } catch (e) {
      // ignore
    }

    const handler = getDocShellChromeEventHandler(docShell);
    handler.removeEventListener(
      "DOMWindowCreated",
      this._onWindowCreated,
      true
    );
    handler.removeEventListener("pageshow", this._onWindowCreated, true);
    handler.removeEventListener("pagehide", this._onWindowHidden, true);

    const windows = this._targetActor.ignoreSubFrames
      ? [docShellWindow]
      : this._getWindowsInDocShell(docShell);
    for (const win of windows) {
      this._knownWindowIDs.delete(getWindowID(win));
    }

    // We only reset it for parent process target actor as the flag should be set in parent
    // process, and thus is set elsewhere for other type of BrowsingContextActor.
    if (this._targetActor.typeName === "parentProcessTarget") {
      docShell.browsingContext.watchedByDevTools = false;
    }
  }

  _getWindowsInDocShell(docShell) {
    return getChildDocShells(docShell).map(d => {
      return d.domWindow;
    });
  }

  onWindowCreated = DevToolsUtils.makeInfallible(function (evt) {
    if (this._targetActor.isDestroyed()) {
      return;
    }

    // If we're in a frame swap (which occurs when toggling RDM, for example), then we can
    // ignore this event, as the window never really went anywhere for our purposes.
    if (evt.inFrameSwap) {
      return;
    }

    const window = evt.target.defaultView;
    if (!window) {
      // Some old UIs might emit unrelated events called pageshow/pagehide on
      // elements which are not documents. Bail in this case. See Bug 1669666.
      return;
    }

    const innerID = getWindowID(window);

    // This handler is called for two events: "DOMWindowCreated" and "pageshow".
    // Bail out if we already processed this window.
    if (this._knownWindowIDs.has(innerID)) {
      return;
    }
    this._knownWindowIDs.set(innerID, window);

    // For a regular page navigation, "DOMWindowCreated" is fired before
    // "pageshow". If the current event is "pageshow" but we have not processed
    // the window yet, it means this is a BF cache navigation. In theory,
    // `event.persisted` should be set for BF cache navigation events, but it is
    // not always available, so we fallback on checking if "pageshow" is the
    // first event received for a given window (see Bug 1378133).
    const isBFCache = evt.type == "pageshow";

    this._targetActor._windowReady(window, { isBFCache });
  }, "DebuggerProgressListener.prototype.onWindowCreated");

  onWindowHidden = DevToolsUtils.makeInfallible(function (evt) {
    if (this._targetActor.isDestroyed()) {
      return;
    }

    // If we're in a frame swap (which occurs when toggling RDM, for example), then we can
    // ignore this event, as the window isn't really going anywhere for our purposes.
    if (evt.inFrameSwap) {
      return;
    }

    // Only act as if the window has been destroyed if the 'pagehide' event
    // was sent for a persisted window (persisted is set when the page is put
    // and frozen in the bfcache). If the page isn't persisted, the observer's
    // inner-window-destroyed event will handle it.
    if (!evt.persisted) {
      return;
    }

    const window = evt.target.defaultView;
    if (!window) {
      // Some old UIs might emit unrelated events called pageshow/pagehide on
      // elements which are not documents. Bail in this case. See Bug 1669666.
      return;
    }

    this._targetActor._windowDestroyed(window, { isFrozen: true });
    this._knownWindowIDs.delete(getWindowID(window));
  }, "DebuggerProgressListener.prototype.onWindowHidden");

  observe = DevToolsUtils.makeInfallible(function (subject, topic) {
    if (this._targetActor.isDestroyed()) {
      return;
    }

    // Because this observer will be called for all inner-window-destroyed in
    // the application, we need to filter out events for windows we are not
    // watching
    const innerID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
    const window = this._knownWindowIDs.get(innerID);
    if (window) {
      this._knownWindowIDs.delete(innerID);
      this._targetActor._windowDestroyed(window, { id: innerID });
    }

    // Bug 1598364: when debugging browser.xhtml from the Browser Toolbox
    // the DOMWindowCreated/pageshow/pagehide event listeners have to be
    // re-registered against the next document when we reload browser.html
    // (or navigate to another doc).
    // That's because we registered the listener on docShell.domWindow as
    // top level windows don't have a chromeEventHandler.
    if (
      this._watchedDocShells.has(window) &&
      !window.docShell.chromeEventHandler
    ) {
      // First cleanup all the existing listeners
      this.unwatch(window.docShell);
      // Re-register new ones. The docShell is already referencing the new document.
      this.watch(window.docShell);
    }
  }, "DebuggerProgressListener.prototype.observe");

  onStateChange = DevToolsUtils.makeInfallible(function (
    progress,
    request,
    flag,
    status
  ) {
    if (this._targetActor.isDestroyed()) {
      return;
    }
    progress.QueryInterface(Ci.nsIDocShell);
    if (progress.isBeingDestroyed()) {
      return;
    }

    const isStart = flag & Ci.nsIWebProgressListener.STATE_START;
    const isStop = flag & Ci.nsIWebProgressListener.STATE_STOP;
    const isDocument = flag & Ci.nsIWebProgressListener.STATE_IS_DOCUMENT;
    const isWindow = flag & Ci.nsIWebProgressListener.STATE_IS_WINDOW;

    // Ideally, we would fetch navigationStart from window.performance.timing.navigationStart
    // but as WindowGlobal isn't instantiated yet we don't have access to it.
    // This is ultimately handed over to DocumentEventListener, which uses this.
    // See its comment about WILL_NAVIGATE_TIME_SHIFT for more details about the related workaround.
    const navigationStart = Date.now();

    // Catch any iframe location change
    if (isDocument && isStop) {
      // Watch document stop to ensure having the new iframe url.
      this._targetActor._notifyDocShellsUpdate([progress]);
    }

    const window = progress.DOMWindow;
    if (isDocument && isStart) {
      // One of the earliest events that tells us a new URI
      // is being loaded in this window.
      const newURI = request instanceof Ci.nsIChannel ? request.URI.spec : null;
      this._targetActor._willNavigate({
        window,
        newURI,
        request,
        isFrameSwitching: false,
        navigationStart,
      });
    }
    if (isWindow && isStop) {
      // Don't dispatch "navigate" event just yet when there is a redirect to
      // about:neterror page.
      // Navigating to about:neterror will make `status` be something else than NS_OK.
      // But for some error like NS_BINDING_ABORTED we don't want to emit any `navigate`
      // event as the page load has been cancelled and the related page document is going
      // to be a dead wrapper.
      if (
        request.status != Cr.NS_OK &&
        request.status != Cr.NS_BINDING_ABORTED
      ) {
        // Instead, listen for DOMContentLoaded as about:neterror is loaded
        // with LOAD_BACKGROUND flags and never dispatches load event.
        // That may be the same reason why there is no onStateChange event
        // for about:neterror loads.
        const handler = getDocShellChromeEventHandler(progress);
        const onLoad = evt => {
          // Ignore events from iframes
          if (evt.target === window.document) {
            handler.removeEventListener("DOMContentLoaded", onLoad, true);
            this._targetActor._navigate(window);
          }
        };
        handler.addEventListener("DOMContentLoaded", onLoad, true);
      } else {
        // Somewhat equivalent of load event.
        // (window.document.readyState == complete)
        this._targetActor._navigate(window);
      }
    }
  },
  "DebuggerProgressListener.prototype.onStateChange");
}
