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

const { Actor } = require("resource://devtools/shared/protocol.js");
const { walkerSpec } = require("resource://devtools/shared/specs/walker.js");

const {
  LongStringActor,
} = require("resource://devtools/server/actors/string.js");
const {
  EXCLUDED_LISTENER,
} = require("resource://devtools/server/actors/inspector/constants.js");

loader.lazyRequireGetter(
  this,
  "nodeFilterConstants",
  "resource://devtools/shared/dom-node-filter-constants.js"
);

loader.lazyRequireGetter(
  this,
  [
    "getFrameElement",
    "isAfterPseudoElement",
    "isBeforePseudoElement",
    "isDirectShadowHostChild",
    "isMarkerPseudoElement",
    "isFrameBlockedByCSP",
    "isFrameWithChildTarget",
    "isShadowHost",
    "isShadowRoot",
    "loadSheet",
  ],
  "resource://devtools/shared/layout/utils.js",
  true
);

loader.lazyRequireGetter(
  this,
  "throttle",
  "resource://devtools/shared/throttle.js",
  true
);

loader.lazyRequireGetter(
  this,
  [
    "allAnonymousContentTreeWalkerFilter",
    "findGridParentContainerForNode",
    "isNodeDead",
    "noAnonymousContentTreeWalkerFilter",
    "nodeDocument",
    "standardTreeWalkerFilter",
  ],
  "resource://devtools/server/actors/inspector/utils.js",
  true
);

loader.lazyRequireGetter(
  this,
  "CustomElementWatcher",
  "resource://devtools/server/actors/inspector/custom-element-watcher.js",
  true
);
loader.lazyRequireGetter(
  this,
  ["DocumentWalker", "SKIP_TO_SIBLING"],
  "resource://devtools/server/actors/inspector/document-walker.js",
  true
);
loader.lazyRequireGetter(
  this,
  ["NodeActor", "NodeListActor"],
  "resource://devtools/server/actors/inspector/node.js",
  true
);
loader.lazyRequireGetter(
  this,
  "NodePicker",
  "resource://devtools/server/actors/inspector/node-picker.js",
  true
);
loader.lazyRequireGetter(
  this,
  "LayoutActor",
  "resource://devtools/server/actors/layout.js",
  true
);
loader.lazyRequireGetter(
  this,
  ["getLayoutChangesObserver", "releaseLayoutChangesObserver"],
  "resource://devtools/server/actors/reflow.js",
  true
);
loader.lazyRequireGetter(
  this,
  "WalkerSearch",
  "resource://devtools/server/actors/utils/walker-search.js",
  true
);

// ContentDOMReference requires ChromeUtils, which isn't available in worker context.
const lazy = {};
if (!isWorker) {
  loader.lazyGetter(
    lazy,
    "ContentDOMReference",
    () =>
      ChromeUtils.importESModule(
        "resource://gre/modules/ContentDOMReference.sys.mjs",
        // ContentDOMReference needs to be retrieved from the shared global
        // since it is a shared singleton.
        { global: "shared" }
      ).ContentDOMReference
  );
}

loader.lazyServiceGetter(
  this,
  "eventListenerService",
  "@mozilla.org/eventlistenerservice;1",
  "nsIEventListenerService"
);

// Minimum delay between two "new-mutations" events.
const MUTATIONS_THROTTLING_DELAY = 100;
// List of mutation types that should -not- be throttled.
const IMMEDIATE_MUTATIONS = ["pseudoClassLock"];

const HIDDEN_CLASS = "__fx-devtools-hide-shortcut__";

// The possible completions to a ':' with added score to give certain values
// some preference.
const PSEUDO_SELECTORS = [
  [":active", 1],
  [":hover", 1],
  [":focus", 1],
  [":visited", 0],
  [":link", 0],
  [":first-letter", 0],
  [":first-child", 2],
  [":before", 2],
  [":after", 2],
  [":lang(", 0],
  [":not(", 3],
  [":first-of-type", 0],
  [":last-of-type", 0],
  [":only-of-type", 0],
  [":only-child", 2],
  [":nth-child(", 3],
  [":nth-last-child(", 0],
  [":nth-of-type(", 0],
  [":nth-last-of-type(", 0],
  [":last-child", 2],
  [":root", 0],
  [":empty", 0],
  [":target", 0],
  [":enabled", 0],
  [":disabled", 0],
  [":checked", 1],
  ["::selection", 0],
  ["::marker", 0],
];

const HELPER_SHEET =
  "data:text/css;charset=utf-8," +
  encodeURIComponent(`
  .__fx-devtools-hide-shortcut__ {
    visibility: hidden !important;
  }
`);

/**
 * We only send nodeValue up to a certain size by default.  This stuff
 * controls that size.
 */
exports.DEFAULT_VALUE_SUMMARY_LENGTH = 50;
var gValueSummaryLength = exports.DEFAULT_VALUE_SUMMARY_LENGTH;

exports.getValueSummaryLength = function () {
  return gValueSummaryLength;
};

exports.setValueSummaryLength = function (val) {
  gValueSummaryLength = val;
};

/**
 * Server side of the DOM walker.
 */
class WalkerActor extends Actor {
  /**
   * Create the WalkerActor
   * @param {DevToolsServerConnection} conn
   *        The server connection.
   * @param {TargetActor} targetActor
   *        The top-level Actor for this tab.
   * @param {Object} options
   *        - {Boolean} showAllAnonymousContent: Show all native anonymous content
   */
  constructor(conn, targetActor, options) {
    super(conn, walkerSpec);
    this.targetActor = targetActor;
    this.rootWin = targetActor.window;
    this.rootDoc = this.rootWin.document;

    // Map of already created node actors, keyed by their corresponding DOMNode.
    this._nodeActorsMap = new Map();

    this._pendingMutations = [];
    this._activePseudoClassLocks = new Set();
    this._mutationBreakpoints = new WeakMap();
    this._anonParents = new WeakMap();
    this.customElementWatcher = new CustomElementWatcher(
      targetActor.chromeEventHandler
    );

    // In this map, the key-value pairs are the overflow causing elements and their
    // respective ancestor scrollable node actor.
    this.overflowCausingElementsMap = new Map();

    this.showAllAnonymousContent = options.showAllAnonymousContent;

    this.walkerSearch = new WalkerSearch(this);

    // Nodes which have been removed from the client's known
    // ownership tree are considered "orphaned", and stored in
    // this set.
    this._orphaned = new Set();

    // The client can tell the walker that it is interested in a node
    // even when it is orphaned with the `retainNode` method.  This
    // list contains orphaned nodes that were so retained.
    this._retainedOrphans = new Set();

    this.onSubtreeModified = this.onSubtreeModified.bind(this);
    this.onSubtreeModified[EXCLUDED_LISTENER] = true;
    this.onNodeRemoved = this.onNodeRemoved.bind(this);
    this.onNodeRemoved[EXCLUDED_LISTENER] = true;
    this.onAttributeModified = this.onAttributeModified.bind(this);
    this.onAttributeModified[EXCLUDED_LISTENER] = true;

    this.onMutations = this.onMutations.bind(this);
    this.onSlotchange = this.onSlotchange.bind(this);
    this.onShadowrootattached = this.onShadowrootattached.bind(this);
    this.onAnonymousrootcreated = this.onAnonymousrootcreated.bind(this);
    this.onAnonymousrootremoved = this.onAnonymousrootremoved.bind(this);
    this.onFrameLoad = this.onFrameLoad.bind(this);
    this.onFrameUnload = this.onFrameUnload.bind(this);
    this.onCustomElementDefined = this.onCustomElementDefined.bind(this);
    this._throttledEmitNewMutations = throttle(
      this._emitNewMutations.bind(this),
      MUTATIONS_THROTTLING_DELAY
    );

    targetActor.on("will-navigate", this.onFrameUnload);
    targetActor.on("window-ready", this.onFrameLoad);

    this.customElementWatcher.on(
      "element-defined",
      this.onCustomElementDefined
    );

    // Keep a reference to the chromeEventHandler for the current targetActor, to make
    // sure we will be able to remove the listener during the WalkerActor destroy().
    this.chromeEventHandler = targetActor.chromeEventHandler;
    // shadowrootattached is a chrome-only event. We enable it below.
    this.chromeEventHandler.addEventListener(
      "shadowrootattached",
      this.onShadowrootattached
    );
    // anonymousrootcreated is a chrome-only event. We enable it below.
    this.chromeEventHandler.addEventListener(
      "anonymousrootcreated",
      this.onAnonymousrootcreated
    );
    this.chromeEventHandler.addEventListener(
      "anonymousrootremoved",
      this.onAnonymousrootremoved
    );
    for (const { document } of this.targetActor.windows) {
      document.devToolsAnonymousAndShadowEventsEnabled = true;
    }

    // Ensure that the root document node actor is ready and
    // managed.
    this.rootNode = this.document();

    this.layoutChangeObserver = getLayoutChangesObserver(this.targetActor);
    this._onReflows = this._onReflows.bind(this);
    this.layoutChangeObserver.on("reflows", this._onReflows);
    this._onResize = this._onResize.bind(this);
    this.layoutChangeObserver.on("resize", this._onResize);

    this._onEventListenerChange = this._onEventListenerChange.bind(this);
    eventListenerService.addListenerChangeListener(this._onEventListenerChange);
  }

  get nodePicker() {
    if (!this._nodePicker) {
      this._nodePicker = new NodePicker(this, this.targetActor);
    }

    return this._nodePicker;
  }

  watchRootNode() {
    if (this.rootNode) {
      this.emit("root-available", this.rootNode);
    }
  }

  /**
   * Callback for eventListenerService.addListenerChangeListener
   * @param nsISimpleEnumerator changesEnum
   *    enumerator of nsIEventListenerChange
   */
  _onEventListenerChange(changesEnum) {
    for (const current of changesEnum.enumerate(Ci.nsIEventListenerChange)) {
      const target = current.target;

      if (this._nodeActorsMap.has(target)) {
        const actor = this.getNode(target);
        const mutation = {
          type: "events",
          target: actor.actorID,
          hasEventListeners: actor.hasEventListeners(/* refreshCache */ true),
        };
        this.queueMutation(mutation);
      }
    }
  }

