/* 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 EventEmitter = require("resource://devtools/shared/event-emitter.js");

/**
 * Actor and Front implementations
 */

/**
 * 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 {DevToolsServerConnection|DevToolsClient} [conn]
 *   Either a DevToolsServerConnection or a DevToolsClient. Must have
 *   addActorPool, removeActorPool, and poolFor.
 *   conn can be null if the subclass provides a conn property.
 * @param {string} [label]
 *   An optional label for the Pool.
 * @class
 */
class Pool extends EventEmitter {
  constructor(conn, label) {
    super();

    if (conn) {
      this.conn = conn;
    }
    this.label = label;

    // Will be individually flipped to true by Actor/Front classes.
    // Will also only be exposed via Actor/Front::isDestroyed().
    this._isDestroyed = false;
  }

  __poolMap = null;
  parentPool = null;

  /**
   * Return the parent pool for this client.
   */
  getParent() {
    return this.parentPool;
  }

  /**
   * A pool is at the top of its pool hierarchy if it has:
   * - no parent
   * - or it is its own parent
   */
  isTopPool() {
    const parent = this.getParent();
    return !parent || parent === this;
  }

  poolFor(actorID) {
    return this.conn.poolFor(actorID);
  }

  /**
   * Override this if you want actors returned by this actor
   * to belong to a different actor by default.
   */
  marshallPool() {
    return this;
  }

  /**
   * Pool is the base class for all actors, even leaf nodes.
   * If the child map is actually referenced, go ahead and create
   * the stuff needed by the pool.
   */
  get _poolMap() {
    if (this.__poolMap) {
      return this.__poolMap;
    }
    this.__poolMap = new Map();
    this.conn.addActorPool(this);
    return this.__poolMap;
  }

  /**
   * Add an actor as a child of this pool.
   */
  manage(actor) {
    if (!actor.actorID) {
      actor.actorID = this.conn.allocID(actor.typeName);
    } else {
      // If the actor is already registered in a pool, remove it without destroying it.
      // This happens for example when an addon is reloaded. To see this behavior, take a
      // look at devtools/server/tests/xpcshell/test_addon_reload.js

      const parent = actor.getParent();
      if (parent && parent !== this) {
        parent.unmanage(actor);
      }
    }
    // Duplicate the ID into another field as `actorID` will be cleared on destruction
    actor.persistedActorID = actor.actorID;

    this._poolMap.set(actor.actorID, actor);
    actor.parentPool = this;
  }

  unmanageChildren(FrontType) {
    for (const front of this.poolChildren()) {
      if (!FrontType || front instanceof FrontType) {
        this.unmanage(front);
      }
    }
  }

  /**
   * Remove an actor as a child of this pool.
   */
  unmanage(actor) {
    if (this.__poolMap) {
      this.__poolMap.delete(actor.actorID);
    }
    actor.parentPool = null;
  }

  // true if the given actor ID exists in the pool.
  has(actorID) {
    return this.__poolMap && this._poolMap.has(actorID);
  }

  /**
   * Search for an actor in this pool, given an actorID
   *
   * @param {string} actorID
   * @returns {Actor/null} Returns null if the actor wasn't found
   */
  getActorByID(actorID) {
    if (this.__poolMap) {
      return this._poolMap.get(actorID);
    }
    return null;
  }

  // Generator that yields each non-self child of the pool.
  *poolChildren() {
    if (!this.__poolMap) {
      return;
    }
    for (const actor of this.__poolMap.values()) {
      // Self-owned actors are ok, but don't need visiting twice.
      if (actor === this) {
        continue;
      }
      yield actor;
    }
  }

  isDestroyed() {
    // Note: _isDestroyed is only flipped from Actor and Front subclasses for
    // now, so this method should not be called on pure Pool instances.
    // See Bug 1717811.
    return this._isDestroyed;
  }

  /**
   * Pools can override this method in order to opt-out of a destroy sequence.
   *
   * For instance, Fronts are destroyed during the toolbox destroy. However when
   * the toolbox is destroyed, the document holding the toolbox is also
   * destroyed. So it should not be necessary to cleanup Fronts during toolbox
   * destroy.
   *
   * For the time being, Fronts (or Pools in general) which want to opt-out of
   * toolbox destroy can override this method and check the value of
   * `this.conn.isToolboxDestroy`.
   */
  skipDestroy() {
    return false;
  }

  /**
   * Destroy this item, removing it from a parent if it has one,
   * and destroying all children if necessary.
   */
  destroy() {
    const parent = this.getParent();
    if (parent) {
      parent.unmanage(this);
    }
    if (!this.__poolMap) {
      return;
    }
    // Immediately clear the poolmap so that we bail out early if the code is reentrant.
    const poolMap = this.__poolMap;
    this.__poolMap = null;

    for (const actor of poolMap.values()) {
      // Self-owned actors are ok, but don't need destroying twice.
      if (actor === this) {
        continue;
      }

      // Some pool-managed values don't extend Pool and won't have skipDestroy
      // defined. For instance, the root actor and the lazy actors.
      if (typeof actor.skipDestroy === "function" && actor.skipDestroy()) {
        continue;
      }

      const destroy = actor.destroy;
      if (destroy) {
        // Disconnect destroy while we're destroying in case of (misbehaving)
        // circular ownership.
        actor.destroy = null;
        destroy.call(actor);
        actor.destroy = destroy;
      }
    }

    // `conn` might be null for LazyPool in an unexplained way.
    if (this.conn) {
      this.conn.removeActorPool(this);
      this.conn = null;
    }
  }
}

exports.Pool = Pool;
