/* 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, {
  assert: "chrome://remote/content/shared/webdriver/Assert.sys.mjs",
  dom: "chrome://remote/content/shared/DOM.sys.mjs",
  error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
  generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
  Log: "chrome://remote/content/shared/Log.sys.mjs",
  pprint: "chrome://remote/content/shared/Format.sys.mjs",
});

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

/**
 * @typedef {object} IncludeShadowTreeMode
 */

/**
 * Enum of include shadow tree modes supported by the serialization.
 *
 * @readonly
 * @enum {IncludeShadowTreeMode}
 */
export const IncludeShadowTreeMode = {
  All: "all",
  None: "none",
  Open: "open",
};

/**
 * @typedef {object} OwnershipModel
 */

/**
 * Enum of ownership models supported by the serialization.
 *
 * @readonly
 * @enum {OwnershipModel}
 */
export const OwnershipModel = {
  None: "none",
  Root: "root",
};

/**
 * Extra options for deserializing remote values.
 *
 * @typedef {object} ExtraDeserializationOptions
 *
 * @property {NodeCache=} nodeCache
 *     The cache containing DOM node references.
 * @property {Function=} emitScriptMessage
 *     The function to emit "script.message" event.
 */

/**
 * Extra options for serializing remote values.
 *
 * @typedef {object} ExtraSerializationOptions
 *
 * @property {NodeCache=} nodeCache
 *     The cache containing DOM node references.
 * @property {Map<BrowsingContext, Array<string>>} seenNodeIds
 *     Map of browsing contexts to their seen node ids during the current
 *     serialization.
 */

/**
 * An object which holds the information of how
 * ECMAScript objects should be serialized.
 *
 * @typedef {object} SerializationOptions
 *
 * @property {number} [maxDomDepth=0]
 *     Depth of a serialization of DOM Nodes. Defaults to 0.
 * @property {number} [maxObjectDepth=null]
 *     Depth of a serialization of objects. Defaults to null.
 * @property {IncludeShadowTreeMode} [includeShadowTree=IncludeShadowTreeMode.None]
 *     Mode of a serialization of shadow dom. Defaults to "none".
 */

const TYPED_ARRAY_CLASSES = [
  "Uint8Array",
  "Uint8ClampedArray",
  "Uint16Array",
  "Uint32Array",
  "Int8Array",
  "Int16Array",
  "Int32Array",
  "Float32Array",
  "Float64Array",
  "BigInt64Array",
  "BigUint64Array",
];

/**
 * Build the serialized RemoteValue.
 *
 * @returns {object}
 *     An object with a mandatory `type` property, and optional `handle`,
 *     depending on the OwnershipModel, used for the serialization and
 *     on the value's type.
 */
function buildSerialized(type, handle = null) {
  const serialized = { type };

  if (handle !== null) {
    serialized.handle = handle;
  }

  return serialized;
}

/**
 * Helper to deserialize value list.
 *
 * @see https://w3c.github.io/webdriver-bidi/#deserialize-value-list
 *
 * @param {Array} serializedValueList
 *     List of serialized values.
 * @param {Realm} realm
 *     The Realm in which the value is deserialized.
 * @param {ExtraDeserializationOptions} extraOptions
 *     Extra Remote Value deserialization options.
 *
 * @returns {Array} List of deserialized values.
 *
 * @throws {InvalidArgumentError}
 *     If <var>serializedValueList</var> is not an array.
 */
function deserializeValueList(serializedValueList, realm, extraOptions) {
  lazy.assert.array(
    serializedValueList,
    lazy.pprint`Expected "serializedValueList" to be an array, got ${serializedValueList}`
  );

  const deserializedValues = [];

  for (const item of serializedValueList) {
    deserializedValues.push(deserialize(item, realm, extraOptions));
  }

  return deserializedValues;
}

/**
 * Helper to deserialize key-value list.
 *
 * @see https://w3c.github.io/webdriver-bidi/#deserialize-key-value-list
 *
 * @param {Array} serializedKeyValueList
 *     List of serialized key-value.
 * @param {Realm} realm
 *     The Realm in which the value is deserialized.
 * @param {ExtraDeserializationOptions} extraOptions
 *     Extra Remote Value deserialization options.
 *
 * @returns {Array} List of deserialized key-value.
 *
 * @throws {InvalidArgumentError}
 *     If <var>serializedKeyValueList</var> is not an array or
 *     not an array of key-value arrays.
 */
