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

const { assert } = require("devtools/shared/DevToolsUtils");
const { appinfo } = require("Services");
const { DOM: dom, createClass, createFactory, PropTypes } = require("devtools/client/shared/vendor/react");
const { connect } = require("devtools/client/shared/vendor/react-redux");
const { censusDisplays, labelDisplays, treeMapDisplays, diffingState, viewState } = require("./constants");
const { toggleRecordingAllocationStacks } = require("./actions/allocations");
const { setCensusDisplayAndRefresh } = require("./actions/census-display");
const { setLabelDisplayAndRefresh } = require("./actions/label-display");
const { setTreeMapDisplayAndRefresh } = require("./actions/tree-map-display");

const {
  getCustomCensusDisplays,
  getCustomLabelDisplays,
  getCustomTreeMapDisplays,
} = require("devtools/client/memory/utils");
const {
  selectSnapshotForDiffingAndRefresh,
  toggleDiffing,
  expandDiffingCensusNode,
  collapseDiffingCensusNode,
  focusDiffingCensusNode,
} = require("./actions/diffing");
const { setFilterStringAndRefresh } = require("./actions/filter");
const { pickFileAndExportSnapshot, pickFileAndImportSnapshotAndCensus } = require("./actions/io");
const {
  selectSnapshotAndRefresh,
  takeSnapshotAndCensus,
  clearSnapshots,
  deleteSnapshot,
  fetchImmediatelyDominated,
  expandCensusNode,
  collapseCensusNode,
  focusCensusNode,
  expandDominatorTreeNode,
  collapseDominatorTreeNode,
  focusDominatorTreeNode,
  fetchIndividuals,
  focusIndividual,
} = require("./actions/snapshot");
const { changeViewAndRefresh, popViewAndRefresh } = require("./actions/view");
const { resizeShortestPaths } = require("./actions/sizes");
const Toolbar = createFactory(require("./components/toolbar"));
const List = createFactory(require("./components/list"));
const SnapshotListItem = createFactory(require("./components/snapshot-list-item"));
const Heap = createFactory(require("./components/heap"));
const { app: appModel } = require("./models");

