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

loader.lazyRequireGetter(
  this,
  ["getCommandAndArgs"],
  "resource://devtools/server/actors/webconsole/commands/parser.js",
  true
);

loader.lazyGetter(this, "l10n", () => {
  return new Localization(
    [
      "devtools/shared/webconsole-commands.ftl",
      "devtools/server/actors/webconsole/commands/experimental-commands.ftl",
    ],
    true
  );
});

const lazy = {};
ChromeUtils.defineESModuleGetters(
  lazy,
  {
    JSTracer: "resource://devtools/server/tracer/tracer.sys.mjs",
  },
  { global: "contextual" }
);

const USAGE_STRING_MAPPING = {
  block: "webconsole-commands-usage-block",
  trace: "webconsole-commands-usage-trace3",
  unblock: "webconsole-commands-usage-unblock",
};

/**
 * WebConsole commands manager.
 *
 * Defines a set of functions / variables ("commands") that are available from
 * the Web Console but not from the web page.
 *
 */
const WebConsoleCommandsManager = {
  // Flag used by eager evaluation in order to allow the execution of commands
  // which are side effect free and disallow all the others.
  SIDE_EFFECT_FREE: Symbol("SIDE_EFFECT_FREE"),

  // Map of command name to command function or property descriptor (see register method)
  _registeredCommands: new Map(),
  // Map of command name to optional array of accepted argument names
  _validArguments: new Map(),
  // Set of command names that are side effect free
  _sideEffectFreeCommands: new Set(),

  /**
   * Register a new command.
   *
   * @param {object} options
   * @param {string} options.name
   *        The command name (exemple: "$", "screenshot",...))
   * @param {boolean} isSideEffectFree
   *        Tells if the command is free of any side effect to know
   *        if it can run in eager console evaluation.
   * @param {function|object} options.command
   *        The command to register.
   *        It can be:
   *          - a function for the command like "$()" or ":screenshot"
   *            which triggers some code.
   *          - a property descriptor for getters like "$0",
   *            which only returns a value.
   * @param {Array<string>} options.validArguments
   *        Optional list of valid arguments.
   *        If passed, we will assert that passed arguments are all valid on execution.
   *
   *  The command function or the command getter are passed a:
   *   - "owner" object as their first parameter (see the example below).
   *     See _createOwnerObject for definition.
   *   - "args" object with all parameters when this is ran as a ":my-command" command.
   *     See getCommandAndArgs for definition.
   *
   * Note that if you want to support `--help` argument, you need to provide a usage string in:
   * devtools/shared/locales/en-US/webconsole-commands.properties
   *
   * @example
   *
   *   WebConsoleCommandsManager.register("$", function (owner, selector)
   *   {
   *     return owner.window.document.querySelector(selector);
   *   },
   *   ["my-argument"]);
   *
   *   WebConsoleCommandsManager.register("$0", {
   *     get: function(owner) {
   *       return owner.makeDebuggeeValue(owner.selectedNode);
   *     }
   *   });
   */
  register({ name, isSideEffectFree, command, validArguments }) {
    if (
      typeof command != "function" &&
      !(typeof command == "object" && typeof command.get == "function")
    ) {
      throw new Error(
        "Invalid web console command. It can only be a function, or an object with a function as 'get' attribute"
      );
    }
    if (typeof isSideEffectFree !== "boolean") {
      throw new Error(
        "Invalid web console command. 'isSideEffectFree' attribute should be set and be a boolean"
      );
    }
    this._registeredCommands.set(name, command);
    if (validArguments) {
      this._validArguments.set(name, validArguments);
    }
    if (isSideEffectFree) {
      this._sideEffectFreeCommands.add(name);
    }
  },

  /**
   * Return the name of all registered commands.
   *
   * @return {Array} List of all command names.
   */
  getAllCommandNames() {
    return [...this._registeredCommands.keys()];
  },

  /**
   * There is two types of "commands" here.
   *
   * - Functions or variables exposed in the scope of the evaluated string from the WebConsole input.
   *   Example: $(), $0, copy(), clear(),...
   * - "True commands", which can also be ran from the WebConsole input with ":" prefix.
   *   Example: this list of commands.
   *   Note that some "true commands" are not exposed as function (see getColonOnlyCommandNames).
   *
   * The following list distinguish these "true commands" from the first category.
   * It especially avoid any JavaScript evaluation when the frontend tries to execute
   * a string starting with ':' character.
   */
  getAllColonCommandNames() {
    return ["block", "help", "history", "screenshot", "unblock", "trace"];
  },

  /**
   * Some commands are not exposed in the scope of the evaluated string,
   * and can only be used via `:command-name`.
   */
  getColonOnlyCommandNames() {
    return ["screenshot", "trace"];
  },

  /**
   * Map of all command objects keyed by command name.
   * Commands object are the objects passed to register() method.
   *
   * @return {Map<string -> command>}
   */
  getAllCommands() {
    return this._registeredCommands;
  },

  /**
   * Is the command name possibly overriding a symbol which
   * already exists in the paused frame or the global into which
   * we are about to execute into?
   */
  _isCommandNameAlreadyInScope(name, frame, dbgGlobal) {
    if (frame && frame.environment) {
      return !!frame.environment.find(name);
    }

    // Fallback on global scope when Debugger.Frame doesn't come along an
    // Environment, or is not a frame.

    try {
      // This can throw in Browser Toolbox tests
      const globalEnv = dbgGlobal.asEnvironment();
      if (globalEnv) {
        return !!dbgGlobal.asEnvironment().find(name);
      }
    } catch {}

    return !!dbgGlobal.getOwnPropertyDescriptor(name);
  },

  _createOwnerObject(
    consoleActor,
    debuggerGlobal,
    evalInput,
    selectedNodeActorID
  ) {
    const owner = {
      window: consoleActor.evalGlobal,
      makeDebuggeeValue: debuggerGlobal.makeDebuggeeValue.bind(debuggerGlobal),
      createValueGrip: consoleActor.createValueGrip.bind(consoleActor),
      preprocessDebuggerObject:
        consoleActor.preprocessDebuggerObject.bind(consoleActor),
      helperResult: null,
      consoleActor,
      evalInput,
    };
    if (selectedNodeActorID) {
      const actor = consoleActor.conn.getActor(selectedNodeActorID);
      if (actor) {
        owner.selectedNode = actor.rawNode;
      }
    }
    return owner;
  },

  _getCommandsForCurrentEnvironment() {
    // Not supporting extra commands in workers yet.  This should be possible to
    // add one by one as long as they don't require jsm/mjs, Cu, etc.
    return isWorker ? new Map() : this.getAllCommands();
  },

  /**
   * Create an object with the API we expose to the Web Console during
   * JavaScript evaluation.
   * This object inherits properties and methods from the Web Console actor.
   *
   * @param object consoleActor
   *        The related web console actor evaluating some code.
   * @param object debuggerGlobal
   *        A Debugger.Object that wraps a content global. This is used for the
   *        Web Console Commands.
   * @param object frame (optional)
   *        The frame where the string was evaluated.
   * @param string evalInput
   *        String to evaluate.
   * @param string selectedNodeActorID
   *        The Node actor ID of the currently selected DOM Element, if any is selected.
   * @param bool preferConsoleCommandsOverLocalSymbols
   *        If true, define all bindings even if there's conflicting existing
   *        symbols.  This is for the case evaluating non-user code in frame
   *        environment.
   *
   * @return object
   *         Object with two properties:
   *         - 'bindings', the object with all commands set as attribute on this object.
   *         - 'getHelperResult', a live getter returning the additional data the last command
   *           which executed want to convey to the frontend.
   *           (The return value of commands isn't returned to the client but it only
   *            returned to the code ran from console evaluation)
   */
  getWebConsoleCommands(
    consoleActor,
    debuggerGlobal,
    frame,
    evalInput,
    selectedNodeActorID,
    preferConsoleCommandsOverLocalSymbols
  ) {
    const bindings = Object.create(null);

    const owner = this._createOwnerObject(
      consoleActor,
      debuggerGlobal,
      evalInput,
      selectedNodeActorID
    );

    const evalGlobal = consoleActor.evalGlobal;
    function maybeExport(obj, name) {
      if (typeof obj[name] != "function") {
        return;
      }

      // By default, chrome-implemented functions that are exposed to content
      // refuse to accept arguments that are cross-origin for the caller. This
      // is generally the safe thing, but causes problems for certain console
      // helpers like cd(), where we users sometimes want to pass a cross-origin
      // window. To circumvent this restriction, we use exportFunction along
      // with a special option designed for this purpose. See bug 1051224.
      obj[name] = Cu.exportFunction(obj[name], evalGlobal, {
        allowCrossOriginArguments: true,
      });
    }

    const commands = this._getCommandsForCurrentEnvironment();

    const colonOnlyCommandNames = this.getColonOnlyCommandNames();
    for (const [name, command] of commands) {
      // When we run user code in frame, we want to avoid overriding existing
      // symbols with commands.
      //
      // When we run user code in global scope, all bindings are automatically
      // shadowed, except for "help" function which is checked by getEvalInput.
      //
      // When preferConsoleCommandsOverLocalSymbols is true, ignore symbols in
      // the current scope and always use commands ones.
      if (
        !preferConsoleCommandsOverLocalSymbols &&
        (frame || name === "help") &&
        this._isCommandNameAlreadyInScope(name, frame, debuggerGlobal)
      ) {
        continue;
      }
      // Also ignore commands which can only be run with the `:` prefix.
      if (colonOnlyCommandNames.includes(name)) {
        continue;
      }

      const descriptor = {
        // We force the enumerability and the configurability (so the
        // WebConsoleActor can reconfigure the property).
        enumerable: true,
        configurable: true,
      };

      if (typeof command === "function") {
        // Function commands
        descriptor.value = command.bind(undefined, owner);
        maybeExport(descriptor, "value");

        // Unfortunately evalWithBindings will access all bindings values,
        // which would trigger a debuggee native call because bindings's property
        // is using Cu.exportFunction.
        // Put a magic symbol attribute on them in order to carefully accept
        // all bindings as being side effect safe by default.
        if (this._sideEffectFreeCommands.has(name)) {
          descriptor.value.isSideEffectFree = this.SIDE_EFFECT_FREE;
        }

        // Make sure the helpers can be used during eval.
        descriptor.value = debuggerGlobal.makeDebuggeeValue(descriptor.value);
      } else if (typeof command?.get === "function") {
        // Getter commands
        descriptor.get = command.get.bind(undefined, owner);
        maybeExport(descriptor, "get");

        // See comment in previous block.
        if (this._sideEffectFreeCommands.has(name)) {
          descriptor.get.isSideEffectFree = this.SIDE_EFFECT_FREE;
        }
      }
      Object.defineProperty(bindings, name, descriptor);
    }

    return {
      // Use a method as commands will update owner.helperResult later
      getHelperResult() {
        return owner.helperResult;
      },
      bindings,
    };
  },

  /**
   * Create a function for given ':command'-style command.
   *
   * @param object consoleActor
   *        The related web console actor evaluating some code.
   * @param object debuggerGlobal
   *        A Debugger.Object that wraps a content global. This is used for the
   *        Web Console Commands.
   * @param string selectedNodeActorID
   *        The Node actor ID of the currently selected DOM Element, if any is selected.
   * @param string evalInput
   *        String to evaluate.
   *
   * @return object
   *         Object with two properties:
   *         - 'commandFunc', a function corresponds to the 'commandName'
   *         - 'getHelperResult', a live getter returning the data the command
   *           which executed want to convey to the frontend.
   */
  executeCommand(consoleActor, debuggerGlobal, selectedNodeActorID, evalInput) {
    const { command, args } = getCommandAndArgs(evalInput);
    const commands = this._getCommandsForCurrentEnvironment();
    if (!commands.has(command)) {
      throw new Error(`Unsupported command '${command}'`);
    }

    if (args.help || args.usage) {
      const l10nKey = USAGE_STRING_MAPPING[command];
      if (l10nKey) {
        const message = l10n.formatValueSync(l10nKey);
        if (message && message !== l10nKey) {
          return {
            result: null,
            helperResult: {
              type: "usage",
              message,
            },
          };
        }
      }
    }

    const validArguments = this._validArguments.get(command);
    if (validArguments) {
      for (const key of Object.keys(args)) {
        if (!validArguments.includes(key)) {
          throw new Error(
            `:${command} command doesn't support '${key}' argument.`
          );
        }
      }
    }

    const owner = this._createOwnerObject(
      consoleActor,
      debuggerGlobal,
      evalInput,
      selectedNodeActorID
    );

    const commandFunction = commands.get(command);

    // This is where we run the command passed to register method
    const result = commandFunction(owner, args);

    return {
      result,

      // commandFunction may mutate owner.helperResult which is used
      // to convey additional data to the frontend.
      helperResult: owner.helperResult,
    };
  },
};