function deserializeKeyValueList(serializedKeyValueList, realm, extraOptions) {
  lazy.assert.array(
    serializedKeyValueList,
    lazy.pprint`Expected "serializedKeyValueList" to be an array, got ${serializedKeyValueList}`
  );

  const deserializedKeyValueList = [];

  for (const serializedKeyValue of serializedKeyValueList) {
    if (!Array.isArray(serializedKeyValue) || serializedKeyValue.length != 2) {
      throw new lazy.error.InvalidArgumentError(
        `Expected key-value pair to be an array with 2 elements, got ${serializedKeyValue}`
      );
    }
    const [serializedKey, serializedValue] = serializedKeyValue;
    const deserializedKey =
      typeof serializedKey == "string"
        ? serializedKey
        : deserialize(serializedKey, realm, extraOptions);
    const deserializedValue = deserialize(serializedValue, realm, extraOptions);

    deserializedKeyValueList.push([deserializedKey, deserializedValue]);
  }

  return deserializedKeyValueList;
}

/**
 * Deserialize a Node as referenced by the shared unique reference.
 *
 * This unique reference can be shared by WebDriver clients with the WebDriver
 * classic implementation (Marionette) if the reference is for an Element or
 * ShadowRoot.
 *
 * @param {string} sharedRef
 *     Shared unique reference of the Node.
 * @param {Realm} realm
 *     The Realm in which the value is deserialized.
 * @param {ExtraDeserializationOptions} extraOptions
 *     Extra Remote Value deserialization options.
 *
 * @returns {Node} The deserialized DOM node.
 */
function deserializeSharedReference(sharedRef, realm, extraOptions) {
  const { nodeCache } = extraOptions;

  const browsingContext = realm.browsingContext;
  if (!browsingContext) {
    throw new lazy.error.NoSuchNodeError("Realm isn't a Window global");
  }

  const node = nodeCache.getNode(browsingContext, sharedRef);

  if (node === null) {
    throw new lazy.error.NoSuchNodeError(
      `The node with the reference ${sharedRef} is not known`
    );
  }

  // Bug 1819902: Instead of a browsing context check compare the origin
  const isSameBrowsingContext = sharedRef => {
    const nodeDetails = nodeCache.getReferenceDetails(sharedRef);

    if (nodeDetails.isTopBrowsingContext && browsingContext.parent === null) {
      // As long as Navigables are not available any cross-group navigation will
      // cause a swap of the current top-level browsing context. The only unique
      // identifier in such a case is the browser id the top-level browsing
      // context actually lives in.
      return nodeDetails.browserId === browsingContext.browserId;
    }

    return nodeDetails.browsingContextId === browsingContext.id;
  };

  if (!isSameBrowsingContext(sharedRef)) {
    return null;
  }

  return node;
}

/**
 * Deserialize a local value.
 *
 * @see https://w3c.github.io/webdriver-bidi/#deserialize-local-value
 *
 * @param {object} serializedValue
 *     Value of any type to be deserialized.
 * @param {Realm} realm
 *     The Realm in which the value is deserialized.
 * @param {ExtraDeserializationOptions} extraOptions
 *     Extra Remote Value deserialization options.
 *
 * @returns {object} Deserialized representation of the value.
 */
