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

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  clearTimeout: "resource://gre/modules/Timer.sys.mjs",
  setTimeout: "resource://gre/modules/Timer.sys.mjs",

  error: "chrome://remote/content/shared/messagehandler/Errors.sys.mjs",
  Log: "chrome://remote/content/shared/Log.sys.mjs",
  RootMessageHandlerRegistry:
    "chrome://remote/content/shared/messagehandler/RootMessageHandlerRegistry.sys.mjs",
  WindowGlobalMessageHandler:
    "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs",
});

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

ChromeUtils.defineLazyGetter(lazy, "WebDriverError", () => {
  return ChromeUtils.importESModule(
    "chrome://remote/content/shared/webdriver/Errors.sys.mjs"
  ).error.WebDriverError;
});

// Set the timeout delay before a command is considered as potentially timing
// out. This can be customized by a preference mostly for tests. Regular
// implementation should use DEFAULT_COMMAND_DELAY;
const DEFAULT_COMMAND_DELAY = 10000;
const PREF_REMOTE_COMMAND_DELAY = "remote.messagehandler.test.command.delay";

ChromeUtils.defineLazyGetter(lazy, "commandDelay", () =>
  Services.prefs.getIntPref(PREF_REMOTE_COMMAND_DELAY, DEFAULT_COMMAND_DELAY)
);

const PING_DELAY = 1000;
const PING_TIMEOUT = Symbol();

/**
 * Parent actor for the MessageHandlerFrame JSWindowActor. The
 * MessageHandlerFrame actor is used by RootTransport to communicate between
 * ROOT MessageHandlers and WINDOW_GLOBAL MessageHandlers.
 */
export class MessageHandlerFrameParent extends JSWindowActorParent {
  #destroyed;

  constructor() {
    super();
    this.#destroyed = false;
  }

  didDestroy() {
    this.#destroyed = true;
  }

  async receiveMessage(message) {
    switch (message.name) {
      case "MessageHandlerFrameChild:sendCommand": {
        return this.#handleSendCommandMessage(message.data);
      }
      case "MessageHandlerFrameChild:messageHandlerEvent": {
        return this.#handleMessageHandlerEventMessage(message.data);
      }
      default:
        throw new Error("Unsupported message:" + message.name);
    }
  }

  /**
   * Send a command to the corresponding MessageHandlerFrameChild actor via a
   * JSWindowActor query.
   *
   * @param {Command} command
   *     The command to forward. See type definition in MessageHandler.js
   * @param {string} sessionId
   *     ID of the session that sent the command.
   * @returns {Promise}
   *     Promise that will resolve with the result of query sent to the
   *     MessageHandlerFrameChild actor.
   */
  async sendCommand(command, sessionId) {
    const timer = lazy.setTimeout(
      () => this.#sendPing(command),
      lazy.commandDelay
    );

    const result = await this.sendQuery(
      "MessageHandlerFrameParent:sendCommand",
      {
        command,
        sessionId,
      }
    );

    lazy.clearTimeout(timer);

    if (result?.error) {
      if (result.isMessageHandlerError) {
        throw lazy.error.MessageHandlerError.fromJSON(result.error);
      }

      // TODO: Do not assume WebDriver is the session protocol, see Bug 1779026.
      throw lazy.WebDriverError.fromJSON(result.error);
    }

    return result;
  }

  async #handleMessageHandlerEventMessage(messageData) {
    const { data, name, relatedContexts, sessionId } = messageData;
    const [moduleName] = name.split(".");

    // Re-emit the event on the RootMessageHandler.
    const messageHandler =
      lazy.RootMessageHandlerRegistry.getExistingMessageHandler(sessionId);

    if (!messageHandler) {
      // If there is no message handler for the provided session id, this is a
      // late event for an already destroyed session, bail out.
      // Bug 1730913: A trace could be added here once Bug 1730913 is resolved.
      // Until that, this would lead to too much log pollution, because content
      // process modules will not be destroyed until the corresponding window
      // is destroyed (either closed or navigates).
      return;
    }

    // TODO: getModuleInstance expects a CommandDestination in theory,
    // but only uses the MessageHandler type in practice, see Bug 1776389.
    const module = messageHandler.moduleCache.getModuleInstance(moduleName, {
      type: lazy.WindowGlobalMessageHandler.type,
    });
    let eventPayload = data;

    // Modify an event payload if there is a special method in the targeted module.
    // If present it can be found in windowglobal-in-root module.
    if (module?.interceptEvent) {
      eventPayload = await module.interceptEvent(name, data);

      if (eventPayload === null) {
        lazy.logger.trace(
          `${moduleName}.interceptEvent returned null, skipping event: ${name}, data: ${data}`
        );
        return;
      }
      // Make sure that an event payload is returned.
      if (!eventPayload) {
        throw new Error(
          `${moduleName}.interceptEvent doesn't return the event payload`
        );
      }
    }
    messageHandler.emitEvent(name, eventPayload, relatedContexts);
  }

  async #handleSendCommandMessage(messageData) {
    const { sessionId, command } = messageData;

    // Flag the command as coming from windowglobal modules / content processes.
    command.fromContentProcess = true;

    const messageHandler =
      lazy.RootMessageHandlerRegistry.getExistingMessageHandler(sessionId);
    try {
      return await messageHandler.handleCommand(command);
    } catch (e) {
      if (e?.isRemoteError) {
        return {
          error: e.toJSON(),
          isMessageHandlerError: e.isMessageHandlerError,
        };
      }
      throw e;
    }
  }

  async #sendPing(command) {
    const commandName = `${command.moduleName}.${command.commandName}`;
    const destination = command.destination.id;

    if (this.#destroyed) {
      // If the JSWindowActor was destroyed already, no need to send a ping.
      return;
    }

    lazy.logger.trace(
      `MessageHandlerFrameParent command ${commandName} to ${destination} ` +
        `takes more than ${lazy.commandDelay / 1000} seconds to resolve, sending ping`
    );

    try {
      const result = await Promise.race([
        this.sendQuery("MessageHandlerFrameParent:sendPing"),
        new Promise(r => lazy.setTimeout(() => r(PING_TIMEOUT), PING_DELAY)),
      ]);

      if (result === PING_TIMEOUT) {
        lazy.logger.warn(
          `MessageHandlerFrameParent ping for command ${commandName} to ${destination} timed out`
        );
      } else {
        lazy.logger.trace(
          `MessageHandlerFrameParent ping for command ${commandName} to ${destination} was successful`
        );
      }
    } catch (e) {
      if (!this.#destroyed) {
        // Only swallow errors if the JSWindowActor pair was destroyed while
        // waiting for the ping response.
        throw e;
      }

      lazy.logger.trace(
        `MessageHandlerFrameParent ping for command ${commandName} to ${destination}` +
          ` lost after JSWindowActor was destroyed`
      );
    }
  }
}
