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

"use strict";

const { objectSpec } = require("resource://devtools/shared/specs/object.js");
const {
  FrontClassWithSpec,
  registerFront,
} = require("resource://devtools/shared/protocol.js");
const {
  LongStringFront,
} = require("resource://devtools/client/fronts/string.js");

const SUPPORT_ENUM_ENTRIES_SET = new Set([
  "CustomStateSet",
  "FormData",
  "Headers",
  "HighlightRegistry",
  "Map",
  "MIDIInputMap",
  "MIDIOutputMap",
  "Set",
  "Storage",
  "URLSearchParams",
  "WeakMap",
  "WeakSet",
]);

/**
 * A ObjectFront is used as a front end for the ObjectActor that is
 * created on the server, hiding implementation details.
 */
class ObjectFront extends FrontClassWithSpec(objectSpec) {
  constructor(conn = null, targetFront = null, parentFront = null, data) {
    if (!parentFront) {
      throw new Error("ObjectFront require a parent front");
    }

    super(conn, targetFront, parentFront);

    this._grip = data;
    this.actorID = this._grip.actor;
    this.valid = true;

    parentFront.manage(this);
  }

  form(data) {
    this.actorID = data.actor;
    this._grip = data;
  }

  skipDestroy() {
    // Object fronts are simple fronts, they don't need to be cleaned up on
    // toolbox destroy. `conn` is a DebuggerClient instance, check the
    // `isToolboxDestroy` flag to skip the destroy.
    return this.conn && this.conn.isToolboxDestroy;
  }

  getGrip() {
    return this._grip;
  }

  get isFrozen() {
    return this._grip.frozen;
  }

  get isSealed() {
    return this._grip.sealed;
  }

  get isExtensible() {
    return this._grip.extensible;
  }

  /**
   * Request the prototype and own properties of the object.
   */
  async getPrototypeAndProperties() {
    const result = await super.prototypeAndProperties();

    if (result.prototype) {
      result.prototype = getAdHocFrontOrPrimitiveGrip(result.prototype, this);
    }

    // The result packet can have multiple properties that hold grips which we may need
    // to turn into fronts.
    const gripKeys = ["value", "getterValue", "get", "set"];

    if (result.ownProperties) {
      Object.entries(result.ownProperties).forEach(([key, descriptor]) => {
        if (descriptor) {
          for (const gripKey of gripKeys) {
            if (descriptor.hasOwnProperty(gripKey)) {
              result.ownProperties[key][gripKey] = getAdHocFrontOrPrimitiveGrip(
                descriptor[gripKey],
                this
              );
            }
          }
        }
      });
    }

    if (result.safeGetterValues) {
      Object.entries(result.safeGetterValues).forEach(([key, descriptor]) => {
        if (descriptor) {
          for (const gripKey of gripKeys) {
            if (descriptor.hasOwnProperty(gripKey)) {
              result.safeGetterValues[key][gripKey] =
                getAdHocFrontOrPrimitiveGrip(descriptor[gripKey], this);
            }
          }
        }
      });
    }

    if (result.ownSymbols) {
      result.ownSymbols.forEach((descriptor, i, arr) => {
        if (descriptor) {
          for (const gripKey of gripKeys) {
            if (descriptor.hasOwnProperty(gripKey)) {
              arr[i][gripKey] = getAdHocFrontOrPrimitiveGrip(
                descriptor[gripKey],
                this
              );
            }
          }
        }
      });
    }

    return result;
  }

  /**
   * Request a PropertyIteratorFront instance to ease listing
   * properties for this object.
   *
   * @param options Object
   *        A dictionary object with various boolean attributes:
   *        - ignoreIndexedProperties Boolean
   *          If true, filters out Array items.
   *          e.g. properties names between `0` and `object.length`.
   *        - ignoreNonIndexedProperties Boolean
   *          If true, filters out items that aren't array items
   *          e.g. properties names that are not a number between `0`
   *          and `object.length`.
   *        - sort Boolean
   *          If true, the iterator will sort the properties by name
   *          before dispatching them.
   */
  enumProperties(options) {
    return super.enumProperties(options);
  }

  /**
   * Request a PropertyIteratorFront instance to enumerate entries in a
   * Map/Set-like object.
   */
  enumEntries() {
    if (!SUPPORT_ENUM_ENTRIES_SET.has(this._grip.class)) {
      console.error(
        `enumEntries can't be called for "${
          this._grip.class
        }" grips. Supported grips are: ${[...SUPPORT_ENUM_ENTRIES_SET].join(
          ", "
        )}.`
      );
      return null;
    }
    return super.enumEntries();
  }