  // Returns the JSON representation of this object over the wire.
  form() {
    return {
      actor: this.actorID,
      root: this.rootNode.form(),
      rfpCSSColorScheme: ChromeUtils.shouldResistFingerprinting(
        "CSSPrefersColorScheme",
        null
      ),
      traits: {},
    };
  }

  toString() {
    return "[WalkerActor " + this.actorID + "]";
  }

  getDocumentWalkerFilter() {
    // Allow native anonymous content (like <video> controls) if preffed on
    return this.showAllAnonymousContent
      ? allAnonymousContentTreeWalkerFilter
      : standardTreeWalkerFilter;
  }

  getDocumentWalker(node, skipTo) {
    return new DocumentWalker(node, this.rootWin, {
      filter: this.getDocumentWalkerFilter(),
      skipTo,
      showAnonymousContent: true,
    });
  }

  destroy() {
    if (this._destroyed) {
      return;
    }
    this._destroyed = true;
    super.destroy();
    try {
      this.clearPseudoClassLocks();
      this._activePseudoClassLocks = null;

      this.overflowCausingElementsMap.clear();
      this.overflowCausingElementsMap = null;

      this._hoveredNode = null;
      this.rootWin = null;
      this.rootDoc = null;
      this.rootNode = null;
      this.layoutHelpers = null;
      this._orphaned = null;
      this._retainedOrphans = null;

      this.targetActor.off("will-navigate", this.onFrameUnload);
      this.targetActor.off("window-ready", this.onFrameLoad);
      this.customElementWatcher.off(
        "element-defined",
        this.onCustomElementDefined
      );

      this.chromeEventHandler.removeEventListener(
        "shadowrootattached",
        this.onShadowrootattached
      );
      this.chromeEventHandler.removeEventListener(
        "anonymousrootcreated",
        this.onAnonymousrootcreated
      );
      this.chromeEventHandler.removeEventListener(
        "anonymousrootremoved",
        this.onAnonymousrootremoved
      );

      // This attribute is just for devtools, so we can unset once we're done.
      for (const { document } of this.targetActor.windows) {
        document.devToolsAnonymousAndShadowEventsEnabled = false;
      }

      this.onFrameLoad = null;
      this.onFrameUnload = null;

      this.customElementWatcher.destroy();
      this.customElementWatcher = null;

      this.walkerSearch.destroy();

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

      this.layoutChangeObserver.off("reflows", this._onReflows);
      this.layoutChangeObserver.off("resize", this._onResize);
      this.layoutChangeObserver = null;
      releaseLayoutChangesObserver(this.targetActor);

      eventListenerService.removeListenerChangeListener(
        this._onEventListenerChange
      );

      // Only nullify some key attributes after having removed all the listeners
      // as they may still be used in the related listeners.
      this._nodeActorsMap = null;
      this.onMutations = null;

      this.layoutActor = null;
      this.targetActor = null;
      this.chromeEventHandler = null;

      this.emit("destroyed");
    } catch (e) {
      console.error(e);
    }
  }

  release() {}

  unmanage(actor) {
    if (actor instanceof NodeActor) {
      if (
        this._activePseudoClassLocks &&
        this._activePseudoClassLocks.has(actor)
      ) {
        this.clearPseudoClassLocks(actor);
      }

      this.customElementWatcher.unmanageNode(actor);

      this._nodeActorsMap.delete(actor.rawNode);
    }
    super.unmanage(actor);
  }

  /**
   * Determine if the walker has come across this DOM node before.
   * @param {DOMNode} rawNode
   * @return {Boolean}
   */
  hasNode(rawNode) {
    return this._nodeActorsMap.has(rawNode);
  }

  /**
   * If the walker has come across this DOM node before, then get the
   * corresponding node actor.
   * @param {DOMNode} rawNode
   * @return {NodeActor}
   */
  getNode(rawNode) {
    return this._nodeActorsMap.get(rawNode);
  }

  /**
   * Internal helper that will either retrieve the existing NodeActor for the
   * provided node or create the actor on the fly if it doesn't exist.
   * This method should only be called when we are sure that the node should be
   * known by the client and that the parent node is already known.
   *
   * Otherwise prefer `getNode` to only retrieve known actors or `attachElement`
   * to create node actors recursively.
   *
   * @param  {DOMNode} node
   *         The node for which we want to create or get an actor
   * @return {NodeActor} The corresponding NodeActor
   */
  _getOrCreateNodeActor(node) {
    let actor = this.getNode(node);
    if (actor) {
      return actor;
    }

    actor = new NodeActor(this, node);

    // Add the node actor as a child of this walker actor, assigning
    // it an actorID.
    this.manage(actor);
    this._nodeActorsMap.set(node, actor);

    if (node.nodeType === Node.DOCUMENT_NODE) {
      actor.watchDocument(node, this.onMutations);
    }

    if (isShadowRoot(actor.rawNode)) {
      actor.watchDocument(node.ownerDocument, this.onMutations);
      actor.watchSlotchange(this.onSlotchange);
    }

    this.customElementWatcher.manageNode(actor);

    return actor;
  }

  /**
   * When a custom element is defined, send a customElementDefined mutation for all the
   * NodeActors using this tag name.
   */
  onCustomElementDefined({ actors }) {
    actors.forEach(actor =>
      this.queueMutation({
        target: actor.actorID,
        type: "customElementDefined",
        customElementLocation: actor.getCustomElementLocation(),
      })
    );
  }

  _onReflows() {
    // Going through the nodes the walker knows about, see which ones have had their
    // containerType, display, scrollable or overflow state changed and send events if any.
    const containerTypeChanges = [];
    const displayTypeChanges = [];
    const scrollableStateChanges = [];

    const currentOverflowCausingElementsMap = new Map();

    for (const [node, actor] of this._nodeActorsMap) {
      if (Cu.isDeadWrapper(node)) {
        continue;
      }

      const displayType = actor.displayType;
      const isDisplayed = actor.isDisplayed;

      if (
        displayType !== actor.currentDisplayType ||
        isDisplayed !== actor.wasDisplayed
      ) {
        displayTypeChanges.push(actor);

        // Updating the original value
        actor.currentDisplayType = displayType;
        actor.wasDisplayed = isDisplayed;
      }

      const isScrollable = actor.isScrollable;
      if (isScrollable !== actor.wasScrollable) {
        scrollableStateChanges.push(actor);
        actor.wasScrollable = isScrollable;
      }

      if (isScrollable) {
        this.updateOverflowCausingElements(
          actor,
          currentOverflowCausingElementsMap
        );
      }

      const containerType = actor.containerType;
      if (containerType !== actor.currentContainerType) {
        containerTypeChanges.push(actor);
        actor.currentContainerType = containerType;
      }
    }

    // Get the NodeActor for each node in the symmetric difference of
    // currentOverflowCausingElementsMap and this.overflowCausingElementsMap
    const overflowStateChanges = [...currentOverflowCausingElementsMap.keys()]
      .filter(node => !this.overflowCausingElementsMap.has(node))
      .concat(
        [...this.overflowCausingElementsMap.keys()].filter(
          node => !currentOverflowCausingElementsMap.has(node)
        )
      )
      .filter(node => this.hasNode(node))
      .map(node => this.getNode(node));

    this.overflowCausingElementsMap = currentOverflowCausingElementsMap;

    if (overflowStateChanges.length) {
      this.emit("overflow-change", overflowStateChanges);
    }

    if (displayTypeChanges.length) {
      this.emit("display-change", displayTypeChanges);
    }

    if (scrollableStateChanges.length) {
      this.emit("scrollable-change", scrollableStateChanges);
    }

    if (containerTypeChanges.length) {
      this.emit("container-type-change", containerTypeChanges);
    }
  }

  /**
   * When the browser window gets resized, relay the event to the front.
   */
  _onResize() {
    this.emit("resize");
  }

  /**
   * Ensures that the node is attached and it can be accessed from the root.
   *
   * @param {(Node|NodeActor)} nodes The nodes
   * @return {Object} An object compatible with the disconnectedNode type.
   */
  attachElement(node) {
    const { nodes, newParents } = this.attachElements([node]);
    return {
      node: nodes[0],
      newParents,
    };
  }

  /**
   * Ensures that the nodes are attached and they can be accessed from the root.
   *
   * @param {(Node[]|NodeActor[])} nodes The nodes
   * @return {Object} An object compatible with the disconnectedNodeArray type.
   */
  attachElements(nodes) {
    const nodeActors = [];
    const newParents = new Set();
    for (let node of nodes) {
      if (!(node instanceof NodeActor)) {
        // If an anonymous node was passed in and we aren't supposed to know
        // about it, then use the closest ancestor.
        if (!this.showAllAnonymousContent) {
          while (
            node &&
            standardTreeWalkerFilter(node) != nodeFilterConstants.FILTER_ACCEPT
          ) {
            node = this.rawParentNode(node);
          }
          if (!node) {
            continue;
          }
        }

        node = this._getOrCreateNodeActor(node);
      }

      this.ensurePathToRoot(node, newParents);
      // If nodes may be an array of raw nodes, we're sure to only have
      // NodeActors with the following array.
      nodeActors.push(node);
    }

    return {
      nodes: nodeActors,
      newParents: [...newParents],
    };
  }

  /**
   * Return the document node that contains the given node,
   * or the root node if no node is specified.
   * @param NodeActor node
   *        The node whose document is needed, or null to
   *        return the root.
   */
  document(node) {
    const doc = isNodeDead(node) ? this.rootDoc : nodeDocument(node.rawNode);
    return this._getOrCreateNodeActor(doc);
  }

  /**
   * Return the documentElement for the document containing the
   * given node.
   * @param NodeActor node
   *        The node whose documentElement is requested, or null
   *        to use the root document.
   */
  documentElement(node) {
    const elt = isNodeDead(node)
      ? this.rootDoc.documentElement
      : nodeDocument(node.rawNode).documentElement;
    return this._getOrCreateNodeActor(elt);
  }

  parentNode(node) {
    const parent = this.rawParentNode(node);
    if (parent) {
      return this._getOrCreateNodeActor(parent);
    }

    return null;
  }

  rawParentNode(node) {
    const rawNode = node instanceof NodeActor ? node.rawNode : node;
    if (rawNode == this.rootDoc) {
      return null;
    }
    return InspectorUtils.getParentForNode(rawNode, /* anonymous = */ true);
  }

