import React from 'react';
import classNames from 'classnames';
import { connect } from 'react-redux';
import { fromJS, Map as makeMap, List as makeList } from 'immutable';
import theme from 'weaveworks-ui-components/lib/theme';

import NodeContainer from './node-container';
import EdgeContainer from './edge-container';
import { getAdjacentNodes, hasSelectedNode as hasSelectedNodeFn } from '../utils/topology-utils';
import { graphExceedsComplexityThreshSelector } from '../selectors/topology';
import { nodeNetworksSelector, selectedNetworkNodesIdsSelector } from '../selectors/node-networks';
import { searchNodeMatchesSelector } from '../selectors/search';
import { nodeMetricSelector } from '../selectors/node-metric';
import {
  highlightedNodeIdsSelector,
  highlightedEdgeIdsSelector
} from '../selectors/graph-view/decorators';
import {
  selectedScaleSelector,
  layoutNodesSelector,
  layoutEdgesSelector
} from '../selectors/graph-view/layout';

import { NODE_BASE_SIZE } from '../constants/styles';
import {
  BLURRED_EDGES_LAYER,
  BLURRED_NODES_LAYER,
  NORMAL_EDGES_LAYER,
  NORMAL_NODES_LAYER,
  HIGHLIGHTED_EDGES_LAYER,
  HIGHLIGHTED_NODES_LAYER,
  HOVERED_EDGES_LAYER,
  HOVERED_NODES_LAYER,
} from '../constants/naming';


class NodesChartElements extends React.Component {
  constructor(props, context) {
    super(props, context);

    this.renderNode = this.renderNode.bind(this);
    this.renderEdge = this.renderEdge.bind(this);
    this.renderElement = this.renderElement.bind(this);
    this.nodeDisplayLayer = this.nodeDisplayLayer.bind(this);
    this.edgeDisplayLayer = this.edgeDisplayLayer.bind(this);

    // Node decorators
    this.nodeHighlightedDecorator = this.nodeHighlightedDecorator.bind(this);
    this.nodeFocusedDecorator = this.nodeFocusedDecorator.bind(this);
    this.nodeBlurredDecorator = this.nodeBlurredDecorator.bind(this);
    this.nodeMatchesDecorator = this.nodeMatchesDecorator.bind(this);
    this.nodeNetworksDecorator = this.nodeNetworksDecorator.bind(this);
    this.nodeMetricDecorator = this.nodeMetricDecorator.bind(this);
    this.nodeScaleDecorator = this.nodeScaleDecorator.bind(this);

    // Edge decorators
    this.edgeFocusedDecorator = this.edgeFocusedDecorator.bind(this);
    this.edgeBlurredDecorator = this.edgeBlurredDecorator.bind(this);
    this.edgeHighlightedDecorator = this.edgeHighlightedDecorator.bind(this);
    this.edgeScaleDecorator = this.edgeScaleDecorator.bind(this);
    this.state={
      nodeColor:'#5B8FF9'
    }
  }

  nodeDisplayLayer(node) {
    if (node.get('id') === this.props.mouseOverNodeId) {
      return HOVERED_NODES_LAYER;
    } if (node.get('blurred') && !node.get('focused')) {
      return BLURRED_NODES_LAYER;
    } if (node.get('highlighted')) {
      return HIGHLIGHTED_NODES_LAYER;
    }
    return NORMAL_NODES_LAYER;
  }

  edgeDisplayLayer(edge) {
    if (edge.get('id') === this.props.mouseOverEdgeId) {
      return HOVERED_EDGES_LAYER;
    } if (edge.get('blurred') && !edge.get('focused')) {
      return BLURRED_EDGES_LAYER;
    } if (edge.get('highlighted')) {
      return HIGHLIGHTED_EDGES_LAYER;
    }
    return NORMAL_EDGES_LAYER;
  }

  nodeHighlightedDecorator(node) {
    const nodeSelected = (this.props.selectedNodeId === node.get('id'));
    const nodeHighlighted = this.props.highlightedNodeIds.has(node.get('id'));
    return node.set('highlighted', nodeHighlighted || nodeSelected);
  }

