/* 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/. */

import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  Log: "chrome://remote/content/shared/Log.sys.mjs",
  readSessionData:
    "chrome://remote/content/shared/messagehandler/sessiondata/SessionDataReader.sys.mjs",
  RootMessageHandler:
    "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs",
  WindowGlobalMessageHandler:
    "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs",
});

ChromeUtils.defineLazyGetter(lazy, "logger", () => lazy.Log.get());

/**
 * Map of MessageHandler type to MessageHandler subclass.
 */
ChromeUtils.defineLazyGetter(
  lazy,
  "MessageHandlerClasses",
  () =>
    new Map([
      [lazy.RootMessageHandler.type, lazy.RootMessageHandler],
      [lazy.WindowGlobalMessageHandler.type, lazy.WindowGlobalMessageHandler],
    ])
);

/**
 * Get the MessageHandler subclass corresponding to the provided type.

 * @param {string} type
 *     MessageHandler type, one of MessageHandler.type.
 * @returns {Class}
 *     A MessageHandler subclass
 * @throws {Error}
 *      Throws if no MessageHandler subclass is found for the provided type.
 */
export function getMessageHandlerClass(type) {
  if (!lazy.MessageHandlerClasses.has(type)) {
    throw new Error(`No MessageHandler class available for type "${type}"`);
  }
  return lazy.MessageHandlerClasses.get(type);
}

/**
 * The MessageHandlerRegistry allows to create and retrieve MessageHandler
 * instances for different session ids.
 *
 * A MessageHandlerRegistry instance is bound to a specific MessageHandler type
 * and context. All MessageHandler instances created by the same registry will
 * use the type and context of the registry, but each will be associated to a
 * different session id.
 *
 * The registry is useful to retrieve the appropriate MessageHandler instance
 * after crossing a technical boundary (eg process, thread...).
 */
export class MessageHandlerRegistry extends EventEmitter {
  /**
   * @param {string} type
   *     MessageHandler type, one of MessageHandler.type.
   * @param {object} context
   *     The context object, which depends on the type.
   */
  constructor(type, context) {
    super();

    this._messageHandlerClass = getMessageHandlerClass(type);
    this._context = context;
    this._type = type;

    /**
     * Map of session id to MessageHandler instance
     */
    this._messageHandlersMap = new Map();

    this._onMessageHandlerDestroyed =
      this._onMessageHandlerDestroyed.bind(this);
    this._onMessageHandlerEvent = this._onMessageHandlerEvent.bind(this);
  }

  /**
   * Create all message handlers for the current context, based on the content
   * of the session data.
   * This should typically be called when the context is ready to be used and
   * to receive/send commands.
   */
  createAllMessageHandlers() {
    const data = lazy.readSessionData();
    for (const [sessionId, sessionDataItems] of data) {
      // Create a message handler for this context for each active message
      // handler session.
      // TODO: In the future, to support debugging use cases we might want to
      // only create a message handler if there is relevant data.
      // For automation scenarios, this is less critical.
      this._createMessageHandler(sessionId, sessionDataItems);
    }
  }

  destroy() {
    this._messageHandlersMap.forEach(messageHandler => {
      messageHandler.destroy();
    });
  }

  /**
   * Retrieve all MessageHandler instances held in this registry, for all
   * session IDs.
   *
   * @returns {Iterable.<MessageHandler>}
   *     Iterator of MessageHandler instances
   */
  getAllMessageHandlers() {
    return this._messageHandlersMap.values();
  }

  /**
   * Retrieve an existing MessageHandler instance matching the provided session
   * id. Returns null if no MessageHandler was found.
   *
   * @param {string} sessionId
   *     ID of the session the handler is used for.
   * @returns {MessageHandler=}
   *     A MessageHandler instance, null if not found.
   */
  getExistingMessageHandler(sessionId) {
    return this._messageHandlersMap.get(sessionId);
  }

  /**
   * Retrieve the MessageHandler instance registered for the provided session
   * id. Will create and register a MessageHander if no instance was found.
   *
   * @param {string} sessionId
   *     ID of the session the handler is used for.
   * @returns {MessageHandler}
   *     A MessageHandler instance.
   */
  getOrCreateMessageHandler(sessionId) {
    let messageHandler = this.getExistingMessageHandler(sessionId);
    if (!messageHandler) {
      messageHandler = this._createMessageHandler(sessionId);
    }

    return messageHandler;
  }

  /**
   * Retrieve an already registered RootMessageHandler instance matching the
   * provided sessionId.
   *
   * @param {string} sessionId
   *     ID of the session the handler is used for.
   * @returns {RootMessageHandler}
   *     A RootMessageHandler instance.
   * @throws {Error}
   *     If no root MessageHandler can be found for the provided session id.
   */
  getRootMessageHandler(sessionId) {
    const rootMessageHandler = this.getExistingMessageHandler(
      sessionId,
      lazy.RootMessageHandler.type
    );
    if (!rootMessageHandler) {
      throw new Error(
        `Unable to find a root MessageHandler for session id ${sessionId}`
      );
    }
    return rootMessageHandler;
  }

  toString() {
    return `[object ${this.constructor.name}]`;
  }

  /**
   * Create a new MessageHandler instance.
   *
   * @param {string} sessionId
   *     ID of the session the handler will be used for.
   * @param {Array<SessionDataItem>=} sessionDataItems
   *     Optional array of session data items to be applied automatically to the
   *     MessageHandler.
   * @returns {MessageHandler}
   *     A new MessageHandler instance.
   */
  _createMessageHandler(sessionId, sessionDataItems) {
    const messageHandler = new this._messageHandlerClass(
      sessionId,
      this._context,
      this
    );

    messageHandler.on(
      "message-handler-destroyed",
      this._onMessageHandlerDestroyed
    );
    messageHandler.on("message-handler-event", this._onMessageHandlerEvent);

    messageHandler.initialize(sessionDataItems);

    this._messageHandlersMap.set(sessionId, messageHandler);

    lazy.logger.trace(
      `Created MessageHandler ${this._type} for session ${sessionId}`
    );

    return messageHandler;
  }

  // Event handlers

  _onMessageHandlerDestroyed(eventName, messageHandler) {
    messageHandler.off(
      "message-handler-destroyed",
      this._onMessageHandlerDestroyed
    );
    messageHandler.off("message-handler-event", this._onMessageHandlerEvent);
    this._messageHandlersMap.delete(messageHandler.sessionId);

    lazy.logger.trace(
      `Unregistered MessageHandler ${messageHandler.constructor.type} for session ${messageHandler.sessionId}`
    );
  }

  _onMessageHandlerEvent(eventName, messageHandlerEvent) {
    // The registry simply re-emits MessageHandler events so that consumers
    // don't have to attach listeners to individual MessageHandler instances.
    this.emit("message-handler-registry-event", messageHandlerEvent);
  }
}