  /**
   * If the given NodeActor only has a single text node as a child with a text
   * content small enough to be inlined, return that child's NodeActor.
   *
   * @param NodeActor node
   */
  inlineTextChild({ rawNode }) {
    // Quick checks to prevent creating a new walker if possible.
    if (
      isMarkerPseudoElement(rawNode) ||
      isBeforePseudoElement(rawNode) ||
      isAfterPseudoElement(rawNode) ||
      isShadowHost(rawNode) ||
      rawNode.nodeType != Node.ELEMENT_NODE ||
      !!rawNode.children.length ||
      isFrameWithChildTarget(this.targetActor, rawNode) ||
      isFrameBlockedByCSP(rawNode)
    ) {
      return undefined;
    }

    const children = this._rawChildren(rawNode, /* includeAssigned = */ true);
    const firstChild = children[0];

    // Bail out if:
    // - more than one child
    // - unique child is not a text node
    // - unique child is a text node, but is too long to be inlined
    // - we are a slot -> these are always represented on their own lines with
    //                    a link to the original node.
    // - we are a flex item -> these are always shown on their own lines so they can be
    //                         selected by the flexbox inspector.
    const isAssignedToSlot =
      firstChild &&
      rawNode.nodeName === "SLOT" &&
      isDirectShadowHostChild(firstChild);

    const isFlexItem = !!firstChild?.parentFlexElement;

    if (
      !firstChild ||
      children.length > 1 ||
      firstChild.nodeType !== Node.TEXT_NODE ||
      firstChild.nodeValue.length > gValueSummaryLength ||
      isAssignedToSlot ||
      isFlexItem
    ) {
      return undefined;
    }

    return this._getOrCreateNodeActor(firstChild);
  }

  /**
   * Mark a node as 'retained'.
   *
   * A retained node is not released when `releaseNode` is called on its
   * parent, or when a parent is released with the `cleanup` option to
   * `getMutations`.
   *
   * When a retained node's parent is released, a retained mode is added to
   * the walker's "retained orphans" list.
   *
   * Retained nodes can be deleted by providing the `force` option to
   * `releaseNode`.  They will also be released when their document
   * has been destroyed.
   *
   * Retaining a node makes no promise about its children;  They can
   * still be removed by normal means.
   */
  retainNode(node) {
    node.retained = true;
  }

  /**
   * Remove the 'retained' mark from a node.  If the node was a
   * retained orphan, release it.
   */
  unretainNode(node) {
    node.retained = false;
    if (this._retainedOrphans.has(node)) {
      this._retainedOrphans.delete(node);
      this.releaseNode(node);
    }
  }

  /**
   * Release actors for a node and all child nodes.
   */
  releaseNode(node, options = {}) {
    if (isNodeDead(node)) {
      return;
    }

    if (node.retained && !options.force) {
      this._retainedOrphans.add(node);
      return;
    }

    if (node.retained) {
      // Forcing a retained node to go away.
      this._retainedOrphans.delete(node);
    }

    for (const child of this._rawChildren(node.rawNode)) {
      const childActor = this.getNode(child);
      if (childActor) {
        this.releaseNode(childActor, options);
      }
    }

    node.destroy();
  }

  /**
   * Add any nodes between `node` and the walker's root node that have not
   * yet been seen by the client.
   */
  ensurePathToRoot(node, newParents = new Set()) {
    if (!node) {
      return newParents;
    }
    let parent = this.rawParentNode(node);
    while (parent) {
      let parentActor = this.getNode(parent);
      if (parentActor) {
        // This parent did exist, so the client knows about it.
        return newParents;
      }
      // This parent didn't exist, so hasn't been seen by the client yet.
      parentActor = this._getOrCreateNodeActor(parent);
      newParents.add(parentActor);
      parent = this.rawParentNode(parentActor);
    }
    return newParents;
  }

  /**
   * Return the number of children under the provided NodeActor.
   *
   * @param NodeActor node
   *    See JSDoc for children()
   * @param object options
   *    See JSDoc for children()
   * @return Number the number of children
   */
  countChildren(node, options = {}) {
    return this._getChildren(node, options).nodes.length;
  }

  /**
   * Return children of the given node.  By default this method will return
   * all children of the node, but there are options that can restrict this
   * to a more manageable subset.
   *
   * @param NodeActor node
   *    The node whose children you're curious about.
   * @param object options
   *    Named options:
   *    `maxNodes`: The set of nodes returned by the method will be no longer
   *       than maxNodes.
   *    `start`: If a node is specified, the list of nodes will start
   *       with the given child.  Mutally exclusive with `center`.
   *    `center`: If a node is specified, the given node will be as centered
   *       as possible in the list, given how close to the ends of the child
   *       list it is.  Mutually exclusive with `start`.
   *
   * @returns an object with three items:
   *    hasFirst: true if the first child of the node is included in the list.
   *    hasLast: true if the last child of the node is included in the list.
   *    nodes: Array of NodeActor representing the nodes returned by the request.
   */
  children(node, options = {}) {
    const { hasFirst, hasLast, nodes } = this._getChildren(node, options);
    return {
      hasFirst,
      hasLast,
      nodes: nodes.map(n => this._getOrCreateNodeActor(n)),
    };
  }

  /**
   * Returns the raw children of the DOM node, with anonymous content filtered as needed
   * @param Node rawNode.
   * @param boolean includeAssigned
   *   Whether <slot> assigned children should be returned. See
   *   HTMLSlotElement.assignedNodes().
   * @returns Array<Node> the list of children.
   */
  _rawChildren(rawNode, includeAssigned) {
    const filter = this.showAllAnonymousContent
      ? allAnonymousContentTreeWalkerFilter
      : standardTreeWalkerFilter;
    const ret = [];
    const children = InspectorUtils.getChildrenForNode(
      rawNode,
      /* anonymous = */ true,
      includeAssigned
    );
    for (const child of children) {
      if (filter(child) == nodeFilterConstants.FILTER_ACCEPT) {
        ret.push(child);
      }
    }
    return ret;
  }

  /**
   * Return chidlren of the given node. Contrary to children children(), this method only
   * returns DOMNodes. Therefore it will not create NodeActor wrappers and will not
   * update the nodeActors map for the discovered nodes either. This makes this method
   * safe to call when you are not sure if the discovered nodes will be communicated to
   * the client.
   *
   * @param NodeActor node
   *    See JSDoc for children()
   * @param object options
   *    See JSDoc for children()
   * @return  an object with three items:
   *    hasFirst: true if the first child of the node is included in the list.
   *    hasLast: true if the last child of the node is included in the list.
   *    nodes: Array of DOMNodes.
   */
  // eslint-disable-next-line complexity
  _getChildren(node, options = {}) {
    if (isNodeDead(node) || isFrameBlockedByCSP(node.rawNode)) {
      return { hasFirst: true, hasLast: true, nodes: [] };
    }

    if (options.center && options.start) {
      throw Error("Can't specify both 'center' and 'start' options.");
    }

    let maxNodes = options.maxNodes || -1;
    if (maxNodes == -1) {
      maxNodes = Number.MAX_VALUE;
    }

    let nodes = this._rawChildren(node.rawNode, /* includeAssigned = */ true);
    let hasFirst = true;
    let hasLast = true;
    if (nodes.length > maxNodes) {
      let startIndex;
      if (options.center) {
        const centerIndex = nodes.indexOf(options.center.rawNode);
        const backwardCount = Math.floor(maxNodes / 2);
        // If centering would hit the end, just read the last maxNodes nodes.
        if (centerIndex - backwardCount + maxNodes >= nodes.length) {
          startIndex = nodes.length - maxNodes;
        } else {
          startIndex = Math.max(0, centerIndex - backwardCount);
        }
      } else if (options.start) {
        startIndex = Math.max(0, nodes.indexOf(options.start.rawNode));
      } else {
        startIndex = 0;
      }
      const endIndex = Math.min(startIndex + maxNodes, nodes.length);
      hasFirst = startIndex == 0;
      hasLast = endIndex >= nodes.length;
      nodes = nodes.slice(startIndex, endIndex);
    }

    return { hasFirst, hasLast, nodes };
  }

  /**
   * Get the next sibling of a given node.  Getting nodes one at a time
   * might be inefficient, be careful.
   */
  nextSibling(node) {
    if (isNodeDead(node)) {
      return null;
    }

    const walker = this.getDocumentWalker(node.rawNode);
    const sibling = walker.nextSibling();
    return sibling ? this._getOrCreateNodeActor(sibling) : null;
  }

  /**
   * Get the previous sibling of a given node.  Getting nodes one at a time
   * might be inefficient, be careful.
   */
  previousSibling(node) {
    if (isNodeDead(node)) {
      return null;
    }

    const walker = this.getDocumentWalker(node.rawNode);
    const sibling = walker.previousSibling();
    return sibling ? this._getOrCreateNodeActor(sibling) : null;
  }

  /**
   * Helper function for the `children` method: Read forward in the sibling
   * list into an array with `count` items, including the current node.
   */
  _readForward(walker, count) {
    const ret = [];

    let node = walker.currentNode;
    do {
      if (!walker.isSkippedNode(node)) {
        // The walker can be on a node that would be filtered out if it didn't find any
        // other node to fallback to.
        ret.push(node);
      }
      node = walker.nextSibling();
    } while (node && --count);
    return ret;
  }

  /**
   * Return the first node in the document that matches the given selector.
   * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelector
   *
   * @param NodeActor baseNode
   * @param string selector
   */
  querySelector(baseNode, selector) {
    if (isNodeDead(baseNode)) {
      return {};
    }

    const node = baseNode.rawNode.querySelector(selector);
    if (!node) {
      return {};
    }

    return this.attachElement(node);
  }

  /**
   * Return a NodeListActor with all nodes that match the given selector.
   * See https://developer.mozilla.org/en-US/docs/Web/API/Element.querySelectorAll
   *
   * @param NodeActor baseNode
   * @param string selector
   */
  querySelectorAll(baseNode, selector) {
    let nodeList = null;

    try {
      nodeList = baseNode.rawNode.querySelectorAll(selector);
    } catch (e) {
      // Bad selector. Do nothing as the selector can come from a searchbox.
    }

    return new NodeListActor(this, nodeList);
  }