export function deserialize(serializedValue, realm, extraOptions) {
  const { handle, sharedId, type, value } = serializedValue;

  // With a shared id present deserialize as node reference.
  if (sharedId !== undefined) {
    lazy.assert.string(
      sharedId,
      lazy.pprint`Expected "sharedId" to be a string, got ${sharedId}`
    );

    return deserializeSharedReference(sharedId, realm, extraOptions);
  }

  // With a handle present deserialize as remote reference.
  if (handle !== undefined) {
    lazy.assert.string(
      handle,
      lazy.pprint`Expected "handle" to be a string, got ${handle}`
    );

    const object = realm.getObjectForHandle(handle);
    if (!object) {
      throw new lazy.error.NoSuchHandleError(
        `Unable to find an object reference for "handle" ${handle}`
      );
    }

    return object;
  }

  lazy.assert.string(
    type,
    lazy.pprint`Expected "type" to be a string, got ${type}`
  );

  // Primitive protocol values
  switch (type) {
    case "undefined":
      return undefined;
    case "null":
      return null;
    case "string":
      lazy.assert.string(
        value,
        lazy.pprint`Expected "value" to be a string, got ${value}`
      );
      return value;
    case "number":
      // If value is already a number return its value.
      if (typeof value === "number") {
        return value;
      }

      // Otherwise it has to be one of the special strings
      lazy.assert.in(
        value,
        ["NaN", "-0", "Infinity", "-Infinity"],
        lazy.pprint`Expected "value" to be one of "NaN", "-0", "Infinity", "-Infinity", got ${value}`
      );
      return Number(value);
    case "boolean":
      lazy.assert.boolean(
        value,
        lazy.pprint`Expected "value" to be a boolean, got ${value}`
      );
      return value;
    case "bigint":
      lazy.assert.string(
        value,
        lazy.pprint`Expected "value" to be a string, got ${value}`
      );
      try {
        return BigInt(value);
      } catch (e) {
        throw new lazy.error.InvalidArgumentError(
          `Failed to deserialize value as BigInt: ${value}`
        );
      }

    // Script channel
    case "channel": {
      const channel = message =>
        extraOptions.emitScriptMessage(realm, value, message);
      return realm.cloneIntoRealm(channel);
    }

    // Non-primitive protocol values
    case "array":
      const array = realm.cloneIntoRealm([]);
      deserializeValueList(value, realm, extraOptions).forEach(v =>
        array.push(v)
      );
      return array;
    case "date":
      // We want to support only Date Time String format,
      // check if the value follows it.
      if (!ChromeUtils.isISOStyleDate(value)) {
        throw new lazy.error.InvalidArgumentError(
          `Expected "value" for Date to be a Date Time string, got ${value}`
        );
      }

      return realm.cloneIntoRealm(new Date(value));
    case "map":
      const map = realm.cloneIntoRealm(new Map());
      deserializeKeyValueList(value, realm, extraOptions).forEach(([k, v]) =>
        map.set(k, v)
      );

      return map;
    case "object":
      const object = realm.cloneIntoRealm({});
      deserializeKeyValueList(value, realm, extraOptions).forEach(
        ([k, v]) => (object[k] = v)
      );
      return object;
    case "regexp":
      lazy.assert.object(
        value,
        lazy.pprint`Expected "value" for RegExp to be an object, got ${value}`
      );
      const { pattern, flags } = value;
      lazy.assert.string(
        pattern,
        lazy.pprint`Expected "pattern" for RegExp to be a string, got ${pattern}`
      );
      if (flags !== undefined) {
        lazy.assert.string(
          flags,
          lazy.pprint`Expected "flags" for RegExp to be a string, got ${flags}`
        );
      }
      try {
        return realm.cloneIntoRealm(new RegExp(pattern, flags));
      } catch (e) {
        throw new lazy.error.InvalidArgumentError(
          `Failed to deserialize value as RegExp: ${value}`
        );
      }
    case "set":
      const set = realm.cloneIntoRealm(new Set());
      deserializeValueList(value, realm, extraOptions).forEach(v => set.add(v));
      return set;
  }

  lazy.logger.warn(`Unsupported type for local value ${type}`);
  return undefined;
}

/**
 * Helper to retrieve the handle id for a given object, for the provided realm
 * and ownership type.
 *
 * See https://w3c.github.io/webdriver-bidi/#handle-for-an-object
 *
 * @param {Realm} realm
 *     The Realm from which comes the value being serialized.
 * @param {OwnershipModel} ownershipType
 *     The ownership model to use for this serialization.
 * @param {object} object
 *     The object being serialized.
 *
 * @returns {string} The unique handle id for the object. Will be null if the
 *     Ownership type is "none".
 */
function getHandleForObject(realm, ownershipType, object) {
  if (ownershipType === OwnershipModel.None) {
    return null;
  }
  return realm.getHandleForObject(object);
}

/**
 * Gets or creates a new shared unique reference for the DOM node.
 *
 * This unique reference can be shared by WebDriver clients with the WebDriver
 * classic implementation (Marionette) if the reference is for an Element or
 * ShadowRoot.
 *
 * @param {Node} node
 *    Node to create the unique reference for.
 * @param {ExtraSerializationOptions} extraOptions
 *     Extra Remote Value serialization options.
 *
 * @returns {string}
 *    Shared unique reference for the Node.
 */
