/* eslint-disable import/no-webpack-loader-syntax, import/no-unresolved */
import debug from 'debug';

import 'moment/locale/zh-cn'
moment.locale('zh-cn');
import moment from 'moment';
import {
  size, each, includes, isEqual
} from 'lodash';
import {
  fromJS,
  is as isDeepEqual,
  List as makeList,
  Map as makeMap,
  OrderedMap as makeOrderedMap,
} from 'immutable';

import ActionTypes from '../constants/action-types';
import {
  GRAPH_VIEW_MODE,
  TABLE_VIEW_MODE,
} from '../constants/naming';
import {
  graphExceedsComplexityThreshSelector,
  isResourceViewModeSelector,
} from '../selectors/topology';
import { isPausedSelector } from '../selectors/time-travel';
import { activeTopologyZoomCacheKeyPathSelector } from '../selectors/zooming';
import { applyPinnedSearches } from '../utils/search-utils';
import {
  findTopologyById,
  setTopologyUrlsById,
  updateTopologyIds,
  filterHiddenTopologies,
  addTopologyFullname,
  getDefaultTopology,
} from '../utils/topology-utils';

const log = debug('scope:app-store');
const error = debug('scope:error');

// Helpers

const topologySorter = topology => topology.get('rank');

// Initial values

export const initialState = makeMap({
  capabilities: makeMap(),
  contrastMode: false,
  controlPipes: makeOrderedMap(), // pipeId -> controlPipe
  controlStatus: makeMap(),
  currentTopology: null,
  currentTopologyId: null,
  errorUrl: null,
  exportingGraph: false,
  forceRelayout: false,
  gridSortedBy: null,
  gridSortedDesc: null,
  hostname: '...',
  hoveredMetricType: null,
  initialNodesLoaded: false,
  mouseOverEdgeId: null,
  mouseOverNodeId: null,
  nodeDetails: makeOrderedMap(), // nodeId -> details
  nodes: makeOrderedMap(), // nodeId -> node
  // nodes cache, infrequently updated, used for search & resource view
  // topologyId -> nodes
  nodesByTopology: makeMap(),
  nodesLoaded: false,
  // class of metric, e.g. 'cpu', rather than 'host_cpu' or 'process_cpu'.
  // allows us to keep the same metric "type" selected when the topology changes.
  pausedAt: null,
  pinnedMetricType: null,
  pinnedNetwork: null,
  // list of node filters
  pinnedSearches: makeList(),
  plugins: makeList(),
  routeSet: false,
  searchFocused: false,
  searchQuery: '',
  selectedNetwork: null,
  selectedNodeId: null,
  showingHelp: false,
  showingNetworks: false,
  showingTroubleshootingMenu: false,
  storeViewState: true,
  timeTravelTransitioning: false,
  topologies: makeList(),
  topologiesLoaded: false,
  topologyOptions: makeOrderedMap(), // topologyId -> options
  topologyUrlsById: makeOrderedMap(), // topologyId -> topologyUrl
  topologyViewMode: GRAPH_VIEW_MODE,
  version: null,
  versionUpdate: null,
  // Set some initial numerical values to prevent NaN in case of edgy race conditions.
  viewport: makeMap({ height: 0, width: 0 }),
  websocketClosed: false,
  zoomCache: makeMap(),
});

function calcSelectType(topology) {
  const result = {
    ...topology,
    options: topology.options && topology.options.map((option) => {
      // Server doesn't return the `selectType` key unless the option is something other than `one`.
      // Default to `one` if undefined, so the component doesn't have to handle this.
      option.selectType = option.selectType || 'one';
      return option;
    })
  };

  if (topology.sub_topologies) {
    result.sub_topologies = topology.sub_topologies.map(calcSelectType);
  }
  return result;
}