  /**
   * Return the node in the baseNode rootNode matching the passed id referenced in a
   * idref/idreflist attribute, as those are scoped within a shadow root.
   *
   * @param NodeActor baseNode
   * @param string id
   */
  getIdrefNode(baseNode, id) {
    if (isNodeDead(baseNode)) {
      return {};
    }

    // Get the document or the shadow root for baseNode
    const rootNode = baseNode.rawNode.getRootNode({ composed: false });
    if (!rootNode) {
      return {};
    }

    const node = rootNode.getElementById(id);
    if (!node) {
      return {};
    }

    return this.attachElement(node);
  }

  /**
   * Get a list of nodes that match the given selector in all known frames of
   * the current content page.
   * @param {String} selector.
   * @return {Array}
   */
  _multiFrameQuerySelectorAll(selector) {
    let nodes = [];

    for (const { document } of this.targetActor.windows) {
      try {
        nodes = [...nodes, ...document.querySelectorAll(selector)];
      } catch (e) {
        // Bad selector. Do nothing as the selector can come from a searchbox.
      }
    }

    return nodes;
  }

  /**
   * Get a list of nodes that match the given XPath in all known frames of
   * the current content page.
   * @param {String} xPath.
   * @return {Array}
   */
  _multiFrameXPath(xPath) {
    const nodes = [];

    for (const window of this.targetActor.windows) {
      const document = window.document;
      try {
        const result = document.evaluate(
          xPath,
          document.documentElement,
          null,
          window.XPathResult.ORDERED_NODE_SNAPSHOT_TYPE,
          null
        );

        for (let i = 0; i < result.snapshotLength; i++) {
          nodes.push(result.snapshotItem(i));
        }
      } catch (e) {
        // Bad XPath. Do nothing as the XPath can come from a searchbox.
      }
    }

    return nodes;
  }

  /**
   * Return a NodeListActor with all nodes that match the given XPath in all
   * frames of the current content page.
   * @param {String} xPath
   */
  multiFrameXPath(xPath) {
    return new NodeListActor(this, this._multiFrameXPath(xPath));
  }

  /**
   * Search the document for a given string.
   * Results will be searched with the walker-search module (searches through
   * tag names, attribute names and values, and text contents).
   *
   * @returns {searchresult}
   *            - {NodeList} list
   *            - {Array<Object>} metadata. Extra information with indices that
   *                              match up with node list.
   */
  search(query) {
    const results = this.walkerSearch.search(query);
    const nodeList = new NodeListActor(
      this,
      results.map(r => r.node)
    );

    return {
      list: nodeList,
      metadata: [],
    };
  }

  /**
   * Returns a list of matching results for CSS selector autocompletion.
   *
   * @param string query
   *        The selector query being completed
   * @param string completing
   *        The exact token being completed out of the query
   * @param string selectorState
   *        One of "pseudo", "id", "tag", "class", "null"
   */
  // eslint-disable-next-line complexity
  getSuggestionsForQuery(query, completing, selectorState) {
    const sugs = {
      classes: new Map(),
      tags: new Map(),
      ids: new Map(),
    };
    let result = [];
    let nodes = null;
    // Filtering and sorting the results so that protocol transfer is miminal.
    switch (selectorState) {
      case "pseudo":
        result = PSEUDO_SELECTORS.filter(item => {
          return item[0].startsWith(":" + completing);
        });
        break;

      case "class":
        if (!query) {
          nodes = this._multiFrameQuerySelectorAll("[class]");
        } else {
          nodes = this._multiFrameQuerySelectorAll(query);
        }
        for (const node of nodes) {
          for (const className of node.classList) {
            sugs.classes.set(className, (sugs.classes.get(className) | 0) + 1);
          }
        }
        sugs.classes.delete("");
        sugs.classes.delete(HIDDEN_CLASS);
        for (const [className, count] of sugs.classes) {
          if (className.startsWith(completing)) {
            result.push(["." + CSS.escape(className), count, selectorState]);
          }
        }
        break;

      case "id":
        if (!query) {
          nodes = this._multiFrameQuerySelectorAll("[id]");
        } else {
          nodes = this._multiFrameQuerySelectorAll(query);
        }
        for (const node of nodes) {
          sugs.ids.set(node.id, (sugs.ids.get(node.id) | 0) + 1);
        }
        for (const [id, count] of sugs.ids) {
          if (id.startsWith(completing) && id !== "") {
            result.push(["#" + CSS.escape(id), count, selectorState]);
          }
        }
        break;

      case "tag":
        if (!query) {
          nodes = this._multiFrameQuerySelectorAll("*");
        } else {
          nodes = this._multiFrameQuerySelectorAll(query);
        }
        for (const node of nodes) {
          const tag = node.localName;
          sugs.tags.set(tag, (sugs.tags.get(tag) | 0) + 1);
        }
        for (const [tag, count] of sugs.tags) {
          if (new RegExp("^" + completing + ".*", "i").test(tag)) {
            result.push([tag, count, selectorState]);
          }
        }

        // For state 'tag' (no preceding # or .) and when there's no query (i.e.
        // only one word) then search for the matching classes and ids
        if (!query) {
          result = [
            ...result,
            ...this.getSuggestionsForQuery(null, completing, "class")
              .suggestions,
            ...this.getSuggestionsForQuery(null, completing, "id").suggestions,
          ];
        }

        break;

      case "null":
        nodes = this._multiFrameQuerySelectorAll(query);
        for (const node of nodes) {
          sugs.ids.set(node.id, (sugs.ids.get(node.id) | 0) + 1);
          const tag = node.localName;
          sugs.tags.set(tag, (sugs.tags.get(tag) | 0) + 1);
          for (const className of node.classList) {
            sugs.classes.set(className, (sugs.classes.get(className) | 0) + 1);
          }
        }
        for (const [tag, count] of sugs.tags) {
          tag && result.push([tag, count]);
        }
        for (const [id, count] of sugs.ids) {
          id && result.push(["#" + id, count]);
        }
        sugs.classes.delete("");
        sugs.classes.delete(HIDDEN_CLASS);
        for (const [className, count] of sugs.classes) {
          className && result.push(["." + className, count]);
        }
    }

    // Sort by count (desc) and name (asc)
    result = result.sort((a, b) => {
      // Computed a sortable string with first the inverted count, then the name
      let sortA = 10000 - a[1] + a[0];
      let sortB = 10000 - b[1] + b[0];

      // Prefixing ids, classes and tags, to group results
      const firstA = a[0].substring(0, 1);
      const firstB = b[0].substring(0, 1);

      const getSortKeyPrefix = firstLetter => {
        if (firstLetter === "#") {
          return "2";
        }
        if (firstLetter === ".") {
          return "1";
        }
        return "0";
      };

      sortA = getSortKeyPrefix(firstA) + sortA;
      sortB = getSortKeyPrefix(firstB) + sortB;

      // String compare
      return sortA.localeCompare(sortB);
    });

    result = result.slice(0, 25);

    return {
      query,
      suggestions: result,
    };
  }

  /**
   * Add a pseudo-class lock to a node.
   *
   * @param NodeActor node
   * @param string pseudo
   *    A pseudoclass: ':hover', ':active', ':focus', ':focus-within'
   * @param options
   *    Options object:
   *    `parents`: True if the pseudo-class should be added
   *      to parent nodes.
   *    `enabled`: False if the pseudo-class should be locked
   *      to 'off'. Defaults to true.
   *
   * @returns An empty packet.  A "pseudoClassLock" mutation will
   *    be queued for any changed nodes.
   */
  addPseudoClassLock(node, pseudo, options = {}) {
    if (isNodeDead(node)) {
      return;
    }

    // There can be only one node locked per pseudo, so dismiss all existing
    // ones
    for (const locked of this._activePseudoClassLocks) {
      if (InspectorUtils.hasPseudoClassLock(locked.rawNode, pseudo)) {
        this._removePseudoClassLock(locked, pseudo);
      }
    }

    const enabled = options.enabled === undefined || options.enabled;
    this._addPseudoClassLock(node, pseudo, enabled);

    if (!options.parents) {
      return;
    }

    const walker = this.getDocumentWalker(node.rawNode);
    let cur;
    while ((cur = walker.parentNode())) {
      const curNode = this._getOrCreateNodeActor(cur);
      this._addPseudoClassLock(curNode, pseudo, enabled);
    }
  }

  _queuePseudoClassMutation(node) {
    this.queueMutation({
      target: node.actorID,
      type: "pseudoClassLock",
      pseudoClassLocks: node.writePseudoClassLocks(),
    });
  }

  _addPseudoClassLock(node, pseudo, enabled) {
    if (node.rawNode.nodeType !== Node.ELEMENT_NODE) {
      return false;
    }
    InspectorUtils.addPseudoClassLock(node.rawNode, pseudo, enabled);
    this._activePseudoClassLocks.add(node);
    this._queuePseudoClassMutation(node);
    return true;
  }

  hideNode(node) {
    if (isNodeDead(node)) {
      return;
    }

    loadSheet(node.rawNode.ownerGlobal, HELPER_SHEET);
    node.rawNode.classList.add(HIDDEN_CLASS);
  }

  unhideNode(node) {
    if (isNodeDead(node)) {
      return;
    }

    node.rawNode.classList.remove(HIDDEN_CLASS);
  }

  /**
   * Remove a pseudo-class lock from a node.
   *
   * @param NodeActor node
   * @param string pseudo
   *    A pseudoclass: ':hover', ':active', ':focus', ':focus-within'
   * @param options
   *    Options object:
   *    `parents`: True if the pseudo-class should be removed
   *      from parent nodes.
   *
   * @returns An empty response.  "pseudoClassLock" mutations
   *    will be emitted for any changed nodes.
   */
  removePseudoClassLock(node, pseudo, options = {}) {
    if (isNodeDead(node)) {
      return;
    }

    this._removePseudoClassLock(node, pseudo);

    // Remove pseudo class for children as we don't want to allow
    // turning it on for some childs without setting it on some parents
    for (const locked of this._activePseudoClassLocks) {
      if (
        node.rawNode.contains(locked.rawNode) &&
        InspectorUtils.hasPseudoClassLock(locked.rawNode, pseudo)
      ) {
        this._removePseudoClassLock(locked, pseudo);
      }
    }

    if (!options.parents) {
      return;
    }

    const walker = this.getDocumentWalker(node.rawNode);
    let cur;
    while ((cur = walker.parentNode())) {
      const curNode = this._getOrCreateNodeActor(cur);
      this._removePseudoClassLock(curNode, pseudo);
    }
  }

