/* 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("devtools/shared/DevToolsUtils");
const { actions, diffingState, viewState } = require("../constants");
const telemetry = require("../telemetry");
const {
  getSnapshot,
  censusIsUpToDate,
  snapshotIsDiffable,
  findSelectedSnapshot,
} = require("../utils");
// This is a circular dependency, so do not destructure the needed properties.
const snapshotActions = require("./snapshot");

/**
 * Toggle diffing mode on or off.
 */
const toggleDiffing = 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 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,
      });

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

      try {
        ({ delta: report, parentMap } = yield 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,
    });

    telemetry.countDiff({ 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
 */
const selectSnapshotForDiffingAndRefresh = exports.selectSnapshotForDiffingAndRefresh = function (heapWorker, snapshot) {
  return function* (dispatch, getState) {
    assert(getState().diffing,
           "If we are selecting for diffing, we must be in diffing mode");
    dispatch(selectSnapshotForDiffing(snapshot));
    yield dispatch(refreshDiffing(heapWorker));
  };
};

/**
 * Expand the given node in the diffing's census's delta-report.
 *
 * @param {CensusTreeNode} node
 */
const expandDiffingCensusNode = 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
 */
const collapseDiffingCensusNode = 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
 */
const focusDiffingCensusNode = exports.focusDiffingCensusNode = function (node) {
  return {
    type: actions.FOCUS_DIFFING_CENSUS_NODE,
    node,
  };
};