function getSharedIdForNode(node, extraOptions) {
  const { nodeCache, seenNodeIds } = extraOptions;

  if (!Node.isInstance(node)) {
    return null;
  }

  const browsingContext = node.ownerGlobal.browsingContext;
  if (!browsingContext) {
    return null;
  }

  return nodeCache.getOrCreateNodeReference(node, seenNodeIds);
}

/**
 * Helper to serialize an Array-like object.
 *
 * @see https://w3c.github.io/webdriver-bidi/#serialize-an-array-like
 *
 * @param {string} production
 *     Type of object
 * @param {string} handleId
 *     The unique id of the <var>value</var>.
 * @param {boolean} knownObject
 *     Indicates if the <var>value</var> has already been serialized.
 * @param {object} value
 *     The Array-like object to serialize.
 * @param {SerializationOptions} serializationOptions
 *     Options which define how ECMAScript objects should be serialized.
 * @param {OwnershipModel} ownershipType
 *     The ownership model to use for this serialization.
 * @param {Map} serializationInternalMap
 *     Map of internal ids.
 * @param {Realm} realm
 *     The Realm from which comes the value being serialized.
 * @param {ExtraSerializationOptions} extraOptions
 *     Extra Remote Value serialization options.
 *
 * @returns {object} Object for serialized values.
 */
function serializeArrayLike(
  production,
  handleId,
  knownObject,
  value,
  serializationOptions,
  ownershipType,
  serializationInternalMap,
  realm,
  extraOptions
) {
  const serialized = buildSerialized(production, handleId);
  setInternalIdsIfNeeded(serializationInternalMap, serialized, value);

  if (!knownObject && serializationOptions.maxObjectDepth !== 0) {
    serialized.value = serializeList(
      value,
      serializationOptions,
      ownershipType,
      serializationInternalMap,
      realm,
      extraOptions
    );
  }

  return serialized;
}

/**
 * Helper to serialize as a list.
 *
 * @see https://w3c.github.io/webdriver-bidi/#serialize-as-a-list
 *
 * @param {Iterable} iterable
 *     List of values to be serialized.
 * @param {SerializationOptions} serializationOptions
 *     Options which define how ECMAScript objects should be serialized.
 * @param {OwnershipModel} ownershipType
 *     The ownership model to use for this serialization.
 * @param {Map} serializationInternalMap
 *     Map of internal ids.
 * @param {Realm} realm
 *     The Realm from which comes the value being serialized.
 * @param {ExtraSerializationOptions} extraOptions
 *     Extra Remote Value serialization options.
 *
 * @returns {Array} List of serialized values.
 */
function serializeList(
  iterable,
  serializationOptions,
  ownershipType,
  serializationInternalMap,
  realm,
  extraOptions
) {
  const { maxObjectDepth } = serializationOptions;
  const serialized = [];
  const childSerializationOptions = {
    ...serializationOptions,
  };
  if (maxObjectDepth !== null) {
    childSerializationOptions.maxObjectDepth = maxObjectDepth - 1;
  }

  for (const item of iterable) {
    serialized.push(
      serialize(
        item,
        childSerializationOptions,
        ownershipType,
        serializationInternalMap,
        realm,
        extraOptions
      )
    );
  }

  return serialized;
}

/**
 * Helper to serialize as a mapping.
 *
 * @see https://w3c.github.io/webdriver-bidi/#serialize-as-a-mapping
 *
 * @param {Iterable} iterable
 *     List of values to be serialized.
 * @param {SerializationOptions} serializationOptions
 *     Options which define how ECMAScript objects should be serialized.
 * @param {OwnershipModel} ownershipType
 *     The ownership model to use for this serialization.
 * @param {Map} serializationInternalMap
 *     Map of internal ids.
 * @param {Realm} realm
 *     The Realm from which comes the value being serialized.
 * @param {ExtraSerializationOptions} extraOptions
 *     Extra Remote Value serialization options.
 *
 * @returns {Array} List of serialized values.
 */