  _removePseudoClassLock(node, pseudo) {
    if (node.rawNode.nodeType != Node.ELEMENT_NODE) {
      return false;
    }
    InspectorUtils.removePseudoClassLock(node.rawNode, pseudo);
    if (!node.writePseudoClassLocks()) {
      this._activePseudoClassLocks.delete(node);
    }

    this._queuePseudoClassMutation(node);
    return true;
  }

  /**
   * Clear all the pseudo-classes on a given node or all nodes.
   * @param {NodeActor} node Optional node to clear pseudo-classes on
   */
  clearPseudoClassLocks(node) {
    if (node && isNodeDead(node)) {
      return;
    }

    if (node) {
      InspectorUtils.clearPseudoClassLocks(node.rawNode);
      this._activePseudoClassLocks.delete(node);
      this._queuePseudoClassMutation(node);
    } else {
      for (const locked of this._activePseudoClassLocks) {
        InspectorUtils.clearPseudoClassLocks(locked.rawNode);
        this._activePseudoClassLocks.delete(locked);
        this._queuePseudoClassMutation(locked);
      }
    }
  }

  /**
   * Get a node's innerHTML property.
   */
  innerHTML(node) {
    let html = "";
    if (!isNodeDead(node)) {
      html = node.rawNode.innerHTML;
    }
    return new LongStringActor(this.conn, html);
  }

  /**
   * Set a node's innerHTML property.
   *
   * @param {NodeActor} node The node.
   * @param {string} value The piece of HTML content.
   */
  setInnerHTML(node, value) {
    if (isNodeDead(node)) {
      return;
    }

    const rawNode = node.rawNode;
    if (
      rawNode.nodeType !== rawNode.ownerDocument.ELEMENT_NODE &&
      rawNode.nodeType !== rawNode.ownerDocument.DOCUMENT_FRAGMENT_NODE
    ) {
      throw new Error("Can only change innerHTML to element or fragment nodes");
    }
    // eslint-disable-next-line no-unsanitized/property
    rawNode.innerHTML = value;
  }

  /**
   * Get a node's outerHTML property.
   *
   * @param {NodeActor} node The node.
   */
  outerHTML(node) {
    let outerHTML = "";
    if (!isNodeDead(node)) {
      outerHTML = node.rawNode.outerHTML;
    }
    return new LongStringActor(this.conn, outerHTML);
  }

  /**
   * Set a node's outerHTML property.
   *
   * @param {NodeActor} node The node.
   * @param {string} value The piece of HTML content.
   */
  setOuterHTML(node, value) {
    if (isNodeDead(node)) {
      return;
    }

    const rawNode = node.rawNode;
    const doc = nodeDocument(rawNode);
    const win = doc.defaultView;
    let parser;
    if (!win) {
      throw new Error("The window object shouldn't be null");
    } else {
      // We create DOMParser under window object because we want a content
      // DOMParser, which means all the DOM objects created by this DOMParser
      // will be in the same DocGroup as rawNode.parentNode. Then the newly
      // created nodes can be adopted into rawNode.parentNode.
      parser = new win.DOMParser();
    }

    const mimeType = rawNode.tagName === "svg" ? "image/svg+xml" : "text/html";
    const parsedDOM = parser.parseFromString(value, mimeType);
    const parentNode = rawNode.parentNode;

    // Special case for head and body.  Setting document.body.outerHTML
    // creates an extra <head> tag, and document.head.outerHTML creates
    // an extra <body>.  So instead we will call replaceChild with the
    // parsed DOM, assuming that they aren't trying to set both tags at once.
    if (rawNode.tagName === "BODY") {
      if (parsedDOM.head.innerHTML === "") {
        parentNode.replaceChild(parsedDOM.body, rawNode);
      } else {
        // eslint-disable-next-line no-unsanitized/property
        rawNode.outerHTML = value;
      }
    } else if (rawNode.tagName === "HEAD") {
      if (parsedDOM.body.innerHTML === "") {
        parentNode.replaceChild(parsedDOM.head, rawNode);
      } else {
        // eslint-disable-next-line no-unsanitized/property
        rawNode.outerHTML = value;
      }
    } else if (node.isDocumentElement()) {
      // Unable to set outerHTML on the document element. Fall back by
      // setting attributes manually. Then replace all the child nodes.
      const finalAttributeModifications = [];
      const attributeModifications = {};
      for (const attribute of rawNode.attributes) {
        attributeModifications[attribute.name] = null;
      }
      for (const attribute of parsedDOM.documentElement.attributes) {
        attributeModifications[attribute.name] = attribute.value;
      }
      for (const key in attributeModifications) {
        finalAttributeModifications.push({
          attributeName: key,
          newValue: attributeModifications[key],
        });
      }
      node.modifyAttributes(finalAttributeModifications);

      rawNode.replaceChildren(...parsedDOM.firstElementChild.childNodes);
    } else {
      // eslint-disable-next-line no-unsanitized/property
      rawNode.outerHTML = value;
    }
  }

  /**
   * Insert adjacent HTML to a node.
   *
   * @param {Node} node
   * @param {string} position One of "beforeBegin", "afterBegin", "beforeEnd",
   *                          "afterEnd" (see Element.insertAdjacentHTML).
   * @param {string} value The HTML content.
   */
  insertAdjacentHTML(node, position, value) {
    if (isNodeDead(node)) {
      return { node: [], newParents: [] };
    }

    const rawNode = node.rawNode;
    const isInsertAsSibling =
      position === "beforeBegin" || position === "afterEnd";

    // Don't insert anything adjacent to the document element.
    if (isInsertAsSibling && node.isDocumentElement()) {
      throw new Error("Can't insert adjacent element to the root.");
    }

    const rawParentNode = rawNode.parentNode;
    if (!rawParentNode && isInsertAsSibling) {
      throw new Error("Can't insert as sibling without parent node.");
    }

    // We can't use insertAdjacentHTML, because we want to return the nodes
    // being created (so the front can remove them if the user undoes
    // the change). So instead, use Range.createContextualFragment().
    const range = rawNode.ownerDocument.createRange();
    if (position === "beforeBegin" || position === "afterEnd") {
      range.selectNode(rawNode);
    } else {
      range.selectNodeContents(rawNode);
    }
    // eslint-disable-next-line no-unsanitized/method
    const docFrag = range.createContextualFragment(value);
    const newRawNodes = Array.from(docFrag.childNodes);
    switch (position) {
      case "beforeBegin":
        rawParentNode.insertBefore(docFrag, rawNode);
        break;
      case "afterEnd":
        // Note: if the second argument is null, rawParentNode.insertBefore
        // behaves like rawParentNode.appendChild.
        rawParentNode.insertBefore(docFrag, rawNode.nextSibling);
        break;
      case "afterBegin":
        rawNode.insertBefore(docFrag, rawNode.firstChild);
        break;
      case "beforeEnd":
        rawNode.appendChild(docFrag);
        break;
      default:
        throw new Error(
          "Invalid position value. Must be either " +
            "'beforeBegin', 'beforeEnd', 'afterBegin' or 'afterEnd'."
        );
    }

    return this.attachElements(newRawNodes);
  }

  /**
   * Duplicate a specified node
   *
   * @param {NodeActor} node The node to duplicate.
   */
  duplicateNode({ rawNode }) {
    const clonedNode = rawNode.cloneNode(true);
    rawNode.parentNode.insertBefore(clonedNode, rawNode.nextSibling);
  }

  /**
   * Test whether a node is a document or a document element.
   *
   * @param {NodeActor} node The node to remove.
   * @return {boolean} True if the node is a document or a document element.
   */
  isDocumentOrDocumentElementNode(node) {
    return (
      (node.rawNode.ownerDocument &&
        node.rawNode.ownerDocument.documentElement === this.rawNode) ||
      node.rawNode.nodeType === Node.DOCUMENT_NODE
    );
  }

  /**
   * Removes a node from its parent node.
   *
   * @param {NodeActor} node The node to remove.
   * @returns The node's nextSibling before it was removed.
   */
  removeNode(node) {
    if (isNodeDead(node) || this.isDocumentOrDocumentElementNode(node)) {
      throw Error("Cannot remove document, document elements or dead nodes.");
    }

    const nextSibling = this.nextSibling(node);
    node.rawNode.remove();
    // Mutation events will take care of the rest.
    return nextSibling;
  }

  /**
   * Removes an array of nodes from their parent node.
   *
   * @param {NodeActor[]} nodes The nodes to remove.
   */
  removeNodes(nodes) {
    // Check that all nodes are valid before processing the removals.
    for (const node of nodes) {
      if (isNodeDead(node) || this.isDocumentOrDocumentElementNode(node)) {
        throw Error("Cannot remove document, document elements or dead nodes");
      }
    }

    for (const node of nodes) {
      node.rawNode.remove();
      // Mutation events will take care of the rest.
    }
  }

  /**
   * Insert a node into the DOM.
   */
  insertBefore(node, parent, sibling) {
    if (
      isNodeDead(node) ||
      isNodeDead(parent) ||
      (sibling && isNodeDead(sibling))
    ) {
      return;
    }

    const rawNode = node.rawNode;
    const rawParent = parent.rawNode;
    const rawSibling = sibling ? sibling.rawNode : null;

    // Don't bother inserting a node if the document position isn't going
    // to change. This prevents needless iframes reloading and mutations.
    if (rawNode.parentNode === rawParent) {
      let currentNextSibling = this.nextSibling(node);
      currentNextSibling = currentNextSibling
        ? currentNextSibling.rawNode
        : null;

      if (rawNode === rawSibling || currentNextSibling === rawSibling) {
        return;
      }
    }

    rawParent.insertBefore(rawNode, rawSibling);
  }

