/* 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, {
  addDebuggerToGlobal: "resource://gre/modules/jsdebugger.sys.mjs",

  generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
});

ChromeUtils.defineLazyGetter(lazy, "dbg", () => {
  // eslint-disable-next-line mozilla/reject-globalThis-modification
  lazy.addDebuggerToGlobal(globalThis);
  return new Debugger();
});

/**
 * @typedef {string} RealmType
 */

/**
 * Enum of realm types.
 *
 * @readonly
 * @enum {RealmType}
 */
export const RealmType = {
  AudioWorklet: "audio-worklet",
  DedicatedWorker: "dedicated-worker",
  PaintWorklet: "paint-worklet",
  ServiceWorker: "service-worker",
  SharedWorker: "shared-worker",
  Window: "window",
  Worker: "worker",
  Worklet: "worklet",
};

/**
 * Base class that wraps any kind of WebDriver BiDi realm.
 */
export class Realm {
  #handleObjectMap;
  #id;

  constructor() {
    this.#id = lazy.generateUUID();

    // Map of unique handles (UUIDs) to objects belonging to this realm.
    this.#handleObjectMap = new Map();
  }

  destroy() {
    this.#handleObjectMap = null;
  }

  /**
   * Get the browsing context of the realm instance.
   */
  get browsingContext() {
    return null;
  }

  /**
   * Get the unique identifier of the realm instance.
   *
   * @returns {string} The unique identifier.
   */
  get id() {
    return this.#id;
  }

  /**
   * A getter to get a realm origin.
   *
   * It's required to be implemented in the sub class.
   */
  get origin() {
    throw new Error("Not implemented");
  }

  /**
   * Ensure the provided object can be used within this realm.

   * @param {object} obj
   *     Any non-primitive object.

   * @returns {object}
   *     An object usable in the current realm.
   */
  cloneIntoRealm(obj) {
    return obj;
  }

  /**
   * Remove the reference corresponding to the provided unique handle.
   *
   * @param {string} handle
   *     The unique handle of an object reference tracked in this realm.
   */
  removeObjectHandle(handle) {
    this.#handleObjectMap.delete(handle);
  }

  /**
   * Get a new unique handle for the provided object, creating a strong
   * reference on the object.
   *
   * @param {object} object
   *     Any non-primitive object.
   * @returns {string} The unique handle created for this strong reference.
   */
  getHandleForObject(object) {
    const handle = lazy.generateUUID();
    this.#handleObjectMap.set(handle, object);
    return handle;
  }

  /**
   * Get the basic realm information.
   *
   * @returns {BaseRealmInfo}
   */
  getInfo() {
    return {
      realm: this.#id,
      origin: this.origin,
    };
  }

  /**
   * Retrieve the object corresponding to the provided unique handle.
   *
   * @param {string} handle
   *     The unique handle of an object reference tracked in this realm.
   * @returns {object} object
   *     Any non-primitive object.
   */
  getObjectForHandle(handle) {
    return this.#handleObjectMap.get(handle);
  }
}

/**
 * Wrapper for Window realms including sandbox objects.
 */
export class WindowRealm extends Realm {
  #realmAutomationFeaturesEnabled;
  #globalObject;
  #globalObjectReference;
  #isSandbox;
  #sandboxName;
  #userActivationEnabled;
  #window;

  static type = RealmType.Window;