exports.WebConsoleCommandsManager = WebConsoleCommandsManager;

/*
 * Built-in commands.
 *
 * A list of helper functions used by Firebug can be found here:
 *   http://getfirebug.com/wiki/index.php/Command_Line_API
 */

/**
 * Find the first node matching a CSS selector.
 *
 * @param string selector
 *        A string that is passed to window.document.querySelector
 * @param [optional] Node element
 *        An optional Node to replace window.document
 * @return Node or null
 *         The result of calling document.querySelectorAll(selector).
 */
WebConsoleCommandsManager.register({
  name: "$",
  isSideEffectFree: true,
  command(owner, selector, element) {
    try {
      if (
        element &&
        element.querySelector &&
        (element.nodeType == Node.ELEMENT_NODE ||
          element.nodeType == Node.DOCUMENT_NODE ||
          element.nodeType == Node.DOCUMENT_FRAGMENT_NODE)
      ) {
        return element.querySelector(selector);
      }
      return owner.window.document.querySelector(selector);
    } catch (err) {
      // Throw an error like `err` but that belongs to `owner.window`.
      throw new owner.window.DOMException(err.message, err.name);
    }
  },
});

/**
 * Find the nodes matching a CSS selector.
 *
 * @param string selector
 *        A string that is passed to window.document.querySelectorAll.
 * @param [optional] Node element
 *        An optional root Node, defaults to window.document
 * @return array of Node
 *         The result of calling document.querySelector(selector) in an array.
 */
