/* 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 {
  Component,
  createRef,
  createFactory,
} = require("resource://devtools/client/shared/vendor/react.js");
const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js");
const { div, span } = dom;
const Actions = require("resource://devtools/client/netmonitor/src/actions/index.js");
const {
  PANELS,
} = require("resource://devtools/client/netmonitor/src/constants.js");
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js");
const {
  connect,
} = require("resource://devtools/client/shared/redux/visibility-handler-connect.js");
const TreeViewClass = require("resource://devtools/client/shared/components/tree/TreeView.js");
const TreeView = createFactory(TreeViewClass);
const LabelCell = createFactory(
  require("resource://devtools/client/shared/components/tree/LabelCell.js")
);
const {
  SearchProvider,
} = require("resource://devtools/client/netmonitor/src/components/search/search-provider.js");
const Toolbar = createFactory(
  require("resource://devtools/client/netmonitor/src/components/search/Toolbar.js")
);
const StatusBar = createFactory(
  require("resource://devtools/client/netmonitor/src/components/search/StatusBar.js")
);
const {
  limitTooltipLength,
} = require("resource://devtools/client/netmonitor/src/utils/tooltips.js");
// There are two levels in the search panel tree hierarchy:
// 0: Resource - represents the source request object
// 1: Search Result - represents a match coming from the parent resource
const RESOURCE_LEVEL = 0;
const SEARCH_RESULT_LEVEL = 1;

/**
 * This component is responsible for rendering all search results
 * coming from the current search.
 */
class SearchPanel extends Component {
  static get propTypes() {
    return {
      clearSearchResults: PropTypes.func.isRequired,
      openSearch: PropTypes.func.isRequired,
      closeSearch: PropTypes.func.isRequired,
      search: PropTypes.func.isRequired,
      caseSensitive: PropTypes.bool,
      connector: PropTypes.object.isRequired,
      addSearchQuery: PropTypes.func.isRequired,
      query: PropTypes.string.isRequired,
      results: PropTypes.array,
      navigate: PropTypes.func.isRequired,
      isDisplaying: PropTypes.bool.isRequired,
    };
  }

  constructor(props) {
    super(props);

    this.searchboxRef = createRef();
    this.renderValue = this.renderValue.bind(this);
    this.renderLabel = this.renderLabel.bind(this);
    this.onClickTreeRow = this.onClickTreeRow.bind(this);
    this.provider = SearchProvider;
  }

  componentDidMount() {
    if (this.searchboxRef) {
      this.searchboxRef.current.focus();
    }
  }

  componentDidUpdate(prevProps) {
    if (this.props.isDisplaying && !prevProps.isDisplaying) {
      this.searchboxRef.current.focus();
    }
  }

  onClickTreeRow(path, event, member) {
    if (member.object.parentResource) {
      this.props.navigate(member.object);
    }
  }

  /**
   * Custom TreeView label rendering. The search result
   * value isn't rendered in separate column, but in the
   * same column as the label (to save space).
   */
  renderLabel(props) {
    const { member } = props;
    const level = member.level || 0;
    const className = level == RESOURCE_LEVEL ? "resourceCell" : "resultCell";

    // Customize label rendering by adding a suffix/value
    const renderSuffix = () => {
      return dom.span(
        {
          className,
        },
        " ",
        this.renderValue(props)
      );
    };

    return LabelCell({
      ...props,
      title:
        member.level == 1
          ? limitTooltipLength(member.object.value)
          : this.provider.getResourceTooltipLabel(member.object),
      renderSuffix,
    });
  }

  renderTree() {
    const { results } = this.props;
    return TreeView({
      object: results,
      provider: this.provider,
      expandableStrings: false,
      renderLabelCell: this.renderLabel,
      columns: [],
      onClickRow: this.onClickTreeRow,
    });
  }

  /**
   * Custom tree value rendering. This method is responsible for
   * rendering highlighted query string within the search result
   * result tree.
   */
  renderValue(props) {
    const { member } = props;
    const { query, caseSensitive } = this.props;

    // Handle only second level (zero based) that displays
    // the search result. Find the query string inside the
    // search result value (`props.object`) and render it
    // within a span element with proper class name.
    // level 0 = resource name
    if (member.level === SEARCH_RESULT_LEVEL) {
      const { object } = member;

      // Handles multiple matches in a string
      if (object.startIndex && object.startIndex.length > 1) {
        let indexStart = 0;
        const allMatches = object.startIndex.map((match, index) => {
          if (index === 0) {
            indexStart = match - 50;
          }

          const highlightedMatch = [
            span(
              { key: "match-" + match },
              object.value.substring(indexStart, match - query.length)
            ),
            span(
              {
                className: "query-match",
                key: "match-" + match + "-highlight",
              },
              object.value.substring(match - query.length, match)
            ),
          ];

          indexStart = match;

          return highlightedMatch;
        });

        return span(
          {
            title: limitTooltipLength(object.value),
          },
          allMatches
        );
      }

      const indexStart = caseSensitive
        ? object.value.indexOf(query)
        : object.value.toLowerCase().indexOf(query.toLowerCase());
      const indexEnd = indexStart + query.length;

      // Handles a match in a string
      if (indexStart >= 0) {
        return span(
          { title: limitTooltipLength(object.value) },
          span({}, object.value.substring(0, indexStart)),
          span(
            { className: "query-match" },
            object.value.substring(indexStart, indexStart + query.length)
          ),
          span({}, object.value.substring(indexEnd, object.value.length))
        );
      }

      // Default for key:value matches where query might not
      // be present in the value, but found in the key.
      return span(
        { title: limitTooltipLength(object.value) },
        span({}, object.value)
      );
    }

    return this.provider.getValue(member.object);
  }

  render() {
    const {
      openSearch,
      closeSearch,
      clearSearchResults,
      connector,
      addSearchQuery,
      search,
    } = this.props;
    return div(
      { className: "search-panel", style: { width: "100%" } },
      Toolbar({
        searchboxRef: this.searchboxRef,
        openSearch,
        closeSearch,
        clearSearchResults,
        addSearchQuery,
        search,
        connector,
      }),
      div(
        { className: "search-panel-content", style: { width: "100%" } },
        this.renderTree()
      ),
      StatusBar()
    );
  }
}

module.exports = connect(
  state => ({
    query: state.search.query,
    caseSensitive: state.search.caseSensitive,
    results: state.search.results,
    ongoingSearch: state.search.ongoingSearch,
    isDisplaying: state.ui.selectedActionBarTabId === PANELS.SEARCH,
    status: state.search.status,
  }),
  dispatch => ({
    closeSearch: () => dispatch(Actions.closeSearch()),
    openSearch: () => dispatch(Actions.openSearch()),
    search: () => dispatch(Actions.search()),
    clearSearchResults: () => dispatch(Actions.clearSearchResults()),
    addSearchQuery: query => dispatch(Actions.addSearchQuery(query)),
    navigate: searchResult => dispatch(Actions.navigate(searchResult)),
  })
)(SearchPanel);
