/* 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 {
  createCommandsDictionary,
} = require("resource://devtools/shared/commands/index.js");
const { DevToolsLoader } = ChromeUtils.importESModule(
  "resource://devtools/shared/loader/Loader.sys.mjs"
);
loader.lazyRequireGetter(
  this,
  "DevToolsServer",
  "resource://devtools/server/devtools-server.js",
  true
);
// eslint-disable-next-line mozilla/reject-some-requires
loader.lazyRequireGetter(
  this,
  "DevToolsClient",
  "resource://devtools/client/devtools-client.js",
  true
);

/**
 * Functions for creating Commands for all debuggable contexts.
 *
 * All methods of this `CommandsFactory` object receive argument to describe to
 * which particular context we want to debug. And all returns a new instance of `commands` object.
 * Commands are implemented by modules defined in devtools/shared/commands.
 */
exports.CommandsFactory = {
  /**
   * Create commands for a given local tab.
   *
   * @param {Tab} tab: A local Firefox tab, running in this process.
   * @param {object} options
   * @param {DevToolsClient} options.client: An optional DevToolsClient. If none is passed,
   *        a new one will be created.
   * @param {DevToolsClient} options.isWebExtension: An optional boolean to flag commands
   *        that are created for the WebExtension codebase.
   * @returns {object} Commands
   */
  async forTab(tab, { client, isWebExtension } = {}) {
    if (!client) {
      client = await createLocalClient();
    }

    const descriptor = await client.mainRoot.getTab({ tab, isWebExtension });
    descriptor.doNotAttachThreadActor = isWebExtension;
    const commands = await createCommandsDictionary(descriptor);
    return commands;
  },

  /**
   * Chrome mochitest don't have access to any "tab",
   * so that the only way to attach to a fake tab is call RootFront.getTab
   * without any argument.
   */
  async forCurrentTabInChromeMochitest() {
    const client = await createLocalClient();
    const descriptor = await client.mainRoot.getTab();
    const commands = await createCommandsDictionary(descriptor);
    return commands;
  },

  /**
   * Create commands for the main process.
   *
   * @param {object} options
   * @param {DevToolsClient} options.client: An optional DevToolsClient. If none is passed,
   *        a new one will be created.
   * @param {boolean} enableWindowGlobalThreadActors: An optional boolean for on test.
   * @returns {object} Commands
   */
  async forMainProcess({
    client,
    enableWindowGlobalThreadActors = false,
  } = {}) {
    if (!client) {
      client = await createLocalClient();
    }

    const descriptor = await client.mainRoot.getMainProcess();
    const commands = await createCommandsDictionary(
      descriptor,
      enableWindowGlobalThreadActors
    );
    return commands;
  },

  /**
   * Create commands for a given remote tab.
   *
   * Note that it can also be used for local tab, but isLocalTab attribute
   * on commands.descriptorFront will be false.
   *
   * @param {number} browserId: Identify which tab we should create commands for.
   * @param {object} options
   * @param {DevToolsClient} options.client: An optional DevToolsClient. If none is passed,
   *        a new one will be created.
   * @returns {object} Commands
   */
  async forRemoteTab(browserId, { client } = {}) {
    if (!client) {
      client = await createLocalClient();
    }

    const descriptor = await client.mainRoot.getTab({ browserId });
    const commands = await createCommandsDictionary(descriptor);
    return commands;
  },

  /**
   * Create commands for a given main process worker.
   *
   * @param {string} id: WorkerDebugger's id, which is a unique ID computed by the platform code.
   *        These ids are exposed via WorkerDescriptor's id attributes.
   *        WorkerDescriptors can be retrieved via MainFront.listAllWorkers()/listWorkers().
   * @param {object} options
   * @param {DevToolsClient} options.client: An optional DevToolsClient. If none is passed,
   *        a new one will be created.
   * @returns {object} Commands
   */
  async forWorker(id, { client } = {}) {
    if (!client) {
      client = await createLocalClient();
    }

    const descriptor = await client.mainRoot.getWorker(id);
    const commands = await createCommandsDictionary(descriptor);
    return commands;
  },

  /**
   * Create commands for a Web Extension.
   *
   * @param {string} id The Web Extension ID to debug.
   * @param {object} options
   * @param {DevToolsClient} options.client: An optional DevToolsClient. If none is passed,
   *        a new one will be created.
   * @returns {object} Commands
   */
  async forAddon(id, { client } = {}) {
    if (!client) {
      client = await createLocalClient();
    }

    const descriptor = await client.mainRoot.getAddon({ id });
    const commands = await createCommandsDictionary(descriptor);
    return commands;
  },

  /**
   * This method will spawn a special `DevToolsClient`
   * which is meant to debug the same Firefox instance
   * and especially be able to debug chrome code.
   * The chrome code typically runs in the system principal.
   * This principal is a singleton which is shared among most Firefox internal codebase
   * (JSM, privileged html documents, JS-XPCOM,...)
   * In order to be able to debug these script we need to connect to a special DevToolsServer
   * that runs in a dedicated and distinct system principal which is different from
   * the one shared with the rest of Firefox frontend codebase.
   */
  async spawnClientToDebugSystemPrincipal() {
    // The Browser console ends up using the debugger in autocomplete.
    // Because the debugger can't be running in the same compartment than its debuggee,
    // we have to load the server in a dedicated Loader, flagged with
    // `freshCompartment`, which will force it to be loaded in another compartment.
    const customLoader = new DevToolsLoader({
      freshCompartment: true,
    });
    const { DevToolsServer: customDevToolsServer } = customLoader.require(
      "resource://devtools/server/devtools-server.js"
    );

    customDevToolsServer.init();

    // We want all the actors (root, browser and target-scoped) to be registered on the
    // DevToolsServer. This is needed so the Browser Console can retrieve:
    // - the console actors, which are target-scoped (See Bug 1416105)
    // - the screenshotActor, which is browser-scoped (for the `:screenshot` command)
    customDevToolsServer.registerAllActors();

    customDevToolsServer.allowChromeProcess = true;

    const client = new DevToolsClient(customDevToolsServer.connectPipe());
    await client.connect();

    return client;
  },

  /**
   * One method to handle the whole setup sequence to connect to RDP backend for the Browser Console.
   *
   * This will instantiate a special DevTools module loader for the DevToolsServer.
   * Then spawn a DevToolsClient to connect to it.
   * Get a Main Process Descriptor from it.
   * Finally spawn a commands object for this descriptor.
   */
  async forBrowserConsole() {
    // The Browser console ends up using the debugger in autocomplete.
    // Because the debugger can't be running in the same compartment than its debuggee,
    // we have to load the server in a dedicated Loader and so spawn a special client
    const client = await this.spawnClientToDebugSystemPrincipal();

    const descriptor = await client.mainRoot.getMainProcess();

    descriptor.doNotAttachThreadActor = true;

    // Force fetching the first top level target right away.
    await descriptor.getTarget();

    const commands = await createCommandsDictionary(descriptor);
    return commands;
  },
};

async function createLocalClient() {
  // Make sure the DevTools server is started.
  ensureDevToolsServerInitialized();

  // Create the client and connect it to the local server.
  const client = new DevToolsClient(DevToolsServer.connectPipe());
  await client.connect();

  return client;
}
// Also expose this method for tests which would like to create a client
// without involving commands. This would typically be tests against the Watcher actor
// and requires to prevent having TargetCommand from running.
// Or tests which are covering RootFront or global actor's fronts.
exports.createLocalClientForTests = createLocalClient;

function ensureDevToolsServerInitialized() {
  // Since a remote protocol connection will be made, let's start the
  // DevToolsServer here, once and for all tools.
  DevToolsServer.init();

  // Enable all the actors. We may not need all of them and registering
  // only root and target might be enough
  DevToolsServer.registerAllActors();

  // Enable being able to get child process actors
  // Same, this might not be useful
  DevToolsServer.allowChromeProcess = true;
}