  /**
   * Editing a node's tagname actually means creating a new node with the same
   * attributes, removing the node and inserting the new one instead.
   * This method does not return anything as mutation events are taking care of
   * informing the consumers about changes.
   */
  editTagName(node, tagName) {
    if (isNodeDead(node)) {
      return null;
    }

    const oldNode = node.rawNode;

    // Create a new element with the same attributes as the current element and
    // prepare to replace the current node with it.
    let newNode;
    try {
      newNode = nodeDocument(oldNode).createElement(tagName);
    } catch (x) {
      // Failed to create a new element with that tag name, ignore the change,
      // and signal the error to the front.
      return Promise.reject(
        new Error("Could not change node's tagName to " + tagName)
      );
    }

    const attrs = oldNode.attributes;
    for (let i = 0; i < attrs.length; i++) {
      newNode.setAttribute(attrs[i].name, attrs[i].value);
    }

    // Insert the new node, and transfer the old node's children.
    oldNode.parentNode.insertBefore(newNode, oldNode);
    while (oldNode.firstChild) {
      newNode.appendChild(oldNode.firstChild);
    }

    oldNode.remove();
    return null;
  }

  /**
   * Gets the state of the mutation breakpoint types for this actor.
   *
   * @param {NodeActor} node The node to get breakpoint info for.
   */
  getMutationBreakpoints(node) {
    let bps;
    if (!isNodeDead(node)) {
      bps = this._breakpointInfoForNode(node.rawNode);
    }

    return (
      bps || {
        subtree: false,
        removal: false,
        attribute: false,
      }
    );
  }

  /**
   * Set the state of some subset of mutation breakpoint types for this actor.
   *
   * @param {NodeActor} node The node to set breakpoint info for.
   * @param {Object} bps A subset of the breakpoints for this actor that
   *                            should be updated to new states.
   */
  setMutationBreakpoints(node, bps) {
    if (isNodeDead(node)) {
      return;
    }
    const rawNode = node.rawNode;

    if (
      rawNode.ownerDocument &&
      rawNode.getRootNode({ composed: true }) != rawNode.ownerDocument
    ) {
      // We only allow watching for mutations on nodes that are attached to
      // documents. That allows us to clean up our mutation listeners when all
      // of the watched nodes have been removed from the document.
      return;
    }

    // This argument has nullable fields so we want to only update boolean
    // field values.
    const bpsForNode = Object.keys(bps).reduce((obj, bp) => {
      if (typeof bps[bp] === "boolean") {
        obj[bp] = bps[bp];
      }
      return obj;
    }, {});

    this._updateMutationBreakpointState("api", rawNode, {
      ...this.getMutationBreakpoints(node),
      ...bpsForNode,
    });
  }

  /**
   * Update the mutation breakpoint state for the given DOM node.
   *
   * @param {Node} rawNode The DOM node.
   * @param {Object} bpsForNode The state of each mutation bp type we support.
   */
  _updateMutationBreakpointState(mutationReason, rawNode, bpsForNode) {
    const rawDoc = rawNode.ownerDocument || rawNode;

    const docMutationBreakpoints = this._mutationBreakpointsForDoc(
      rawDoc,
      true /* createIfNeeded */
    );
    let originalBpsForNode = this._breakpointInfoForNode(rawNode);

    if (!bpsForNode && !originalBpsForNode) {
      return;
    }

    bpsForNode = bpsForNode || {};
    originalBpsForNode = originalBpsForNode || {};

    if (Object.values(bpsForNode).some(Boolean)) {
      docMutationBreakpoints.nodes.set(rawNode, bpsForNode);
    } else {
      docMutationBreakpoints.nodes.delete(rawNode);
    }
    if (originalBpsForNode.subtree && !bpsForNode.subtree) {
      docMutationBreakpoints.counts.subtree -= 1;
    } else if (!originalBpsForNode.subtree && bpsForNode.subtree) {
      docMutationBreakpoints.counts.subtree += 1;
    }

    if (originalBpsForNode.removal && !bpsForNode.removal) {
      docMutationBreakpoints.counts.removal -= 1;
    } else if (!originalBpsForNode.removal && bpsForNode.removal) {
      docMutationBreakpoints.counts.removal += 1;
    }

    if (originalBpsForNode.attribute && !bpsForNode.attribute) {
      docMutationBreakpoints.counts.attribute -= 1;
    } else if (!originalBpsForNode.attribute && bpsForNode.attribute) {
      docMutationBreakpoints.counts.attribute += 1;
    }

    this._updateDocumentMutationListeners(rawDoc);

    const actor = this.getNode(rawNode);
    if (actor) {
      this.queueMutation({
        target: actor.actorID,
        type: "mutationBreakpoint",
        mutationBreakpoints: this.getMutationBreakpoints(actor),
        mutationReason,
      });
    }
  }

  /**
   * Controls whether this DOM document has event listeners attached for
   * handling of DOM mutation breakpoints.
   *
   * @param {Document} rawDoc The DOM document.
   */
  _updateDocumentMutationListeners(rawDoc) {
    const docMutationBreakpoints = this._mutationBreakpointsForDoc(rawDoc);
    if (!docMutationBreakpoints) {
      rawDoc.devToolsWatchingDOMMutations = false;
      return;
    }

    const anyBreakpoint =
      docMutationBreakpoints.counts.subtree > 0 ||
      docMutationBreakpoints.counts.removal > 0 ||
      docMutationBreakpoints.counts.attribute > 0;

    rawDoc.devToolsWatchingDOMMutations = anyBreakpoint;

    if (docMutationBreakpoints.counts.subtree > 0) {
      this.chromeEventHandler.addEventListener(
        "devtoolschildinserted",
        this.onSubtreeModified,
        true /* capture */
      );
    } else {
      this.chromeEventHandler.removeEventListener(
        "devtoolschildinserted",
        this.onSubtreeModified,
        true /* capture */
      );
    }

    if (anyBreakpoint) {
      this.chromeEventHandler.addEventListener(
        "devtoolschildremoved",
        this.onNodeRemoved,
        true /* capture */
      );
    } else {
      this.chromeEventHandler.removeEventListener(
        "devtoolschildremoved",
        this.onNodeRemoved,
        true /* capture */
      );
    }

    if (docMutationBreakpoints.counts.attribute > 0) {
      this.chromeEventHandler.addEventListener(
        "devtoolsattrmodified",
        this.onAttributeModified,
        true /* capture */
      );
    } else {
      this.chromeEventHandler.removeEventListener(
        "devtoolsattrmodified",
        this.onAttributeModified,
        true /* capture */
      );
    }
  }

  _breakOnMutation(mutationType, targetNode, ancestorNode, action) {
    this.targetActor.threadActor.pauseForMutationBreakpoint(
      mutationType,
      targetNode,
      ancestorNode,
      action
    );
  }

  _mutationBreakpointsForDoc(rawDoc, createIfNeeded = false) {
    let docMutationBreakpoints = this._mutationBreakpoints.get(rawDoc);
    if (!docMutationBreakpoints && createIfNeeded) {
      docMutationBreakpoints = {
        counts: {
          subtree: 0,
          removal: 0,
          attribute: 0,
        },
        nodes: new Map(),
      };
      this._mutationBreakpoints.set(rawDoc, docMutationBreakpoints);
    }
    return docMutationBreakpoints;
  }

  _breakpointInfoForNode(target) {
    const docMutationBreakpoints = this._mutationBreakpointsForDoc(
      target.ownerDocument || target
    );
    return (
      (docMutationBreakpoints && docMutationBreakpoints.nodes.get(target)) ||
      null
    );
  }

  onNodeRemoved(evt) {
    const mutationBpInfo = this._breakpointInfoForNode(evt.target);
    const hasNodeRemovalEvent = mutationBpInfo?.removal;

    this._clearMutationBreakpointsFromSubtree(evt.target);
    if (hasNodeRemovalEvent) {
      this._breakOnMutation("nodeRemoved", evt.target);
    } else {
      this.onSubtreeModified(evt);
    }
  }

  onAttributeModified(evt) {
    const mutationBpInfo = this._breakpointInfoForNode(evt.target);
    if (mutationBpInfo?.attribute) {
      this._breakOnMutation("attributeModified", evt.target);
    }
  }

  onSubtreeModified(evt) {
    const action = evt.type === "devtoolschildinserted" ? "add" : "remove";
    let node = evt.target;
    if (node.isNativeAnonymous && !this.showAllAnonymousContent) {
      return;
    }
    while ((node = node.parentNode) !== null) {
      const mutationBpInfo = this._breakpointInfoForNode(node);
      if (mutationBpInfo?.subtree) {
        this._breakOnMutation("subtreeModified", evt.target, node, action);
        break;
      }
    }
  }

  _clearMutationBreakpointsFromSubtree(targetNode) {
    const targetDoc = targetNode.ownerDocument || targetNode;
    const docMutationBreakpoints = this._mutationBreakpointsForDoc(targetDoc);
    if (!docMutationBreakpoints || docMutationBreakpoints.nodes.size === 0) {
      // Bail early for performance. If the doc has no mutation BPs, there is
      // no reason to iterate through the children looking for things to detach.
      return;
    }

    // The walker is not limited to the subtree of the argument node, so we
    // need to ensure that we stop walking when we leave the subtree.
    const nextWalkerSibling = this._getNextTraversalSibling(targetNode);

    const walker = new DocumentWalker(targetNode, this.rootWin, {
      filter: noAnonymousContentTreeWalkerFilter,
      skipTo: SKIP_TO_SIBLING,
    });

    do {
      this._updateMutationBreakpointState("detach", walker.currentNode, null);
    } while (walker.nextNode() && walker.currentNode !== nextWalkerSibling);
  }

  _getNextTraversalSibling(targetNode) {
    const walker = new DocumentWalker(targetNode, this.rootWin, {
      filter: noAnonymousContentTreeWalkerFilter,
      skipTo: SKIP_TO_SIBLING,
    });

    while (!walker.nextSibling()) {
      if (!walker.parentNode()) {
        // If we try to step past the walker root, there is no next sibling.
        return null;
      }
    }
    return walker.currentNode;
  }