WebConsoleCommandsManager.register({
  name: "$$",
  isSideEffectFree: true,
  command(owner, selector, element) {
    let scope = owner.window.document;
    try {
      if (
        element &&
        element.querySelectorAll &&
        (element.nodeType == Node.ELEMENT_NODE ||
          element.nodeType == Node.DOCUMENT_NODE ||
          element.nodeType == Node.DOCUMENT_FRAGMENT_NODE)
      ) {
        scope = element;
      }
      const nodes = scope.querySelectorAll(selector);
      const result = new owner.window.Array();
      // Calling owner.window.Array.from() doesn't work without accessing the
      // wrappedJSObject, so just loop through the results instead.
      for (let i = 0; i < nodes.length; i++) {
        result.push(nodes[i]);
      }
      return result;
    } catch (err) {
      // Throw an error like `err` but that belongs to `owner.window`.
      throw new owner.window.DOMException(err.message, err.name);
    }
  },
});

/**
 * Find the nodes matching a CSS selector, including those inside shadow DOM
 *
 * @param string selector
 *        A string that is passed to all `querySelectorAll` calls performed by this command.
 * @param [optional] Node element
 *        An optional root Node, defaults to window.document
 * @return array of Node
 *         An array containing the nodes returned by calling `querySelectorAll(selector)`
 *         on `element` and on all shadow hosts under element (recursively).
 */