  /**
   * Request a SymbolIteratorFront instance to enumerate symbols in an object.
   */
  enumSymbols() {
    if (this._grip.type !== "object") {
      console.error("enumSymbols is only valid for objects grips.");
      return null;
    }
    return super.enumSymbols();
  }

  /**
   * Request the property descriptor of the object's specified property.
   *
   * @param name string The name of the requested property.
   */
  getProperty(name) {
    return super.property(name);
  }

  /**
   * Request the value of the object's specified property.
   *
   * @param name string The name of the requested property.
   * @param receiverId string|null The actorId of the receiver to be used for getters.
   */
  async getPropertyValue(name, receiverId) {
    const response = await super.propertyValue(name, receiverId);

    if (response.value) {
      const { value } = response;
      if (value.return) {
        response.value.return = getAdHocFrontOrPrimitiveGrip(
          value.return,
          this
        );
      }

      if (value.throw) {
        response.value.throw = getAdHocFrontOrPrimitiveGrip(value.throw, this);
      }
    }
    return response;
  }

  /**
   * Get the body of a custom formatted object.
   */
  async customFormatterBody() {
    const result = await super.customFormatterBody();

    if (!result?.customFormatterBody) {
      return result;
    }

    const createFrontsInJsonMl = item => {
      if (Array.isArray(item)) {
        return item.map(i => createFrontsInJsonMl(i));
      }
      return getAdHocFrontOrPrimitiveGrip(item, this);
    };

    result.customFormatterBody = createFrontsInJsonMl(
      result.customFormatterBody
    );

    return result;
  }

  /**
   * Request the prototype of the object.
   */
  async getPrototype() {
    const result = await super.prototype();

    if (!result.prototype) {
      return result;
    }

    result.prototype = getAdHocFrontOrPrimitiveGrip(result.prototype, this);

    return result;
  }

  /**
   * Request the state of a promise.
   */
  async getPromiseState() {
    if (this._grip.class !== "Promise") {
      console.error("getPromiseState is only valid for promise grips.");
      return null;
    }

    let response, promiseState;
    try {
      response = await super.promiseState();
      promiseState = response.promiseState;
    } catch (error) {
      // @backward-compat { version 85 } On older server, the promiseState request didn't
      // didn't exist (bug 1552648). The promise state was directly included in the grip.
      if (error.message.includes("unrecognizedPacketType")) {
        promiseState = this._grip.promiseState;
        response = { promiseState };
      } else {
        throw error;
      }
    }

    const { value, reason } = promiseState;

    if (value) {
      promiseState.value = getAdHocFrontOrPrimitiveGrip(value, this);
    }

    if (reason) {
      promiseState.reason = getAdHocFrontOrPrimitiveGrip(reason, this);
    }

    return response;
  }

  /**
   * Request the target and handler internal slots of a proxy.
   */
  async getProxySlots() {
    if (this._grip.class !== "Proxy") {
      console.error("getProxySlots is only valid for proxy grips.");
      return null;
    }

    const response = await super.proxySlots();
    const { proxyHandler, proxyTarget } = response;

    if (proxyHandler) {
      response.proxyHandler = getAdHocFrontOrPrimitiveGrip(proxyHandler, this);
    }

    if (proxyTarget) {
      response.proxyTarget = getAdHocFrontOrPrimitiveGrip(proxyTarget, this);
    }

    return response;
  }

  get isSyntaxError() {
    return this._grip.preview && this._grip.preview.name == "SyntaxError";
  }
}

/**
 * When we are asking the server for the value of a given variable, we might get different
 * type of objects:
 * - a primitive (string, number, null, false, boolean)
 * - a long string
 * - an "object" (i.e. not primitive nor long string)
 *
 * Each of those type need a different front, or none:
 * - a primitive does not allow further interaction with the server, so we don't need
 *   to have a dedicated front.
 * - a long string needs a longStringFront to be able to retrieve the full string.
 * - an object need an objectFront to retrieve properties, symbols and prototype.
 *
 * In the case an ObjectFront is created, we also check if the object has properties
 * that should be turned into fronts as well.
 *
 * @param {string | number | object} options: The packet returned by the server.
 * @param {Front} parentFront
 *
 * @returns {number | string | object | LongStringFront | ObjectFront}
 */
