/* 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 { DevToolsServer } = require("resource://devtools/server/devtools-server.js");
const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
loader.lazyRequireGetter(
  this,
  "ObjectUtils",
  "resource://devtools/server/actors/object/utils.js"
);
loader.lazyRequireGetter(
  this,
  "PropertyIterators",
  "resource://devtools/server/actors/object/property-iterator.js"
);
loader.lazyRequireGetter(
  this,
  "propertyDescriptor",
  "resource://devtools/server/actors/object/property-descriptor.js",
  true
);

// Number of items to preview in objects, arrays, maps, sets, lists,
// collections, etc.
const OBJECT_PREVIEW_MAX_ITEMS = 10;

const ERROR_CLASSNAMES = new Set([
  "Error",
  "EvalError",
  "RangeError",
  "ReferenceError",
  "SyntaxError",
  "TypeError",
  "URIError",
  "InternalError",
  "AggregateError",
  "CompileError",
  "DebuggeeWouldRun",
  "LinkError",
  "RuntimeError",
  "Exception", // This related to Components.Exception()
  "SuppressedError",
]);
const ARRAY_LIKE_CLASSNAMES = new Set([
  "DOMStringList",
  "DOMTokenList",
  "CSSRuleList",
  "MediaList",
  "StyleSheetList",
  "NamedNodeMap",
  "FileList",
  "NodeList",
]);
const OBJECT_WITH_URL_CLASSNAMES = new Set([
  "CSSImportRule",
  "CSSStyleSheet",
  "Location",
]);

/**
 * Functions for adding information to ObjectActor grips for the purpose of
 * having customized output. This object holds arrays mapped by
 * Debugger.Object.prototype.class.
 *
 * In each array you can add functions that take three
 * arguments:
 *   - the ObjectActor instance and its hooks to make a preview for,
 *   - the grip object being prepared for the client,
 *   - the depth of the object compared to the top level object,
 *     when we are inspecting nested attributes.
 *
 * Functions must return false if they cannot provide preview
 * information for the debugger object, or true otherwise.
 */