WebConsoleCommandsManager.register({
  name: "$$$",
  isSideEffectFree: true,
  command(owner, selector, element) {
    let scope = owner.window.document;
    try {
      if (
        element?.querySelectorAll &&
        (element.nodeType == Node.ELEMENT_NODE ||
          element.nodeType == Node.DOCUMENT_NODE ||
          element.nodeType == Node.DOCUMENT_FRAGMENT_NODE)
      ) {
        scope = element;
      }

      const result = new owner.window.Array();

      const collectElements = root => {
        const nodes = root.querySelectorAll(selector);
        // Calling owner.window.Array.from() doesn't work without accessing the
        // wrappedJSObject, so just loop through the results instead.
        for (let i = 0, len = nodes.length; i < len; i++) {
          result.push(nodes[i]);
        }

        // If the scope is a host, run the query inside its shadow DOM
        if (root.openOrClosedShadowRoot) {
          collectElements(root.openOrClosedShadowRoot);
        }

        // Finally, run the query for all hosts in scope
        const all = root.querySelectorAll("*");
        for (let i = 0, len = all.length; i < len; i++) {
          const el = all[i];
          if (el.openOrClosedShadowRoot) {
            collectElements(el.openOrClosedShadowRoot);
          }
        }
      };

      collectElements(scope);

      return result;
    } catch (err) {
      // Throw an error like `err` but that belongs to `owner.window`.
      throw new owner.window.DOMException(err.message, err.name);
    }
  },
});