  /**
   *
   * @param {Window} window
   *     The window global to wrap.
   * @param {object} options
   * @param {string=} options.sandboxName
   *     Name of the sandbox to create if specified. Defaults to `null`.
   */
  constructor(window, options = {}) {
    const { sandboxName = null } = options;

    super();

    this.#isSandbox = sandboxName !== null;
    this.#sandboxName = sandboxName;
    this.#window = window;
    this.#globalObject = this.#isSandbox ? this.#createSandbox() : this.#window;
    this.#globalObjectReference = lazy.dbg.makeGlobalObjectReference(
      this.#globalObject
    );
    this.#realmAutomationFeaturesEnabled = false;
    this.#userActivationEnabled = false;
  }

  destroy() {
    if (this.#realmAutomationFeaturesEnabled) {
      lazy.dbg.disableAsyncStack(this.#globalObject);
      lazy.dbg.disableUnlimitedStacksCapturing(this.#globalObject);
      this.#realmAutomationFeaturesEnabled = false;
    }

    this.#globalObjectReference = null;
    this.#globalObject = null;
    this.#window = null;

    super.destroy();
  }

  get browsingContext() {
    return this.#window.browsingContext;
  }

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

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

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

  get origin() {
    return this.#window.origin;
  }

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

  set userActivationEnabled(enable) {
    if (enable === this.#userActivationEnabled) {
      return;
    }

    const document = this.#window.document;
    if (enable) {
      document.notifyUserGestureActivation();
    } else {
      document.clearUserGestureActivation();
    }

    this.#userActivationEnabled = enable;
  }

  #createDebuggerObject(obj) {
    return this.#globalObjectReference.makeDebuggeeValue(obj);
  }

  #createSandbox() {
    const win = this.#window;
    const opts = {
      sameZoneAs: win,
      sandboxPrototype: win,
      wantComponents: false,
      wantXrays: true,
    };

    return new Cu.Sandbox(win, opts);
  }

  #enableRealmAutomationFeatures() {
    if (!this.#realmAutomationFeaturesEnabled) {
      lazy.dbg.enableAsyncStack(this.#globalObject);
      lazy.dbg.enableUnlimitedStacksCapturing(this.#globalObject);
      this.#realmAutomationFeaturesEnabled = true;
    }
  }

  /**
   * Clone the provided object into the scope of this Realm (either a window
   * global, or a sandbox).
   *
   * @param {object} obj
   *     Any non-primitive object.
   *
   * @returns {object}
   *     The cloned object.
   */
  cloneIntoRealm(obj) {
    return Cu.cloneInto(obj, this.#globalObject, { cloneFunctions: true });
  }

  /**
   * Evaluates a provided expression in the context of the current realm.
   *
   * @param {string} expression
   *     The expression to evaluate.
   * @param {DebuggerFrame=} pausedDebuggerFrame
   *     The current paused debugger frame to use for the evaluation (optional).
   *
   * @returns {object}
   *     - evaluationStatus {EvaluationStatus} One of "normal", "throw".
   *     - exceptionDetails {ExceptionDetails=} the details of the exception if
   *       the evaluation status was "throw".
   *     - result {RemoteValue=} the result of the evaluation serialized as a
   *       RemoteValue if the evaluation status was "normal".
   */
  executeInRealm(expression, pausedDebuggerFrame) {
    this.#enableRealmAutomationFeatures();

    if (pausedDebuggerFrame) {
      return pausedDebuggerFrame.evalWithBindings(
        expression,
        {},
        {
          bypassCSP: true,
          url: this.#window.document.baseURI,
        }
      );
    }

    return this.#globalObjectReference.executeInGlobal(expression, {
      bypassCSP: true,
      url: this.#window.document.baseURI,
    });
  }

  /**
   * Call a function in the context of the current realm.
   *
   * @param {string} functionDeclaration
   *     The body of the function to call.
   * @param {Array<object>} functionArguments
   *     The arguments to pass to the function call.
   * @param {object} thisParameter
   *     The value of the `this` keyword for the function call.
   * @param {DebuggerFrame=} pausedDebuggerFrame
   *     The current paused debugger frame to use for the evaluation (optional).
   *
   * @returns {object}
   *     - evaluationStatus {EvaluationStatus} One of "normal", "throw".
   *     - exceptionDetails {ExceptionDetails=} the details of the exception if
   *       the evaluation status was "throw".
   *     - result {RemoteValue=} the result of the evaluation serialized as a
   *       RemoteValue if the evaluation status was "normal".
   */
  executeInRealmWithBindings(
    functionDeclaration,
    functionArguments,
    thisParameter,
    pausedDebuggerFrame
  ) {
    this.#enableRealmAutomationFeatures();
    const expression = `(${functionDeclaration}).apply(__bidi_this, __bidi_args)`;

    const args = this.cloneIntoRealm([]);
    for (const arg of functionArguments) {
      args.push(arg);
    }

    const bindings = {
      __bidi_args: this.#createDebuggerObject(args),
      __bidi_this: this.#createDebuggerObject(thisParameter),
    };

    if (pausedDebuggerFrame) {
      return pausedDebuggerFrame.evalWithBindings(expression, bindings, {
        bypassCSP: true,
        url: this.#window.document.baseURI,
      });
    }

    return this.#globalObjectReference.executeInGlobalWithBindings(
      expression,
      bindings,
      {
        bypassCSP: true,
        url: this.#window.document.baseURI,
      }
    );
  }

  /**
   * Get the realm information.
   *
   * @returns {object}
   *     - context {BrowsingContext} The browsing context, associated with the realm.
   *     - id {string} The realm unique identifier.
   *     - origin {string} The serialization of an origin.
   *     - sandbox {string=} The name of the sandbox.
   *     - type {RealmType.Window} The window realm type.
   */
  getInfo() {
    const baseInfo = super.getInfo();
    const info = {
      ...baseInfo,
      context: this.#window.browsingContext,
      type: WindowRealm.type,
    };

    if (this.#isSandbox) {
      info.sandbox = this.#sandboxName;
    }

    return info;
  }

  /**
   * Log an error caused by a script evaluation.
   *
   * @param {string} message
   *     The error message.
   * @param {Stack} stack
   *     The JavaScript stack trace.
   */
  reportError(message, stack) {
    const { column, line } = stack;

    const scriptErrorClass = Cc["@mozilla.org/scripterror;1"];
    const scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError);

    scriptError.initWithWindowID(
      message,
      this.#window.document.baseURI,
      line,
      column,
      Ci.nsIScriptError.errorFlag,
      "content javascript",
      this.#window.windowGlobalChild.innerWindowId
    );
    Services.console.logMessage(scriptError);
  }
}