function serializeMapping(
  iterable,
  serializationOptions,
  ownershipType,
  serializationInternalMap,
  realm,
  extraOptions
) {
  const { maxObjectDepth } = serializationOptions;
  const serialized = [];
  const childSerializationOptions = {
    ...serializationOptions,
  };
  if (maxObjectDepth !== null) {
    childSerializationOptions.maxObjectDepth = maxObjectDepth - 1;
  }

  for (const [key, item] of iterable) {
    const serializedKey =
      typeof key == "string"
        ? key
        : serialize(
            key,
            childSerializationOptions,
            ownershipType,
            serializationInternalMap,
            realm,
            extraOptions
          );
    const serializedValue = serialize(
      item,
      childSerializationOptions,
      ownershipType,
      serializationInternalMap,
      realm,
      extraOptions
    );

    serialized.push([serializedKey, serializedValue]);
  }

  return serialized;
}

/**
 * Helper to serialize as a Node.
 *
 * @param {Node} node
 *     Node to be serialized.
 * @param {SerializationOptions} serializationOptions
 *     Options which define how ECMAScript objects should be serialized.
 * @param {OwnershipModel} ownershipType
 *     The ownership model to use for this serialization.
 * @param {Map} serializationInternalMap
 *     Map of internal ids.
 * @param {Realm} realm
 *     The Realm from which comes the value being serialized.
 * @param {ExtraSerializationOptions} extraOptions
 *     Extra Remote Value serialization options.
 *
 * @returns {object} Serialized value.
 */
function serializeNode(
  node,
  serializationOptions,
  ownershipType,
  serializationInternalMap,
  realm,
  extraOptions
) {
  const { includeShadowTree, maxDomDepth } = serializationOptions;
  const isAttribute = Attr.isInstance(node);
  const isElement = Element.isInstance(node);

  const serialized = {
    nodeType: node.nodeType,
  };

  if (node.nodeValue !== null) {
    serialized.nodeValue = node.nodeValue;
  }

  if (isElement || isAttribute) {
    serialized.localName = node.localName;
    serialized.namespaceURI = node.namespaceURI;
  }

  serialized.childNodeCount = node.childNodes.length;
  if (
    maxDomDepth !== 0 &&
    (!lazy.dom.isShadowRoot(node) ||
      (includeShadowTree === IncludeShadowTreeMode.Open &&
        node.mode === "open") ||
      includeShadowTree === IncludeShadowTreeMode.All)
  ) {
    const children = [];
    const childSerializationOptions = {
      ...serializationOptions,
    };
    if (maxDomDepth !== null) {
      childSerializationOptions.maxDomDepth = maxDomDepth - 1;
    }
    for (const child of node.childNodes) {
      children.push(
        serialize(
          child,
          childSerializationOptions,
          ownershipType,
          serializationInternalMap,
          realm,
          extraOptions
        )
      );
    }

    serialized.children = children;
  }

  if (isElement) {
    serialized.attributes = [...node.attributes].reduce((map, attr) => {
      map[attr.name] = attr.value;
      return map;
    }, {});

    const shadowRoot = node.openOrClosedShadowRoot;
    serialized.shadowRoot = null;
    if (shadowRoot !== null) {
      serialized.shadowRoot = serialize(
        shadowRoot,
        serializationOptions,
        ownershipType,
        serializationInternalMap,
        realm,
        extraOptions
      );
    }
  }

  if (lazy.dom.isShadowRoot(node)) {
    serialized.mode = node.mode;
  }

  return serialized;
}

/**
 * Serialize a value as a remote value.
 *
 * @see https://w3c.github.io/webdriver-bidi/#serialize-as-a-remote-value
 *
 * @param {object} value
 *     Value of any type to be serialized.
 * @param {SerializationOptions} serializationOptions
 *     Options which define how ECMAScript objects should be serialized.
 * @param {OwnershipModel} ownershipType
 *     The ownership model to use for this serialization.
 * @param {Map} serializationInternalMap
 *     Map of internal ids.
 * @param {Realm} realm
 *     The Realm from which comes the value being serialized.
 * @param {ExtraSerializationOptions} extraOptions
 *     Extra Remote Value serialization options.
 *
 * @returns {object} Serialized representation of the value.
 */