/**
 * Returns the result of the last console input evaluation
 *
 * @return object|undefined
 * Returns last console evaluation or undefined
 */
WebConsoleCommandsManager.register({
  name: "$_",
  isSideEffectFree: true,
  command: {
    get(owner) {
      return owner.consoleActor.getLastConsoleInputEvaluation();
    },
  },
});

/**
 * Runs an xPath query and returns all matched nodes.
 *
 * @param string xPath
 *        xPath search query to execute.
 * @param [optional] Node context
 *        Context to run the xPath query on. Uses window.document if not set.
 * @param [optional] string|number resultType
          Specify the result type. Default value XPathResult.ANY_TYPE
 * @return array of Node
 */
WebConsoleCommandsManager.register({
  name: "$x",
  isSideEffectFree: true,
  command(
    owner,
    xPath,
    context,
    resultType = owner.window.XPathResult.ANY_TYPE
  ) {
    const nodes = new owner.window.Array();
    // Not waiving Xrays, since we want the original Document.evaluate function,
    // instead of anything that's been redefined.
    const doc = owner.window.document;
    context = context || doc;
    switch (resultType) {
      case "number":
        resultType = owner.window.XPathResult.NUMBER_TYPE;
        break;

      case "string":
        resultType = owner.window.XPathResult.STRING_TYPE;
        break;

      case "bool":
        resultType = owner.window.XPathResult.BOOLEAN_TYPE;
        break;

      case "node":
        resultType = owner.window.XPathResult.FIRST_ORDERED_NODE_TYPE;
        break;

      case "nodes":
        resultType = owner.window.XPathResult.UNORDERED_NODE_ITERATOR_TYPE;
        break;
    }
    const results = doc.evaluate(xPath, context, null, resultType, null);
    if (results.resultType === owner.window.XPathResult.NUMBER_TYPE) {
      return results.numberValue;
    }
    if (results.resultType === owner.window.XPathResult.STRING_TYPE) {
      return results.stringValue;
    }
    if (results.resultType === owner.window.XPathResult.BOOLEAN_TYPE) {
      return results.booleanValue;
    }
    if (
      results.resultType === owner.window.XPathResult.ANY_UNORDERED_NODE_TYPE ||
      results.resultType === owner.window.XPathResult.FIRST_ORDERED_NODE_TYPE
    ) {
      return results.singleNodeValue;
    }
    if (
      results.resultType ===
        owner.window.XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE ||
      results.resultType === owner.window.XPathResult.ORDERED_NODE_SNAPSHOT_TYPE
    ) {
      for (let i = 0; i < results.snapshotLength; i++) {
        nodes.push(results.snapshotItem(i));
      }
      return nodes;
    }

    let node;
    while ((node = results.iterateNext())) {
      nodes.push(node);
    }

    return nodes;
  },
});