const previewers = {
  String: [
    function(objectActor, grip, depth) {
      return wrappedPrimitivePreviewer(
        String,
        objectActor,
        grip,
        depth
      );
    },
  ],

  Boolean: [
    function(objectActor, grip, depth) {
      return wrappedPrimitivePreviewer(
        Boolean,
        objectActor,
        grip,
        depth
      );
    },
  ],

  Number: [
    function(objectActor, grip, depth) {
      return wrappedPrimitivePreviewer(
        Number,
        objectActor,
        grip,
        depth
      );
    },
  ],

  Symbol: [
    function(objectActor, grip, depth) {
      return wrappedPrimitivePreviewer(
        Symbol,
        objectActor,
        grip,
        depth
      );
    },
  ],

  Function: [
    function(objectActor, grip, depth) {
      const { obj } = objectActor;
      if (obj.name) {
        grip.name = obj.name;
      }

      if (obj.displayName) {
        grip.displayName = obj.displayName.substr(0, 500);
      }

      if (obj.parameterNames) {
        grip.parameterNames = obj.parameterNames;
      }

      // Check if the developer has added a de-facto standard displayName
      // property for us to use.
      let userDisplayName;
      try {
        userDisplayName = obj.getOwnPropertyDescriptor("displayName");
      } catch (e) {
        // The above can throw "permission denied" errors when the debuggee
        // does not subsume the function's compartment.
      }

      if (
        userDisplayName &&
        typeof userDisplayName.value == "string" &&
        userDisplayName.value
      ) {
        grip.userDisplayName = objectActor.createValueGrip(userDisplayName.value, depth);
      }

      grip.isAsync = obj.isAsyncFunction;
      grip.isGenerator = obj.isGeneratorFunction;

      if (obj.script) {
        // NOTE: Debugger.Script.prototype.startColumn is 1-based.
        //       Convert to 0-based, while keeping the wasm's column (1) as is.
        //       (bug 1863878)
        const columnBase = obj.script.format === "wasm" ? 0 : 1;
        grip.location = {
          url: obj.script.url,
          line: obj.script.startLine,
          column: obj.script.startColumn - columnBase,
        };
      }

      return true;
    },
  ],

  RegExp: [
    function(objectActor, grip, depth) {
      let str;
      if (isWorker) {
        // For some reason, the following incantation on the worker thread returns "/undefined/undefined"
        // str = RegExp.prototype.toString.call(objectActor.obj.unsafeDereference());
        //
        // The following method will throw in case of method being overloaded by the page,
        // and a more generic previewer will render the object.
        try {
          str = DevToolsUtils.callPropertyOnObject(objectActor.obj, "toString");
        } catch(e) {
          // Ensure displaying something in case of error.
          // Otherwise this would render an object with an empty label
          grip.displayString = "RegExp with overloaded toString";
        }
      } else {
        const { RegExp } = objectActor.targetActor.targetGlobal;
        str = RegExp.prototype.toString.call(objectActor.safeRawObj);
      }

      if (typeof str != "string") {
        return false;
      }

      grip.displayString = objectActor.createValueGrip(str, depth);
      return true;
    },
  ],

  Date: [
    function(objectActor, grip, depth) {
      let time;
      if (isWorker) {
        // Also, targetGlobal is an opaque wrapper, from which we can't access its Date object,
        // so fallback to the privileged one
        //
        // In worker objectActor.safeRawObj is considered unsafe and is null,
        // so retrieve the objectActor.rawObj object directly from Debugger.Object.unsafeDereference
        time = Date.prototype.getTime.call(objectActor.rawObj);
      } else {
        const { Date } = objectActor.targetActor.targetGlobal;
        time = Date.prototype.getTime.call(objectActor.safeRawObj);
      }
      if (typeof time != "number") {
        return false;
      }

      grip.preview = {
        timestamp: objectActor.createValueGrip(time, depth),
      };
      return true;
    },
  ],

  Array: [
    function(objectActor, grip, depth) {
      const length = ObjectUtils.getArrayLength(objectActor.obj);

      grip.preview = {
        kind: "ArrayLike",
        length: length,
      };

      if (depth > 1) {
        return true;
      }

      const { obj, rawObj } = objectActor;
      const items = (grip.preview.items = []);

      for (let i = 0; i < length; ++i) {
        if (rawObj && !isWorker) {
          // Array Xrays filter out various possibly-unsafe properties (like
          // functions, and claim that the value is undefined instead. This
          // is generally the right thing for privileged code accessing untrusted
          // objects, but quite confusing for Object previews. So we manually
          // override this protection by waiving Xrays on the array, and re-applying
          // Xrays on any indexed value props that we pull off of it.
          const desc = Object.getOwnPropertyDescriptor(Cu.waiveXrays(rawObj), i);
          if (desc && !desc.get && !desc.set) {
            let value = Cu.unwaiveXrays(desc.value);
            value = ObjectUtils.makeDebuggeeValueIfNeeded(obj, value);
            items.push(objectActor.createValueGrip(value, depth));
          } else if (!desc) {
            items.push(null);
          } else {
            const item = {};
            if (desc.get) {
              let getter = Cu.unwaiveXrays(desc.get);
              getter = ObjectUtils.makeDebuggeeValueIfNeeded(obj, getter);
              item.get = objectActor.createValueGrip(getter, depth);
            }
            if (desc.set) {
              let setter = Cu.unwaiveXrays(desc.set);
              setter = ObjectUtils.makeDebuggeeValueIfNeeded(obj, setter);
              item.set = objectActor.createValueGrip(setter, depth);
            }
            items.push(item);
          }
        } else if (rawObj && !obj.getOwnPropertyDescriptor(i)) {
          items.push(null);
        } else {
          // Workers do not have access to Cu.
          const value = DevToolsUtils.getProperty(obj, i);
          items.push(objectActor.createValueGrip(value, depth));
        }

        if (items.length == OBJECT_PREVIEW_MAX_ITEMS) {
          break;
        }
      }

      return true;
    },
  ],

  Set: [
    function(objectActor, grip, depth) {
      const size = DevToolsUtils.getProperty(objectActor.obj, "size");
      if (typeof size != "number") {
        return false;
      }

      grip.preview = {
        kind: "ArrayLike",
        length: size,
      };

      // Avoid recursive object grips.
      if (depth > 1) {
        return true;
      }

      const items = (grip.preview.items = []);
      for (const item of PropertyIterators.enumSetEntries(objectActor, depth)) {
        items.push(item);
        if (items.length == OBJECT_PREVIEW_MAX_ITEMS) {
          break;
        }
      }

      return true;
    },
  ],

  WeakSet: [
    function(objectActor, grip, depth) {
      const enumEntries = PropertyIterators.enumWeakSetEntries(objectActor, depth);

      grip.preview = {
        kind: "ArrayLike",
        length: enumEntries.size,
      };

      // Avoid recursive object grips.
      if (depth > 1) {
        return true;
      }

      const items = (grip.preview.items = []);
      for (const item of enumEntries) {
        items.push(item);
        if (items.length == OBJECT_PREVIEW_MAX_ITEMS) {
          break;
        }
      }

      return true;
    },
  ],

  Map: [
    function(objectActor, grip, depth) {
      const size = DevToolsUtils.getProperty(objectActor.obj, "size");
      if (typeof size != "number") {
        return false;
      }

      grip.preview = {
        kind: "MapLike",
        size: size,
      };

      if (depth > 1) {
        return true;
      }

      const entries = (grip.preview.entries = []);
      for (const entry of PropertyIterators.enumMapEntries(objectActor, depth)) {
        entries.push(entry);
        if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) {
          break;
        }
      }

      return true;
    },
  ],

  WeakMap: [
    function(objectActor, grip, depth) {
      const enumEntries = PropertyIterators.enumWeakMapEntries(objectActor, depth);

      grip.preview = {
        kind: "MapLike",
        size: enumEntries.size,
      };

      if (depth > 1) {
        return true;
      }

      const entries = (grip.preview.entries = []);
      for (const entry of enumEntries) {
        entries.push(entry);
        if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) {
          break;
        }
      }

      return true;
    },
  ],

  URLSearchParams: [
    function(objectActor, grip, depth) {
      const enumEntries = PropertyIterators.enumURLSearchParamsEntries(objectActor, depth);

      grip.preview = {
        kind: "MapLike",
        size: enumEntries.size,
      };

      if (depth > 1) {
        return true;
      }

      const entries = (grip.preview.entries = []);
      for (const entry of enumEntries) {
        entries.push(entry);
        if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) {
          break;
        }
      }

      return true;
    },
  ],

  FormData: [
    function(objectActor, grip, depth) {
      const enumEntries = PropertyIterators.enumFormDataEntries(objectActor, depth);

      grip.preview = {
        kind: "MapLike",
        size: enumEntries.size,
      };

      if (depth > 1) {
        return true;
      }

      const entries = (grip.preview.entries = []);
      for (const entry of enumEntries) {
        entries.push(entry);
        if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) {
          break;
        }
      }

      return true;
    },
  ],

  Headers: [
    function(objectActor, grip, depth) {
      // Bug 1863776: Headers can't be yet previewed from workers
      if (isWorker) {
        return false;
      }
      const enumEntries = PropertyIterators.enumHeadersEntries(objectActor, depth);

      grip.preview = {
        kind: "MapLike",
        size: enumEntries.size,
      };

      if (depth > 1) {
        return true;
      }

      const entries = (grip.preview.entries = []);
      for (const entry of enumEntries) {
        entries.push(entry);
        if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) {
          break;
        }
      }

      return true;
    },
  ],


  HighlightRegistry: [
    function(objectActor, grip, depth) {
      const enumEntries = PropertyIterators.enumHighlightRegistryEntries(objectActor, depth);

      grip.preview = {
        kind: "MapLike",
        size: enumEntries.size,
      };

      if (depth > 1) {
        return true;
      }

      const entries = (grip.preview.entries = []);
      for (const entry of enumEntries) {
        entries.push(entry);
        if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) {
          break;
        }
      }

      return true;
    },
  ],

  MIDIInputMap: [
    function(objectActor, grip, depth) {
      const enumEntries = PropertyIterators.enumMidiInputMapEntries(
        objectActor,
        depth
      );

      grip.preview = {
        kind: "MapLike",
        size: enumEntries.size,
      };

      if (depth > 1) {
        return true;
      }

      const entries = (grip.preview.entries = []);
      for (const entry of enumEntries) {
        entries.push(entry);
        if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) {
          break;
        }
      }

      return true;
    },
  ],

  MIDIOutputMap: [
    function(objectActor, grip, depth) {
      const enumEntries = PropertyIterators.enumMidiOutputMapEntries(
        objectActor,
        depth
      );

      grip.preview = {
        kind: "MapLike",
        size: enumEntries.size,
      };

      if (depth > 1) {
        return true;
      }

      const entries = (grip.preview.entries = []);
      for (const entry of enumEntries) {
        entries.push(entry);
        if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) {
          break;
        }
      }

      return true;
    },
  ],

  DOMStringMap: [
    function(objectActor, grip, depth) {
      const { obj, safeRawObj } = objectActor;
      if (!safeRawObj) {
        return false;
      }

      const keys = obj.getOwnPropertyNames();
      grip.preview = {
        kind: "MapLike",
        size: keys.length,
      };

      if (depth > 1) {
        return true;
      }

      const entries = (grip.preview.entries = []);
      for (const key of keys) {
        const value = ObjectUtils.makeDebuggeeValueIfNeeded(obj, safeRawObj[key]);
        entries.push([key, objectActor.createValueGrip(value, depth)]);
        if (entries.length == OBJECT_PREVIEW_MAX_ITEMS) {
          break;
        }
      }

      return true;
    },
  ],

  Promise: [
    function(objectActor, grip, depth) {
      const { state, value, reason } = ObjectUtils.getPromiseState(objectActor.obj);
      const ownProperties = Object.create(null);
      ownProperties["<state>"] = { value: state };
      let ownPropertiesLength = 1;

      // Only expose <value> or <reason> in top-level promises, to avoid recursion.
      // <state> is not problematic because it's a string.
      if (depth === 1) {
        if (state == "fulfilled") {
          ownProperties["<value>"] = { value: objectActor.createValueGrip(value, depth) };
          ++ownPropertiesLength;
        } else if (state == "rejected") {
          ownProperties["<reason>"] = { value: objectActor.createValueGrip(reason, depth) };
          ++ownPropertiesLength;
        }
      }

      grip.preview = {
        kind: "Object",
        ownProperties,
        ownPropertiesLength,
      };

      return true;
    },
  ],

  Proxy: [
    function(objectActor, grip, depth) {
      // Only preview top-level proxies, avoiding recursion. Otherwise, since both the
      // target and handler can also be proxies, we could get an exponential behavior.
      if (depth > 1) {
        return true;
      }

      const { obj } = objectActor;

      // The `isProxy` getter of the debuggee object only detects proxies without
      // security wrappers. If false, the target and handler are not available.
      const hasTargetAndHandler = obj.isProxy;

      grip.preview = {
        kind: "Object",
        ownProperties: Object.create(null),
        ownPropertiesLength: 2 * hasTargetAndHandler,
      };

      if (hasTargetAndHandler) {
        Object.assign(grip.preview.ownProperties, {
          "<target>": { value: objectActor.createValueGrip(obj.proxyTarget, depth) },
          "<handler>": { value: objectActor.createValueGrip(obj.proxyHandler, depth) },
        });
      }

      return true;
    },
  ],

  CustomStateSet: [
    function(objectActor, grip, depth) {
      const size = DevToolsUtils.getProperty(objectActor.obj, "size");
      if (typeof size != "number") {
        return false;
      }

      grip.preview = {
        kind: "ArrayLike",
        length: size,
      };

      const items = (grip.preview.items = []);
      for (const item of PropertyIterators.enumCustomStateSetEntries(objectActor, depth)) {
        items.push(item);
        if (items.length == OBJECT_PREVIEW_MAX_ITEMS) {
          break;
        }
      }

      return true;
    },
  ],
};

