/* 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 { loader } from "resource://devtools/shared/loader/Loader.sys.mjs";
import { EventEmitter } from "resource://gre/modules/EventEmitter.sys.mjs";

const { WatcherRegistry } = ChromeUtils.importESModule(
  "resource://devtools/server/actors/watcher/WatcherRegistry.sys.mjs",
  {
    // WatcherRegistry needs to be a true singleton and loads ActorManagerParent
    // which also has to be a true singleton.
    loadInDevToolsLoader: false,
  }
);

const lazy = {};

loader.lazyRequireGetter(
  lazy,
  "JsWindowActorTransport",
  "resource://devtools/shared/transport/js-window-actor-transport.js",
  true
);

export class DevToolsFrameParent extends JSWindowActorParent {
  constructor() {
    super();

    // Map of DevToolsServerConnection's used to forward the messages from/to
    // the client. The connections run in the parent process, as this code. We
    // may have more than one when there is more than one client debugging the
    // same frame. For example, a content toolbox and the browser toolbox.
    //
    // The map is indexed by the connection prefix.
    // The values are objects containing the following properties:
    // - actor: the frame target actor(as a form)
    // - connection: the DevToolsServerConnection used to communicate with the
    //   frame target actor
    // - prefix: the forwarding prefix used by the connection to know
    //   how to forward packets to the frame target
    // - transport: the JsWindowActorTransport
    //
    // Reminder about prefixes: all DevToolsServerConnections have a `prefix`
    // which can be considered as a kind of id. On top of this, parent process
    // DevToolsServerConnections also have forwarding prefixes because they are
    // responsible for forwarding messages to content process connections.
    this._connections = new Map();

    this._onConnectionClosed = this._onConnectionClosed.bind(this);
    EventEmitter.decorate(this);
  }

  /**
   * Request the content process to create the Frame Target if there is one
   * already available that matches the Browsing Context ID
   */
  async instantiateTarget({
    watcherActorID,
    connectionPrefix,
    sessionContext,
    sessionData,
  }) {
    await this.sendQuery("DevToolsFrameParent:instantiate-already-available", {
      watcherActorID,
      connectionPrefix,
      sessionContext,
      sessionData,
    });
  }

  /**
   * @param {object} arg
   * @param {object} arg.sessionContext
   * @param {object} arg.options
   * @param {boolean} arg.options.isModeSwitching
   *        true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
   */
  destroyTarget({ watcherActorID, sessionContext, options }) {
    this.sendAsyncMessage("DevToolsFrameParent:destroy", {
      watcherActorID,
      sessionContext,
      options,
    });
  }

  /**
   * Communicate to the content process that some data have been added.
   */
  async addSessionDataEntry({ watcherActorID, sessionContext, type, entries }) {
    try {
      await this.sendQuery("DevToolsFrameParent:addSessionDataEntry", {
        watcherActorID,
        sessionContext,
        type,
        entries,
      });
    } catch (e) {
      console.warn(
        "Failed to add session data entry for frame targets in browsing context",
        this.browsingContext.id
      );
      console.warn(e);
    }
  }

  /**
   * Communicate to the content process that some data have been removed.
   */
  removeSessionDataEntry({ watcherActorID, sessionContext, type, entries }) {
    this.sendAsyncMessage("DevToolsFrameParent:removeSessionDataEntry", {
      watcherActorID,
      sessionContext,
      type,
      entries,
    });
  }

  connectFromContent({ watcherActorID, forwardingPrefix, actor }) {
    const watcher = WatcherRegistry.getWatcher(watcherActorID);

    if (!watcher) {
      throw new Error(
        `Watcher Actor with ID '${watcherActorID}' can't be found.`
      );
    }
    const connection = watcher.conn;

    connection.on("closed", this._onConnectionClosed);

    // Create a js-window-actor based transport.
    const transport = new lazy.JsWindowActorTransport(this, forwardingPrefix);
    transport.hooks = {
      onPacket: connection.send.bind(connection),
      onTransportClosed() {},
    };
    transport.ready();

    connection.setForwarding(forwardingPrefix, transport);

    this._connections.set(watcher.conn.prefix, {
      watcher,
      connection,
      // This prefix is the prefix of the DevToolsServerConnection, running
      // in the content process, for which we should forward packets to, based on its prefix.
      // While `watcher.connection` is also a DevToolsServerConnection, but from this process,
      // the parent process. It is the one receiving Client packets and the one, from which
      // we should forward packets from.
      forwardingPrefix,
      transport,
      actor,
    });

    watcher.notifyTargetAvailable(actor);
  }

  _onConnectionClosed(status, connectionPrefix) {
    this._unregisterWatcher(connectionPrefix);
  }

  /**
   * Given a watcher connection prefix, unregister everything related to the Watcher
   * in this JSWindowActor.
   *
   * @param {String} connectionPrefix
   *        The connection prefix of the watcher to unregister
   */
  async _unregisterWatcher(connectionPrefix) {
    const connectionInfo = this._connections.get(connectionPrefix);
    if (!connectionInfo) {
      return;
    }
    const { forwardingPrefix, transport, connection } = connectionInfo;
    this._connections.delete(connectionPrefix);

    connection.off("closed", this._onConnectionClosed);
    if (transport) {
      // If we have a child transport, the actor has already
      // been created. We need to stop using this transport.
      transport.close();
    }

    connection.cancelForwarding(forwardingPrefix);
  }

  /**
   * Destroy everything that we did related to the current WindowGlobal that
   * this JSWindow Actor represents:
   *  - close all transports that were used as bridge to communicate with the
   *    DevToolsFrameChild, running in the content process
   *  - unregister these transports from DevToolsServer (cancelForwarding)
   *  - notify the client, via the WatcherActor that all related targets,
   *    one per client/connection are all destroyed
   *
   * Note that with bfcacheInParent, we may reuse a JSWindowActor pair after closing all connections.
   * This is can happen outside of the destruction of the actor.
   * We may reuse a DevToolsFrameParent and DevToolsFrameChild pair.
   * When navigating away, we will destroy them and call this method.
   * Then when navigating back, we will reuse the same instances.
   * So that we should be careful to keep the class fully function and only clear all its state.
   *
   * @param {object} options
   * @param {boolean} options.isModeSwitching
   *        true when this is called as the result of a change to the devtools.browsertoolbox.scope pref
   */
  _closeAllConnections(options) {
    for (const { actor, watcher } of this._connections.values()) {
      watcher.notifyTargetDestroyed(actor, options);
      this._unregisterWatcher(watcher.conn.prefix);
    }
    this._connections.clear();
  }

  /**
   * Supported Queries
   */

  sendPacket(packet, prefix) {
    this.sendAsyncMessage("DevToolsFrameParent:packet", { packet, prefix });
  }

  /**
   * JsWindowActor API
   */

  receiveMessage(message) {
    switch (message.name) {
      case "DevToolsFrameChild:connectFromContent":
        return this.connectFromContent(message.data);
      case "DevToolsFrameChild:packet":
        return this.emit("packet-received", message);
      case "DevToolsFrameChild:destroy":
        for (const { form, watcherActorID } of message.data.actors) {
          const watcher = WatcherRegistry.getWatcher(watcherActorID);
          // As we instruct to destroy all targets when the watcher is destroyed,
          // we may easily receive the target destruction notification *after*
          // the watcher has been removed from the registry.
          if (watcher) {
            watcher.notifyTargetDestroyed(form, message.data.options);
            this._unregisterWatcher(watcher.conn.prefix);
          }
        }
        return null;
      case "DevToolsFrameChild:bf-cache-navigation-pageshow":
        for (const watcherActor of WatcherRegistry.getWatchersForBrowserId(
          this.browsingContext.browserId
        )) {
          watcherActor.emit("bf-cache-navigation-pageshow", {
            windowGlobal: this.browsingContext.currentWindowGlobal,
          });
        }
        return null;
      case "DevToolsFrameChild:bf-cache-navigation-pagehide":
        for (const watcherActor of WatcherRegistry.getWatchersForBrowserId(
          this.browsingContext.browserId
        )) {
          watcherActor.emit("bf-cache-navigation-pagehide", {
            windowGlobal: this.browsingContext.currentWindowGlobal,
          });
        }
        return null;
      default:
        throw new Error(
          "Unsupported message in DevToolsFrameParent: " + message.name
        );
    }
  }

  didDestroy() {
    this._closeAllConnections();
  }
}