/**
 * Returns the currently selected object in the highlighter.
 *
 * @return Object representing the current selection in the
 *         Inspector, or null if no selection exists.
 */
WebConsoleCommandsManager.register({
  name: "$0",
  isSideEffectFree: true,
  command: {
    get(owner) {
      return owner.makeDebuggeeValue(owner.selectedNode);
    },
  },
});

/**
 * Clears the output of the WebConsole.
 */
WebConsoleCommandsManager.register({
  name: "clear",
  isSideEffectFree: false,
  command(owner) {
    owner.helperResult = {
      type: "clearOutput",
    };
  },
});

/**
 * Clears the input history of the WebConsole.
 */
WebConsoleCommandsManager.register({
  name: "clearHistory",
  isSideEffectFree: false,
  command(owner) {
    owner.helperResult = {
      type: "clearHistory",
    };
  },
});

/**
 * Returns the result of Object.keys(object).
 *
 * @param object object
 *        Object to return the property names from.
 * @return array of strings
 */
WebConsoleCommandsManager.register({
  name: "keys",
  isSideEffectFree: true,
  command(owner, object) {
    // Need to waive Xrays so we can iterate functions and accessor properties
    return Cu.cloneInto(Object.keys(Cu.waiveXrays(object)), owner.window);
  },
});

/**
 * Returns the values of all properties on object.
 *
 * @param object object
 *        Object to display the values from.
 * @return array of string
 */
WebConsoleCommandsManager.register({
  name: "values",
  isSideEffectFree: true,
  command(owner, object) {
    const values = [];
    // Need to waive Xrays so we can iterate functions and accessor properties
    const waived = Cu.waiveXrays(object);
    const names = Object.getOwnPropertyNames(waived);

    for (const name of names) {
      values.push(waived[name]);
    }

    return Cu.cloneInto(values, owner.window);
  },
});

/**
 * Opens a help window in MDN.
 */
WebConsoleCommandsManager.register({
  name: "help",
  isSideEffectFree: false,
  command(owner) {
    owner.helperResult = { type: "help" };
  },
});

/**
 * Inspects the passed object. This is done by opening the PropertyPanel.
 *
 * @param object object
 *        Object to inspect.
 */
WebConsoleCommandsManager.register({
  name: "inspect",
  isSideEffectFree: false,
  command(owner, object, forceExpandInConsole = false) {
    const dbgObj = owner.preprocessDebuggerObject(
      owner.makeDebuggeeValue(object)
    );

    const grip = owner.createValueGrip(dbgObj);
    owner.helperResult = {
      type: "inspectObject",
      input: owner.evalInput,
      object: grip,
      forceExpandInConsole,
    };
  },
});

/**
 * Copy the String representation of a value to the clipboard.
 *
 * @param any value
 *        A value you want to copy as a string.
 * @return void
 */
WebConsoleCommandsManager.register({
  name: "copy",
  isSideEffectFree: false,
  command(owner, value) {
    let payload;
    try {
      if (Element.isInstance(value)) {
        payload = value.outerHTML;
      } else if (typeof value == "string") {
        payload = value;
      } else {
        // Need to waive Xrays so we can iterate accessor properties.
        // If Cu is not defined, we are running on a worker thread, where xrays don't exist.
        if (value && Cu) {
          value = Cu.waiveXrays(value);
        }
        payload = JSON.stringify(value, null, "  ");
      }
    } catch (ex) {
      owner.helperResult = {
        type: "error",
        message: "webconsole.error.commands.copyError",
        messageArgs: [ex.toString()],
      };
      return;
    }
    owner.helperResult = {
      type: "copyValueToClipboard",
      value: payload,
    };
  },
});

/**
 * Take a screenshot of a page.
 *
 * @param object args
 *               The arguments to be passed to the screenshot
 * @return void
 */
WebConsoleCommandsManager.register({
  name: "screenshot",
  isSideEffectFree: false,
  command(owner, args = {}) {
    owner.helperResult = {
      type: "screenshotOutput",
      args,
    };
  },
});