export function serialize(
  value,
  serializationOptions,
  ownershipType,
  serializationInternalMap,
  realm,
  extraOptions
) {
  const { maxObjectDepth } = serializationOptions;
  const type = typeof value;

  // Primitive protocol values
  if (type == "undefined") {
    return { type };
  } else if (Object.is(value, null)) {
    return { type: "null" };
  } else if (Object.is(value, NaN)) {
    return { type: "number", value: "NaN" };
  } else if (Object.is(value, -0)) {
    return { type: "number", value: "-0" };
  } else if (Object.is(value, Infinity)) {
    return { type: "number", value: "Infinity" };
  } else if (Object.is(value, -Infinity)) {
    return { type: "number", value: "-Infinity" };
  } else if (type == "bigint") {
    return { type, value: value.toString() };
  } else if (["boolean", "number", "string"].includes(type)) {
    return { type, value };
  }

  const handleId = getHandleForObject(realm, ownershipType, value);
  const knownObject = serializationInternalMap.has(value);

  // Set the OwnershipModel to use for all complex object serializations.
  ownershipType = OwnershipModel.None;

  // Remote values

  // symbols are primitive JS values which can only be serialized
  // as remote values.
  if (type == "symbol") {
    return buildSerialized("symbol", handleId);
  }

  // All other remote values are non-primitives and their
  // className can be extracted with ChromeUtils.getClassName
  const className = ChromeUtils.getClassName(value);
  if (["Array", "HTMLCollection", "NodeList"].includes(className)) {
    return serializeArrayLike(
      className.toLowerCase(),
      handleId,
      knownObject,
      value,
      serializationOptions,
      ownershipType,
      serializationInternalMap,
      realm,
      extraOptions
    );
  } else if (className == "RegExp") {
    const serialized = buildSerialized("regexp", handleId);
    serialized.value = { pattern: value.source, flags: value.flags };
    return serialized;
  } else if (className == "Date") {
    const serialized = buildSerialized("date", handleId);
    serialized.value = value.toISOString();
    return serialized;
  } else if (className == "Map") {
    const serialized = buildSerialized("map", handleId);
    setInternalIdsIfNeeded(serializationInternalMap, serialized, value);

    if (!knownObject && maxObjectDepth !== 0) {
      serialized.value = serializeMapping(
        value.entries(),
        serializationOptions,
        ownershipType,
        serializationInternalMap,
        realm,
        extraOptions
      );
    }
    return serialized;
  } else if (className == "Set") {
    const serialized = buildSerialized("set", handleId);
    setInternalIdsIfNeeded(serializationInternalMap, serialized, value);

    if (!knownObject && maxObjectDepth !== 0) {
      serialized.value = serializeList(
        value.values(),
        serializationOptions,
        ownershipType,
        serializationInternalMap,
        realm,
        extraOptions
      );
    }
    return serialized;
  } else if (
    ["ArrayBuffer", "Function", "Promise", "WeakMap", "WeakSet"].includes(
      className
    )
  ) {
    return buildSerialized(className.toLowerCase(), handleId);
  } else if (className.includes("Generator")) {
    return buildSerialized("generator", handleId);
  } else if (lazy.error.isError(value)) {
    return buildSerialized("error", handleId);
  } else if (Cu.isProxy(value)) {
    return buildSerialized("proxy", handleId);
  } else if (TYPED_ARRAY_CLASSES.includes(className)) {
    return buildSerialized("typedarray", handleId);
  } else if (Node.isInstance(value)) {
    const serialized = buildSerialized("node", handleId);

    value = Cu.unwaiveXrays(value);

    // Get or create the shared id for WebDriver classic compat from the node.
    const sharedId = getSharedIdForNode(value, extraOptions);
    if (sharedId !== null) {
      serialized.sharedId = sharedId;
    }

    setInternalIdsIfNeeded(serializationInternalMap, serialized, value);

    if (!knownObject) {
      serialized.value = serializeNode(
        value,
        serializationOptions,
        ownershipType,
        serializationInternalMap,
        realm,
        extraOptions
      );
    }

    return serialized;
  } else if (Window.isInstance(value)) {
    const serialized = buildSerialized("window", handleId);
    const window = Cu.unwaiveXrays(value);

    if (window.browsingContext.parent == null) {
      serialized.value = {
        context: window.browsingContext.browserId.toString(),
        isTopBrowsingContext: true,
      };
    } else {
      serialized.value = {
        context: window.browsingContext.id.toString(),
      };
    }

    return serialized;
  } else if (ChromeUtils.isDOMObject(value)) {
    const serialized = buildSerialized("object", handleId);
    return serialized;
  }

  // Otherwise serialize the JavaScript object as generic object.
  const serialized = buildSerialized("object", handleId);
  setInternalIdsIfNeeded(serializationInternalMap, serialized, value);

  if (!knownObject && maxObjectDepth !== 0) {
    serialized.value = serializeMapping(
      Object.entries(value),
      serializationOptions,
      ownershipType,
      serializationInternalMap,
      realm,
      extraOptions
    );
  }
  return serialized;
}

