/* 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 {
  isNodeValid,
} = require("resource://devtools/server/actors/highlighters/utils/markup.js");
const {
  BoxModelHighlighter,
} = require("resource://devtools/server/actors/highlighters/box-model.js");

// How many maximum nodes can be highlighted at the same time by the SelectorHighlighter
const MAX_HIGHLIGHTED_ELEMENTS = 100;

/**
 * The SelectorHighlighter runs a given selector through querySelectorAll on the
 * document of the provided context node and then uses the BoxModelHighlighter
 * to highlight the matching nodes
 */
class SelectorHighlighter {
  constructor(highlighterEnv, inspector) {
    this.highlighterEnv = highlighterEnv;
    this.inspector = inspector;
    this._highlighters = [];
  }

  /**
   * Show a BoxModelHighlighter on each node that matches a given selector.
   *
   * @param {DOMNode} node
   *        A context node used to get the document element on which to run
   *        querySelectorAll(). This node will not be highlighted.
   * @param {object} options
   *        Configuration options for SelectorHighlighter.
   *        All of the options for BoxModelHighlighter.show() are also valid here.
   * @param {string} options.selector
   *        Required. CSS selector used with querySelectorAll() to find matching elements.
   */
  async show(node, options = {}) {
    this.hide();

    if (!isNodeValid(node) || !options.selector) {
      return false;
    }

    let nodes = [];

    if (options.ruleActorID && this.inspector) {
      const pageStyle = await this.inspector.getPageStyle();
      const rule = pageStyle.getActorByID(options.ruleActorID);
      if (rule) {
        nodes = rule.rawRule.querySelectorAll(node.getRootNode());
      }
    } else {
      try {
        nodes = node.ownerDocument.querySelectorAll(options.selector);
      } catch (e) {
        // It's fine if the provided selector is invalid, `nodes` will be an empty array.
      }
    }

    // Prevent passing the `selector` option to BoxModelHighlighter
    delete options.selector;

    const promises = [];
    for (let i = 0; i < Math.min(nodes.length, MAX_HIGHLIGHTED_ELEMENTS); i++) {
      promises.push(this._showHighlighter(nodes[i], options));
    }

    await Promise.all(promises);
    return true;
  }

  /**
   * Create an instance of BoxModelHighlighter, wait for it to be ready
   * (see CanvasFrameAnonymousContentHelper.initialize()),
   * then show the highlighter on the given node with the given configuration options.
   *
   * @param  {DOMNode} node
   *         Node to be highlighted
   * @param  {object} options
   *         Configuration options for the BoxModelHighlighter
   * @return {Promise} Promise that resolves when the BoxModelHighlighter is ready
   */
  async _showHighlighter(node, options) {
    const highlighter = new BoxModelHighlighter(this.highlighterEnv);
    await highlighter.isReady;

    highlighter.show(node, options);
    this._highlighters.push(highlighter);
  }

  hide() {
    for (const highlighter of this._highlighters) {
      highlighter.destroy();
    }
    this._highlighters = [];
  }

  destroy() {
    this.hide();
    this.highlighterEnv = null;
  }
}
exports.SelectorHighlighter = SelectorHighlighter;
