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

/* global global, postMessage */

/*
 * Worker debugger script that listens for requests to start a `DevToolsServer` for a
 * worker in a process.  Loaded into a specific worker during worker-connector.js'
 * `connectToWorker` which is called from the same process as the worker.
 */

// This function is used to do remote procedure calls from the worker to the
// main thread. It is exposed as a built-in global to every module by the
// worker loader. To make sure the worker loader can access it, it needs to be
// defined before loading the worker loader script below.
let nextId = 0;
const workerGlobal = globalThis;

workerGlobal.rpc = function (method, ...params) {
  return new Promise((resolve, reject) => {
    const id = nextId++;
    workerGlobal.addEventListener("message", function onMessageForRpc(event) {
      const packet = JSON.parse(event.data);
      if (packet.type !== "rpc" || packet.id !== id) {
        return;
      }
      if (packet.error) {
        reject(packet.error);
      } else {
        resolve(packet.result);
      }
      workerGlobal.removeEventListener("message", onMessageForRpc);
    });

    postMessage(
      JSON.stringify({
        type: "rpc",
        method,
        params,
        id,
      })
    );
  });
};

const { worker } = ChromeUtils.importESModule(
  "resource://devtools/shared/loader/worker-loader.sys.mjs",
  { global: "current" }
);

const { WorkerTargetActor } = worker.require(
  "resource://devtools/server/actors/targets/worker.js"
);
const { DevToolsServer } = worker.require(
  "resource://devtools/server/devtools-server.js"
);

DevToolsServer.createRootActor = function () {
  throw new Error("Should never get here!");
};

// This file is only instanciated once for a given WorkerDebugger, which means that
// multiple toolbox could end up using the same instance of this script. In order to handle
// that, we handle a Map of the different connections, keyed by forwarding prefix.
const connections = new Map();

export async function handleDevToolsPacket(packet) {
  switch (packet.type) {
    case "connect": {
      const { forwardingPrefix } = packet;

      // Force initializing the server each time on connect
      // as it may have been destroyed by a previous, now closed toolbox.
      // Once the last connection drops, the server auto destroy itself.
      DevToolsServer.init();

      // Step 3: Create a connection to the parent.
      const connection = DevToolsServer.connectToParent(
        forwardingPrefix,
        workerGlobal
      );

      // Step 4: Create a WorkerTarget actor.
      const workerTargetActor = new WorkerTargetActor(
        connection,
        global,
        packet.workerDebuggerData,
        packet.options.sessionContext
      );
      // Make the worker manage itself so it is put in a Pool and assigned an actorID.
      workerTargetActor.manage(workerTargetActor);

      // Step 5: Send a response packet to the parent to notify
      // it that a connection has been established.
      connections.set(forwardingPrefix, {
        connection,
        workerTargetActor,
      });

      // Immediately notify about the target actor form,
      // so that we can emit RDP events from the target actor
      // and have them correctly routed up to the frontend.
      // The target front has to be first created by receiving its form
      // before being able to receive RDP events.
      postMessage(
        JSON.stringify({
          type: "connected",
          forwardingPrefix,
          workerTargetForm: workerTargetActor.form(),
        })
      );

      // We might receive data to watch.
      if (packet.options.sessionData) {
        const promises = [];
        for (const [type, entries] of Object.entries(
          packet.options.sessionData
        )) {
          promises.push(
            workerTargetActor.addOrSetSessionDataEntry(
              type,
              entries,
              false,
              "set"
            )
          );
        }
        await Promise.all(promises);
      }

      // Finally, notify when we are done processing session data
      // We are processing breakpoints, which means we can release the execution of the worker
      // from the main thread via `WorkerDebugger.setDebuggerReady(true)`
      postMessage(JSON.stringify({ type: "session-data-processed" }));

      break;
    }

    case "add-or-set-session-data-entry":
      await connections
        .get(packet.forwardingPrefix)
        .workerTargetActor.addOrSetSessionDataEntry(
          packet.dataEntryType,
          packet.entries,
          packet.updateType
        );
      postMessage(JSON.stringify({ type: "session-data-entry-added-or-set" }));
      break;

    case "remove-session-data-entry":
      await connections
        .get(packet.forwardingPrefix)
        .workerTargetActor.removeSessionDataEntry(
          packet.dataEntryType,
          packet.entries
        );
      break;

    case "disconnect":
      // This will destroy the associate WorkerTargetActor (and the actors it manages).
      if (connections.has(packet.forwardingPrefix)) {
        connections.get(packet.forwardingPrefix).connection.close();
        connections.delete(packet.forwardingPrefix);
      }
      break;
  }
}