  /**
   * Get any pending mutation records.  Must be called by the client after
   * the `new-mutations` notification is received.  Returns an array of
   * mutation records.
   *
   * Mutation records have a basic structure:
   *
   * {
   *   type: attributes|characterData|childList,
   *   target: <domnode actor ID>,
   * }
   *
   * And additional attributes based on the mutation type:
   *
   * `attributes` type:
   *   attributeName: <string> - the attribute that changed
   *   attributeNamespace: <string> - the attribute's namespace URI, if any.
   *   newValue: <string> - The new value of the attribute, if any.
   *
   * `characterData` type:
   *   newValue: <string> - the new nodeValue for the node
   *
   * `childList` type is returned when the set of children for a node
   * has changed.  Includes extra data, which can be used by the client to
   * maintain its ownership subtree.
   *
   *   added: array of <domnode actor ID> - The list of actors *previously
   *     seen by the client* that were added to the target node.
   *   removed: array of <domnode actor ID> The list of actors *previously
   *     seen by the client* that were removed from the target node.
   *   inlineTextChild: If the node now has a single text child, it will
   *     be sent here.
   *
   * Actors that are included in a MutationRecord's `removed` but
   * not in an `added` have been removed from the client's ownership
   * tree (either by being moved under a node the client has seen yet
   * or by being removed from the tree entirely), and is considered
   * 'orphaned'.
   *
   * Keep in mind that if a node that the client hasn't seen is moved
   * into or out of the target node, it will not be included in the
   * removedNodes and addedNodes list, so if the client is interested
   * in the new set of children it needs to issue a `children` request.
   */
  getMutations(options = {}) {
    const pending = this._pendingMutations || [];
    this._pendingMutations = [];
    this._waitingForGetMutations = false;

    if (options.cleanup) {
      for (const node of this._orphaned) {
        // Release the orphaned node.  Nodes or children that have been
        // retained will be moved to this._retainedOrphans.
        this.releaseNode(node);
      }
      this._orphaned = new Set();
    }

    return pending;
  }

  queueMutation(mutation) {
    if (!this.actorID || this._destroyed) {
      // We've been destroyed, don't bother queueing this mutation.
      return;
    }

    // Add the mutation to the list of mutations to be retrieved next.
    this._pendingMutations.push(mutation);

    // Bail out if we already emitted a new-mutations event and are waiting for a client
    // to retrieve them.
    if (this._waitingForGetMutations) {
      return;
    }

    if (IMMEDIATE_MUTATIONS.includes(mutation.type)) {
      this._emitNewMutations();
    } else {
      /**
       * If many mutations are fired at the same time, clients might sequentially request
       * children/siblings for updated nodes, which can be costly. By throttling the calls
       * to getMutations, duplicated mutations will be ignored.
       */
      this._throttledEmitNewMutations();
    }
  }

  _emitNewMutations() {
    if (!this.actorID || this._destroyed) {
      // Bail out if the actor was destroyed after throttling this call.
      return;
    }

    if (this._waitingForGetMutations || !this._pendingMutations.length) {
      // Bail out if we already fired the new-mutation event or if no mutations are
      // waiting to be retrieved.
      return;
    }

    this._waitingForGetMutations = true;
    this.emit("new-mutations");
  }

  /**
   * Handles mutations from the DOM mutation observer API.
   *
   * @param array[MutationRecord] mutations
   *    See https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver#MutationRecord
   */
  onMutations(mutations) {
    // Don't send a mutation event if the mutation target would be ignored by the walker
    // filter function.
    const documentWalkerFilter = this.getDocumentWalkerFilter();
    if (
      mutations.every(
        mutation =>
          documentWalkerFilter(mutation.target) ===
          nodeFilterConstants.FILTER_SKIP
      )
    ) {
      return;
    }

    // Notify any observers that want *all* mutations (even on nodes that aren't
    // referenced).  This is not sent over the protocol so can only be used by
    // scripts running in the server process.
    this.emit("any-mutation");

    for (const change of mutations) {
      const targetActor = this.getNode(change.target);
      if (!targetActor) {
        continue;
      }
      const targetNode = change.target;
      const type = change.type;
      const mutation = {
        type,
        target: targetActor.actorID,
      };

      if (type === "attributes") {
        mutation.attributeName = change.attributeName;
        mutation.attributeNamespace = change.attributeNamespace || undefined;
        mutation.newValue = targetNode.hasAttribute(mutation.attributeName)
          ? targetNode.getAttribute(mutation.attributeName)
          : null;
      } else if (type === "characterData") {
        mutation.newValue = targetNode.nodeValue;
        this._maybeQueueInlineTextChildMutation(change, targetNode);
      } else if (type === "childList") {
        // Get the list of removed and added actors that the client has seen
        // so that it can keep its ownership tree up to date.
        const removedActors = [];
        const addedActors = [];
        for (const removed of change.removedNodes) {
          const removedActor = this.getNode(removed);
          if (!removedActor) {
            // If the client never encountered this actor we don't need to
            // mention that it was removed.
            continue;
          }
          // While removed from the tree, nodes are saved as orphaned.
          this._orphaned.add(removedActor);
          removedActors.push(removedActor.actorID);
        }
        for (const added of change.addedNodes) {
          const addedActor = this.getNode(added);
          if (!addedActor) {
            // If the client never encounted this actor we don't need to tell
            // it about its addition for ownership tree purposes - if the
            // client wants to see the new nodes it can ask for children.
            continue;
          }
          // The actor is reconnected to the ownership tree, unorphan
          // it and let the client know so that its ownership tree is up
          // to date.
          this._orphaned.delete(addedActor);
          addedActors.push(addedActor.actorID);
        }

        mutation.numChildren = targetActor.numChildren;
        mutation.removed = removedActors;
        mutation.added = addedActors;

        const inlineTextChild = this.inlineTextChild(targetActor);
        if (inlineTextChild) {
          mutation.inlineTextChild = inlineTextChild.form();
        }
      }
      this.queueMutation(mutation);
    }
  }

  /**
   * Check if the provided mutation could change the way the target element is
   * inlined with its parent node. If it might, a custom mutation of type
   * "inlineTextChild" will be queued.
   *
   * @param {MutationRecord} mutation
   *        A characterData type mutation
   */
  _maybeQueueInlineTextChildMutation(mutation) {
    const { oldValue, target } = mutation;
    const newValue = target.nodeValue;
    const limit = gValueSummaryLength;

    if (
      (oldValue.length <= limit && newValue.length <= limit) ||
      (oldValue.length > limit && newValue.length > limit)
    ) {
      // Bail out if the new & old values are both below/above the size limit.
      return;
    }

    const parentActor = this.getNode(target.parentNode);
    if (!parentActor || parentActor.rawNode.children.length) {
      // If the parent node has other children, a character data mutation will
      // not change anything regarding inlining text nodes.
      return;
    }

    const inlineTextChild = this.inlineTextChild(parentActor);
    this.queueMutation({
      type: "inlineTextChild",
      target: parentActor.actorID,
      inlineTextChild: inlineTextChild ? inlineTextChild.form() : undefined,
    });
  }

  onSlotchange(event) {
    const target = event.target;
    const targetActor = this.getNode(target);
    if (!targetActor) {
      return;
    }

    this.queueMutation({
      type: "slotchange",
      target: targetActor.actorID,
    });
  }

  /**
   * Fires when an anonymous root is created.
   * This is needed because regular mutation observers don't fire on some kinds
   * of NAC creation. We want to treat this like a regular insertion.
   */
  onAnonymousrootcreated(event) {
    const root = event.target;

    // Don't trigger a mutation if the document walker would filter out the element.
    const documentWalkerFilter = this.getDocumentWalkerFilter();
    if (documentWalkerFilter(root) === nodeFilterConstants.FILTER_SKIP) {
      return;
    }

    const parent = this.rawParentNode(root);
    if (!parent) {
      // These events are async. The node might have been removed already, in
      // which case there's nothing to do anymore.
      return;
    }
    // By the time onAnonymousrootremoved fires, the node is already detached
    // from its parent, so we need to remember it by hand.
    this._anonParents.set(root, parent);
    this.onMutations([
      {
        type: "childList",
        target: parent,
        addedNodes: [root],
        removedNodes: [],
      },
    ]);
  }

  /**
   * @see onAnonymousrootcreated
   */
  onAnonymousrootremoved(event) {
    const root = event.target;
    const parent = this._anonParents.get(root);
    if (!parent) {
      return;
    }
    this._anonParents.delete(root);
    this.onMutations([
      {
        type: "childList",
        target: parent,
        addedNodes: [],
        removedNodes: [root],
      },
    ]);
  }

  onShadowrootattached(event) {
    const actor = this.getNode(event.target);
    if (!actor) {
      return;
    }

    const mutation = {
      type: "shadowRootAttached",
      target: actor.actorID,
    };
    this.queueMutation(mutation);
  }

  onFrameLoad({ window, isTopLevel }) {
    // By the time we receive the DOMContentLoaded event, we might have been destroyed
    if (this._destroyed) {
      return;
    }
    const { readyState } = window.document;
    if (readyState != "interactive" && readyState != "complete") {
      // The document is not loaded, so we want to register to fire again when the
      // DOM has been loaded.
      window.addEventListener(
        "DOMContentLoaded",
        this.onFrameLoad.bind(this, { window, isTopLevel }),
        { once: true }
      );
      return;
    }

    window.document.shadowRootAttachedEventEnabled = true;

    if (isTopLevel) {
      // If we initialize the inspector while the document is loading,
      // we may already have a root document set in the constructor.
      if (
        this.rootDoc &&
        this.rootDoc !== window.document &&
        !Cu.isDeadWrapper(this.rootDoc) &&
        this.rootDoc.defaultView
      ) {
        this.onFrameUnload({ window: this.rootDoc.defaultView });
      }
      // Update all DOM objects references to target the new document.
      this.rootWin = window;
      this.rootDoc = window.document;
      this.rootNode = this.document();
      this.emit("root-available", this.rootNode);
    } else {
      const frame = getFrameElement(window);
      const frameActor = this.getNode(frame);
      if (frameActor) {
        // If the parent frame is in the map of known node actors, create the
        // actor for the new document and emit a root-available event.
        const documentActor = this._getOrCreateNodeActor(window.document);
        this.emit("root-available", documentActor);
      }
    }
  }

  // Returns true if domNode is in window or a subframe.
  _childOfWindow(window, domNode) {
    while (domNode) {
      const win = nodeDocument(domNode).defaultView;
      if (win === window) {
        return true;
      }
      domNode = getFrameElement(win);
    }
    return false;
  }