const MemoryApp = createClass({
  displayName: "MemoryApp",

  propTypes: appModel,

  getDefaultProps() {
    return {};
  },

  componentDidMount() {
    // Attach the keydown listener directly to the window. When an element that
    // has the focus (such as a tree node) is removed from the DOM, the focus
    // falls back to the body.
    window.addEventListener("keydown", this.onKeyDown);
  },

  componentWillUnmount() {
    window.removeEventListener("keydown", this.onKeyDown);
  },

  childContextTypes: {
    front: PropTypes.any,
    heapWorker: PropTypes.any,
    toolbox: PropTypes.any,
  },

  getChildContext() {
    return {
      front: this.props.front,
      heapWorker: this.props.heapWorker,
      toolbox: this.props.toolbox,
    };
  },

  onKeyDown(e) {
    let { snapshots, dispatch, heapWorker } = this.props;
    const selectedSnapshot = snapshots.find(s => s.selected);
    const selectedIndex = snapshots.indexOf(selectedSnapshot);

    let isOSX = appinfo.OS == "Darwin";
    let isAccelKey = (isOSX && e.metaKey) || (!isOSX && e.ctrlKey);

    // On ACCEL+UP, select previous snapshot.
    if (isAccelKey && e.key === "ArrowUp") {
      let previousIndex = Math.max(0, selectedIndex - 1);
      let previousSnapshotId = snapshots[previousIndex].id;
      dispatch(selectSnapshotAndRefresh(heapWorker, previousSnapshotId));
    }

    // On ACCEL+DOWN, select next snapshot.
    if (isAccelKey && e.key === "ArrowDown") {
      let nextIndex = Math.min(snapshots.length - 1, selectedIndex + 1);
      let nextSnapshotId = snapshots[nextIndex].id;
      dispatch(selectSnapshotAndRefresh(heapWorker, nextSnapshotId));
    }
  },

  _getCensusDisplays() {
    const customDisplays = getCustomCensusDisplays();
    const custom = Object.keys(customDisplays).reduce((arr, key) => {
      arr.push(customDisplays[key]);
      return arr;
    }, []);

    return [
      censusDisplays.coarseType,
      censusDisplays.allocationStack,
      censusDisplays.invertedAllocationStack,
    ].concat(custom);
  },

  _getLabelDisplays() {
    const customDisplays = getCustomLabelDisplays();
    const custom = Object.keys(customDisplays).reduce((arr, key) => {
      arr.push(customDisplays[key]);
      return arr;
    }, []);

    return [
      labelDisplays.coarseType,
      labelDisplays.allocationStack,
    ].concat(custom);
  },

  _getTreeMapDisplays() {
    const customDisplays = getCustomTreeMapDisplays();
    const custom = Object.keys(customDisplays).reduce((arr, key) => {
      arr.push(customDisplays[key]);
      return arr;
    }, []);

    return [
      treeMapDisplays.coarseType
    ].concat(custom);
  },

  render() {
    let {
      dispatch,
      snapshots,
      front,
      heapWorker,
      allocations,
      toolbox,
      filter,
      diffing,
      view,
      sizes,
      censusDisplay,
      labelDisplay,
      individuals,
    } = this.props;

    const selectedSnapshot = snapshots.find(s => s.selected);

    const onClickSnapshotListItem = diffing && diffing.state === diffingState.SELECTING
      ? snapshot => dispatch(selectSnapshotForDiffingAndRefresh(heapWorker, snapshot))
      : snapshot => dispatch(selectSnapshotAndRefresh(heapWorker, snapshot.id));

    return (
      dom.div(
        {
          id: "memory-tool"
        },

        Toolbar({
          snapshots,
          censusDisplays: this._getCensusDisplays(),
          censusDisplay,
          onCensusDisplayChange: newDisplay =>
            dispatch(setCensusDisplayAndRefresh(heapWorker, newDisplay)),
          onImportClick: () => dispatch(pickFileAndImportSnapshotAndCensus(heapWorker)),
          onClearSnapshotsClick: () => dispatch(clearSnapshots(heapWorker)),
          onTakeSnapshotClick: () => dispatch(takeSnapshotAndCensus(front, heapWorker)),
          onToggleRecordAllocationStacks: () =>
            dispatch(toggleRecordingAllocationStacks(front)),
          allocations,
          filterString: filter,
          setFilterString: filterString =>
            dispatch(setFilterStringAndRefresh(filterString, heapWorker)),
          diffing,
          onToggleDiffing: () => dispatch(toggleDiffing()),
          view,
          labelDisplays: this._getLabelDisplays(),
          labelDisplay,
          onLabelDisplayChange: newDisplay =>
            dispatch(setLabelDisplayAndRefresh(heapWorker, newDisplay)),
          treeMapDisplays: this._getTreeMapDisplays(),
          onTreeMapDisplayChange: newDisplay =>
            dispatch(setTreeMapDisplayAndRefresh(heapWorker, newDisplay)),
          onViewChange: v => dispatch(changeViewAndRefresh(v, heapWorker)),
        }),

        dom.div(
          {
            id: "memory-tool-container"
          },

          List({
            itemComponent: SnapshotListItem,
            items: snapshots,
            onSave: snapshot => dispatch(pickFileAndExportSnapshot(snapshot)),
            onDelete: snapshot => dispatch(deleteSnapshot(heapWorker, snapshot)),
            onClick: onClickSnapshotListItem,
            diffing,
          }),

          Heap({
            snapshot: selectedSnapshot,
            diffing,
            onViewSourceInDebugger: frame => toolbox.viewSourceInDebugger(frame.source, frame.line),
            onSnapshotClick: () =>
              dispatch(takeSnapshotAndCensus(front, heapWorker)),
            onLoadMoreSiblings: lazyChildren =>
              dispatch(fetchImmediatelyDominated(heapWorker,
                                                 selectedSnapshot.id,
                                                 lazyChildren)),
            onPopView: () => dispatch(popViewAndRefresh(heapWorker)),
            individuals,
            onViewIndividuals: node => {
              const snapshotId = diffing
                ? diffing.secondSnapshotId
                : selectedSnapshot.id;
              dispatch(fetchIndividuals(heapWorker,
                                        snapshotId,
                                        censusDisplay.breakdown,
                                        node.reportLeafIndex));
            },
            onFocusIndividual: node => {
              assert(view.state === viewState.INDIVIDUALS,
                     "Should be in the individuals view");
              dispatch(focusIndividual(node));
            },
            onCensusExpand: (census, node) => {
              if (diffing) {
                assert(diffing.census === census,
                       "Should only expand active census");
                dispatch(expandDiffingCensusNode(node));
              } else {
                assert(selectedSnapshot && selectedSnapshot.census === census,
                       "If not diffing, should be expanding on selected snapshot's census");
                dispatch(expandCensusNode(selectedSnapshot.id, node));
              }
            },
            onCensusCollapse: (census, node) => {
              if (diffing) {
                assert(diffing.census === census,
                       "Should only collapse active census");
                dispatch(collapseDiffingCensusNode(node));
              } else {
                assert(selectedSnapshot && selectedSnapshot.census === census,
                       "If not diffing, should be collapsing on selected snapshot's census");
                dispatch(collapseCensusNode(selectedSnapshot.id, node));
              }
            },
            onCensusFocus: (census, node) => {
              if (diffing) {
                assert(diffing.census === census,
                       "Should only focus nodes in active census");
                dispatch(focusDiffingCensusNode(node));
              } else {
                assert(selectedSnapshot && selectedSnapshot.census === census,
                       "If not diffing, should be focusing on nodes in selected snapshot's census");
                dispatch(focusCensusNode(selectedSnapshot.id, node));
              }
            },
            onDominatorTreeExpand: node => {
              assert(view.state === viewState.DOMINATOR_TREE,
                     "If expanding dominator tree nodes, should be in dominator tree view");
              assert(selectedSnapshot, "...and we should have a selected snapshot");
              assert(selectedSnapshot.dominatorTree,
                     "...and that snapshot should have a dominator tree");
              dispatch(expandDominatorTreeNode(selectedSnapshot.id, node));
            },
            onDominatorTreeCollapse: node => {
              assert(view.state === viewState.DOMINATOR_TREE,
                     "If collapsing dominator tree nodes, should be in dominator tree view");
              assert(selectedSnapshot, "...and we should have a selected snapshot");
              assert(selectedSnapshot.dominatorTree,
                     "...and that snapshot should have a dominator tree");
              dispatch(collapseDominatorTreeNode(selectedSnapshot.id, node));
            },
            onDominatorTreeFocus: node => {
              assert(view.state === viewState.DOMINATOR_TREE,
                     "If focusing dominator tree nodes, should be in dominator tree view");
              assert(selectedSnapshot, "...and we should have a selected snapshot");
              assert(selectedSnapshot.dominatorTree,
                     "...and that snapshot should have a dominator tree");
              dispatch(focusDominatorTreeNode(selectedSnapshot.id, node));
            },
            onShortestPathsResize: newSize => {
              dispatch(resizeShortestPaths(newSize));
            },
            sizes,
            view,
          })
        )
      )
    );
  },
});

/**
 * Passed into react-redux's `connect` method that is called on store change
 * and passed to components.
 */
function mapStateToProps(state) {
  return state;
}

module.exports = connect(mapStateToProps)(MemoryApp);