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

/* eslint no-shadow: ["error", { "allow": ["name", "length"] }] */

import PropTypes from "resource://devtools/client/shared/vendor/react-prop-types.mjs";
import { span } from "resource://devtools/client/shared/vendor/react-dom-factories.mjs";

import { interleave, wrapRender } from "./rep-utils.mjs";
import PropRep from "./prop-rep.mjs";
import { MODE } from "./constants.mjs";

/**
 * Renders generic grip. Grip is client representation
 * of remote JS object and is used as an input object
 * for this rep component.
 */

GripRep.propTypes = {
  object: PropTypes.object.isRequired,
  mode: PropTypes.oneOf(Object.values(MODE)),
  isInterestingProp: PropTypes.func,
  title: PropTypes.string,
  onDOMNodeMouseOver: PropTypes.func,
  onDOMNodeMouseOut: PropTypes.func,
  onInspectIconClick: PropTypes.func,
  noGrip: PropTypes.bool,
  shouldRenderTooltip: PropTypes.bool,
};

const DEFAULT_TITLE = "Object";

function GripRep(props) {
  const { mode = MODE.SHORT, object, shouldRenderTooltip } = props;

  const config = {
    "data-link-actor-id": object.actor,
    className: "objectBox objectBox-object",
  };

  if (mode === MODE.TINY) {
    const propertiesLength = getPropertiesLength(object);

    const tinyModeItems = [];
    if (getTitle(props, object) !== DEFAULT_TITLE) {
      tinyModeItems.push(getTitleElement(props, object));
    } else {
      tinyModeItems.push(
        span(
          {
            className: "objectLeftBrace",
          },
          "{"
        ),
        propertiesLength > 0
          ? span(
              {
                key: "more",
                className: "more-ellipsis",
              },
              "…"
            )
          : null,
        span(
          {
            className: "objectRightBrace",
          },
          "}"
        )
      );
    }

    config.title = shouldRenderTooltip ? getTitle(props, object) : null;

    return span(config, ...tinyModeItems);
  }

  if (mode === MODE.HEADER) {
    config.title = shouldRenderTooltip ? getTitle(props, object) : null;
    return span(config, getTitleElement(props, object));
  }

  const propsArray = safePropIterator(props, object, maxLengthMap.get(mode));

  config.title = shouldRenderTooltip ? getTitle(props, object) : null;

  return span(
    config,
    getTitleElement(props, object),
    span(
      {
        className: "objectLeftBrace",
      },
      " { "
    ),
    ...interleave(propsArray, ", "),
    span(
      {
        className: "objectRightBrace",
      },
      " }"
    )
  );
}

function getTitleElement(props, object) {
  return span(
    {
      className: "objectTitle",
    },
    getTitle(props, object)
  );
}

function getTitle(props, object) {
  return props.title || object.class || DEFAULT_TITLE;
}

function getPropertiesLength(object) {
  let propertiesLength =
    object.preview && object.preview.ownPropertiesLength
      ? object.preview.ownPropertiesLength
      : object.ownPropertyLength;

  if (object.preview && object.preview.safeGetterValues) {
    propertiesLength += Object.keys(object.preview.safeGetterValues).length;
  }

  if (object.preview && object.preview.ownSymbols) {
    propertiesLength += object.preview.ownSymbolsLength;
  }

  if (object.preview && object.preview.privateProperties) {
    propertiesLength += object.preview.privatePropertiesLength;
  }

  return propertiesLength;
}

function safePropIterator(props, object, max) {
  max = typeof max === "undefined" ? maxLengthMap.get(MODE.SHORT) : max;
  try {
    return propIterator(props, object, max);
  } catch (err) {
    console.error(err);
  }
  return [];
}