function getAdHocFrontOrPrimitiveGrip(packet, parentFront) {
  // We only want to try to create a front when it makes sense, i.e when it has an
  // actorID, unless:
  // - it's a Symbol (See Bug 1600299)
  // - it's a mapEntry (the preview.key and preview.value properties can hold actors)
  // - it's a highlightRegistryEntry (the preview.value properties can hold actors)
  // - or it is already a front (happens when we are using the legacy listeners in the ResourceCommand)
  const isPacketAnObject = packet && typeof packet === "object";
  const isFront = !!packet?.typeName;
  if (
    !isPacketAnObject ||
    packet.type == "symbol" ||
    (packet.type !== "mapEntry" &&
      packet.type !== "highlightRegistryEntry" &&
      !packet.actor) ||
    isFront
  ) {
    return packet;
  }

  const { conn } = parentFront;
  // If the parent front is a target, consider it as the target to use for all objects
  const targetFront = parentFront.isTargetFront
    ? parentFront
    : parentFront.targetFront;

  // We may have already created a front for this object actor since some actor (e.g. the
  // thread actor) cache the object actors they create.
  const existingFront = conn.getFrontByID(packet.actor);
  if (existingFront) {
    // This methods replicates Protocol.js logic when we receive an actor "form" (here `packet`):
    // https://searchfox.org/mozilla-central/rev/aecbd5cdd28a09e11872bc829d9e6e4b943e6e49/devtools/shared/protocol/types.js#346
    // We notify the Object Front about the new "form" so that it can update itself
    // with latest data provided by the server.
    // This will help ensure that the object previews get updated.
    existingFront.form(packet);

    // The `packet` may contain nested actor forms which should be converted into Fronts.
    createChildFronts(existingFront, packet);

    return existingFront;
  }

  const { type } = packet;

  if (type === "longString") {
    const longStringFront = new LongStringFront(conn, targetFront, parentFront);
    longStringFront.form(packet);
    parentFront.manage(longStringFront);
    return longStringFront;
  }

  if (
    (type === "mapEntry" || type === "highlightRegistryEntry") &&
    packet.preview
  ) {
    const { key, value } = packet.preview;
    packet.preview.key = getAdHocFrontOrPrimitiveGrip(
      key,
      parentFront,
      targetFront
    );
    packet.preview.value = getAdHocFrontOrPrimitiveGrip(
      value,
      parentFront,
      targetFront
    );
    return packet;
  }

  const objectFront = new ObjectFront(conn, targetFront, parentFront, packet);
  createChildFronts(objectFront, packet);
  return objectFront;
}

/**
 * Create child fronts of the passed object front given a packet. Those child fronts are
 * usually mapping actors of the packet sub-properties (preview items, promise fullfilled
 * values, …).
 *
 * @param {ObjectFront} objectFront
 * @param {string | number | object} packet: The packet returned by the server
 */
function createChildFronts(objectFront, packet) {
  if (packet.preview) {
    const { message, entries } = packet.preview;

    // The message could be a longString.
    if (packet.preview.message) {
      packet.preview.message = getAdHocFrontOrPrimitiveGrip(
        message,
        objectFront
      );
    }

    // Handle Map/WeakMap preview entries (the preview might be directly used if has all the
    // items needed, i.e. if the Map has less than 10 items).
    if (entries && Array.isArray(entries)) {
      packet.preview.entries = entries.map(([key, value]) => [
        getAdHocFrontOrPrimitiveGrip(key, objectFront),
        getAdHocFrontOrPrimitiveGrip(value, objectFront),
      ]);
    }
  }

  if (packet && typeof packet.ownProperties === "object") {
    for (const [name, descriptor] of Object.entries(packet.ownProperties)) {
      // The descriptor can have multiple properties that hold grips which we may need
      // to turn into fronts.
      const gripKeys = ["value", "getterValue", "get", "set"];
      for (const key of gripKeys) {
        if (
          descriptor &&
          typeof descriptor === "object" &&
          descriptor.hasOwnProperty(key)
        ) {
          packet.ownProperties[name][key] = getAdHocFrontOrPrimitiveGrip(
            descriptor[key],
            objectFront
          );
        }
      }
    }
  }

  // Handle custom formatters
  if (packet && packet.useCustomFormatter && Array.isArray(packet.header)) {
    const createFrontsInJsonMl = item => {
      if (Array.isArray(item)) {
        return item.map(i => createFrontsInJsonMl(i));
      }
      return getAdHocFrontOrPrimitiveGrip(item, objectFront);
    };

    packet.header = createFrontsInJsonMl(packet.header);
  }
}

registerFront(ObjectFront);

exports.ObjectFront = ObjectFront;
exports.getAdHocFrontOrPrimitiveGrip = getAdHocFrontOrPrimitiveGrip;