  onFrameUnload({ window }) {
    // Any retained orphans that belong to this document
    // or its children need to be released, and a mutation sent
    // to notify of that.
    const releasedOrphans = [];

    for (const retained of this._retainedOrphans) {
      if (
        Cu.isDeadWrapper(retained.rawNode) ||
        this._childOfWindow(window, retained.rawNode)
      ) {
        this._retainedOrphans.delete(retained);
        releasedOrphans.push(retained.actorID);
        this.releaseNode(retained, { force: true });
      }
    }

    if (releasedOrphans.length) {
      this.queueMutation({
        target: this.rootNode.actorID,
        type: "unretained",
        nodes: releasedOrphans,
      });
    }

    const doc = window.document;
    const documentActor = this.getNode(doc);
    if (!documentActor) {
      return;
    }

    // Removing a frame also removes any mutation breakpoints set on that
    // document so that clients can clear their set of active breakpoints.
    const mutationBps = this._mutationBreakpointsForDoc(doc);
    const nodes = mutationBps ? Array.from(mutationBps.nodes.keys()) : [];
    for (const node of nodes) {
      this._updateMutationBreakpointState("unload", node, null);
    }

    this.emit("root-destroyed", documentActor);

    // Cleanup root doc references if we just unloaded the top level root
    // document.
    if (this.rootDoc === doc) {
      this.rootDoc = null;
      this.rootNode = null;
    }

    // Release the actor for the unloaded document.
    this.releaseNode(documentActor, { force: true });
  }

  /**
   * Check if a node is attached to the DOM tree of the current page.
   * @param {Node} rawNode
   * @return {Boolean} false if the node is removed from the tree or within a
   * document fragment
   */
  _isInDOMTree(rawNode) {
    let walker;
    try {
      walker = this.getDocumentWalker(rawNode);
    } catch (e) {
      // The DocumentWalker may throw NS_ERROR_ILLEGAL_VALUE when the node isn't found as a legit children of its parent
      // ex: <iframe> manually added as immediate child of another <iframe>
      if (e.name == "NS_ERROR_ILLEGAL_VALUE") {
        return false;
      }
      throw e;
    }
    let current = walker.currentNode;

    // Reaching the top of tree
    while (walker.parentNode()) {
      current = walker.currentNode;
    }

    // The top of the tree is a fragment or is not rootDoc, hence rawNode isn't
    // attached
    if (
      current.nodeType === Node.DOCUMENT_FRAGMENT_NODE ||
      current !== this.rootDoc
    ) {
      return false;
    }

    // Otherwise the top of the tree is rootDoc, hence rawNode is in rootDoc
    return true;
  }

  /**
   * @see _isInDomTree
   */
  isInDOMTree(node) {
    if (isNodeDead(node)) {
      return false;
    }
    return this._isInDOMTree(node.rawNode);
  }

  /**
   * Given a windowID return the NodeActor for the corresponding frameElement,
   * unless it's the root window
   */
  getNodeActorFromWindowID(windowID) {
    let win;

    try {
      win = Services.wm.getOuterWindowWithId(windowID);
    } catch (e) {
      // ignore
    }

    if (!win) {
      return {
        error: "noWindow",
        message: "The related docshell is destroyed or not found",
      };
    } else if (!win.frameElement) {
      // the frame element of the root document is privileged & thus
      // inaccessible, so return the document body/element instead
      return this.attachElement(
        win.document.body || win.document.documentElement
      );
    }

    return this.attachElement(win.frameElement);
  }

  /**
   * Given a contentDomReference return the NodeActor for the corresponding frameElement.
   */
  getNodeActorFromContentDomReference(contentDomReference) {
    let rawNode = lazy.ContentDOMReference.resolve(contentDomReference);
    if (!rawNode || !this._isInDOMTree(rawNode)) {
      return null;
    }

    // This is a special case for the document object whereby it is considered
    // as document.documentElement (the <html> node)
    if (rawNode.defaultView && rawNode === rawNode.defaultView.document) {
      rawNode = rawNode.documentElement;
    }

    return this.attachElement(rawNode);
  }

  /**
   * Given a StyleSheet resource ID, commonly used in the style-editor, get its
   * ownerNode and return the corresponding walker's NodeActor.
   * Note that getNodeFromActor was added later and can now be used instead.
   */
  getStyleSheetOwnerNode(resourceId) {
    const manager = this.targetActor.getStyleSheetsManager();
    const ownerNode = manager.getOwnerNode(resourceId);
    return this.attachElement(ownerNode);
  }

  /**
   * This method can be used to retrieve NodeActor for DOM nodes from other
   * actors in a way that they can later be highlighted in the page, or
   * selected in the inspector.
   * If an actor has a reference to a DOM node, and the UI needs to know about
   * this DOM node (and possibly select it in the inspector), the UI should
   * first retrieve a reference to the walkerFront:
   *
   * // Make sure the inspector/walker have been initialized first.
   * const inspectorFront = await toolbox.target.getFront("inspector");
   * // Retrieve the walker.
   * const walker = inspectorFront.walker;
   *
   * And then call this method:
   *
   * // Get the nodeFront from my actor, passing the ID and properties path.
   * walker.getNodeFromActor(myActorID, ["element"]).then(nodeFront => {
   *   // Use the nodeFront, e.g. select the node in the inspector.
   *   toolbox.getPanel("inspector").selection.setNodeFront(nodeFront);
   * });
   *
   * @param {String} actorID The ID for the actor that has a reference to the
   * DOM node.
   * @param {Array} path Where, on the actor, is the DOM node stored. If in the
   * scope of the actor, the node is available as `this.data.node`, then this
   * should be ["data", "node"].
   * @return {NodeActor} The attached NodeActor, or null if it couldn't be
   * found.
   */
  getNodeFromActor(actorID, path) {
    const actor = this.conn.getActor(actorID);
    if (!actor) {
      return null;
    }

    let obj = actor;
    for (const name of path) {
      if (!(name in obj)) {
        return null;
      }
      obj = obj[name];
    }

    return this.attachElement(obj);
  }

  /**
   * Returns an instance of the LayoutActor that is used to retrieve CSS layout-related
   * information.
   *
   * @return {LayoutActor}
   */
  getLayoutInspector() {
    if (!this.layoutActor) {
      this.layoutActor = new LayoutActor(this.conn, this.targetActor, this);
    }

    return this.layoutActor;
  }

  /**
   * Returns the parent grid DOMNode of the given node if it exists, otherwise, it
   * returns null.
   */
  getParentGridNode(node) {
    if (isNodeDead(node)) {
      return null;
    }

    const parentGridNode = findGridParentContainerForNode(node.rawNode);
    return parentGridNode ? this._getOrCreateNodeActor(parentGridNode) : null;
  }

  /**
   * Returns the offset parent DOMNode of the given node if it exists, otherwise, it
   * returns null.
   */
  getOffsetParent(node) {
    if (isNodeDead(node)) {
      return null;
    }

    const offsetParent = node.rawNode.offsetParent;

    if (!offsetParent) {
      return null;
    }

    return this._getOrCreateNodeActor(offsetParent);
  }

  getEmbedderElement(browsingContextID) {
    const browsingContext = BrowsingContext.get(browsingContextID);
    let rawNode = browsingContext.embedderElement;
    if (!this._isInDOMTree(rawNode)) {
      return null;
    }

    // This is a special case for the document object whereby it is considered
    // as document.documentElement (the <html> node)
    if (rawNode.defaultView && rawNode === rawNode.defaultView.document) {
      rawNode = rawNode.documentElement;
    }

    return this.attachElement(rawNode);
  }

  pick(doFocus, isLocalTab) {
    this.nodePicker.pick(doFocus, isLocalTab);
  }

  cancelPick() {
    this.nodePicker.cancelPick();
  }

  clearPicker() {
    this.nodePicker.resetHoveredNodeReference();
  }

  /**
   * Given a scrollable node, find its descendants which are causing overflow in it and
   * add their raw nodes to the map as keys with the scrollable element as the values.
   *
   * @param {NodeActor} scrollableNode A scrollable node.
   * @param {Map} map The map to which the overflow causing elements are added.
   */
  updateOverflowCausingElements(scrollableNode, map) {
    if (
      isNodeDead(scrollableNode) ||
      scrollableNode.rawNode.nodeType !== Node.ELEMENT_NODE
    ) {
      return;
    }

    const overflowCausingChildren = [
      ...InspectorUtils.getOverflowingChildrenOfElement(scrollableNode.rawNode),
    ];

    for (let overflowCausingChild of overflowCausingChildren) {
      // overflowCausingChild is a Node, but not necessarily an Element.
      // So, get the containing Element
      if (overflowCausingChild.nodeType !== Node.ELEMENT_NODE) {
        overflowCausingChild = overflowCausingChild.parentElement;
      }
      map.set(overflowCausingChild, scrollableNode);
    }
  }

  /**
   * Returns an array of the overflow causing elements' NodeActor for the given node.
   *
   * @param {NodeActor} node The scrollable node.
   * @return {Array<NodeActor>} An array of the overflow causing elements.
   */
  getOverflowCausingElements(node) {
    if (
      isNodeDead(node) ||
      node.rawNode.nodeType !== Node.ELEMENT_NODE ||
      !node.isScrollable
    ) {
      return [];
    }

    const overflowCausingElements = [
      ...InspectorUtils.getOverflowingChildrenOfElement(node.rawNode),
    ].map(overflowCausingChild => {
      if (overflowCausingChild.nodeType !== Node.ELEMENT_NODE) {
        overflowCausingChild = overflowCausingChild.parentElement;
      }

      return overflowCausingChild;
    });

    return this.attachElements(overflowCausingElements);
  }

  /**
   * Return the scrollable ancestor node which has overflow because of the given node.
   *
   * @param {NodeActor} overflowCausingNode
   */
  getScrollableAncestorNode(overflowCausingNode) {
    if (
      isNodeDead(overflowCausingNode) ||
      !this.overflowCausingElementsMap.has(overflowCausingNode.rawNode)
    ) {
      return null;
    }

    return this.overflowCausingElementsMap.get(overflowCausingNode.rawNode);
  }
}

exports.WalkerActor = WalkerActor;
