/* 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";

/* global EVENTS */

// React & Redux
const {
  Component,
  createFactory,
} = require("resource://devtools/client/shared/vendor/react.mjs");
const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs");
const {
  connect,
} = require("resource://devtools/client/shared/vendor/react-redux.js");

const TreeView = createFactory(
  ChromeUtils.importESModule(
    "resource://devtools/client/shared/components/tree/TreeView.mjs"
  ).default
);
// Reps
const { MODE } = ChromeUtils.importESModule(
  "resource://devtools/client/shared/components/reps/index.mjs"
);

const {
  fetchChildren,
} = require("resource://devtools/client/accessibility/actions/accessibles.js");

const {
  L10N,
} = require("resource://devtools/client/accessibility/utils/l10n.js");
const {
  isFiltered,
} = require("resource://devtools/client/accessibility/utils/audit.js");
const AccessibilityRow = createFactory(
  require("resource://devtools/client/accessibility/components/AccessibilityRow.js")
);
const AccessibilityRowValue = createFactory(
  require("resource://devtools/client/accessibility/components/AccessibilityRowValue.js")
);
const {
  Provider,
} = require("resource://devtools/client/accessibility/provider.js");

const { scrollIntoView } = ChromeUtils.importESModule(
  "resource://devtools/client/shared/scroll.mjs"
);

/**
 * Renders Accessibility panel tree.
 */
class AccessibilityTree extends Component {
  static get propTypes() {
    return {
      toolboxDoc: PropTypes.object.isRequired,
      dispatch: PropTypes.func.isRequired,
      accessibles: PropTypes.object,
      expanded: PropTypes.object,
      selected: PropTypes.string,
      highlighted: PropTypes.object,
      filtered: PropTypes.bool,
      getAccessibilityTreeRoot: PropTypes.func.isRequired,
      startListeningForAccessibilityEvents: PropTypes.func.isRequired,
      stopListeningForAccessibilityEvents: PropTypes.func.isRequired,
      highlightAccessible: PropTypes.func.isRequired,
      unhighlightAccessible: PropTypes.func.isRequired,
    };
  }

  constructor(props) {
    super(props);

    this.onNameChange = this.onNameChange.bind(this);
    this.onReorder = this.onReorder.bind(this);
    this.onTextChange = this.onTextChange.bind(this);
    this.renderValue = this.renderValue.bind(this);
    this.scrollSelectedRowIntoView = this.scrollSelectedRowIntoView.bind(this);
  }

  /**
   * Add accessibility event listeners that affect tree rendering and updates.
   */
  // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1774507
  UNSAFE_componentWillMount() {
    this.props.startListeningForAccessibilityEvents({
      reorder: this.onReorder,
      "name-change": this.onNameChange,
      "text-change": this.onTextChange,
    });
    window.on(
      EVENTS.NEW_ACCESSIBLE_FRONT_INSPECTED,
      this.scrollSelectedRowIntoView
    );
    return null;
  }

  componentDidUpdate(prevProps) {
    // When filtering is toggled, make sure that the selected row remains in
    // view.
    if (this.props.filtered !== prevProps.filtered) {
      this.scrollSelectedRowIntoView();
    }

    window.emit(EVENTS.ACCESSIBILITY_INSPECTOR_UPDATED);
  }

  /**
   * Remove accessible event listeners.
   */
  componentWillUnmount() {
    this.props.stopListeningForAccessibilityEvents({
      reorder: this.onReorder,
      "name-change": this.onNameChange,
      "text-change": this.onTextChange,
    });

    window.off(
      EVENTS.NEW_ACCESSIBLE_FRONT_INSPECTED,
      this.scrollSelectedRowIntoView
    );
  }

  /**
   * Handle accessible reorder event. If the accessible is cached and rendered
   * within the accessibility tree, re-fetch its children and re-render the
   * corresponding subtree.
   *
   * @param {object} accessibleFront
   *        accessible front that had its subtree reordered.
   */
  onReorder(accessibleFront) {
    if (this.props.accessibles.has(accessibleFront.actorID)) {
      this.props.dispatch(fetchChildren(accessibleFront));
    }
  }