/**
 * Set default serialization options.
 *
 * @param {SerializationOptions} options
 *    Options which define how ECMAScript objects should be serialized.
 * @returns {SerializationOptions}
 *    Serialiation options with default value added.
 */
export function setDefaultSerializationOptions(options = {}) {
  const serializationOptions = { ...options };
  if (!("maxDomDepth" in serializationOptions)) {
    serializationOptions.maxDomDepth = 0;
  }
  if (!("maxObjectDepth" in serializationOptions)) {
    serializationOptions.maxObjectDepth = null;
  }
  if (!("includeShadowTree" in serializationOptions)) {
    serializationOptions.includeShadowTree = IncludeShadowTreeMode.None;
  }

  return serializationOptions;
}

/**
 * Set default values and assert if serialization options have
 * expected types.
 *
 * @param {SerializationOptions} options
 *    Options which define how ECMAScript objects should be serialized.
 * @returns {SerializationOptions}
 *    Serialiation options with default value added.
 */
export function setDefaultAndAssertSerializationOptions(options = {}) {
  lazy.assert.object(
    options,
    lazy.pprint`Expected "options" to be an object, got ${options}`
  );

  const serializationOptions = setDefaultSerializationOptions(options);

  const { includeShadowTree, maxDomDepth, maxObjectDepth } =
    serializationOptions;

  if (maxDomDepth !== null) {
    lazy.assert.positiveInteger(
      maxDomDepth,
      lazy.pprint`Expected "maxDomDepth" to be a positive integer or null, got ${maxDomDepth}`
    );
  }
  if (maxObjectDepth !== null) {
    lazy.assert.positiveInteger(
      maxObjectDepth,
      lazy.pprint`Expected "maxObjectDepth" to be a positive integer or null, got ${maxObjectDepth}`
    );
  }
  const includeShadowTreeModesValues = Object.values(IncludeShadowTreeMode);
  lazy.assert.that(
    includeShadowTree =>
      includeShadowTreeModesValues.includes(includeShadowTree),
    `Expected "includeShadowTree" to be one of ${includeShadowTreeModesValues}, ` +
      lazy.pprint`got ${includeShadowTree}`
  )(includeShadowTree);

  return serializationOptions;
}

/**
 * Set the internalId property of a provided serialized RemoteValue,
 * and potentially of a previously created serialized RemoteValue,
 * corresponding to the same provided object.
 *
 * @see https://w3c.github.io/webdriver-bidi/#set-internal-ids-if-needed
 *
 * @param {Map} serializationInternalMap
 *     Map of objects to remote values.
 * @param {object} remoteValue
 *     A serialized RemoteValue for the provided object.
 * @param {object} object
 *     Object of any type to be serialized.
 */
function setInternalIdsIfNeeded(serializationInternalMap, remoteValue, object) {
  if (!serializationInternalMap.has(object)) {
    // If the object was not tracked yet in the current serialization, add
    // a new entry in the serialization internal map. An internal id will only
    // be generated if the same object is encountered again.
    serializationInternalMap.set(object, remoteValue);
  } else {
    // This is at least the second time this object is encountered, retrieve the
    // original remote value stored for this object.
    const previousRemoteValue = serializationInternalMap.get(object);

    if (!previousRemoteValue.internalId) {
      // If the original remote value has no internal id yet, generate a uuid
      // and update the internalId of the original remote value with it.
      previousRemoteValue.internalId = lazy.generateUUID();
    }

    // Copy the internalId of the original remote value to the new remote value.
    remoteValue.internalId = previousRemoteValue.internalId;
  }
}

/**
 * Safely stringify a value.
 *
 * @param {object} obj
 *     Value of any type to be stringified.
 *
 * @returns {string} String representation of the value.
 */
export function stringify(obj) {
  let text;
  try {
    text =
      obj !== null && typeof obj === "object" ? obj.toString() : String(obj);
  } catch (e) {
    // The error-case will also be handled in `finally {}`.
  } finally {
    if (typeof text != "string") {
      text = Object.prototype.toString.apply(obj);
    }
  }

  return text;
}
