import debug from 'debug';
import { times } from 'lodash';
import { fromJS, Map as makeMap } from 'immutable';
import { createSelector } from 'reselect';

import { RESOURCES_LAYER_PADDING, RESOURCES_LAYER_HEIGHT } from '../../constants/styles';
import {
  RESOURCE_VIEW_MAX_LAYERS,
  RESOURCE_VIEW_LAYERS,
  TOPOLOGIES_WITH_CAPACITY,
} from '../../constants/resources';
import {
  nodeParentDecoratorByTopologyId,
  nodeMetricSummaryDecoratorByType,
  nodeResourceViewColorDecorator,
  nodeResourceBoxDecorator,
} from '../../decorators/node';


const log = debug('scope:nodes-layout');

// Used for ordering the resource nodes.
const resourceNodeConsumptionComparator = (node) => {
  const metricSummary = node.get('metricSummary');
  return metricSummary.get('showCapacity')
    ? -metricSummary.get('relativeConsumption')
    : -metricSummary.get('absoluteConsumption');
};

// A list of topologies shown in the resource view of the active topology (bottom to top).
export const layersTopologyIdsSelector = createSelector(
  [
    state => state.get('currentTopologyId'),
  ],
  topologyId => fromJS(RESOURCE_VIEW_LAYERS[topologyId] || [])
);

// Calculates the resource view layer Y-coordinate for every topology in the resource view.
export const layerVerticalPositionByTopologyIdSelector = createSelector(
  [
    layersTopologyIdsSelector,
  ],
  (topologiesIds) => {
    let yPositions = makeMap();
    let yCumulative = RESOURCES_LAYER_PADDING;

    topologiesIds.forEach((topologyId) => {
      yCumulative -= RESOURCES_LAYER_HEIGHT + RESOURCES_LAYER_PADDING;
      yPositions = yPositions.set(topologyId, yCumulative);
    });

    return yPositions;
  }
);

// Decorate and filter all the nodes to be displayed in the current resource view, except
// for the exact node horizontal offsets which are calculated from the data created here.
const decoratedNodesByTopologySelector = createSelector(
  [
    layersTopologyIdsSelector,
    state => state.get('pinnedMetricType'),
    // Generate the dependencies for this selector programmatically (because we want their
    // number to be customizable directly by changing the constant). The dependency functions
    // here depend on another selector, but this seems to work quite fine. For example, if
    // layersTopologyIdsSelector = ['hosts', 'containers'] and RESOURCE_VIEW_MAX_LAYERS = 3,
    // this code will generate:
    //   [
    //     state => state.getIn(['nodesByTopology', 'hosts'])
    //     state => state.getIn(['nodesByTopology', 'containers'])
    //     state => state.getIn(['nodesByTopology', undefined])
    //   ]
    // which will all be captured by `topologiesNodes` and processed correctly (even for undefined).
    ...times(RESOURCE_VIEW_MAX_LAYERS, index => (
      state => state.getIn(['nodesByTopology', layersTopologyIdsSelector(state).get(index)])
    ))
  ],
  (layersTopologyIds, pinnedMetricType, ...topologiesNodes) => {
    let nodesByTopology = makeMap();
    let parentLayerTopologyId = null;

    topologiesNodes.forEach((topologyNodes, index) => {
      const layerTopologyId = layersTopologyIds.get(index);
      const parentTopologyNodes = nodesByTopology.get(parentLayerTopologyId, makeMap());
      const showCapacity = TOPOLOGIES_WITH_CAPACITY.includes(layerTopologyId);
      const isBaseLayer = (index === 0);

      const nodeParentDecorator = nodeParentDecoratorByTopologyId(parentLayerTopologyId);
      const nodeMetricSummaryDecorator = nodeMetricSummaryDecoratorByType(
        pinnedMetricType,
        showCapacity
      );

      // Color the node, deduce its anchor point, dimensions and info about its pinned metric.
      const decoratedTopologyNodes = (topologyNodes || makeMap())
        .map(nodeResourceViewColorDecorator)
        .map(nodeMetricSummaryDecorator)
        .map(nodeResourceBoxDecorator)
        .map(nodeParentDecorator);

      const filteredTopologyNodes = decoratedTopologyNodes
        // Filter out the nodes with no parent in the topology of the previous layer, as their
        // positions in the layout could not be determined. The exception is the base layer.
        // TODO: Also make an exception for uncontained nodes (e.g. processes).
        .filter(node => parentTopologyNodes.has(node.get('parentNodeId')) || isBaseLayer)
        // Filter out the nodes with no metric summary data, which is needed to render the node.
        .filter(node => node.get('metricSummary'))
        // Filter out nodes with zero-width, as they will never be shown, no matter how much we
        // zoom in. The example is every node consuming less than 0.005% CPU, as the 2-digit
        // precision will round it down to zero.
        .filter(node => node.get('width') > 0);

      nodesByTopology = nodesByTopology.set(layerTopologyId, filteredTopologyNodes);
      parentLayerTopologyId = layerTopologyId;
    });

    return nodesByTopology;
  }
);