/**
 * Generic previewer for classes wrapping primitives, like String,
 * Number and Boolean.
 *
 * @param object classObj
 *        The class to expect, eg. String. The valueOf() method of the class is
 *        invoked on the given object.
 * @param ObjectActor objectActor
 *        The object actor
 * @param Object grip
 *        The result grip to fill in
 * @param Number depth
 *        Depth of the object compared to the top level object,
 *        when we are inspecting nested attributes.
 * @return Booolean true if the object was handled, false otherwise
 */
function wrappedPrimitivePreviewer(
  classObj,
  objectActor,
  grip,
  depth
) {
  const { safeRawObj } = objectActor;
  let v = null;
  try {
    v = classObj.prototype.valueOf.call(safeRawObj);
  } catch (ex) {
    // valueOf() can throw if the raw JS object is "misbehaved".
    return false;
  }

  if (v === null) {
    return false;
  }

  const canHandle = GenericObject(objectActor, grip, depth);
  if (!canHandle) {
    return false;
  }

  grip.preview.wrappedValue = objectActor.createValueGrip(
    ObjectUtils.makeDebuggeeValueIfNeeded(objectActor.obj, v),
    depth
  );
  return true;
}

/**
 * @param {ObjectActor} objectActor
 * @param {Object} grip: The grip built by the objectActor, for which we need to populate
 *                       the `preview` property.
 * @param {Number} depth
 *        Depth of the object compared to the top level object,
 *        when we are inspecting nested attributes.
 * @returns
 */
