/* 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 { Pool } = require("devtools/shared/protocol");

/**
 * A Special Pool for RootActor and WindowGlobalTargetActor, which allows lazy loaded
 * actors to be added to the pool.
 *
 * Like the Pool, this is a protocol object that can manage the lifetime of other protocol
 * objects. Pools are used on both sides of the connection to help coordinate lifetimes.
 *
 * @param conn
 *   Is a DevToolsServerConnection.  Must have
 *   addActorPool, removeActorPool, and poolFor.
 * @constructor
 */
class LazyPool extends Pool {
  constructor(conn) {
    super(conn);
  }

  // The actor for a given actor id stored in this pool
  getActorByID(actorID) {
    if (this.__poolMap) {
      const entry = this._poolMap.get(actorID);
      if (entry instanceof LazyActor) {
        return entry.createActor();
      }
      return entry;
    }
    return null;
  }
}

exports.LazyPool = LazyPool;

/**
 * Populate |parent._extraActors| as specified by |registeredActors|, reusing whatever
 * actors are already there. Add all actors in the final extra actors table to
 * |pool|. _extraActors is treated as a cache for lazy actors
 *
 * The target actor uses this to instantiate actors that other
 * parts of the browser have specified with ActorRegistry.addTargetScopedActor
 *
 * @param factories
 *     An object whose own property names are the names of properties to add to
 *     some reply packet (say, a target actor grip or the "listTabs" response
 *     form), and whose own property values are actor constructor functions, as
 *     documented for addTargetScopedActor
 *
 * @param parent
 *     The parent TargetActor with which the new actors
 *     will be associated. It should support whatever API the |factories|
 *     constructor functions might be interested in, as it is passed to them.
 *     For the sake of CommonCreateExtraActors itself, it should have at least
 *     the following properties:
 *
 *     - _extraActors
 *        An object whose own property names are factory table (and packet)
 *        property names, and whose values are no-argument actor constructors,
 *        of the sort that one can add to a Pool.
 *
 *     - conn
 *        The DevToolsServerConnection in which the new actors will participate.
 *
 *     - actorID
 *        The actor's name, for use as the new actors' parentID.
 * @param pool
 *     An object which implements the protocol.js Pool interface, and has the
 *     following properties
 *
 *     - manage
 *       a function which adds a given actor to an actor pool
 */
function createExtraActors(registeredActors, pool, parent) {
  // Walk over global actors added by extensions.
  const nameMap = {};
  for (const name in registeredActors) {
    let actor = parent._extraActors[name];
    if (!actor) {
      // Register another factory, but this time specific to this connection.
      // It creates a fake actor that looks like an regular actor in the pool,
      // but without actually instantiating the actor.
      // It will only be instantiated on the first request made to the actor.
      actor = new LazyActor(registeredActors[name], parent, pool);
      parent._extraActors[name] = actor;
    }

    // If the actor already exists in the pool, it may have been instantiated,
    // so make sure not to overwrite it by a non-instantiated version.
    if (!pool.has(actor.actorID)) {
      pool.manage(actor);
    }
    nameMap[name] = actor.actorID;
  }
  return nameMap;
}

exports.createExtraActors = createExtraActors;

/**
 * Creates an "actor-like" object which responds in the same way as an ordinary actor
 * but has fewer capabilities (ie, does not manage lifetimes or have it's own pool).
 *
 *
 * @param factories
 *     An object whose own property names are the names of properties to add to
 *     some reply packet (say, a target actor grip or the "listTabs" response
 *     form), and whose own property values are actor constructor functions, as
 *     documented for addTargetScopedActor
 *
 * @param parent
 *     The parent TargetActor with which the new actors
 *     will be associated. It should support whatever API the |factories|
 *     constructor functions might be interested in, as it is passed to them.
 *     For the sake of CommonCreateExtraActors itself, it should have at least
 *     the following properties:
 *
 *     - _extraActors
 *        An object whose own property names are factory table (and packet)
 *        property names, and whose values are no-argument actor constructors,
 *        of the sort that one can add to a Pool.
 *
 *     - conn
 *        The DevToolsServerConnection in which the new actors will participate.
 *
 *     - actorID
 *        The actor's name, for use as the new actors' parentID.
 * @param pool
 *     An object which implements the protocol.js Pool interface, and has the
 *     following properties
 *
 *     - manage
 *       a function which adds a given actor to an actor pool
 */

function LazyActor(factory, parent, pool) {
  this._options = factory.options;
  this._parentActor = parent;
  this._name = factory.name;
  this._pool = pool;

  // needed for taking a place in a pool
  this.typeName = factory.name;
}

LazyActor.prototype = {
  loadModule(id) {
    const options = this._options;
    try {
      return require(id);
      // Fetch the actor constructor
    } catch (e) {
      throw new Error(
        `Unable to load actor module '${options.id}'\n${e.message}\n${e.stack}\n`
      );
    }
  },

  getConstructor() {
    const options = this._options;
    if (options.constructorFun) {
      // Actor definition registered by testing helpers
      return options.constructorFun;
    }
    // Lazy actor definition, where options contains all the information
    // required to load the actor lazily.
    // Exposes `name` attribute in order to allow removeXXXActor to match
    // the actor by its actor constructor name.
    this.name = options.constructorName;
    const module = this.loadModule(options.id);
    const constructor = module[options.constructorName];
    if (!constructor) {
      throw new Error(
        `Unable to find actor constructor named '${this.name}'. (Is it exported?)`
      );
    }
    return constructor;
  },

  /**
   * Return the parent pool for this lazy actor.
   */
  getParent() {
    return this.conn && this.conn.poolFor(this.actorID);
  },

  /**
   * This will only happen if the actor is destroyed before it is created
   * We do not want to use the Pool destruction method, because this actor
   * has no pool. However, it might have a parent that should unmange this
   * actor
   */
  destroy() {
    const parent = this.getParent();
    if (parent) {
      parent.unmanage(this);
    }
  },

  createActor() {
    // Fetch the actor constructor
    const Constructor = this.getConstructor();
    // Instantiate a new actor instance
    const conn = this._parentActor.conn;
    // this should be taken care of once all actors are moved to protocol.js
    const instance = new Constructor(conn, this._parentActor);
    instance.conn = conn;

    // We want the newly-constructed actor to completely replace the factory
    // actor. Reusing the existing actor ID will make sure Pool.manage
    // replaces the old actor with the new actor.
    instance.actorID = this.actorID;

    this._pool.manage(instance);

    return instance;
  },
};
