/* 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 { WindowGlobalBiDiModule } from "chrome://remote/content/webdriver-bidi/modules/WindowGlobalBiDiModule.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  ContextDescriptorType:
    "chrome://remote/content/shared/messagehandler/MessageHandler.sys.mjs",
  RootMessageHandler:
    "chrome://remote/content/shared/messagehandler/RootMessageHandler.sys.mjs",
  SessionDataCategory:
    "chrome://remote/content/shared/messagehandler/sessiondata/SessionData.sys.mjs",
  WindowGlobalMessageHandler:
    "chrome://remote/content/shared/messagehandler/WindowGlobalMessageHandler.sys.mjs",
});

/**
 * Internal module to set the configuration on the newly created navigables.
 */
class _ConfigurationModule extends WindowGlobalBiDiModule {
  #geolocationConfiguration;
  #preloadScripts;
  #resolveBlockerPromise;

  constructor(messageHandler) {
    super(messageHandler);

    this.#geolocationConfiguration = undefined;
    this.#preloadScripts = new Set();

    Services.obs.addObserver(this, "content-document-global-created");
  }

  destroy() {
    // Unblock the document parsing.
    if (this.#resolveBlockerPromise) {
      this.#resolveBlockerPromise();
    }

    Services.obs.removeObserver(this, "content-document-global-created");

    this.#preloadScripts = null;
    this.#geolocationConfiguration = undefined;
  }

  async observe(subject, topic) {
    if (topic === "content-document-global-created") {
      const window = subject;
      // Ignore events without a window or with the wrong innerWindowId.
      if (
        window !== this.messageHandler.window ||
        window.windowGlobalChild?.innerWindowId !==
          this.messageHandler.innerWindowId
      ) {
        return;
      }

      // Do nothing if there is no configuration to apply.
      if (
        this.#preloadScripts.size === 0 &&
        this.#geolocationConfiguration === undefined
      ) {
        this.#onConfigurationComplete(window);
        return;
      }

      // Block document parsing.
      const blockerPromise = new Promise(resolve => {
        this.#resolveBlockerPromise = resolve;
      });
      window.document.blockParsing(blockerPromise);

      if (this.#geolocationConfiguration !== undefined) {
        await this.messageHandler.handleCommand({
          moduleName: "emulation",
          commandName: "_setGeolocationOverride",
          destination: {
            type: lazy.WindowGlobalMessageHandler.type,
            id: this.messageHandler.context.id,
          },
          params: {
            coordinates: this.#geolocationConfiguration,
          },
        });
      }

      if (this.#preloadScripts.size !== 0) {
        await this.messageHandler.handleCommand({
          moduleName: "script",
          commandName: "_evaluatePreloadScripts",
          destination: {
            type: lazy.WindowGlobalMessageHandler.type,
            id: this.messageHandler.context.id,
          },
          params: {
            scripts: this.#preloadScripts,
          },
        });
      }

      // Continue script parsing.
      this.#resolveBlockerPromise();
      this.#onConfigurationComplete(window);
    }
  }

  async #onConfigurationComplete(window) {
    // parser blocking doesn't work for initial about:blank, so ensure
    // browsing_context.create waits for configuration to complete
    if (window.document.isInitialDocument) {
      await this.messageHandler.forwardCommand({
        moduleName: "browsingContext",
        commandName: "_onConfigurationComplete",
        destination: {
          type: lazy.RootMessageHandler.type,
        },
        params: {
          navigable: this.messageHandler.context,
        },
      });
    }
  }

  #updatePreloadScripts(sessionData) {
    this.#preloadScripts.clear();

    for (const { contextDescriptor, value } of sessionData) {
      if (!this.messageHandler.matchesContext(contextDescriptor)) {
        continue;
      }

      this.#preloadScripts.add(value);
    }
  }

  /**
   * Internal commands
   */

  _applySessionData(params) {
    const { category, sessionData } = params;

    if (category === lazy.SessionDataCategory.PreloadScript) {
      this.#updatePreloadScripts(sessionData);
    }

    // The geolocation override applies only to top-level traversables.
    if (
      category === lazy.SessionDataCategory.GeolocationOverride &&
      !this.messageHandler.context.parent
    ) {
      let geolocationOverridePerContext = null;
      let geolocationOverridePerUserContext = null;

      for (const { contextDescriptor, value } of sessionData) {
        if (!this.messageHandler.matchesContext(contextDescriptor)) {
          continue;
        }

        switch (contextDescriptor.type) {
          case lazy.ContextDescriptorType.TopBrowsingContext: {
            geolocationOverridePerContext = value;
            break;
          }
          case lazy.ContextDescriptorType.UserContext: {
            geolocationOverridePerUserContext = value;
            break;
          }
        }
      }

      // For the geolocation emulations on the previous step, we found session items
      // that would apply an override for a browsing context,a user context, and in some cases globally.
      // Now from these items we have to choose the one that would take precedence.
      // The order is the user context item overrides the global one, and the browsing context overrides the user context item.
      if (
        typeof geolocationOverridePerContext === "object" &&
        geolocationOverridePerContext !== null
      ) {
        this.#geolocationConfiguration = geolocationOverridePerContext;
      } else if (
        typeof geolocationOverridePerUserContext === "object" &&
        geolocationOverridePerUserContext !== null
      ) {
        this.#geolocationConfiguration = geolocationOverridePerUserContext;
      }
    }
  }
}

export const _configuration = _ConfigurationModule;
