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

var gRegisteredModules = Object.create(null);

const ActorRegistry = {
  // Map of global actor names to actor constructors.
  globalActorFactories: {},
  // Map of target-scoped actor names to actor constructors.
  targetScopedActorFactories: {},
  init(connections) {
    this._connections = connections;
  },

  /**
   * Register a CommonJS module with the devtools server.
   * @param id string
   *        The ID of a CommonJS module.
   *        The actor is going to be registered immediately, but loaded only
   *        when a client starts sending packets to an actor with the same id.
   *
   * @param options object
   *        An object with 3 mandatory attributes:
   *        - prefix (string):
   *          The prefix of an actor is used to compute:
   *          - the `actorID` of each new actor instance (ex: prefix1). (See Pool.manage)
   *          - the actor name in the listTabs request. Sending a listTabs
   *            request to the root actor returns actor IDs. IDs are in
   *            dictionaries, with actor names as keys and actor IDs as values.
   *            The actor name is the prefix to which the "Actor" string is
   *            appended. So for an actor with the `console` prefix, the actor
   *            name will be `consoleActor`.
   *        - constructor (string):
   *          the name of the exported symbol to be used as the actor
   *          constructor.
   *        - type (a dictionary of booleans with following attribute names):
   *          - "global"
   *            registers a global actor instance, if true.
   *            A global actor has the root actor as its parent.
   *          - "target"
   *            registers a target-scoped actor instance, if true.
   *            A new actor will be created for each target, such as a tab.
   */
  registerModule(id, options) {
    if (id in gRegisteredModules) {
      return;
    }

    if (!options) {
      throw new Error(
        "ActorRegistry.registerModule requires an options argument"
      );
    }
    const { prefix, constructor, type } = options;
    if (typeof prefix !== "string") {
      throw new Error(
        `Lazy actor definition for '${id}' requires a string ` +
          `'prefix' option.`
      );
    }
    if (typeof constructor !== "string") {
      throw new Error(
        `Lazy actor definition for '${id}' requires a string ` +
          `'constructor' option.`
      );
    }
    if (!("global" in type) && !("target" in type)) {
      throw new Error(
        `Lazy actor definition for '${id}' requires a dictionary ` +
          `'type' option whose attributes can be 'global' or 'target'.`
      );
    }
    const name = prefix + "Actor";
    const mod = {
      id,
      prefix,
      constructorName: constructor,
      type,
      globalActor: type.global,
      targetScopedActor: type.target,
    };
    gRegisteredModules[id] = mod;
    if (mod.targetScopedActor) {
      this.addTargetScopedActor(mod, name);
    }
    if (mod.globalActor) {
      this.addGlobalActor(mod, name);
    }
  },

  /**
   * Unregister a previously-loaded CommonJS module from the devtools server.
   */
  unregisterModule(id) {
    const mod = gRegisteredModules[id];
    if (!mod) {
      throw new Error(
        "Tried to unregister a module that was not previously registered."
      );
    }

    // Lazy actors
    if (mod.targetScopedActor) {
      this.removeTargetScopedActor(mod);
    }
    if (mod.globalActor) {
      this.removeGlobalActor(mod);
    }

    delete gRegisteredModules[id];
  },

  /**
   * Install Firefox-specific actors.
   *
   * /!\ Be careful when adding a new actor, especially global actors.
   * Any new global actor will be exposed and returned by the root actor.
   */
  addBrowserActors() {
    this.registerModule("devtools/server/actors/preference", {
      prefix: "preference",
      constructor: "PreferenceActor",
      type: { global: true },
    });
    this.registerModule("devtools/server/actors/addon/addons", {
      prefix: "addons",
      constructor: "AddonsActor",
      type: { global: true },
    });
    this.registerModule("devtools/server/actors/device", {
      prefix: "device",
      constructor: "DeviceActor",
      type: { global: true },
    });
    this.registerModule("devtools/server/actors/heap-snapshot-file", {
      prefix: "heapSnapshotFile",
      constructor: "HeapSnapshotFileActor",
      type: { global: true },
    });
    // Always register this as a global module, even while there is a pref turning
    // on and off the other performance actor. This actor shouldn't conflict with
    // the other one. These are also lazily loaded so there shouldn't be a performance
    // impact.
    this.registerModule("devtools/server/actors/perf", {
      prefix: "perf",
      constructor: "PerfActor",
      type: { global: true },
    });
    /**
     * Always register parent accessibility actor as a global module. This
     * actor is responsible for all top level accessibility actor functionality
     * that relies on the parent process.
     */
    this.registerModule(
      "devtools/server/actors/accessibility/parent-accessibility",
      {
        prefix: "parentAccessibility",
        constructor: "ParentAccessibilityActor",
        type: { global: true },
      }
    );

    this.registerModule("devtools/server/actors/screenshot", {
      prefix: "screenshot",
      constructor: "ScreenshotActor",
      type: { global: true },
    });
  },

  /**
   * Install target-scoped actors.
   */
  addTargetScopedActors() {
    this.registerModule("devtools/server/actors/webconsole", {
      prefix: "console",
      constructor: "WebConsoleActor",
      type: { target: true },
    });
    this.registerModule("devtools/server/actors/inspector/inspector", {
      prefix: "inspector",
      constructor: "InspectorActor",
      type: { target: true },
    });
    this.registerModule("devtools/server/actors/style-sheets", {
      prefix: "styleSheets",
      constructor: "StyleSheetsActor",
      type: { target: true },
    });
    this.registerModule("devtools/server/actors/memory", {
      prefix: "memory",
      constructor: "MemoryActor",
      type: { target: true },
    });
    this.registerModule("devtools/server/actors/reflow", {
      prefix: "reflow",
      constructor: "ReflowActor",
      type: { target: true },
    });
    this.registerModule("devtools/server/actors/css-properties", {
      prefix: "cssProperties",
      constructor: "CssPropertiesActor",
      type: { target: true },
    });
    this.registerModule("devtools/server/actors/animation", {
      prefix: "animations",
      constructor: "AnimationsActor",
      type: { target: true },
    });
    this.registerModule("devtools/server/actors/emulation/responsive", {
      prefix: "responsive",
      constructor: "ResponsiveActor",
      type: { target: true },
    });
    this.registerModule(
      "devtools/server/actors/addon/webextension-inspected-window",
      {
        prefix: "webExtensionInspectedWindow",
        constructor: "WebExtensionInspectedWindowActor",
        type: { target: true },
      }
    );
    this.registerModule("devtools/server/actors/accessibility/accessibility", {
      prefix: "accessibility",
      constructor: "AccessibilityActor",
      type: { target: true },
    });
    this.registerModule("devtools/server/actors/changes", {
      prefix: "changes",
      constructor: "ChangesActor",
      type: { target: true },
    });
    this.registerModule("devtools/server/actors/manifest", {
      prefix: "manifest",
      constructor: "ManifestActor",
      type: { target: true },
    });
    this.registerModule(
      "devtools/server/actors/network-monitor/network-content",
      {
        prefix: "networkContent",
        constructor: "NetworkContentActor",
        type: { target: true },
      }
    );
    this.registerModule("devtools/server/actors/screenshot-content", {
      prefix: "screenshotContent",
      constructor: "ScreenshotContentActor",
      type: { target: true },
    });
    this.registerModule("devtools/server/actors/tracer", {
      prefix: "tracer",
      constructor: "TracerActor",
      type: { target: true },
    });
    this.registerModule("devtools/server/actors/objects-manager", {
      prefix: "objectsManager",
      constructor: "ObjectsManagerActor",
      type: { target: true },
    });
  },

  /**
   * Registers handlers for new target-scoped request types defined dynamically.
   *
   * Note that the name of the request type is not allowed to clash with existing protocol
   * packet properties, like 'title', 'url' or 'actor', since that would break the protocol.
   *
   * @param options object
   *        - constructorName: (required)
   *          name of actor constructor, which is also used when removing the actor.
   *        One of the following:
   *          - id:
   *            module ID that contains the actor
   *          - constructorFun:
   *            a function to construct the actor
   * @param name string
   *        The name of the new request type.
   */
  addTargetScopedActor(options, name) {
    if (!name) {
      throw Error("addTargetScopedActor requires the `name` argument");
    }
    if (["title", "url", "actor"].includes(name)) {
      throw Error(name + " is not allowed");
    }
    if (this.targetScopedActorFactories.hasOwnProperty(name)) {
      throw Error(name + " already exists");
    }
    this.targetScopedActorFactories[name] = { options, name };
  },

  /**
   * Unregisters the handler for the specified target-scoped request type.
   *
   * When unregistering an existing target-scoped actor, we remove the actor factory as
   * well as all existing instances of the actor.
   *
   * @param actor object, string
   *        In case of object:
   *          The `actor` object being given to related addTargetScopedActor call.
   *        In case of string:
   *          The `name` string being given to related addTargetScopedActor call.
   */
  removeTargetScopedActor(actorOrName) {
    let name;
    if (typeof actorOrName == "string") {
      name = actorOrName;
    } else {
      const actor = actorOrName;
      for (const factoryName in this.targetScopedActorFactories) {
        const handler = this.targetScopedActorFactories[factoryName];
        if (
          handler.options.constructorName == actor.name ||
          handler.options.id == actor.id
        ) {
          name = factoryName;
          break;
        }
      }
    }
    if (!name) {
      return;
    }
    delete this.targetScopedActorFactories[name];
    for (const connID of Object.getOwnPropertyNames(this._connections)) {
      // DevToolsServerConnection in child process don't have rootActor
      if (this._connections[connID].rootActor) {
        this._connections[connID].rootActor.removeActorByName(name);
      }
    }
  },

  /**
   * Registers handlers for new browser-scoped request types defined dynamically.
   *
   * Note that the name of the request type is not allowed to clash with existing protocol
   * packet properties, like 'from', 'tabs' or 'selected', since that would break the protocol.
   *
   * @param options object
   *        - constructorName: (required)
   *          name of actor constructor, which is also used when removing the actor.
   *        One of the following:
   *          - id:
   *            module ID that contains the actor
   *          - constructorFun:
   *            a function to construct the actor
   * @param name string
   *        The name of the new request type.
   */
  addGlobalActor(options, name) {
    if (!name) {
      throw Error("addGlobalActor requires the `name` argument");
    }
    if (["from", "tabs", "selected"].includes(name)) {
      throw Error(name + " is not allowed");
    }
    if (this.globalActorFactories.hasOwnProperty(name)) {
      throw Error(name + " already exists");
    }
    this.globalActorFactories[name] = { options, name };
  },

  /**
   * Unregisters the handler for the specified browser-scoped request type.
   *
   * When unregistering an existing global actor, we remove the actor factory as well as
   * all existing instances of the actor.
   *
   * @param actor object, string
   *        In case of object:
   *          The `actor` object being given to related addGlobalActor call.
   *        In case of string:
   *          The `name` string being given to related addGlobalActor call.
   */
  removeGlobalActor(actorOrName) {
    let name;
    if (typeof actorOrName == "string") {
      name = actorOrName;
    } else {
      const actor = actorOrName;
      for (const factoryName in this.globalActorFactories) {
        const handler = this.globalActorFactories[factoryName];
        if (
          handler.options.constructorName == actor.name ||
          handler.options.id == actor.id
        ) {
          name = factoryName;
          break;
        }
      }
    }
    if (!name) {
      return;
    }
    delete this.globalActorFactories[name];
    for (const connID of Object.getOwnPropertyNames(this._connections)) {
      // DevToolsServerConnection in child process don't have rootActor
      if (this._connections[connID].rootActor) {
        this._connections[connID].rootActor.removeActorByName(name);
      }
    }
  },

  destroy() {
    for (const id of Object.getOwnPropertyNames(gRegisteredModules)) {
      this.unregisterModule(id);
    }
    gRegisteredModules = Object.create(null);

    this.globalActorFactories = {};
    this.targetScopedActorFactories = {};
  },
};

exports.ActorRegistry = ActorRegistry;