// Calculate (and fix) the offsets for all the displayed resource nodes.
export const layoutNodesByTopologyIdSelector = createSelector(
  [
    layersTopologyIdsSelector,
    decoratedNodesByTopologySelector,
  ],
  (layersTopologyIds, nodesByTopology) => {
    let layoutNodes = makeMap();
    let parentTopologyId = null;

    // Calculate the offsets bottom-to top as each layer needs to know exact offsets of its parents.
    layersTopologyIds.forEach((layerTopologyId) => {
      let positionedNodes = makeMap();

      // Get the nodes in the current layer grouped by their parent nodes.
      // Each of those buckets will be positioned and sorted independently.
      const nodesByParent = nodesByTopology
        .get(layerTopologyId, makeMap())
        .groupBy(n => n.get('parentNodeId'));

      nodesByParent.forEach((nodesBucket, parentNodeId) => {
        // Set the initial offset to the offset of the parent (that has already been set).
        // If there is no offset information, i.e. we're processing the base layer, set it to 0.
        const parentNode = layoutNodes.getIn([parentTopologyId, parentNodeId], makeMap());
        let currentOffset = parentNode.get('offset', 0);

        // Sort the nodes in the current bucket and lay them down one after another.
        nodesBucket.sortBy(resourceNodeConsumptionComparator).forEach((node, nodeId) => {
          const positionedNode = node.set('offset', currentOffset);
          positionedNodes = positionedNodes.set(nodeId, positionedNode);
          currentOffset += node.get('width');
        });

        // TODO: This block of code checks for the overlaps which are caused by children
        // consuming more resources than their parent node. This happens due to inconsistent
        // data being sent from the backend and it needs to be fixed there.
        const parentOffset = parentNode.get('offset', 0);
        const parentWidth = parentNode.get('width', currentOffset);
        const totalChildrenWidth = currentOffset - parentOffset;
        // If the total width of the children exceeds the parent node box width, we have a problem.
        // We fix it by shrinking all the children to by a factor to perfectly fit into the parent.
        if (totalChildrenWidth > parentWidth) {
          const shrinkFactor = parentWidth / totalChildrenWidth;
          log(`Inconsistent data: Children of ${parentNodeId} reported to use more `
            + `resource than the node itself - shrinking by factor ${shrinkFactor}`);
          // Shrink all the children.
          nodesBucket.forEach((_, nodeId) => {
            let node = positionedNodes.get(nodeId);
            // Shrink the width of the resource box and update its relative offset.
            node = node.merge(makeMap({
              offset: ((node.get('offset') - parentOffset) * shrinkFactor) + parentOffset,
              width: node.get('width') * shrinkFactor,
            }));
            // Update the metrics summary to reflect the adjusted dimensions for consistent data.
            node = nodeMetricSummaryDecoratorByType(
              node.getIn(['metricSummary', 'type']),
              node.get('showCapacity'),
              shrinkFactor
            )(node);
            // Update the node in the layout.
            positionedNodes = positionedNodes.mergeIn([nodeId], node);
          });
        }
      });

      // Update the layout with the positioned node from the current layer.
      layoutNodes = layoutNodes.mergeIn([layerTopologyId], positionedNodes);
      parentTopologyId = layerTopologyId;
    });

    return layoutNodes;
  }
);
