/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* 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";

// Make this available to both AMD and CJS environments
define(function (require, exports, module) {
  // ReactJS
  const React = require("devtools/client/shared/vendor/react");
  // Dependencies
  const { createFactories, isGrip } = require("./rep-utils");
  const { Caption } = createFactories(require("./caption"));
  const { PropRep } = createFactories(require("./prop-rep"));
  // Shortcuts
  const { span } = React.DOM;

  /**
   * Renders generic grip. Grip is client representation
   * of remote JS object and is used as an input object
   * for this rep component.
   */
  const GripRep = React.createClass({
    displayName: "Grip",

    propTypes: {
      object: React.PropTypes.object.isRequired,
      mode: React.PropTypes.string,
      isInterestingProp: React.PropTypes.func
    },

    getTitle: function (object) {
      if (this.props.objectLink) {
        return this.props.objectLink({
          object: object
        }, object.class);
      }
      return object.class || "Object";
    },

    safePropIterator: function (object, max) {
      max = (typeof max === "undefined") ? 3 : max;
      try {
        return this.propIterator(object, max);
      } catch (err) {
        console.error(err);
      }
      return [];
    },

    propIterator: function (object, max) {
      if (object.preview && Object.keys(object.preview).includes("wrappedValue")) {
        const { Rep } = createFactories(require("./rep"));

        return [Rep({
          object: object.preview.wrappedValue,
          mode: this.props.mode || "tiny",
          defaultRep: Grip,
        })];
      }

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

      let properties = object.preview
        ? object.preview.ownProperties
        : {};
      let propertiesLength = object.preview && object.preview.ownPropertiesLength
        ? object.preview.ownPropertiesLength
        : object.ownPropertyLength;

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

      let indexes = this.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(
          this.getPropIndexes(properties, max - indexes.length, (t, value, name) => {
            return !isInterestingProp(t, value, name);
          })
        );
      }

      const truncate = Object.keys(properties).length > max;
      let props = this.getProps(properties, indexes, truncate);
      if (truncate) {
        // There are some undisplayed props. Then display "more...".
        let objectLink = this.props.objectLink || span;

        props.push(Caption({
          object: objectLink({
            object: object
          }, `${object.ownPropertyLength - max} more…`)
        }));
      }

      return props;
    },

    /**
     * Get props ordered by index.
     *
     * @param {Object} properties Props object.
     * @param {Array} indexes Indexes of props.
     * @param {Boolean} truncate true if the grip will be truncated.
     * @return {Array} Props.
     */
    getProps: function (properties, indexes, truncate) {
      let props = [];

      // Make indexes ordered by ascending.
      indexes.sort(function (a, b) {
        return a - b;
      });

      indexes.forEach((i) => {
        let name = Object.keys(properties)[i];
        let value = this.getPropValue(properties[name]);

        props.push(PropRep(Object.assign({}, this.props, {
          mode: "tiny",
          name: name,
          object: value,
          equal: ": ",
          delim: i !== indexes.length - 1 || truncate ? ", " : "",
          defaultRep: Grip
        })));
      });

      return props;
    },

    /**
     * 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.
     */
    getPropIndexes: function (properties, max, filter) {
      let indexes = [];

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

          // Type is specified in grip's "class" field and for primitive
          // values use typeof.
          let value = this.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.
     */
    getPropValue: function (property) {
      let value = property;
      if (typeof property === "object") {
        let keys = Object.keys(property);
        if (keys.includes("value")) {
          value = property.value;
        } else if (keys.includes("getterValue")) {
          value = property.getterValue;
        }
      }
      return value;
    },

    render: function () {
      let object = this.props.object;
      let props = this.safePropIterator(object,
        (this.props.mode == "long") ? 100 : 3);

      let objectLink = this.props.objectLink || span;
      if (this.props.mode == "tiny") {
        return (
          span({className: "objectBox objectBox-object"},
            this.getTitle(object),
            objectLink({
              className: "objectLeftBrace",
              object: object
            }, "")
          )
        );
      }

      return (
        span({className: "objectBox objectBox-object"},
          this.getTitle(object),
          objectLink({
            className: "objectLeftBrace",
            object: object
          }, " { "),
          ...props,
          objectLink({
            className: "objectRightBrace",
            object: object
          }, " }")
        )
      );
    },
  });

  // Registration
  function supportsObject(object, type) {
    if (!isGrip(object)) {
      return false;
    }
    return (object.preview && object.preview.ownProperties);
  }

  let Grip = {
    rep: GripRep,
    supportsObject: supportsObject
  };

  // Exports from this module
  exports.Grip = Grip;
});