/**
 * Shows a history of commands and expressions previously executed within the command line.
 *
 * @param object args
 *               The arguments to be passed to the history
 * @return void
 */
WebConsoleCommandsManager.register({
  name: "history",
  isSideEffectFree: false,
  command(owner, args = {}) {
    owner.helperResult = {
      type: "historyOutput",
      args,
    };
  },
});

/**
 * Block specific resource from loading
 *
 * @param object args
 *               an object with key "url", i.e. a filter
 *
 * @return void
 */
WebConsoleCommandsManager.register({
  name: "block",
  isSideEffectFree: false,
  command(owner, args = {}) {
    // Note that this command is implemented in the frontend, from actions's input.js
    // We only forward the command arguments back to the client.
    if (!args.url) {
      owner.helperResult = {
        type: "error",
        message: "webconsole.messages.commands.blockArgMissing",
      };
      return;
    }

    owner.helperResult = {
      type: "blockURL",
      args,
    };
  },
  validArguments: ["url"],
});

/**
 * Unblock a blocked a resource
 *
 * @param object filter
 *               an object with key "url", i.e. a filter
 *
 * @return void
 */
WebConsoleCommandsManager.register({
  name: "unblock",
  isSideEffectFree: false,
  command(owner, args = {}) {
    // Note that this command is implemented in the frontend, from actions's input.js
    // We only forward the command arguments back to the client.
    if (!args.url) {
      owner.helperResult = {
        type: "error",
        message: "webconsole.messages.commands.blockArgMissing",
      };
      return;
    }

    owner.helperResult = {
      type: "unblockURL",
      args,
    };
  },
  validArguments: ["url"],
});

/**
 * Toggle JavaScript tracing
 *
 * @param object args
 *        An object with various configuration only valid when starting the tracing.
 *
 * @return void
 */
WebConsoleCommandsManager.register({
  name: "trace",
  isSideEffectFree: false,
  command(owner, args) {
    // Disable :trace command on worker until this feature is enabled by default
    if (isWorker) {
      throw new Error(":trace command isn't supported in workers");
    }

    if (!owner.consoleActor.targetActor.isTracerFeatureEnabled) {
      throw new Error(
        ":trace requires 'devtools.debugger.features.javascript-tracing' preference to be true"
      );
    }
    const tracerActor =
      owner.consoleActor.targetActor.getTargetScopedActor("tracer");
    const logMethod = args.logMethod || "console";
    let traceDOMMutations = null;
    if ("dom-mutations" in args) {
      // When no value is passed, track all types of mutations
      if (args["dom-mutations"] === true) {
        traceDOMMutations = ["add", "attributes", "remove"];
      } else if (typeof args["dom-mutations"] == "string") {
        // Otherwise consider the value as coma seperated list and remove any white space.
        traceDOMMutations = args["dom-mutations"].split(",").map(e => e.trim());
        const acceptedValues = Object.values(lazy.JSTracer.DOM_MUTATIONS);
        if (!traceDOMMutations.every(e => acceptedValues.includes(e))) {
          throw new Error(
            `:trace --dom-mutations only accept a list of strings whose values can be: ${acceptedValues}`
          );
        }
      } else {
        throw new Error(
          ":trace --dom-mutations accept only no arguments, or a list mutation type strings (add,attributes,remove)"
        );
      }
    }
    // Note that toggleTracing does some sanity checks and will throw meaningful error
    // when the arguments are wrong.
    const enabled = tracerActor.toggleTracing({
      logMethod,
      prefix: args.prefix || null,
      traceFunctionReturn: !!args.returns,
      traceValues: !!args.values,
      traceOnNextInteraction: args["on-next-interaction"] || null,
      traceDOMMutations,
      maxDepth: args["max-depth"] || null,
      maxRecords: args["max-records"] || null,
    });

    owner.helperResult = {
      type: "traceOutput",
      enabled,
      logMethod,
    };
  },
  validArguments: [
    "logMethod",
    "max-depth",
    "max-records",
    "on-next-interaction",
    "dom-mutations",
    "prefix",
    "returns",
    "values",
  ],
});
