/* 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 Telemetry = require("resource://devtools/client/shared/telemetry.js");
const {
  FrontClassWithSpec,
  registerFront,
} = require("resource://devtools/shared/protocol.js");
const {
  inspectorSpec,
} = require("resource://devtools/shared/specs/inspector.js");

loader.lazyRequireGetter(
  this,
  "captureScreenshot",
  "resource://devtools/client/shared/screenshot.js",
  true
);
const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  TYPES: "resource://devtools/shared/highlighters.mjs",
});

const TELEMETRY_EYEDROPPER_OPENED = "DEVTOOLS_EYEDROPPER_OPENED_COUNT";
const TELEMETRY_EYEDROPPER_OPENED_MENU =
  "DEVTOOLS_MENU_EYEDROPPER_OPENED_COUNT";
const SHOW_ALL_ANONYMOUS_CONTENT_PREF =
  "devtools.inspector.showAllAnonymousContent";

const telemetry = new Telemetry();

/**
 * Client side of the inspector actor, which is used to create
 * inspector-related actors, including the walker.
 */
class InspectorFront extends FrontClassWithSpec(inspectorSpec) {
  constructor(client, targetFront, parentFront) {
    super(client, targetFront, parentFront);

    this._client = client;
    this._highlighters = new Map();

    // Attribute name from which to retrieve the actorID out of the target actor's form
    this.formAttributeName = "inspectorActor";

    // Map of highlighter types to unsettled promises to create a highlighter of that type
    this._pendingGetHighlighterMap = new Map();

    this.noopStylesheetListener = () => {};
  }

  // async initialization
  async initialize() {
    if (this.initialized) {
      return this.initialized;
    }

    // Watch STYLESHEET resources to fill the ResourceCommand cache.
    // StyleRule front's `get parentStyleSheet()` will query the cache to
    // retrieve the resource corresponding to the parent stylesheet of a rule.
    const { resourceCommand } = this.targetFront.commands;
    // Backup resourceCommand, targetFront.commands might be null in `destroy`.
    this.resourceCommand = resourceCommand;
    await resourceCommand.watchResources([resourceCommand.TYPES.STYLESHEET], {
      onAvailable: this.noopStylesheetListener,
    });

    // Bail out if the inspector is closed while watchResources was pending
    if (this.isDestroyed()) {
      return null;
    }

    const promises = [this._getWalker(), this._getPageStyle()];
    if (this.targetFront.commands.descriptorFront.isTabDescriptor) {
      promises.push(this._enableViewportSizeOnResizeHighlighter());
    }

    this.initialized = await Promise.all(promises);

    return this.initialized;
  }

  async _getWalker() {
    const showAllAnonymousContent = Services.prefs.getBoolPref(
      SHOW_ALL_ANONYMOUS_CONTENT_PREF
    );
    this.walker = await this.getWalker({
      showAllAnonymousContent,
    });

    // We need to reparent the RootNode of remote iframe Walkers
    // so that their parent is the NodeFront of the <iframe>
    // element, coming from another process/target/WalkerFront.
    await this.walker.reparentRemoteFrame();
  }

  hasHighlighter(type) {
    return this._highlighters.has(type);
  }

  async _getPageStyle() {
    this.pageStyle = await super.getPageStyle();
  }

  async _enableViewportSizeOnResizeHighlighter() {
    const highlighter = await this.getOrCreateHighlighterByType(
      lazy.TYPES.VIEWPORT_SIZE_ON_RESIZE
    );
    await highlighter.show(this);
  }

  async getCompatibilityFront() {
    if (!this._compatibility) {
      this._compatibility = await super.getCompatibility();
    }

    return this._compatibility;
  }

  destroy() {
    if (this.isDestroyed()) {
      return;
    }
    this._compatibility = null;

    const { resourceCommand } = this;
    resourceCommand.unwatchResources([resourceCommand.TYPES.STYLESHEET], {
      onAvailable: this.noopStylesheetListener,
    });
    this.resourceCommand = null;

    this.walker = null;

    // CustomHighlighter fronts are managed by InspectorFront and so will be
    // automatically destroyed. But we have to clear the `_highlighters`
    // Map as well as explicitly call `finalize` request on all of them.
    this.destroyHighlighters();
    super.destroy();
  }

  destroyHighlighters() {
    for (const type of this._highlighters.keys()) {
      if (this._highlighters.has(type)) {
        const highlighter = this._highlighters.get(type);
        if (!highlighter.isDestroyed()) {
          highlighter.finalize();
        }
        this._highlighters.delete(type);
      }
    }
  }