function propIterator(props, object, max) {
  if (object.preview && Object.keys(object.preview).includes("wrappedValue")) {
    return [
      props.Rep({
        object: object.preview.wrappedValue,
        mode: props.mode || MODE.TINY,
        defaultRep: Grip,
      }),
    ];
  }

  // Property filter. Show only interesting properties to the user.
  const isInterestingProp =
    props.isInterestingProp ||
    ((type, value) => {
      return (
        type == "boolean" ||
        type == "number" ||
        (type == "string" && !!value.length)
      );
    });

  let properties = object.preview ? object.preview.ownProperties || {} : {};

  const propertiesLength = getPropertiesLength(object);

  if (object.preview && object.preview.safeGetterValues) {
    properties = { ...properties, ...object.preview.safeGetterValues };
  }

  let indexes = getPropIndexes(properties, max, isInterestingProp);
  if (indexes.length < max && indexes.length < propertiesLength) {
    // There are not enough props yet.
    // Then add uninteresting props to display them.
    indexes = indexes.concat(
      getPropIndexes(properties, max - indexes.length, (t, value, name) => {
        return !isInterestingProp(t, value, name);
      })
    );
  }

  // The server synthesizes some property names for a Proxy, like
  // <target> and <handler>; we don't want to quote these because,
  // as synthetic properties, they appear more natural when
  // unquoted. Analogous for a Promise.
  const suppressQuotes = ["Proxy", "Promise"].includes(object.class);
  const propsArray = getProps(props, properties, indexes, suppressQuotes);

  // Show private properties
  if (object.preview && object.preview.privateProperties) {
    const { privateProperties } = object.preview;
    const length = max - indexes.length;

    const privateProps = privateProperties.slice(0, length).map(item => {
      const value = item.descriptor.value;
      const grip = value && value.getGrip ? value.getGrip() : value;

      return PropRep({
        ...props,
        keyClassName: "private",
        mode: MODE.TINY,
        name: item.name,
        object: grip,
        equal: ": ",
        defaultRep: Grip,
        title: null,
        suppressQuotes: true,
      });
    });

    propsArray.push(...privateProps);
  }

  // Show symbols.
  if (object.preview && object.preview.ownSymbols) {
    const { ownSymbols } = object.preview;
    const length = max - indexes.length;

    const symbolsProps = ownSymbols.slice(0, length).map(symbolItem => {
      const symbolValue = symbolItem.descriptor.value;
      const symbolGrip =
        symbolValue && symbolValue.getGrip
          ? symbolValue.getGrip()
          : symbolValue;

      return PropRep({
        ...props,
        mode: MODE.TINY,
        name: symbolItem,
        object: symbolGrip,
        equal: ": ",
        defaultRep: Grip,
        title: null,
        suppressQuotes,
      });
    });

    propsArray.push(...symbolsProps);
  }

  if (
    Object.keys(properties).length > max ||
    propertiesLength > max ||
    // When the object has non-enumerable properties, we don't have them in the
    // packet, but we might want to show there's something in the object.
    propertiesLength > propsArray.length
  ) {
    // There are some undisplayed props. Then display "more...".
    propsArray.push(
      span(
        {
          key: "more",
          className: "more-ellipsis",
        },
        "…"
      )
    );
  }

  return propsArray;
}

/**
 * Get props ordered by index.
 *
 * @param {object} componentProps Grip Component props.
 * @param {object} properties Properties of the object the Grip describes.
 * @param {Array} indexes Indexes of properties.
 * @param {boolean} suppressQuotes true if we should suppress quotes
 *                  on property names.
 * @return {Array} Props.
 */
function getProps(componentProps, properties, indexes, suppressQuotes) {
  // Make indexes ordered by ascending.
  indexes.sort(function (a, b) {
    return a - b;
  });

  const propertiesKeys = Object.keys(properties);
  return indexes.map(i => {
    const name = propertiesKeys[i];
    const value = getPropValue(properties[name]);

    return PropRep({
      ...componentProps,
      mode: MODE.TINY,
      name,
      object: value,
      equal: ": ",
      defaultRep: Grip,
      title: null,
      suppressQuotes,
    });
  });
}

/**
 * Get the indexes of props in the object.
 *
 * @param {object} properties Props object.
 * @param {number} max The maximum length of indexes array.
 * @param {Function} filter Filter the props you want.
 * @return {Array} Indexes of interesting props in the object.
 */
function getPropIndexes(properties, max, filter) {
  const indexes = [];

  try {
    let i = 0;
    for (const name in properties) {
      if (indexes.length >= max) {
        return indexes;
      }

      // Type is specified in grip's "class" field and for primitive
      // values use typeof.
      const value = getPropValue(properties[name]);
      let type = value.class || typeof value;
      type = type.toLowerCase();

      if (filter(type, value, name)) {
        indexes.push(i);
      }
      i++;
    }
  } catch (err) {
    console.error(err);
  }
  return indexes;
}

/**
 * Get the actual value of a property.
 *
 * @param {object} property
 * @return {object} Value of the property.
 */
function getPropValue(property) {
  let value = property;
  if (typeof property === "object") {
    const keys = Object.keys(property);
    if (keys.includes("value")) {
      value = property.value;
    } else if (keys.includes("getterValue")) {
      value = property.getterValue;
    }
  }
  return value;
}

// Registration
function supportsObject(object) {
  if (object?.class === "DeadObject") {
    return true;
  }

  return object?.preview
    ? typeof object.preview.ownProperties !== "undefined"
    : typeof object?.ownPropertyLength !== "undefined";
}

const maxLengthMap = new Map();
maxLengthMap.set(MODE.SHORT, 3);
maxLengthMap.set(MODE.LONG, 10);

// Grip is used in propIterator and has to be defined here.
const rep = wrapRender(GripRep);
const Grip = {
  rep,
  supportsObject,
  maxLengthMap,
};

// Exports from this module
export { rep, supportsObject, maxLengthMap };
