/* 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 {
  COMPATIBILITY_APPEND_NODE_FAILURE,
  COMPATIBILITY_CLEAR_DESTROYED_NODES,
  COMPATIBILITY_INIT_USER_SETTINGS_SUCCESS,
  COMPATIBILITY_INIT_USER_SETTINGS_FAILURE,
  COMPATIBILITY_INTERNAL_APPEND_NODE,
  COMPATIBILITY_INTERNAL_NODE_UPDATE,
  COMPATIBILITY_INTERNAL_REMOVE_NODE,
  COMPATIBILITY_INTERNAL_UPDATE_SELECTED_NODE_ISSUES,
  COMPATIBILITY_REMOVE_NODE_FAILURE,
  COMPATIBILITY_UPDATE_NODE_FAILURE,
  COMPATIBILITY_UPDATE_NODES_FAILURE,
  COMPATIBILITY_UPDATE_SELECTED_NODE_SUCCESS,
  COMPATIBILITY_UPDATE_SELECTED_NODE_FAILURE,
  COMPATIBILITY_UPDATE_SETTINGS_VISIBILITY,
  COMPATIBILITY_UPDATE_TARGET_BROWSERS_START,
  COMPATIBILITY_UPDATE_TARGET_BROWSERS_SUCCESS,
  COMPATIBILITY_UPDATE_TARGET_BROWSERS_FAILURE,
  COMPATIBILITY_UPDATE_TARGET_BROWSERS_COMPLETE,
  COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_START,
  COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_SUCCESS,
  COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_FAILURE,
  COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_COMPLETE,
} = require("resource://devtools/client/inspector/compatibility/actions/index.js");

const INITIAL_STATE = {
  defaultTargetBrowsers: [],
  isSettingsVisible: false,
  isTopLevelTargetProcessing: false,
  selectedNode: null,
  selectedNodeIssues: [],
  topLevelTarget: null,
  topLevelTargetIssues: [],
  targetBrowsers: [],
};

const reducers = {
  [COMPATIBILITY_APPEND_NODE_FAILURE](state, { error }) {
    _showError(COMPATIBILITY_APPEND_NODE_FAILURE, error);
    return state;
  },
  [COMPATIBILITY_CLEAR_DESTROYED_NODES](state) {
    const topLevelTargetIssues = _clearDestroyedNodes(
      state.topLevelTargetIssues
    );
    return Object.assign({}, state, { topLevelTargetIssues });
  },
  [COMPATIBILITY_INIT_USER_SETTINGS_SUCCESS](
    state,
    { defaultTargetBrowsers, targetBrowsers }
  ) {
    return Object.assign({}, state, { defaultTargetBrowsers, targetBrowsers });
  },
  [COMPATIBILITY_INIT_USER_SETTINGS_FAILURE](state, { error }) {
    _showError(COMPATIBILITY_INIT_USER_SETTINGS_FAILURE, error);
    return state;
  },
  [COMPATIBILITY_INTERNAL_APPEND_NODE](state, { node, issues }) {
    const topLevelTargetIssues = _appendTopLevelTargetIssues(
      state.topLevelTargetIssues,
      node,
      issues
    );
    return Object.assign({}, state, { topLevelTargetIssues });
  },
  [COMPATIBILITY_INTERNAL_NODE_UPDATE](state, { node, issues }) {
    const topLevelTargetIssues = _updateTopLevelTargetIssues(
      state.topLevelTargetIssues,
      node,
      issues
    );
    return Object.assign({}, state, { topLevelTargetIssues });
  },
  [COMPATIBILITY_INTERNAL_REMOVE_NODE](state, { node }) {
    const topLevelTargetIssues = _removeNodeOrIssues(
      state.topLevelTargetIssues,
      node,
      []
    );
    return Object.assign({}, state, { topLevelTargetIssues });
  },
  [COMPATIBILITY_INTERNAL_UPDATE_SELECTED_NODE_ISSUES](state, { issues }) {
    return Object.assign({}, state, { selectedNodeIssues: issues });
  },
  [COMPATIBILITY_REMOVE_NODE_FAILURE](state, { error }) {
    _showError(COMPATIBILITY_UPDATE_NODES_FAILURE, error);
    return state;
  },
  [COMPATIBILITY_UPDATE_NODE_FAILURE](state, { error }) {
    _showError(COMPATIBILITY_UPDATE_NODES_FAILURE, error);
    return state;
  },
  [COMPATIBILITY_UPDATE_NODES_FAILURE](state, { error }) {
    _showError(COMPATIBILITY_UPDATE_NODES_FAILURE, error);
    return state;
  },
  [COMPATIBILITY_UPDATE_SELECTED_NODE_SUCCESS](state, { node }) {
    return Object.assign({}, state, { selectedNode: node });
  },
  [COMPATIBILITY_UPDATE_SELECTED_NODE_FAILURE](state, { error }) {
    _showError(COMPATIBILITY_UPDATE_SELECTED_NODE_FAILURE, error);
    return state;
  },
  [COMPATIBILITY_UPDATE_SETTINGS_VISIBILITY](state, { visibility }) {
    return Object.assign({}, state, { isSettingsVisible: visibility });
  },
  [COMPATIBILITY_UPDATE_TARGET_BROWSERS_START](state) {
    return Object.assign({}, state, {
      isTopLevelTargetProcessing: true,
      topLevelTargetIssues: [],
    });
  },
  [COMPATIBILITY_UPDATE_TARGET_BROWSERS_SUCCESS](state, { targetBrowsers }) {
    return Object.assign({}, state, { targetBrowsers });
  },
  [COMPATIBILITY_UPDATE_TARGET_BROWSERS_FAILURE](state, { error }) {
    _showError(COMPATIBILITY_UPDATE_TARGET_BROWSERS_FAILURE, error);
    return state;
  },
  [COMPATIBILITY_UPDATE_TARGET_BROWSERS_COMPLETE](state) {
    return Object.assign({}, state, { isTopLevelTargetProcessing: false });
  },
  [COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_START](state) {
    return Object.assign({}, state, {
      isTopLevelTargetProcessing: true,
      topLevelTargetIssues: [],
    });
  },
  [COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_SUCCESS](state, { target }) {
    return Object.assign({}, state, { topLevelTarget: target });
  },
  [COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_FAILURE](state, { error }) {
    _showError(COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_FAILURE, error);
    return state;
  },
  [COMPATIBILITY_UPDATE_TOP_LEVEL_TARGET_COMPLETE](state) {
    return Object.assign({}, state, { isTopLevelTargetProcessing: false });
  },
};

function _appendTopLevelTargetIssues(targetIssues, node, issues) {
  targetIssues = [...targetIssues];
  for (const issue of issues) {
    const index = _indexOfIssue(targetIssues, issue);
    if (index < 0) {
      issue.nodes = [node];
      targetIssues.push(issue);
      continue;
    }

    const targetIssue = targetIssues[index];
    const nodeIndex = targetIssue.nodes.findIndex(
      issueNode => issueNode.actorID === node.actorID
    );

    if (nodeIndex < 0) {
      targetIssue.nodes = [...targetIssue.nodes, node];
    }
  }
  return targetIssues;
}

function _clearDestroyedNodes(targetIssues) {
  return targetIssues.reduce((newIssues, targetIssue) => {
    const retainedNodes = targetIssue.nodes.filter(
      n => n.targetFront && !n.targetFront.isDestroyed()
    );

    // All the nodes for a given issue are destroyed
    if (retainedNodes.length === 0) {
      // Remove issue.
      return newIssues;
    }

    targetIssue.nodes = retainedNodes;
    return [...newIssues, targetIssue];
  }, []);
}

function _indexOfIssue(issues, issue) {
  return issues.findIndex(
    i => i.type === issue.type && i.property === issue.property
  );
}

function _indexOfNode(issue, node) {
  return issue.nodes.findIndex(n => n.actorID === node.actorID);
}

function _removeNodeOrIssues(targetIssues, node, issues) {
  return targetIssues.reduce((newIssues, targetIssue) => {
    if (issues.length && _indexOfIssue(issues, targetIssue) >= 0) {
      // The targetIssue is still in the node.
      return [...newIssues, targetIssue];
    }

    const indexOfNodeInTarget = _indexOfNode(targetIssue, node);
    if (indexOfNodeInTarget < 0) {
      // The targetIssue does not have the node to remove.
      return [...newIssues, targetIssue];
    }

    // This issue on the updated node is gone.
    if (targetIssue.nodes.length === 1) {
      // Remove issue.
      return newIssues;
    }

    // Remove node from the nodes.
    targetIssue.nodes = [
      ...targetIssue.nodes.slice(0, indexOfNodeInTarget),
      ...targetIssue.nodes.slice(indexOfNodeInTarget + 1),
    ];
    return [...newIssues, targetIssue];
  }, []);
}

function _updateTopLevelTargetIssues(targetIssues, node, issues) {
  // Remove issues or node.
  targetIssues = _removeNodeOrIssues(targetIssues, node, issues);

  // Append issues or node.
  const appendables = issues.filter(issue => {
    const indexOfIssue = _indexOfIssue(targetIssues, issue);
    return (
      indexOfIssue < 0 || _indexOfNode(targetIssues[indexOfIssue], node) < 0
    );
  });
  targetIssues = _appendTopLevelTargetIssues(targetIssues, node, appendables);

  // Update fields.
  for (const issue of issues) {
    const indexOfIssue = _indexOfIssue(targetIssues, issue);

    if (indexOfIssue < 0) {
      continue;
    }

    const targetIssue = targetIssues[indexOfIssue];
    targetIssues[indexOfIssue] = Object.assign(issue, {
      nodes: targetIssue.nodes,
    });
  }

  return targetIssues;
}

function _showError(action, error) {
  console.error(`[${action}] ${error.message}`);
  console.error(error.stack);
}

module.exports = function (state = INITIAL_STATE, action) {
  const reducer = reducers[action.type];
  return reducer ? reducer(state, action) : state;
};
