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

import {
  createFactory,
  Component,
} from "resource://devtools/client/shared/vendor/react.mjs";
import * as dom from "resource://devtools/client/shared/vendor/react-dom-factories.mjs";
import PropTypes from "resource://devtools/client/shared/vendor/react-prop-types.mjs";
import { createFactories } from "resource://devtools/client/shared/react-utils.mjs";

import TreeViewClass from "resource://devtools/client/shared/components/tree/TreeView.mjs";
import { BucketProperty } from "resource://devtools/client/shared/components/tree/ObjectProvider.mjs";
import JsonToolbarClass from "resource://devtools/client/jsonview/components/JsonToolbar.mjs";

import {
  JSON_NUMBER,
  MODE,
} from "resource://devtools/client/shared/components/reps/reps/constants.mjs";
import { Rep } from "resource://devtools/client/shared/components/reps/reps/rep.mjs";

const TreeView = createFactory(TreeViewClass);
const { JsonToolbar } = createFactories(JsonToolbarClass);
const { div } = dom;

const MAX_STRING_LENGTH = 250;

function isObject(value) {
  return Object(value) === value;
}

/**
 * This template represents the 'JSON' panel. The panel is
 * responsible for rendering an expandable tree that allows simple
 * inspection of JSON structure.
 */
class JsonPanel extends Component {
  static get propTypes() {
    return {
      data: PropTypes.oneOfType([
        PropTypes.string,
        PropTypes.array,
        PropTypes.object,
        PropTypes.bool,
        PropTypes.number,
      ]),
      dataSize: PropTypes.number,
      expandedNodes: PropTypes.instanceOf(Set),
      searchFilter: PropTypes.string,
      actions: PropTypes.object,
    };
  }

  constructor(props) {
    super(props);
    this.state = {};
    this.onKeyPress = this.onKeyPress.bind(this);
    this.onFilter = this.onFilter.bind(this);
    this.renderValue = this.renderValue.bind(this);
    this.renderTree = this.renderTree.bind(this);
  }

  componentDidMount() {
    document.addEventListener("keypress", this.onKeyPress, true);
    document.getElementById("json-scrolling-panel").focus();
  }

  componentWillUnmount() {
    document.removeEventListener("keypress", this.onKeyPress, true);
  }

  onKeyPress() {
    // XXX shortcut for focusing the Filter field (see Bug 1178771).
  }

  onFilter(object) {
    if (!this.props.searchFilter) {
      return true;
    }

    const searchFilter = this.props.searchFilter.toLowerCase();

    // For bucket nodes, check if any of their children match
    if (object instanceof BucketProperty) {
      const { object: array, startIndex, endIndex } = object;
      for (let i = startIndex; i <= endIndex; i++) {
        const childJson = JSON.stringify(array[i]);
        if (childJson.toLowerCase().includes(searchFilter)) {
          return true;
        }
      }
      return false;
    }

    const json = object.name + JSON.stringify(object.value);
    return json.toLowerCase().includes(searchFilter);
  }

  renderValue(props) {
    const member = props.member;

    // Hide value for bucket nodes (they show ranges like [0…99])
    if (member.type === "bucket") {
      return null;
    }

    // Hide object summary when non-empty object is expanded (bug 1244912).
    if (isObject(member.value) && member.hasChildren && member.open) {
      return null;
    }

    // Render the value (summary) using Reps library.
    return Rep(
      Object.assign({}, props, {
        cropLimit: MAX_STRING_LENGTH,
        noGrip: true,
        isInContentPage: true,
      })
    );
  }

  renderTree() {
    // Append custom column for displaying values. This column
    // Take all available horizontal space.
    const columns = [
      {
        id: "value",
        width: "100%",
      },
    ];

    // Render tree component.
    return TreeView({
      object: this.props.data,
      mode: MODE.LONG,
      bucketLargeArrays: true,
      onFilter: this.onFilter,
      columns,
      renderValue: this.renderValue,
      expandedNodes: this.props.expandedNodes,
      maxStringLength: MAX_STRING_LENGTH,
    });
  }

  render() {
    let content;
    const data = this.props.data;

    if (!isObject(data)) {
      content = div(
        { className: "jsonPrimitiveValue" },
        Rep({
          object: data,
        })
      );
    } else if (data instanceof Error) {
      content = div({ className: "jsonParseError" }, data + "");
    } else if (data.type === JSON_NUMBER) {
      content = div(
        { className: "jsonPrimitiveValue" },
        Rep({
          object: data,
          noGrip: true,
        })
      );
    } else {
      content = this.renderTree();
    }

    return div(
      { className: "jsonPanelBox tab-panel-inner" },
      JsonToolbar({
        actions: this.props.actions,
        dataSize: this.props.dataSize,
      }),
      div(
        {
          className: "panelContent",
          id: "json-scrolling-panel",
          tabIndex: 0,
        },
        content
      )
    );
  }
}

export default { JsonPanel };