// adds ID field to topology (based on last part of URL path) and save urls in
// map for easy lookup
function processTopologies(state, nextTopologies) {
  // add IDs to topology objects in-place
  const topologiesWithId = updateTopologyIds(nextTopologies);
  // filter out hidden topos
  const visibleTopologies = filterHiddenTopologies(topologiesWithId, state.get('currentTopology'));
  // set `selectType` field for topology and sub_topologies options (recursive).
  const topologiesWithSelectType = visibleTopologies.map(calcSelectType);
  // cache URLs by ID
  state = state.set(
    'topologyUrlsById',
    setTopologyUrlsById(state.get('topologyUrlsById'), topologiesWithSelectType)
  );

  const topologiesWithFullnames = addTopologyFullname(topologiesWithSelectType);
  const immNextTopologies = fromJS(topologiesWithFullnames).sortBy(topologySorter);
  return state.set('topologies', immNextTopologies);
}

function setTopology(state, topologyId) {
  state = state.set('currentTopology', findTopologyById(state.get('topologies'), topologyId));
  return state.set('currentTopologyId', topologyId);
}

export function getDefaultTopologyOptions(state) {
  let topologyOptions = makeOrderedMap();
  state.get('topologies').forEach((topology) => {
    let defaultOptions = makeOrderedMap();
    if (topology.has('options') && topology.get('options')) {
      topology.get('options').forEach((option) => {
        const optionId = option.get('id');
        const defaultValue = option.get('defaultValue');
        defaultOptions = defaultOptions.set(optionId, [defaultValue]);
      });
    }
    if (defaultOptions.size) {
      topologyOptions = topologyOptions.set(topology.get('id'), defaultOptions);
    }
  });
  return topologyOptions;
}

function closeNodeDetails(state, nodeId) {
  const nodeDetails = state.get('nodeDetails');
  if (nodeDetails.size > 0) {
    const popNodeId = nodeId || nodeDetails.keySeq().last();
    // remove pipe if it belongs to the node being closed
    state = state.update(
      'controlPipes',
      controlPipes => controlPipes.filter(pipe => pipe.get('nodeId') !== popNodeId)
    );
    state = state.deleteIn(['nodeDetails', popNodeId]);
  }
  if (state.get('nodeDetails').size === 0 || state.get('selectedNodeId') === nodeId) {
    state = state.set('selectedNodeId', null);
  }
  return state;
}

function closeAllNodeDetails(state) {
  while (state.get('nodeDetails').size) {
    state = closeNodeDetails(state);
  }
  return state;
}

function clearNodes(state) {
  return state
    .update('nodes', nodes => nodes.clear())
    .set('nodesLoaded', false);
}

// TODO: These state changes should probably be calculated from selectors.
function updateStateFromNodes(state) {
  // Apply pinned searches, filters nodes that dont match.
  state = applyPinnedSearches(state);

  // In case node or edge disappears before mouseleave event.
  const nodesIds = state.get('nodes').keySeq();
  if (!nodesIds.contains(state.get('mouseOverNodeId'))) {
    state = state.set('mouseOverNodeId', null);
  }
  if (!nodesIds.some(nodeId => includes(state.get('mouseOverEdgeId'), nodeId))) {
    state = state.set('mouseOverEdgeId', null);
  }

  // Update the nodes cache only if we're not in the resource view mode, as we
  // intentionally want to keep it static before we figure how to keep it up-to-date.
  if (!isResourceViewModeSelector(state)) {
    const nodesForCurrentTopologyKey = ['nodesByTopology', state.get('currentTopologyId')];
    state = state.setIn(nodesForCurrentTopologyKey, state.get('nodes'));
  }

  // Clear the error.
  state = state.set('errorUrl', null);

  return state;
}