  scrollSelectedRowIntoView() {
    const { treeview } = this.refs;
    if (!treeview) {
      return;
    }

    const treeEl = treeview.treeRef.current;
    if (!treeEl) {
      return;
    }

    const selected = treeEl.ownerDocument.querySelector(
      ".treeTable .treeRow.selected"
    );
    if (selected) {
      scrollIntoView(selected, { center: true });
    }
  }

  /**
   * Handle accessible name change event. If the name of an accessible changes
   * and that accessible is cached and rendered within the accessibility tree,
   * re-fetch its parent's children and re-render the corresponding subtree.
   *
   * @param {object} accessibleFront
   *        accessible front that had its name changed.
   * @param {object} parentFront
   *        optional parent accessible front. Note: if it parent is not
   *        present, we assume that the top level document's name has changed
   *        and use accessible walker as a parent.
   */
  onNameChange(accessibleFront, parentFront) {
    const { accessibles, dispatch } = this.props;
    const accessibleWalkerFront = accessibleFront.getParent();
    parentFront = parentFront || accessibleWalkerFront;

    if (
      accessibles.has(accessibleFront.actorID) ||
      accessibles.has(parentFront.actorID)
    ) {
      dispatch(fetchChildren(parentFront));
    }
  }

  /**
   * Handle accessible text change (change/insert/remove) event. If the text of
   * an accessible changes and that accessible is cached and rendered within the
   * accessibility tree, re-fetch its children and re-render the corresponding
   * subtree.
   *
   * @param  {object} accessibleFront
   *         accessible front that had its child text changed.
   */
  onTextChange(accessibleFront) {
    const { accessibles, dispatch } = this.props;
    if (accessibles.has(accessibleFront.actorID)) {
      dispatch(fetchChildren(accessibleFront));
    }
  }

  renderValue(props) {
    return AccessibilityRowValue(props);
  }

  /**
   * Render Accessibility panel content
   */
  render() {
    const columns = [
      {
        id: "default",
        title: L10N.getStr("accessibility.role"),
      },
      {
        id: "value",
        title: L10N.getStr("accessibility.name"),
      },
    ];

    const {
      accessibles,
      dispatch,
      expanded,
      selected,
      highlighted: highlightedItem,
      toolboxDoc,
      filtered,
      getAccessibilityTreeRoot,
      highlightAccessible,
      unhighlightAccessible,
    } = this.props;

    const renderRow = rowProps => {
      const { object } = rowProps.member;
      const highlighted = object === highlightedItem;
      return AccessibilityRow(
        Object.assign({}, rowProps, {
          toolboxDoc,
          highlighted,
          decorator: {
            getRowClass() {
              return highlighted ? ["highlighted"] : [];
            },
          },
          highlightAccessible,
          unhighlightAccessible,
        })
      );
    };
    const className = filtered ? "filtered" : undefined;

    return TreeView({
      ref: "treeview",
      object: getAccessibilityTreeRoot(),
      mode: MODE.SHORT,
      provider: new Provider(accessibles, filtered, dispatch),
      columns,
      className,
      renderValue: this.renderValue,
      renderRow,
      label: L10N.getStr("accessibility.treeName"),
      header: true,
      expandedNodes: expanded,
      selected,
      onClickRow(nodePath, event) {
        if (event.target.classList.contains("theme-twisty")) {
          this.toggle(nodePath);
        }

        this.selectRow(
          this.rows.find(row => row.props.member.path === nodePath),
          { preventAutoScroll: true }
        );

        return true;
      },
      onContextMenuTree(e) {
        // If context menu event is triggered on (or bubbled to) the TreeView, it was
        // done via keyboard. Open context menu for currently selected row.
        let row = this.getSelectedRow();
        if (!row) {
          return;
        }

        row = row.getWrappedInstance();
        row.onContextMenu(e);
      },
    });
  }
}

const mapStateToProps = ({
  accessibles,
  ui: { expanded, selected, highlighted },
  audit: { filters },
}) => ({
  accessibles,
  expanded,
  selected,
  highlighted,
  filtered: isFiltered(filters),
});
// Exports from this module
module.exports = connect(mapStateToProps)(AccessibilityTree);
