/* 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 {
  assert,
  reportException,
} = require("resource://devtools/shared/DevToolsUtils.js");
const {
  actions,
  diffingState,
  viewState,
} = require("resource://devtools/client/memory/constants.js");
const {
  getSnapshot,
  censusIsUpToDate,
  snapshotIsDiffable,
  findSelectedSnapshot,
} = require("resource://devtools/client/memory/utils.js");

/**
 * Toggle diffing mode on or off.
 */
exports.toggleDiffing = function () {
  return function ({ dispatch, getState }) {
    dispatch({
      type: actions.CHANGE_VIEW,
      newViewState: getState().diffing ? viewState.CENSUS : viewState.DIFFING,
      oldDiffing: getState().diffing,
      oldSelected: findSelectedSnapshot(getState()),
    });
  };
};

/**
 * Select the given snapshot for diffing.
 *
 * @param {snapshotModel} snapshot
 */
const selectSnapshotForDiffing = (exports.selectSnapshotForDiffing = function (
  snapshot
) {
  assert(
    snapshotIsDiffable(snapshot),
    "To select a snapshot for diffing, it must be diffable"
  );
  return { type: actions.SELECT_SNAPSHOT_FOR_DIFFING, snapshot };
});

/**
 * Compute the difference between the first and second snapshots.
 *
 * @param {HeapAnalysesClient} heapWorker
 * @param {snapshotModel} first
 * @param {snapshotModel} second
 */
const takeCensusDiff = (exports.takeCensusDiff = function (
  heapWorker,
  first,
  second
) {
  return async function ({ dispatch, getState }) {
    assert(
      snapshotIsDiffable(first),
      `First snapshot must be in a diffable state, found ${first.state}`
    );
    assert(
      snapshotIsDiffable(second),
      `Second snapshot must be in a diffable state, found ${second.state}`
    );

    let report, parentMap;
    let display = getState().censusDisplay;
    let filter = getState().filter;

    if (censusIsUpToDate(filter, display, getState().diffing.census)) {
      return;
    }

    do {
      if (
        !getState().diffing ||
        getState().diffing.firstSnapshotId !== first.id ||
        getState().diffing.secondSnapshotId !== second.id
      ) {
        // If we stopped diffing or stopped and then started diffing a different
        // pair of snapshots, then just give up with diffing this pair. In the
        // latter case, a newly spawned task will handle the diffing for the new
        // pair.
        return;
      }

      display = getState().censusDisplay;
      filter = getState().filter;

      dispatch({
        type: actions.TAKE_CENSUS_DIFF_START,
        first,
        second,
        filter,
        display,
      });

      const opts = display.inverted
        ? { asInvertedTreeNode: true }
        : { asTreeNode: true };
      opts.filter = filter || null;

      try {
        ({ delta: report, parentMap } = await heapWorker.takeCensusDiff(
          first.path,
          second.path,
          { breakdown: display.breakdown },
          opts
        ));
      } catch (error) {
        reportException("actions/diffing/takeCensusDiff", error);
        dispatch({ type: actions.DIFFING_ERROR, error });
        return;
      }
    } while (
      filter !== getState().filter ||
      display !== getState().censusDisplay
    );

    dispatch({
      type: actions.TAKE_CENSUS_DIFF_END,
      first,
      second,
      report,
      parentMap,
      filter,
      display,
    });
  };
});

/**
 * Ensure that the current diffing data is up to date with the currently
 * selected display, filter, etc. If the state is not up-to-date, then a
 * recompute is triggered.
 *
 * @param {HeapAnalysesClient} heapWorker
 */
const refreshDiffing = (exports.refreshDiffing = function (heapWorker) {
  return function ({ dispatch, getState }) {
    if (getState().diffing.secondSnapshotId === null) {
      return;
    }

    assert(getState().diffing.firstSnapshotId, "Should have first snapshot id");

    if (getState().diffing.state === diffingState.TAKING_DIFF) {
      // There is an existing task that will ensure that the diffing data is
      // up-to-date.
      return;
    }

    const { firstSnapshotId, secondSnapshotId } = getState().diffing;

    const first = getSnapshot(getState(), firstSnapshotId);
    const second = getSnapshot(getState(), secondSnapshotId);
    dispatch(takeCensusDiff(heapWorker, first, second));
  };
});

/**
 * Select the given snapshot for diffing and refresh the diffing data if
 * necessary (for example, if two snapshots are now selected for diffing).
 *
 * @param {HeapAnalysesClient} heapWorker
 * @param {snapshotModel} snapshot
 */
exports.selectSnapshotForDiffingAndRefresh = function (heapWorker, snapshot) {
  return async function ({ dispatch, getState }) {
    assert(
      getState().diffing,
      "If we are selecting for diffing, we must be in diffing mode"
    );
    dispatch(selectSnapshotForDiffing(snapshot));
    await dispatch(refreshDiffing(heapWorker));
  };
};

/**
 * Expand the given node in the diffing's census's delta-report.
 *
 * @param {CensusTreeNode} node
 */
exports.expandDiffingCensusNode = function (node) {
  return {
    type: actions.EXPAND_DIFFING_CENSUS_NODE,
    node,
  };
};

/**
 * Collapse the given node in the diffing's census's delta-report.
 *
 * @param {CensusTreeNode} node
 */
exports.collapseDiffingCensusNode = function (node) {
  return {
    type: actions.COLLAPSE_DIFFING_CENSUS_NODE,
    node,
  };
};

/**
 * Focus the given node in the snapshot's census's report.
 *
 * @param {DominatorTreeNode} node
 */
exports.focusDiffingCensusNode = function (node) {
  return {
    type: actions.FOCUS_DIFFING_CENSUS_NODE,
    node,
  };
};