function GenericObject(objectActor, grip, depth) {
  const { obj, safeRawObj } = objectActor;
  if (grip.preview || grip.displayString || depth > 1) {
    return false;
  }

  const preview = (grip.preview = {
    kind: "Object",
    ownProperties: Object.create(null),
  });

  const names = ObjectUtils.getPropNamesFromObject(obj, safeRawObj);
  preview.ownPropertiesLength = names.length;

  let length,
    i = 0;
  let specialStringBehavior = objectActor.className === "String";
  if (specialStringBehavior) {
    length = DevToolsUtils.getProperty(obj, "length");
    if (typeof length != "number") {
      specialStringBehavior = false;
    }
  }

  for (const name of names) {
    if (specialStringBehavior && /^[0-9]+$/.test(name)) {
      const num = parseInt(name, 10);
      if (num.toString() === name && num >= 0 && num < length) {
        continue;
      }
    }

    const desc = propertyDescriptor(objectActor, name, depth, true);
    if (!desc) {
      continue;
    }

    preview.ownProperties[name] = desc;
    if (++i == OBJECT_PREVIEW_MAX_ITEMS) {
      break;
    }
  }

  if (i === OBJECT_PREVIEW_MAX_ITEMS) {
    return true;
  }

  const privatePropertiesSymbols = ObjectUtils.getSafePrivatePropertiesSymbols(
    obj
  );
  if (privatePropertiesSymbols.length > 0) {
    preview.privatePropertiesLength = privatePropertiesSymbols.length;
    preview.privateProperties = [];

    // Retrieve private properties, which are represented as non-enumerable Symbols
    for (const privateProperty of privatePropertiesSymbols) {
      if (
        !privateProperty.description ||
        !privateProperty.description.startsWith("#")
      ) {
        continue;
      }
      const descriptor = propertyDescriptor(objectActor, privateProperty, depth);
      if (!descriptor) {
        continue;
      }

      preview.privateProperties.push(
        Object.assign(
          {
            descriptor,
          },
          objectActor.createValueGrip(privateProperty, depth)
        )
      );

      if (++i == OBJECT_PREVIEW_MAX_ITEMS) {
        break;
      }
    }
  }

  if (i === OBJECT_PREVIEW_MAX_ITEMS) {
    return true;
  }

  const symbols = ObjectUtils.getSafeOwnPropertySymbols(obj);
  if (symbols.length > 0) {
    preview.ownSymbolsLength = symbols.length;
    preview.ownSymbols = [];

    for (const symbol of symbols) {
      const descriptor = propertyDescriptor(objectActor, symbol, depth, true);
      if (!descriptor) {
        continue;
      }

      preview.ownSymbols.push(
        Object.assign(
          {
            descriptor,
          },
          objectActor.createValueGrip(symbol, depth)
        )
      );

      if (++i == OBJECT_PREVIEW_MAX_ITEMS) {
        break;
      }
    }
  }

  if (i === OBJECT_PREVIEW_MAX_ITEMS) {
    return true;
  }

  const safeGetterValues = objectActor._findSafeGetterValues(
    Object.keys(preview.ownProperties),
    depth,
    OBJECT_PREVIEW_MAX_ITEMS - i
  );
  if (Object.keys(safeGetterValues).length) {
    preview.safeGetterValues = safeGetterValues;
  }

  return true;
}

