/* 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";

/*
 * Target actor for the entire parent process.
 *
 * This actor extends WindowGlobalTargetActor.
 * This actor is extended by WebExtensionTargetActor.
 *
 * See devtools/docs/backend/actor-hierarchy.md for more details.
 */

const {
  DevToolsServer,
} = require("resource://devtools/server/devtools-server.js");
const {
  getChildDocShells,
  WindowGlobalTargetActor,
} = require("resource://devtools/server/actors/targets/window-global.js");
const makeDebugger = require("resource://devtools/server/actors/utils/make-debugger.js");

const {
  parentProcessTargetSpec,
} = require("resource://devtools/shared/specs/targets/parent-process.js");

class ParentProcessTargetActor extends WindowGlobalTargetActor {
  /**
   * Creates a target actor for debugging all the chrome content in the parent process.
   * Most of the implementation is inherited from WindowGlobalTargetActor.
   * ParentProcessTargetActor is a child of RootActor, it can be instantiated via
   * RootActor.getProcess request. ParentProcessTargetActor exposes all target-scoped actors
   * via its form() request, like WindowGlobalTargetActor.
   *
   * @param conn DevToolsServerConnection
   *        The connection to the client.
   * @param {Object} options
   *        - isTopLevelTarget: {Boolean} flag to indicate if this is the top
   *          level target of the DevTools session
   *        - sessionContext Object
   *          The Session Context to help know what is debugged.
   *          See devtools/server/actors/watcher/session-context.js
   *        - customSpec Object
   *          WebExtensionTargetActor inherits from ParentProcessTargetActor
   *          and has to use its own protocol.js specification object.
   */
  constructor(
    conn,
    { isTopLevelTarget, sessionContext, customSpec = parentProcessTargetSpec }
  ) {
    super(conn, {
      isTopLevelTarget,
      sessionContext,
      customSpec,
    });

    // This creates a Debugger instance for chrome debugging all globals.
    this.makeDebugger = makeDebugger.bind(null, {
      findDebuggees: dbg => dbg.findAllGlobals(),
      shouldAddNewGlobalAsDebuggee: () => true,
    });

    // Ensure catching the creation of any new content docshell
    this.watchNewDocShells = true;

    this.isRootActor = true;

    // Listen for any new/destroyed chrome docshell
    Services.obs.addObserver(this, "chrome-webnavigation-create");
    Services.obs.addObserver(this, "chrome-webnavigation-destroy");

    // If we are the parent process target actor and not a subclass
    // (i.e. if we aren't the webext target actor)
    // set the parent process docshell:
    if (customSpec == parentProcessTargetSpec) {
      this.setDocShell(this._getInitialDocShell());
    }
  }

  // Overload setDocShell in order to observe all the docshells.
  // WindowGlobalTargetActor only observes the top level one,
  // but we also need to observe all of them for WebExtensionTargetActor subclass.
  setDocShell(initialDocShell) {
    super.setDocShell(initialDocShell);

    // Iterate over all top-level windows.
    for (const { docShell } of Services.ww.getWindowEnumerator()) {
      if (docShell == this.docShell) {
        continue;
      }
      this._progressListener.watch(docShell);
    }
  }

  _getInitialDocShell() {
    // Defines the default docshell selected for the target actor
    let window = Services.wm.getMostRecentWindow(
      DevToolsServer.chromeWindowType
    );

    // Default to any available top level window if there is no expected window
    // eg when running ./mach run --chrome chrome://browser/content/aboutTabCrashed.xhtml --jsdebugger
    if (!window) {
      window = Services.wm.getMostRecentWindow(null);
    }

    // We really want _some_ window at least, so fallback to the hidden window if
    // there's nothing else (such as during early startup).
    if (!window) {
      window = Services.appShell.hiddenDOMWindow;
    }
    return window.docShell;
  }

  /**
   * Getter for the list of all docshells in this targetActor
   * @return {Array}
   */
  get docShells() {
    // Iterate over all top-level windows and all their docshells.
    let docShells = [];
    for (const { docShell } of Services.ww.getWindowEnumerator()) {
      docShells = docShells.concat(getChildDocShells(docShell));
    }

    return docShells;
  }

  observe(subject, topic, data) {
    super.observe(subject, topic, data);
    if (this.isDestroyed()) {
      return;
    }

    subject.QueryInterface(Ci.nsIDocShell);

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

  _detach() {
    if (this.isDestroyed()) {
      return false;
    }

    Services.obs.removeObserver(this, "chrome-webnavigation-create");
    Services.obs.removeObserver(this, "chrome-webnavigation-destroy");

    // Iterate over all top-level windows.
    for (const { docShell } of Services.ww.getWindowEnumerator()) {
      if (docShell == this.docShell) {
        continue;
      }
      this._progressListener.unwatch(docShell);
    }

    return super._detach();
  }
}

exports.ParentProcessTargetActor = ParentProcessTargetActor;
