/* 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 { Domain } from "chrome://remote/content/cdp/domains/Domain.sys.mjs";

const lazy = {};

ChromeUtils.defineESModuleGetters(lazy, {
  NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
});

const MAX_WINDOW_SIZE = 10000000;

export class Emulation extends Domain {
  destructor() {
    this.setUserAgentOverride({ userAgent: "", platform: "" });

    super.destructor();
  }

  /**
   * Overrides the values of device screen dimensions.
   *
   * Values as modified are:
   *   - window.screen.width
   *   - window.screen.height
   *   - window.innerWidth
   *   - window.innerHeight
   *   - "device-width"/"device-height"-related CSS media query results
   *
   * @param {object} options
   * @param {number} options.width
   *     Overriding width value in pixels. 0 disables the override.
   * @param {number} options.height
   *     Overriding height value in pixels. 0 disables the override.
   * @param {number} options.deviceScaleFactor
   *     Overriding device scale factor value. 0 disables the override.
   * @param {number} options.mobile [not supported]
   *     Whether to emulate a mobile device. This includes viewport meta tag,
   *     overlay scrollbars, text autosizing and more.
   * @param {number} options.screenOrientation
   *     Screen orientation override [not supported]
   */
  async setDeviceMetricsOverride(options = {}) {
    const { width, height, deviceScaleFactor } = options;

    if (
      width < 0 ||
      width > MAX_WINDOW_SIZE ||
      height < 0 ||
      height > MAX_WINDOW_SIZE
    ) {
      throw new TypeError(
        `Width and height values must be positive, not greater than ${MAX_WINDOW_SIZE}`
      );
    }

    if (typeof deviceScaleFactor != "number") {
      throw new TypeError("deviceScaleFactor: number expected");
    }

    if (deviceScaleFactor < 0) {
      throw new TypeError("deviceScaleFactor: must be positive");
    }

    const { tab } = this.session.target;
    const { linkedBrowser: browser } = tab;

    const { browsingContext } = this.session.target;
    browsingContext.overrideDPPX = deviceScaleFactor;

    // With a value of 0 the current size is used
    const { layoutViewport } = await this.session.execute(
      this.session.id,
      "Page",
      "getLayoutMetrics"
    );

    const targetWidth = width > 0 ? width : layoutViewport.clientWidth;
    const targetHeight = height > 0 ? height : layoutViewport.clientHeight;

    browser.style.setProperty("min-width", targetWidth + "px");
    browser.style.setProperty("max-width", targetWidth + "px");
    browser.style.setProperty("min-height", targetHeight + "px");
    browser.style.setProperty("max-height", targetHeight + "px");

    browser.ownerDocument.synchronouslyUpdateRemoteBrowserDimensions(
      /* aIncludeInactive = */ true
    );

    // Wait until the viewport has been resized
    await this.executeInChild("_awaitViewportDimensions", {
      width: targetWidth,
      height: targetHeight,
    });
  }

  /**
   * Enables touch on platforms which do not support them.
   *
   * @param {object} options
   * @param {boolean} options.enabled
   *     Whether the touch event emulation should be enabled.
   * @param {number=} options.maxTouchPoints [not yet supported]
   *     Maximum touch points supported. Defaults to one.
   */
  async setTouchEmulationEnabled(options = {}) {
    const { enabled } = options;

    if (typeof enabled != "boolean") {
      throw new TypeError(
        "Invalid parameters (enabled: boolean value expected)"
      );
    }

    const { browsingContext } = this.session.target;
    if (enabled) {
      browsingContext.touchEventsOverride = "enabled";
    } else {
      browsingContext.touchEventsOverride = "none";
    }
  }

  /**
   * Allows overriding user agent with the given string.
   *
   * @param {object} options
   * @param {string} options.userAgent
   *     User agent to use.
   * @param {string=} options.acceptLanguage [not yet supported]
   *     Browser langugage to emulate.
   * @param {string=} options.platform
   *     The platform navigator.platform should return.
   */
  async setUserAgentOverride(options = {}) {
    const { userAgent, platform } = options;

    if (typeof userAgent != "string") {
      throw new TypeError(
        "Invalid parameters (userAgent: string value expected)"
      );
    }

    if (!["undefined", "string"].includes(typeof platform)) {
      throw new TypeError("platform: string value expected");
    }

    const { browsingContext } = this.session.target;

    if (!userAgent.length) {
      browsingContext.customUserAgent = null;
    } else if (this._isValidHTTPRequestHeaderValue(userAgent)) {
      browsingContext.customUserAgent = userAgent;
    } else {
      throw new TypeError("Invalid characters found in userAgent");
    }

    if (platform?.length > 0) {
      browsingContext.customPlatform = platform;
    } else {
      browsingContext.customPlatform = null;
    }
  }

  _isValidHTTPRequestHeaderValue(value) {
    try {
      const channel = lazy.NetUtil.newChannel({
        uri: "http://localhost",
        loadUsingSystemPrincipal: true,
      });
      channel.QueryInterface(Ci.nsIHttpChannel);
      channel.setRequestHeader("X-check", value, false);
      return true;
    } catch (e) {
      return false;
    }
  }
}
