/* 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 {
  immutableUpdate,
  assert,
} = require("resource://devtools/shared/DevToolsUtils.js");
const {
  actions,
  snapshotState: states,
  censusState,
  treeMapState,
  dominatorTreeState,
  viewState,
} = require("resource://devtools/client/memory/constants.js");
const DominatorTreeNode = require("resource://devtools/shared/heapsnapshot/DominatorTreeNode.js");

const handlers = Object.create(null);

handlers[actions.SNAPSHOT_ERROR] = function (snapshots, { id, error }) {
  return snapshots.map(snapshot => {
    return snapshot.id === id
      ? immutableUpdate(snapshot, { state: states.ERROR, error })
      : snapshot;
  });
};

handlers[actions.TAKE_SNAPSHOT_START] = function (snapshots, { snapshot }) {
  return [...snapshots, snapshot];
};

handlers[actions.TAKE_SNAPSHOT_END] = function (snapshots, { id, path }) {
  return snapshots.map(snapshot => {
    return snapshot.id === id
      ? immutableUpdate(snapshot, { state: states.SAVED, path })
      : snapshot;
  });
};

handlers[actions.IMPORT_SNAPSHOT_START] = handlers[actions.TAKE_SNAPSHOT_START];

handlers[actions.READ_SNAPSHOT_START] = function (snapshots, { id }) {
  return snapshots.map(snapshot => {
    return snapshot.id === id
      ? immutableUpdate(snapshot, { state: states.READING })
      : snapshot;
  });
};

handlers[actions.READ_SNAPSHOT_END] = function (
  snapshots,
  { id, creationTime }
) {
  return snapshots.map(snapshot => {
    return snapshot.id === id
      ? immutableUpdate(snapshot, { state: states.READ, creationTime })
      : snapshot;
  });
};

handlers[actions.TAKE_CENSUS_START] = function (
  snapshots,
  { id, display, filter }
) {
  const census = {
    report: null,
    display,
    filter,
    state: censusState.SAVING,
  };

  return snapshots.map(snapshot => {
    return snapshot.id === id
      ? immutableUpdate(snapshot, { census })
      : snapshot;
  });
};

handlers[actions.TAKE_CENSUS_END] = function (
  snapshots,
  { id, report, parentMap, display, filter }
) {
  const census = {
    report,
    parentMap,
    expanded: new Set(),
    display,
    filter,
    state: censusState.SAVED,
  };

  return snapshots.map(snapshot => {
    return snapshot.id === id
      ? immutableUpdate(snapshot, { census })
      : snapshot;
  });
};

handlers[actions.TAKE_CENSUS_ERROR] = function (snapshots, { id, error }) {
  assert(error, "actions with TAKE_CENSUS_ERROR should have an error");

  return snapshots.map(snapshot => {
    if (snapshot.id !== id) {
      return snapshot;
    }

    const census = Object.freeze({
      state: censusState.ERROR,
      error,
    });

    return immutableUpdate(snapshot, { census });
  });
};

handlers[actions.TAKE_TREE_MAP_START] = function (snapshots, { id, display }) {
  const treeMap = {
    report: null,
    display,
    state: treeMapState.SAVING,
  };

  return snapshots.map(snapshot => {
    return snapshot.id === id
      ? immutableUpdate(snapshot, { treeMap })
      : snapshot;
  });
};

handlers[actions.TAKE_TREE_MAP_END] = function (snapshots, action) {
  const { id, report, display } = action;
  const treeMap = {
    report,
    display,
    state: treeMapState.SAVED,
  };

  return snapshots.map(snapshot => {
    return snapshot.id === id
      ? immutableUpdate(snapshot, { treeMap })
      : snapshot;
  });
};

handlers[actions.TAKE_TREE_MAP_ERROR] = function (snapshots, { id, error }) {
  assert(error, "actions with TAKE_TREE_MAP_ERROR should have an error");

  return snapshots.map(snapshot => {
    if (snapshot.id !== id) {
      return snapshot;
    }

    const treeMap = Object.freeze({
      state: treeMapState.ERROR,
      error,
    });

    return immutableUpdate(snapshot, { treeMap });
  });
};

handlers[actions.EXPAND_CENSUS_NODE] = function (snapshots, { id, node }) {
  return snapshots.map(snapshot => {
    if (snapshot.id !== id) {
      return snapshot;
    }

    assert(snapshot.census, "Should have a census");
    assert(snapshot.census.report, "Should have a census report");
    assert(snapshot.census.expanded, "Should have a census's expanded set");

    const expanded = new Set(snapshot.census.expanded);
    expanded.add(node.id);
    const census = immutableUpdate(snapshot.census, { expanded });
    return immutableUpdate(snapshot, { census });
  });
};

handlers[actions.COLLAPSE_CENSUS_NODE] = function (snapshots, { id, node }) {
  return snapshots.map(snapshot => {
    if (snapshot.id !== id) {
      return snapshot;
    }

    assert(snapshot.census, "Should have a census");
    assert(snapshot.census.report, "Should have a census report");
    assert(snapshot.census.expanded, "Should have a census's expanded set");

    const expanded = new Set(snapshot.census.expanded);
    expanded.delete(node.id);
    const census = immutableUpdate(snapshot.census, { expanded });
    return immutableUpdate(snapshot, { census });
  });
};

handlers[actions.FOCUS_CENSUS_NODE] = function (snapshots, { id, node }) {
  return snapshots.map(snapshot => {
    if (snapshot.id !== id) {
      return snapshot;
    }

    assert(snapshot.census, "Should have a census");
    const census = immutableUpdate(snapshot.census, { focused: node });
    return immutableUpdate(snapshot, { census });
  });
};

handlers[actions.SELECT_SNAPSHOT] = function (snapshots, { id }) {
  return snapshots.map(s => immutableUpdate(s, { selected: s.id === id }));
};

handlers[actions.DELETE_SNAPSHOTS_START] = function (snapshots, { ids }) {
  return snapshots.filter(s => !ids.includes(s.id));
};

handlers[actions.DELETE_SNAPSHOTS_END] = function (snapshots) {
  return snapshots;
};

handlers[actions.CHANGE_VIEW] = function (snapshots, { newViewState }) {
  return newViewState === viewState.DIFFING
    ? snapshots.map(s => immutableUpdate(s, { selected: false }))
    : snapshots;
};

handlers[actions.POP_VIEW] = function (snapshots, { previousView }) {
  return snapshots.map(s =>
    immutableUpdate(s, {
      selected: s.id === previousView.selected,
    })
  );
};

handlers[actions.COMPUTE_DOMINATOR_TREE_START] = function (snapshots, { id }) {
  const dominatorTree = Object.freeze({
    state: dominatorTreeState.COMPUTING,
    dominatorTreeId: undefined,
    root: undefined,
  });

  return snapshots.map(snapshot => {
    if (snapshot.id !== id) {
      return snapshot;
    }

    assert(!snapshot.dominatorTree, "Should not have a dominator tree model");
    return immutableUpdate(snapshot, { dominatorTree });
  });
};

handlers[actions.COMPUTE_DOMINATOR_TREE_END] = function (
  snapshots,
  { id, dominatorTreeId }
) {
  return snapshots.map(snapshot => {
    if (snapshot.id !== id) {
      return snapshot;
    }

    assert(snapshot.dominatorTree, "Should have a dominator tree model");
    assert(
      snapshot.dominatorTree.state == dominatorTreeState.COMPUTING,
      "Should be in the COMPUTING state"
    );

    const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
      state: dominatorTreeState.COMPUTED,
      dominatorTreeId,
    });
    return immutableUpdate(snapshot, { dominatorTree });
  });
};

handlers[actions.FETCH_DOMINATOR_TREE_START] = function (
  snapshots,
  { id, display }
) {
  return snapshots.map(snapshot => {
    if (snapshot.id !== id) {
      return snapshot;
    }

    assert(snapshot.dominatorTree, "Should have a dominator tree model");
    assert(
      snapshot.dominatorTree.state !== dominatorTreeState.COMPUTING &&
        snapshot.dominatorTree.state !== dominatorTreeState.ERROR,
      "Should have already computed the dominator tree, found state = " +
        snapshot.dominatorTree.state
    );

    const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
      state: dominatorTreeState.FETCHING,
      root: undefined,
      display,
    });
    return immutableUpdate(snapshot, { dominatorTree });
  });
};

handlers[actions.FETCH_DOMINATOR_TREE_END] = function (
  snapshots,
  { id, root }
) {
  return snapshots.map(snapshot => {
    if (snapshot.id !== id) {
      return snapshot;
    }

    assert(snapshot.dominatorTree, "Should have a dominator tree model");
    assert(
      snapshot.dominatorTree.state == dominatorTreeState.FETCHING,
      "Should be in the FETCHING state"
    );

    let focused;
    if (snapshot.dominatorTree.focused) {
      focused = (function findFocused(node) {
        if (node.nodeId === snapshot.dominatorTree.focused.nodeId) {
          return node;
        }

        if (node.children) {
          const length = node.children.length;
          for (let i = 0; i < length; i++) {
            const result = findFocused(node.children[i]);
            if (result) {
              return result;
            }
          }
        }

        return undefined;
      })(root);
    }

    const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
      state: dominatorTreeState.LOADED,
      root,
      expanded: new Set(),
      focused,
    });

    return immutableUpdate(snapshot, { dominatorTree });
  });
};

handlers[actions.EXPAND_DOMINATOR_TREE_NODE] = function (
  snapshots,
  { id, node }
) {
  return snapshots.map(snapshot => {
    if (snapshot.id !== id) {
      return snapshot;
    }

    assert(snapshot.dominatorTree, "Should have a dominator tree");
    assert(
      snapshot.dominatorTree.expanded,
      "Should have the dominator tree's expanded set"
    );

    const expanded = new Set(snapshot.dominatorTree.expanded);
    expanded.add(node.nodeId);
    const dominatorTree = immutableUpdate(snapshot.dominatorTree, { expanded });
    return immutableUpdate(snapshot, { dominatorTree });
  });
};

handlers[actions.COLLAPSE_DOMINATOR_TREE_NODE] = function (
  snapshots,
  { id, node }
) {
  return snapshots.map(snapshot => {
    if (snapshot.id !== id) {
      return snapshot;
    }

    assert(snapshot.dominatorTree, "Should have a dominator tree");
    assert(
      snapshot.dominatorTree.expanded,
      "Should have the dominator tree's expanded set"
    );

    const expanded = new Set(snapshot.dominatorTree.expanded);
    expanded.delete(node.nodeId);
    const dominatorTree = immutableUpdate(snapshot.dominatorTree, { expanded });
    return immutableUpdate(snapshot, { dominatorTree });
  });
};

handlers[actions.FOCUS_DOMINATOR_TREE_NODE] = function (
  snapshots,
  { id, node }
) {
  return snapshots.map(snapshot => {
    if (snapshot.id !== id) {
      return snapshot;
    }

    assert(snapshot.dominatorTree, "Should have a dominator tree");
    const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
      focused: node,
    });
    return immutableUpdate(snapshot, { dominatorTree });
  });
};

handlers[actions.FETCH_IMMEDIATELY_DOMINATED_START] = function (
  snapshots,
  { id }
) {
  return snapshots.map(snapshot => {
    if (snapshot.id !== id) {
      return snapshot;
    }

    assert(snapshot.dominatorTree, "Should have a dominator tree model");
    assert(
      snapshot.dominatorTree.state == dominatorTreeState.INCREMENTAL_FETCHING ||
        snapshot.dominatorTree.state == dominatorTreeState.LOADED,
      "The dominator tree should be loaded if we are going to " +
        "incrementally fetch children."
    );

    const activeFetchRequestCount = snapshot.dominatorTree
      .activeFetchRequestCount
      ? snapshot.dominatorTree.activeFetchRequestCount + 1
      : 1;

    const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
      state: dominatorTreeState.INCREMENTAL_FETCHING,
      activeFetchRequestCount,
    });

    return immutableUpdate(snapshot, { dominatorTree });
  });
};

handlers[actions.FETCH_IMMEDIATELY_DOMINATED_END] = function (
  snapshots,
  { id, path, nodes, moreChildrenAvailable }
) {
  return snapshots.map(snapshot => {
    if (snapshot.id !== id) {
      return snapshot;
    }

    assert(snapshot.dominatorTree, "Should have a dominator tree model");
    assert(
      snapshot.dominatorTree.root,
      "Should have a dominator tree model root"
    );
    assert(
      snapshot.dominatorTree.state === dominatorTreeState.INCREMENTAL_FETCHING,
      "The dominator tree state should be INCREMENTAL_FETCHING"
    );

    const root = DominatorTreeNode.insert(
      snapshot.dominatorTree.root,
      path,
      nodes,
      moreChildrenAvailable
    );

    const focused = snapshot.dominatorTree.focused
      ? DominatorTreeNode.getNodeByIdAlongPath(
          snapshot.dominatorTree.focused.nodeId,
          root,
          path
        )
      : undefined;

    const activeFetchRequestCount =
      snapshot.dominatorTree.activeFetchRequestCount === 1
        ? undefined
        : snapshot.dominatorTree.activeFetchRequestCount - 1;

    // If there are still outstanding requests, we need to stay in the
    // INCREMENTAL_FETCHING state until they complete.
    const state = activeFetchRequestCount
      ? dominatorTreeState.INCREMENTAL_FETCHING
      : dominatorTreeState.LOADED;

    const dominatorTree = immutableUpdate(snapshot.dominatorTree, {
      state,
      root,
      focused,
      activeFetchRequestCount,
    });

    return immutableUpdate(snapshot, { dominatorTree });
  });
};

handlers[actions.DOMINATOR_TREE_ERROR] = function (snapshots, { id, error }) {
  assert(error, "actions with DOMINATOR_TREE_ERROR should have an error");

  return snapshots.map(snapshot => {
    if (snapshot.id !== id) {
      return snapshot;
    }

    const dominatorTree = Object.freeze({
      state: dominatorTreeState.ERROR,
      error,
    });

    return immutableUpdate(snapshot, { dominatorTree });
  });
};

module.exports = function (snapshots = [], action) {
  const handler = handlers[action.type];
  if (handler) {
    return handler(snapshots, action);
  }
  return snapshots;
};
