/* 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 { MessageHandler } from "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
  getMessageHandlerFrameChildActor:
    "chrome://remote/content/shared/messagehandler/transports/js-window-actors/MessageHandlerFrameChild.sys.mjs",
  RootMessageHandler:
    "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs",
  WindowRealm: "chrome://remote/content/shared/Realm.sys.mjs",
});

/**
 * A WindowGlobalMessageHandler is dedicated to debugging a single window
 * global. It follows the lifecycle of the corresponding window global and will
 * therefore not survive any navigation. This MessageHandler cannot forward
 * commands further to other MessageHandlers and represents a leaf node in a
 * MessageHandler network.
 */
export class WindowGlobalMessageHandler extends MessageHandler {
  #innerWindowId;
  #debuggerEnvironment;
  #realms;

  constructor() {
    super(...arguments);

    this.#innerWindowId = this.context.window.windowGlobalChild.innerWindowId;

    // Maps sandbox names to instances of window realms.
    this.#realms = new Map();

    // The currently paused DebuggerEnvironment.
    this.#debuggerEnvironment = null;
  }

  initialize(sessionDataItems) {
    // Create the default realm, it is mapped to an empty string sandbox name.
    this.#realms.set("", this.#createRealm());

    // This method, even though being async, is not awaited on purpose,
    // since for now the sessionDataItems are passed in response to an event in a for loop.
    this.#applyInitialSessionDataItems(sessionDataItems);

    // With the session data applied the handler is now ready to be used.
    this.emitEvent("window-global-handler-created", {
      contextId: this.contextId,
      innerWindowId: this.#innerWindowId,
    });
  }

  destroy() {
    for (const realm of this.#realms.values()) {
      realm.destroy();
    }
    this.emitEvent("windowglobal-pagehide", {
      context: this.context,
      innerWindowId: this.innerWindowId,
    });
    this.#realms = null;
    this.#debuggerEnvironment = null;

    super.destroy();
  }

  /**
   * Returns the WindowGlobalMessageHandler module path.
   *
   * @returns {string}
   */
  static get modulePath() {
    return "windowglobal";
  }

  /**
   * Returns the WindowGlobalMessageHandler type.
   *
   * @returns {string}
   */
  static get type() {
    return "WINDOW_GLOBAL";
  }

  /**
   * For WINDOW_GLOBAL MessageHandlers, `context` is a BrowsingContext,
   * and BrowsingContext.id can be used as the context id.
   *
   * @param {BrowsingContext} context
   *     WindowGlobalMessageHandler contexts are expected to be
   *     BrowsingContexts.
   * @returns {string}
   *     The browsing context id.
   */
  static getIdFromContext(context) {
    return context.id;
  }

  /**
   * An object describing the current paused debugger environment, including the
   * current Debugger.Frame and global object reference.
   *
   * @typedef {object} DebuggerEnvironment
   * @property {Debugger.Frame} frame
   *     The paused frame.
   * @property {Debugger.Object} global
   *     The global object reference created by the debugger instance which
   *     paused the execution.
   */

  /**
   * Get the currently paused DebuggerEnvironment.
   *
   * @returns {DebuggerEnvironment|null}
   *     The paused debugger environment, or null if not paused.
   */
  get debuggerEnvironment() {
    return this.#debuggerEnvironment;
  }

  /**
   * Set the debugger environment for the currently paused Debugger.Frame and
   * global object.
   *
   * @param {object|null} debuggerEnvironment
   *     An object describing the current debugger environment, null to clear.
   */
  set debuggerEnvironment(debuggerEnvironment) {
    this.#debuggerEnvironment = debuggerEnvironment;
  }

  get innerWindowId() {
    return this.#innerWindowId;
  }

  get realms() {
    return this.#realms;
  }

  get window() {
    return this.context.window;
  }

  #createRealm(sandboxName = null) {
    const realm = new lazy.WindowRealm(this.context.window, {
      sandboxName,
    });