export function rootReducer(state = initialState, action) {
  if (!action.type) {
    error('Payload missing a type!', action);
  }

  switch (action.type) {
    case ActionTypes.BLUR_SEARCH: {
      return state.set('searchFocused', false);
    }

    case ActionTypes.FOCUS_SEARCH: {
      return state.set('searchFocused', true);
    }

    case ActionTypes.CHANGE_TOPOLOGY_OPTION: {
      // set option on parent topology
      const topology = findTopologyById(state.get('topologies'), action.topologyId);
      if (topology) {
        const topologyId = topology.get('parentId') || topology.get('id');
        const optionKey = ['topologyOptions', topologyId, action.option];
        const currentOption = state.getIn(optionKey);

        if (!isEqual(currentOption, action.value)) {
          state = clearNodes(state);
        }

        state = state.setIn(optionKey, action.value);
      }
      return state;
    }

    case ActionTypes.SET_VIEWPORT_DIMENSIONS: {
      return state.mergeIn(['viewport'], {
        height: action.height,
        width: action.width,
      });
    }

    case ActionTypes.SET_EXPORTING_GRAPH: {
      return state.set('exportingGraph', action.exporting);
    }

    case ActionTypes.SORT_ORDER_CHANGED: {
      return state.merge({
        gridSortedBy: action.sortedBy,
        gridSortedDesc: action.sortedDesc,
      });
    }

    case ActionTypes.SET_VIEW_MODE: {
      return state.set('topologyViewMode', action.viewMode);
    }

    case ActionTypes.CACHE_ZOOM_STATE: {
      return state.setIn(activeTopologyZoomCacheKeyPathSelector(state), action.zoomState);
    }

    case ActionTypes.CLEAR_CONTROL_ERROR: {
      return state.removeIn(['controlStatus', action.nodeId, 'error']);
    }

    case ActionTypes.CLICK_BACKGROUND: {
      if (state.get('showingHelp')) {
        state = state.set('showingHelp', false);
      }

      if (state.get('showingTroubleshootingMenu')) {
        state = state.set('showingTroubleshootingMenu', false);
      }
      return closeAllNodeDetails(state);
    }

    case ActionTypes.CLICK_CLOSE_DETAILS: {
      return closeNodeDetails(state, action.nodeId);
    }

    case ActionTypes.CLOSE_TERMINAL: {
      return state.update('controlPipes', controlPipes => controlPipes.clear());
    }

    case ActionTypes.CLICK_FORCE_RELAYOUT: {
      return state.set('forceRelayout', action.forceRelayout);
    }

    case ActionTypes.CLICK_NODE: {
      const prevSelectedNodeId = state.get('selectedNodeId');
      const prevDetailsStackSize = state.get('nodeDetails').size;

      // click on sibling closes all
      state = closeAllNodeDetails(state);
      state.set('nodeShape', action.origin)
      // select new node if it's not the same (in that case just delesect)
      if (prevDetailsStackSize > 1 || prevSelectedNodeId !== action.nodeId) {
        // dont set origin if a node was already selected, suppresses animation
        const origin = prevSelectedNodeId === null ? action.origin : null;
        state = state.setIn(
          ['nodeDetails', action.nodeId],
          {
            id: action.nodeId,
            label: action.label,
            shape:action.origin,
            origin,
            topologyId: action.topologyId || state.get('currentTopologyId'),
          }
        );
        state = state.set('selectedNodeId', action.nodeId);
      }
      return state;
    }

    case ActionTypes.CLICK_RELATIVE: {
      if (state.hasIn(['nodeDetails', action.nodeId])) {
        // bring to front
        const details = state.getIn(['nodeDetails', action.nodeId]);
        state = state.deleteIn(['nodeDetails', action.nodeId]);
        state = state.setIn(['nodeDetails', action.nodeId], details);
      } else {
        state = state.setIn(
          ['nodeDetails', action.nodeId],
          {
            id: action.nodeId,
            label: action.label,
            origin: action.origin,
            topologyId: action.topologyId
          }
        );
      }
      return state;
    }

    case ActionTypes.CLICK_SHOW_TOPOLOGY_FOR_NODE: {
      state = state.update(
        'nodeDetails',
        nodeDetails => nodeDetails.filter((v, k) => k === action.nodeId)
      );
      state = state.update('controlPipes', controlPipes => controlPipes.clear());
      state = state.set('selectedNodeId', action.nodeId);

      if (action.topologyId !== state.get('currentTopologyId')) {
        state = setTopology(state, action.topologyId);
        state = clearNodes(state);
      }

      return state;
    }

    case ActionTypes.CLICK_TOPOLOGY: {
      state = closeAllNodeDetails(state);

      const currentTopologyId = state.get('currentTopologyId');
      if (action.topologyId !== currentTopologyId) {
        state = setTopology(state, action.topologyId);
        state = clearNodes(state);
      }

      return state;
    }

    //
    // time control
    //

    case ActionTypes.RESUME_TIME: {
      state = state.set('timeTravelTransitioning', true);
      return state.set('pausedAt', null);
    }

    case ActionTypes.PAUSE_TIME_AT_NOW: {
      state = state.set('timeTravelTransitioning', false);
      // return state.set('pausedAt', moment().utc().format());
      return state.set('pausedAt', moment().utcOffset(8).format())
    }

    case ActionTypes.JUMP_TO_TIME: {
      state = state.set('timeTravelTransitioning', true);
      return state.set('pausedAt', action.timestamp);
    }

    case ActionTypes.FINISH_TIME_TRAVEL_TRANSITION: {
      state = state.set('timeTravelTransitioning', false);
      return clearNodes(state);
    }

    //
    // websockets
    //

    case ActionTypes.OPEN_WEBSOCKET: {
      return state.set('websocketClosed', false);
    }

    case ActionTypes.CLOSE_WEBSOCKET: {
      return state.set('websocketClosed', true);
    }

    //
    // networks
    //

    case ActionTypes.SHOW_NETWORKS: {
      if (!action.visible) {
        state = state.set('selectedNetwork', null);
        state = state.set('pinnedNetwork', null);
      }
      return state.set('showingNetworks', action.visible);
    }

    case ActionTypes.SELECT_NETWORK: {
      return state.set('selectedNetwork', action.networkId);
    }

    case ActionTypes.PIN_NETWORK: {
      return state.merge({
        pinnedNetwork: action.networkId,
        selectedNetwork: action.networkId
      });
    }

    case ActionTypes.UNPIN_NETWORK: {
      return state.merge({
        pinnedNetwork: null,
      });
    }

    //
    // metrics
    //

    case ActionTypes.HOVER_METRIC: {
      return state.set('hoveredMetricType', action.metricType);
    }

    case ActionTypes.UNHOVER_METRIC: {
      return state.set('hoveredMetricType', null);
    }

    case ActionTypes.PIN_METRIC: {
      return state.set('pinnedMetricType', action.metricType);
    }

    case ActionTypes.UNPIN_METRIC: {
      return state.set('pinnedMetricType', null);
    }

    case ActionTypes.SHOW_HELP: {
      return state.set('showingHelp', true);
    }

    case ActionTypes.HIDE_HELP: {
      return state.set('showingHelp', false);
    }

    case ActionTypes.DESELECT_NODE: {
      return closeNodeDetails(state);
    }

    case ActionTypes.DO_CONTROL: {
      return state.setIn(['controlStatus', action.nodeId], makeMap({
        error: null,
        pending: true
      }));
    }

    case ActionTypes.ENTER_EDGE: {
      return state.set('mouseOverEdgeId', action.edgeId);
    }

    case ActionTypes.ENTER_NODE: {
      return state.set('mouseOverNodeId', action.nodeId);
    }

    case ActionTypes.LEAVE_EDGE: {
      return state.set('mouseOverEdgeId', null);
    }

    case ActionTypes.LEAVE_NODE: {
      return state.set('mouseOverNodeId', null);
    }

    case ActionTypes.DO_CONTROL_ERROR: {
      return state.setIn(['controlStatus', action.nodeId], makeMap({
        error: action.error,
        pending: false
      }));
    }
    case ActionTypes.DO_CONTROL_SUCCESS: {
      return state.setIn(['controlStatus', action.nodeId], makeMap({
        error: null,
        pending: false
      }));
    }

    case ActionTypes.UPDATE_SEARCH: {
      state = state.set('pinnedSearches', makeList(action.pinnedSearches));
      state = state.set('searchQuery', action.searchQuery || '');
      return applyPinnedSearches(state);
    }

    case ActionTypes.RECEIVE_CONTROL_NODE_REMOVED: {
      return closeNodeDetails(state, action.nodeId);
    }

    case ActionTypes.RECEIVE_CONTROL_PIPE: {
      return state.setIn(['controlPipes', action.pipeId], makeOrderedMap({
        control: action.control,
        id: action.pipeId,
        nodeId: action.nodeId,
        raw: action.rawTty,
        resizeTtyControl: action.resizeTtyControl
      }));
    }

    case ActionTypes.RECEIVE_CONTROL_PIPE_STATUS: {
      if (state.hasIn(['controlPipes', action.pipeId])) {
        state = state.setIn(['controlPipes', action.pipeId, 'status'], action.status);
      }
      return state;
    }

    case ActionTypes.RECEIVE_ERROR: {
      if (state.get('errorUrl') !== null) {
        state = state.set('errorUrl', action.errorUrl);
      }
      return state;
    }

    case ActionTypes.RECEIVE_NODE_DETAILS: {
      // Ignore the update if paused and the timestamp didn't change.
      const setTimestamp = state.getIn(['nodeDetails', action.details.id, 'timestamp']);
      if (isPausedSelector(state) && action.requestTimestamp === setTimestamp) {
        return state;
      }

      state = state.set('errorUrl', null);

      // disregard if node is not selected anymore
      if (state.hasIn(['nodeDetails', action.details.id])) {
        state = state.updateIn(['nodeDetails', action.details.id], obj => ({
          ...obj,
          details: action.details,
          notFound: false,
          timestamp: action.requestTimestamp,
        }));
      }
      return state;
    }

    case ActionTypes.SET_RECEIVED_NODES_DELTA: {
      // Turn on the table view if the graph is too complex, but skip
      // this block if the user has already loaded topologies once.
      if (!state.get('initialNodesLoaded') && !state.get('nodesLoaded')) {
        if (state.get('topologyViewMode') === GRAPH_VIEW_MODE) {
          state = graphExceedsComplexityThreshSelector(state)
            ? state.set('topologyViewMode', TABLE_VIEW_MODE) : state;
        }
        state = state.set('initialNodesLoaded', true);
      }
      return state.set('nodesLoaded', true);
    }

    case ActionTypes.RECEIVE_NODES_DELTA: {
      // Ignore periodic nodes updates after the first load when paused.
      if (state.get('nodesLoaded') && state.get('pausedAt')) {
        return state;
      }

      log(
        'RECEIVE_NODES_DELTA',
        'remove', size(action.delta.remove),
        'update', size(action.delta.update),
        'add', size(action.delta.add),
        'reset', action.delta.reset
      );

      if (action.delta.reset) {
        state = state.set('nodes', makeMap());
      }

      // remove nodes that no longer exist
      each(action.delta.remove, (nodeId) => {
        state = state.deleteIn(['nodes', nodeId]);
      });

      // update existing nodes
      each(action.delta.update, (node) => {
        if (state.hasIn(['nodes', node.id])) {
          // TODO: Implement a manual deep update here, as it might bring a great benefit
          // to our nodes selectors (e.g. layout engine would be completely bypassed if the
          // adjacencies would stay the same but the metrics would get updated).
          state = state.setIn(['nodes', node.id], fromJS(node));
        }
      });

      // add new nodes
      each(action.delta.add, (node) => {
        state = state.setIn(['nodes', node.id], fromJS(node));
      });

      return updateStateFromNodes(state);
    }

    case ActionTypes.RECEIVE_NODES: {
      state = state.set('timeTravelTransitioning', false);
      state = state.set('nodes', fromJS(action.nodes));
      state = state.set('nodesLoaded', true);
      return updateStateFromNodes(state);
    }

    case ActionTypes.RECEIVE_NODES_FOR_TOPOLOGY: {
      return state.setIn(['nodesByTopology', action.topologyId], fromJS(action.nodes));
    }

    case ActionTypes.RECEIVE_NOT_FOUND: {
      if (state.hasIn(['nodeDetails', action.nodeId])) {
        state = state.updateIn(['nodeDetails', action.nodeId], obj => ({
          ...obj,
          notFound: true,
          timestamp: action.requestTimestamp,
        }));
      }
      return state;
    }

    case ActionTypes.RECEIVE_TOPOLOGIES: {
      state = state.set('errorUrl', null);
      state = state.update('topologyUrlsById', topologyUrlsById => topologyUrlsById.clear());
      state = processTopologies(state, action.topologies);
      const currentTopologyId = state.get('currentTopologyId');
      if (!currentTopologyId || !findTopologyById(state.get('topologies'), currentTopologyId)) {
        state = state.set('currentTopologyId', getDefaultTopology(state.get('topologies')));
        log(`Set currentTopologyId to ${state.get('currentTopologyId')}`);
      }
      state = setTopology(state, state.get('currentTopologyId'));

      // Expand topology options with topologies' defaults on first load, but let
      // the current state of topologyOptions (which at this point reflects the
      // URL state) still take the precedence over defaults.
      if (!state.get('topologiesLoaded')) {
        const options = getDefaultTopologyOptions(state).mergeDeep(state.get('topologyOptions'));
        state = state.set('topologyOptions', options);
        state = state.set('topologiesLoaded', true);
      }

      return state;
    }

    case ActionTypes.RECEIVE_API_DETAILS: {
      state = state.set('errorUrl', null);

      return state.merge({
        capabilities: action.capabilities,
        hostname: action.hostname,
        plugins: action.plugins,
        version: action.version,
        versionUpdate: action.newVersion,
      });
    }

    case ActionTypes.ROUTE_TOPOLOGY: {
      state = state.set('routeSet', true);
      state = state.set('pinnedSearches', makeList(action.state.pinnedSearches));
      state = state.set('searchQuery', action.state.searchQuery || '');
      if (state.get('currentTopologyId') !== action.state.topologyId) {
        state = clearNodes(state);
      }
      state = setTopology(state, action.state.topologyId);
      state = state.merge({
        pinnedMetricType: action.state.pinnedMetricType,
        selectedNodeId: action.state.selectedNodeId,
      });
      if (action.state.topologyOptions) {
        const options = getDefaultTopologyOptions(state).mergeDeep(action.state.topologyOptions);
        state = state.set('topologyOptions', options);
      }
      if (action.state.topologyViewMode) {
        state = state.set('topologyViewMode', action.state.topologyViewMode);
      }
      if (action.state.gridSortedBy) {
        state = state.set('gridSortedBy', action.state.gridSortedBy);
      }
      if (action.state.gridSortedDesc !== undefined) {
        state = state.set('gridSortedDesc', action.state.gridSortedDesc);
      }
      if (action.state.contrastMode !== undefined) {
        state = state.set('contrastMode', action.state.contrastMode);
      }
      if (action.state.showingNetworks) {
        state = state.set('showingNetworks', action.state.showingNetworks);
      }
      if (action.state.pinnedNetwork) {
        state = state.set('pinnedNetwork', action.state.pinnedNetwork);
        state = state.set('selectedNetwork', action.state.pinnedNetwork);
      }
      if (action.state.controlPipe) {
        state = state.set('controlPipes', makeOrderedMap({
          [action.state.controlPipe.id]:
            makeOrderedMap(action.state.controlPipe)
        }));
      } else {
        state = state.update('controlPipes', controlPipes => controlPipes.clear());
      }
      if (action.state.nodeDetails) {
        const actionNodeDetails = makeOrderedMap(action.state.nodeDetails.map(h => [h.id, h]));
        // check if detail IDs have changed
        if (!isDeepEqual(state.get('nodeDetails').keySeq(), actionNodeDetails.keySeq())) {
          state = state.set('nodeDetails', actionNodeDetails);
        }
      } else {
        state = state.update('nodeDetails', nodeDetails => nodeDetails.clear());
      }
      return state;
    }

    case ActionTypes.DEBUG_TOOLBAR_INTERFERING: {
      return action.fn(state);
    }

    case ActionTypes.TOGGLE_TROUBLESHOOTING_MENU: {
      return state.set('showingTroubleshootingMenu', !state.get('showingTroubleshootingMenu'));
    }

    case ActionTypes.CHANGE_INSTANCE: {
      state = closeAllNodeDetails(state);
      return state;
    }

    case ActionTypes.TOGGLE_CONTRAST_MODE: {
      return state.set('contrastMode', action.enabled);
    }

    case ActionTypes.SHUTDOWN: {
      return clearNodes(state);
    }

    case ActionTypes.MONITOR_STATE: {
      return state.set('monitor', action.monitor);
    }

    case ActionTypes.SET_STORE_VIEW_STATE: {
      return state.set('storeViewState', action.storeViewState);
    }

    default: {
      return state;
    }
  }
}

export default rootReducer;