  nodeFocusedDecorator(node) {
    const nodeSelected = (this.props.selectedNodeId === node.get('id'));
    const isNeighborOfSelected = this.props.neighborsOfSelectedNode.includes(node.get('id'));
    return node.set('focused', nodeSelected || isNeighborOfSelected);
  }

  nodeBlurredDecorator(node) {
    const belongsToNetwork = this.props.selectedNetworkNodesIds.contains(node.get('id'));
    const noMatches = this.props.searchNodeMatches.get(node.get('id'), makeMap()).isEmpty();
    const notMatched = (this.props.searchQuery && !node.get('highlighted') && noMatches);
    const notFocused = (this.props.selectedNodeId && !node.get('focused'));
    const notInNetwork = (this.props.selectedNetwork && !belongsToNetwork);
    return node.set('blurred', notMatched || notFocused || notInNetwork);
  }

  nodeMatchesDecorator(node) {
    return node.set('matches', this.props.searchNodeMatches.get(node.get('id')));
  }

  nodeNetworksDecorator(node) {
    return node.set('networks', this.props.nodeNetworks.get(node.get('id')));
  }

  nodeMetricDecorator(node) {
    return node.set('metric', this.props.nodeMetric.get(node.get('id')));
  }

  nodeScaleDecorator(node) {
    return node.set('scale', node.get('focused') ? this.props.selectedScale : 1);
  }

  edgeHighlightedDecorator(edge) {
    return edge.set('highlighted', this.props.highlightedEdgeIds.has(edge.get('id')));
  }

  edgeFocusedDecorator(edge) {
    const sourceSelected = (this.props.selectedNodeId === edge.get('source'));
    const targetSelected = (this.props.selectedNodeId === edge.get('target'));
    return edge.set('focused', this.props.hasSelectedNode && (sourceSelected || targetSelected));
  }

  edgeBlurredDecorator(edge) {
    const { selectedNodeId, searchNodeMatches, selectedNetworkNodesIds } = this.props;
    const sourceSelected = (selectedNodeId === edge.get('source'));
    const targetSelected = (selectedNodeId === edge.get('target'));
    const otherNodesSelected = this.props.hasSelectedNode && !sourceSelected && !targetSelected;
    const sourceNoMatches = searchNodeMatches.get(edge.get('source'), makeMap()).isEmpty();
    const targetNoMatches = searchNodeMatches.get(edge.get('target'), makeMap()).isEmpty();
    const notMatched = this.props.searchQuery && (sourceNoMatches || targetNoMatches);
    const sourceInNetwork = selectedNetworkNodesIds.contains(edge.get('source'));
    const targetInNetwork = selectedNetworkNodesIds.contains(edge.get('target'));
    const notInNetwork = this.props.selectedNetwork && (!sourceInNetwork || !targetInNetwork);
    return edge.set('blurred', !edge.get('highlighted') && !edge.get('focused')
      && (otherNodesSelected || notMatched || notInNetwork));
  }

  edgeScaleDecorator(edge) {
    return edge.set('scale', edge.get('focused') ? this.props.selectedScale : 1);
  }

  renderNode(node) {
    const { isAnimated } = this.props;
  
    // old versions of scope reports have a node shape of `storagesheet`
    // if so, normalise to `sheet`
    const shape = node.get('shape') === 'storagesheet' ? 'sheet' : node.get('shape');

    return (
      <NodeContainer
        matches={node.get('matches')}
        networks={node.get('networks')}
        metric={node.get('metric')}
        focused={node.get('focused')}
        highlighted={node.get('highlighted')}
        shape={shape}
        tag={node.get('tag')}
        stacked={node.get('stack')}
        key={node.get('id')}
        id={node.get('id')}
        label={node.get('label')}
        labelMinor={node.get('labelMinor')}
        pseudo={node.get('pseudo')}
        rank={node.get('rank')}
        x={node.get('x')}
        y={node.get('y')}
        size={node.get('scale') * NODE_BASE_SIZE}
        isAnimated={isAnimated}
        color={node.get('color')}
      />
    );
  }

  renderEdge(edge) {
    const { isAnimated } = this.props;
    return (
      <EdgeContainer
        key={edge.get('id')}
        id={edge.get('id')}
        source={edge.get('source')}
        target={edge.get('target')}
        waypoints={edge.get('points')}
        highlighted={edge.get('highlighted')}
        focused={edge.get('focused')}
        scale={edge.get('scale')}
        isAnimated={isAnimated}
      />
    );
  }