    this.emitEvent("realm-created", {
      realmInfo: realm.getInfo(),
      innerWindowId: this.innerWindowId,
    });

    return realm;
  }

  #getRealmFromSandboxName(sandboxName = null) {
    if (sandboxName === null || sandboxName === "") {
      return this.#realms.get("");
    }

    if (this.#realms.has(sandboxName)) {
      return this.#realms.get(sandboxName);
    }

    const realm = this.#createRealm(sandboxName);

    this.#realms.set(sandboxName, realm);

    return realm;
  }

  async #applyInitialSessionDataItems(sessionDataItems) {
    if (!Array.isArray(sessionDataItems)) {
      return;
    }

    const destination = {
      type: WindowGlobalMessageHandler.type,
    };

    // Create a Map with the structure moduleName -> category -> relevant session data items.
    const structuredUpdates = new Map();
    for (const sessionDataItem of sessionDataItems) {
      const { category, contextDescriptor, moduleName } = sessionDataItem;

      if (!this.matchesContext(contextDescriptor)) {
        continue;
      }
      if (!structuredUpdates.has(moduleName)) {
        // Skip session data item if the module is not present
        // for the destination.
        if (!this.moduleCache.hasModuleClass(moduleName, destination)) {
          continue;
        }
        structuredUpdates.set(moduleName, new Map());
      }

      if (!structuredUpdates.get(moduleName).has(category)) {
        structuredUpdates.get(moduleName).set(category, new Set());
      }

      structuredUpdates.get(moduleName).get(category).add(sessionDataItem);
    }

    const sessionDataPromises = [];

    for (const [moduleName, categories] of structuredUpdates.entries()) {
      for (const [category, relevantSessionData] of categories.entries()) {
        sessionDataPromises.push(
          this.handleCommand({
            moduleName,
            commandName: "_applySessionData",
            params: {
              category,
              sessionData: Array.from(relevantSessionData),
              initial: true,
            },
            destination,
          })
        );
      }
    }

    await Promise.all(sessionDataPromises);
  }

  forwardCommand(command) {
    switch (command.destination.type) {
      case lazy.RootMessageHandler.type:
        return lazy
          .getMessageHandlerFrameChildActor(this)
          .sendCommand(command, this.sessionId);
      default:
        throw new Error(
          `Cannot forward command to "${command.destination.type}" from "${this.constructor.type}".`
        );
    }
  }

  /**
   * If <var>realmId</var> is null or not provided get the realm for
   * a given <var>sandboxName</var>, otherwise find the realm
   * in the cache with the realm id equal given <var>realmId</var>.
   *
   * @param {object} options
   * @param {string|null=} options.realmId
   *     The realm id.
   * @param {string=} options.sandboxName
   *     The name of sandbox
   *
   * @returns {Realm}
   *     The realm object.
   */
  getRealm(options = {}) {
    const { realmId = null, sandboxName } = options;
    if (realmId === null) {
      return this.#getRealmFromSandboxName(sandboxName);
    }

    const realm = Array.from(this.#realms.values()).find(
      realm => realm.id === realmId
    );

    if (realm) {
      return realm;
    }

    throw new lazy.error.NoSuchFrameError(`Realm with id ${realmId} not found`);
  }

  /**
   * Check if the context matches a provided context descriptor.
   *
   * @param {object} contextDescriptor
   *     A context descriptor.
   * @returns {boolean}
   *     Return true if the context matches a provided context descriptor,
   *     false otherwise.
   */
  matchesContext(contextDescriptor) {
    return this.contextsMatchDescriptor([this.context], contextDescriptor);
  }

  /**
   * Send a command to the root MessageHandler.
   *
   * @param {Command} command
   *     The command to send to the root MessageHandler.
   * @returns {Promise}
   *     A promise which resolves with the return value of the command.
   */
  sendRootCommand(command) {
    return this.handleCommand({
      ...command,
      destination: {
        type: lazy.RootMessageHandler.type,
      },
    });
  }
}