  async getHighlighterByType(typeName) {
    let highlighter = null;
    try {
      highlighter = await super.getHighlighterByType(typeName);
    } catch (_) {
      throw new Error(
        "The target doesn't support " +
          `creating highlighters by types or ${typeName} is unknown`
      );
    }
    return highlighter;
  }

  getKnownHighlighter(type) {
    return this._highlighters.get(type);
  }

  /**
   * Return a highlighter instance of the given type.
   * If an instance was previously created, return it. Else, create and return a new one.
   *
   * Store a promise for the request to create a new highlighter. If another request
   * comes in before that promise is resolved, wait for it to resolve and return the
   * highlighter instance it resolved with instead of creating a new request.
   *
   * @param  {String} type
   *         Highlighter type
   * @return {Promise}
   *         Promise which resolves with a highlighter instance of the given type
   */
  async getOrCreateHighlighterByType(type) {
    let front = this._highlighters.get(type);
    let pendingGetHighlighter = this._pendingGetHighlighterMap.get(type);

    if (!front && !pendingGetHighlighter) {
      pendingGetHighlighter = (async () => {
        const highlighter = await this.getHighlighterByType(type);
        this._highlighters.set(type, highlighter);
        this._pendingGetHighlighterMap.delete(type);
        return highlighter;
      })();

      this._pendingGetHighlighterMap.set(type, pendingGetHighlighter);
    }

    if (pendingGetHighlighter) {
      front = await pendingGetHighlighter;
    }

    return front;
  }

  async pickColorFromPage(options) {
    let screenshot = null;

    // @backward-compat { version 87 } ScreenshotContentActor was only added in 87.
    //                  When connecting to older server, the eyedropper will  use drawWindow
    //                  to retrieve the screenshot of the page (that's a decent fallback,
    //                  even if it doesn't handle remote frames).
    if (this.targetFront.hasActor("screenshotContent")) {
      try {
        // We use the screenshot actors as it can retrieve an image of the current viewport,
        // handling remote frame if need be.
        const { data } = await captureScreenshot(this.targetFront, {
          browsingContextID: this.targetFront.browsingContextID,
          disableFlash: true,
          ignoreDprForFileScale: true,
        });
        screenshot = data;
      } catch (e) {
        // We simply log the error and still call pickColorFromPage as it will default to
        // use drawWindow in order to get the screenshot of the page (that's a decent
        // fallback, even if it doesn't handle remote frames).
        console.error(
          "Error occured when taking a screenshot for the eyedropper",
          e
        );
      }
    }

    await super.pickColorFromPage({
      ...options,
      screenshot,
    });

    if (options?.fromMenu) {
      telemetry.getHistogramById(TELEMETRY_EYEDROPPER_OPENED_MENU).add(true);
    } else {
      telemetry.getHistogramById(TELEMETRY_EYEDROPPER_OPENED).add(true);
    }
  }

  /**
   * Given a node grip, return a NodeFront on the right context.
   *
   * @param {Object} grip: The node grip.
   * @returns {Promise<NodeFront|null>} A promise that resolves with  a NodeFront or null
   *                                    if the NodeFront couldn't be created/retrieved.
   */
  async getNodeFrontFromNodeGrip(grip) {
    return this.getNodeActorFromContentDomReference(grip.contentDomReference);
  }

  async getNodeActorFromContentDomReference(contentDomReference) {
    const { browsingContextId } = contentDomReference;
    // If the contentDomReference lives in the same browsing context id than the
    // current one, we can directly use the current walker.
    if (this.targetFront.browsingContextID === browsingContextId) {
      return this.walker.getNodeActorFromContentDomReference(
        contentDomReference
      );
    }

    // If the contentDomReference has a different browsing context than the current one,
    // we are either in Fission or in the Multiprocess Browser Toolbox, so we need to
    // retrieve the walker of the WindowGlobalTarget.
    // Get the target for this remote frame element

    // Tab and Process Descriptors expose a Watcher, which should be used to
    // fetch the node's target.
    let target;
    const { watcherFront } = this.targetFront.commands;
    if (watcherFront) {
      target = await watcherFront.getWindowGlobalTarget(browsingContextId);
    } else {
      // For descriptors which don't expose a watcher (e.g. WebExtension)
      // we used to call RootActor::getBrowsingContextDescriptor, but it was
      // removed in FF77.
      // Support for watcher in WebExtension descriptors is Bug 1644341.
      throw new Error(
        `Unable to call getNodeActorFromContentDomReference for ${this.targetFront.actorID}`
      );
    }
    const { walker } = await target.getFront("inspector");
    return walker.getNodeActorFromContentDomReference(contentDomReference);
  }
}

exports.InspectorFront = InspectorFront;
registerFront(InspectorFront);