// Preview functions that do not rely on the object class.
previewers.Object = [
  function TypedArray({ obj }, grip, depth) {
    if (!ObjectUtils.isTypedArray(obj)) {
      return false;
    }

    grip.preview = {
      kind: "ArrayLike",
      length: ObjectUtils.getArrayLength(obj),
    };

    if (depth > 1) {
      return true;
    }

    const previewLength = Math.min(
      OBJECT_PREVIEW_MAX_ITEMS,
      grip.preview.length
    );
    grip.preview.items = [];
    for (let i = 0; i < previewLength; i++) {
      const desc = obj.getOwnPropertyDescriptor(i);
      if (!desc) {
        break;
      }
      grip.preview.items.push(desc.value);
    }

    return true;
  },

  function Error(objectActor, grip, depth) {
    if (!ERROR_CLASSNAMES.has(objectActor.className)) {
      return false;
    }

    const { obj } = objectActor;

    // The name and/or message could be getters, and even if it's unsafe, we do want
    // to show it to the user (See Bug 1710694).
    const name = DevToolsUtils.getProperty(obj, "name", true);
    const msg = DevToolsUtils.getProperty(obj, "message", true);
    const stack = DevToolsUtils.getProperty(obj, "stack");
    const fileName = DevToolsUtils.getProperty(obj, "fileName");
    const lineNumber = DevToolsUtils.getProperty(obj, "lineNumber");
    const columnNumber = DevToolsUtils.getProperty(obj, "columnNumber");

    grip.preview = {
      kind: "Error",
      name: objectActor.createValueGrip(name, depth),
      message: objectActor.createValueGrip(msg, depth),
      stack: objectActor.createValueGrip(stack, depth),
      fileName: objectActor.createValueGrip(fileName, depth),
      lineNumber: objectActor.createValueGrip(lineNumber, depth),
      columnNumber: objectActor.createValueGrip(columnNumber, depth),
    };

    const errorHasCause = obj.getOwnPropertyNames().includes("cause");
    if (errorHasCause) {
      grip.preview.cause = objectActor.createValueGrip(
        DevToolsUtils.getProperty(obj, "cause", true),
        depth
      );
    }

    return true;
  },

  function CSSMediaRule(objectActor, grip, depth) {
    const { safeRawObj } = objectActor;
    if (!safeRawObj || objectActor.className != "CSSMediaRule" || isWorker) {
      return false;
    }
    grip.preview = {
      kind: "ObjectWithText",
      text: objectActor.createValueGrip(safeRawObj.conditionText, depth),
    };
    return true;
  },

  function CSSStyleRule(objectActor, grip, depth) {
    const { safeRawObj } = objectActor;
    if (!safeRawObj || objectActor.className != "CSSStyleRule" || isWorker) {
      return false;
    }
    grip.preview = {
      kind: "ObjectWithText",
      text: objectActor.createValueGrip(safeRawObj.selectorText, depth),
    };
    return true;
  },

  function ObjectWithURL(objectActor, grip, depth) {
    const { safeRawObj } = objectActor;
    if (isWorker || !safeRawObj) {
      return false;
    }

    const isWindow = Window.isInstance(safeRawObj);
    if (!OBJECT_WITH_URL_CLASSNAMES.has(objectActor.className) && !isWindow) {
      return false;
    }

    let url;
    if (isWindow && safeRawObj.location) {
      try {
        url = safeRawObj.location.href;
      } catch(e) {
        // This can happen when we have a cross-process window.
        // In such case, let's retrieve the url from the iframe.
        // For window.top from a remote iframe, there's no way we can't retrieve the URL,
        // so return a label that help user know what's going on.
        url = safeRawObj.browsingContext?.embedderElement?.src || "Restricted";
      }
    } else if (safeRawObj.href) {
      url = safeRawObj.href;
    } else {
      return false;
    }

    grip.preview = {
      kind: "ObjectWithURL",
      url: objectActor.createValueGrip(url, depth),
    };

    return true;
  },

  function ArrayLike(objectActor, grip, depth) {
    const { safeRawObj } = objectActor;
    if (
      !safeRawObj ||
      !ARRAY_LIKE_CLASSNAMES.has(objectActor.className) ||
      typeof safeRawObj.length != "number" ||
      isWorker
    ) {
      return false;
    }

    grip.preview = {
      kind: "ArrayLike",
      length: safeRawObj.length,
    };

    if (depth > 1) {
      return true;
    }

    const items = (grip.preview.items = []);

    for (
      let i = 0;
      i < safeRawObj.length && items.length < OBJECT_PREVIEW_MAX_ITEMS;
      i++
    ) {
      const value = ObjectUtils.makeDebuggeeValueIfNeeded(objectActor.obj, safeRawObj[i]);
      items.push(objectActor.createValueGrip(value, depth));
    }

    return true;
  },

  function CSSStyleDeclaration(objectActor, grip, depth) {
    const { safeRawObj, className } = objectActor;
    if (
      !safeRawObj ||
      (className != "CSSStyleDeclaration" && className != "CSS2Properties") ||
      isWorker
    ) {
      return false;
    }

    grip.preview = {
      kind: "MapLike",
      size: safeRawObj.length,
    };

    const entries = (grip.preview.entries = []);

    for (let i = 0; i < OBJECT_PREVIEW_MAX_ITEMS && i < safeRawObj.length; i++) {
      const prop = safeRawObj[i];
      const value = safeRawObj.getPropertyValue(prop);
      entries.push([prop, objectActor.createValueGrip(value, depth)]);
    }

    return true;
  },

  function DOMNode(objectActor, grip, depth) {
    const { safeRawObj } = objectActor;
    if (
      objectActor.className == "Object" ||
      !safeRawObj ||
      !Node.isInstance(safeRawObj) ||
      isWorker
    ) {
      return false;
    }

    const { obj, className } = objectActor;

    const preview = (grip.preview = {
      kind: "DOMNode",
      nodeType: safeRawObj.nodeType,
      nodeName: safeRawObj.nodeName,
      isConnected: safeRawObj.isConnected === true,
    });

    if (safeRawObj.nodeType == safeRawObj.DOCUMENT_NODE && safeRawObj.location) {
      preview.location = objectActor.createValueGrip(safeRawObj.location.href, depth);
    } else if (className == "DocumentFragment") {
      preview.childNodesLength = safeRawObj.childNodes.length;

      if (depth < 2) {
        preview.childNodes = [];
        for (const node of safeRawObj.childNodes) {
          const actor = objectActor.createValueGrip(obj.makeDebuggeeValue(node), depth);
          preview.childNodes.push(actor);
          if (preview.childNodes.length == OBJECT_PREVIEW_MAX_ITEMS) {
            break;
          }
        }
      }
    } else if (Element.isInstance(safeRawObj)) {
      // For HTML elements (in an HTML document, at least), the nodeName is an
      // uppercased version of the actual element name.  Check for HTML
      // elements, that is elements in the HTML namespace, and lowercase the
      // nodeName in that case.
      if (safeRawObj.namespaceURI == "http://www.w3.org/1999/xhtml") {
        preview.nodeName = preview.nodeName.toLowerCase();
      }

      // Add preview for DOM element attributes.
      preview.attributes = {};
      preview.attributesLength = safeRawObj.attributes.length;
      for (const attr of safeRawObj.attributes) {
        preview.attributes[attr.nodeName] = objectActor.createValueGrip(attr.value, depth);
      }
    } else if (className == "Attr") {
      preview.value = objectActor.createValueGrip(safeRawObj.value, depth);
    } else if (
      className == "Text" ||
      className == "CDATASection" ||
      className == "Comment"
    ) {
      preview.textContent = objectActor.createValueGrip(safeRawObj.textContent, depth);
    }

    return true;
  },

  function DOMEvent(objectActor, grip, depth) {
    const { safeRawObj } = objectActor;
    if (!safeRawObj || !Event.isInstance(safeRawObj) || isWorker) {
      return false;
    }

    const { obj, className } = objectActor;
    const preview = (grip.preview = {
      kind: "DOMEvent",
      type: safeRawObj.type,
      properties: Object.create(null),
    });

    if (depth < 2) {
      const target = obj.makeDebuggeeValue(safeRawObj.target);
      preview.target = objectActor.createValueGrip(target, depth);
    }

    if (className == "KeyboardEvent") {
      preview.eventKind = "key";
      preview.modifiers = ObjectUtils.getModifiersForEvent(safeRawObj);
    }

    const props = ObjectUtils.getPropsForEvent(className);

    // Add event-specific properties.
    for (const prop of props) {
      let value = safeRawObj[prop];
      if (ObjectUtils.isObjectOrFunction(value)) {
        // Skip properties pointing to objects.
        if (depth > 1) {
          continue;
        }
        value = obj.makeDebuggeeValue(value);
      }
      preview.properties[prop] = objectActor.createValueGrip(value, depth);
    }

    // Add any properties we find on the event object.
    if (!props.length) {
      let i = 0;
      for (const prop in safeRawObj) {
        let value = safeRawObj[prop];
        if (
          prop == "target" ||
          prop == "type" ||
          value === null ||
          typeof value == "function"
        ) {
          continue;
        }
        if (value && typeof value == "object") {
          if (depth > 1) {
            continue;
          }
          value = obj.makeDebuggeeValue(value);
        }
        preview.properties[prop] = objectActor.createValueGrip(value, depth);
        if (++i == OBJECT_PREVIEW_MAX_ITEMS) {
          break;
        }
      }
    }

    return true;
  },

  function DOMException(objectActor, grip, depth) {
    const { safeRawObj } = objectActor;
    if (!safeRawObj || objectActor.className !== "DOMException" || isWorker) {
      return false;
    }

    grip.preview = {
      kind: "DOMException",
      name: objectActor.createValueGrip(safeRawObj.name, depth),
      message: objectActor.createValueGrip(safeRawObj.message, depth),
      code: objectActor.createValueGrip(safeRawObj.code, depth),
      result: objectActor.createValueGrip(safeRawObj.result, depth),
      filename: objectActor.createValueGrip(safeRawObj.filename, depth),
      lineNumber: objectActor.createValueGrip(safeRawObj.lineNumber, depth),
      columnNumber: objectActor.createValueGrip(safeRawObj.columnNumber, depth),
      stack: objectActor.createValueGrip(safeRawObj.stack, depth),
    };

    return true;
  },

  function Object(objectActor, grip, depth) {
    return GenericObject(objectActor, grip, depth);
  },
];

module.exports = previewers;