  renderOverlay(element) {
    // NOTE: This piece of code is a bit hacky - as we can't set the absolute coords for the
    // SVG element, we set the zoom level high enough that we're sure it covers the screen.
    const className = classNames('nodes-chart-overlay', { active: element.get('isActive') });
    const scale = (this.props.selectedScale || 1) * 100000;
    return (
      <rect
        className={className}
        key="nodes-chart-overlay"
        transform={`scale(${scale})`}
        fill={theme.colors.purple25}
        x={-1}
        y={-1}
        width={2}
        height={2}
      />
    );
  }

  renderElement(element) {
    if (element.get('isOverlay')) {
      return this.renderOverlay(element);
    }
    // This heuristics is not ideal but it works.
    return element.get('points') ? this.renderEdge(element) : this.renderNode(element);
  }

  render() {
    const nodes = this.props.layoutNodes.toIndexedSeq()
      .map(this.nodeHighlightedDecorator)
      .map(this.nodeFocusedDecorator)
      .map(this.nodeBlurredDecorator)
      .map(this.nodeMatchesDecorator)
      .map(this.nodeNetworksDecorator)
      .map(this.nodeMetricDecorator)
      .map(this.nodeScaleDecorator)
      .groupBy(this.nodeDisplayLayer);

    const edges = this.props.layoutEdges.toIndexedSeq()
      .map(this.edgeHighlightedDecorator)
      .map(this.edgeFocusedDecorator)
      .map(this.edgeBlurredDecorator)
      .map(this.edgeScaleDecorator)
      .groupBy(this.edgeDisplayLayer);

    // NOTE: The elements need to be arranged into a single array outside
    // of DOM structure for React rendering engine to do smart rearrangements
    // without unnecessary re-rendering of the elements themselves. So e.g.
    // rendering the element layers individually below would be significantly slower.
    const orderedElements = makeList([
      edges.get(BLURRED_EDGES_LAYER, makeList()),
      nodes.get(BLURRED_NODES_LAYER, makeList()),
      fromJS([{ isActive: !!nodes.get(BLURRED_NODES_LAYER), isOverlay: true }]),
      edges.get(NORMAL_EDGES_LAYER, makeList()),
      nodes.get(NORMAL_NODES_LAYER, makeList()),
      edges.get(HIGHLIGHTED_EDGES_LAYER, makeList()),
      nodes.get(HIGHLIGHTED_NODES_LAYER, makeList()),
      edges.get(HOVERED_EDGES_LAYER, makeList()),
      nodes.get(HOVERED_NODES_LAYER, makeList()),
    ]).flatten(true);

    return (
      <g className="tour-step-anchor nodes-chart-elements">
        {orderedElements.map(this.renderElement)}
      </g>
    );
  }
}


function mapStateToProps(state) {
  return {
    contrastMode: state.get('contrastMode'),
    hasSelectedNode: hasSelectedNodeFn(state),
    highlightedEdgeIds: highlightedEdgeIdsSelector(state),
    highlightedNodeIds: highlightedNodeIdsSelector(state),
    isAnimated: !graphExceedsComplexityThreshSelector(state),
    layoutEdges: layoutEdgesSelector(state),
    layoutNodes: layoutNodesSelector(state),
    mouseOverEdgeId: state.get('mouseOverEdgeId'),
    mouseOverNodeId: state.get('mouseOverNodeId'),
    neighborsOfSelectedNode: getAdjacentNodes(state),
    nodeMetric: nodeMetricSelector(state),
    nodeNetworks: nodeNetworksSelector(state),
    searchNodeMatches: searchNodeMatchesSelector(state),
    searchQuery: state.get('searchQuery'),
    selectedNetwork: state.get('selectedNetwork'),
    selectedNetworkNodesIds: selectedNetworkNodesIdsSelector(state),
    selectedNodeId: state.get('selectedNodeId'),
    selectedScale: selectedScaleSelector(state),
  };
}

export default connect(mapStateToProps)(NodesChartElements);
