import React from 'react';
import classNames from 'classnames';
import { connect } from 'react-redux';
import {
  find, get, union, sortBy, groupBy, concat, debounce
} from 'lodash';

import { NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT } from '../../constants/limits';
import { TABLE_ROW_FOCUS_DEBOUNCE_INTERVAL } from '../../constants/timer';

import ShowMore from '../show-more';
import NodeDetailsTableRow from './node-details-table-row';
import NodeDetailsTableHeaders from './node-details-table-headers';
import { ipToPaddedString } from '../../utils/string-utils';
import { moveElement, insertElement } from '../../utils/array-utils';
import {
  isIP, isNumeric, defaultSortDesc, getTableColumnsStyles
} from '../../utils/node-details-utils';


function getDefaultSortedBy(columns, nodes) {
  // default sorter specified by columns
  const defaultSortColumn = find(columns, { defaultSort: true });
  if (defaultSortColumn) {
    return defaultSortColumn.id;
  }
  // otherwise choose first metric
  const firstNodeWithMetrics = find(nodes, n => get(n, ['metrics', 0]));
  if (firstNodeWithMetrics) {
    return get(firstNodeWithMetrics, ['metrics', 0, 'id']);
  }

  return 'label';
}


function maybeToLower(value) {
  if (!value || !value.toLowerCase) {
    return value;
  }
  return value.toLowerCase();
}


function getNodeValue(node, header) {
  const fieldId = header && header.id;
  if (fieldId !== null) {
    let field = union(node.metrics, node.metadata).find(f => f.id === fieldId);

    if (field) {
      if (isIP(header)) {
        // Format the IPs so that they are sorted numerically.
        return ipToPaddedString(field.value);
      } if (isNumeric(header)) {
        return parseFloat(field.value);
      }
      return field.value;
    }

    if (node.parents) {
      field = node.parents.find(f => f.topologyId === fieldId);
      if (field) {
        return field.label;
      }
    }

    if (node[fieldId] !== undefined && node[fieldId] !== null) {
      return node[fieldId];
    }
  }

  return null;
}


function getValueForSortedBy(sortedByHeader) {
  return node => maybeToLower(getNodeValue(node, sortedByHeader));
}


function getMetaDataSorters(nodes) {
  // returns an array of sorters that will take a node
  return get(nodes, [0, 'metadata'], []).map((field, index) => (node) => {
    const nodeMetadataField = node.metadata && node.metadata[index];
    if (nodeMetadataField) {
      if (isNumeric(nodeMetadataField)) {
        return parseFloat(nodeMetadataField.value);
      }
      return nodeMetadataField.value;
    }
    return null;
  });
}


function sortNodes(nodes, getValue, sortedDesc) {
  const sortedNodes = sortBy(
    nodes,
    getValue,
    getMetaDataSorters(nodes)
  );
  if (sortedDesc) {
    sortedNodes.reverse();
  }
  return sortedNodes;
}


function getSortedNodes(nodes, sortedByHeader, sortedDesc) {
  const getValue = getValueForSortedBy(sortedByHeader);
  const withAndWithoutValues = groupBy(nodes, (n) => {
    if (!n || n.valueEmpty) {
      return 'withoutValues';
    }
    const v = getValue(n);
    return v !== null && v !== undefined ? 'withValues' : 'withoutValues';
  });
  const withValues = sortNodes(withAndWithoutValues.withValues, getValue, sortedDesc);
  const withoutValues = sortNodes(withAndWithoutValues.withoutValues, getValue, sortedDesc);

  return concat(withValues, withoutValues);
}


// By inserting this fake invisible row into the table, with the help of
// some CSS trickery, we make the inner scrollable content of the table
// have a minimal height. That prevents auto-scroll under a focus if the
// number of table rows shrinks.
function minHeightConstraint(height = 0) {
  return <tr className="min-height-constraint" style={{ height }} />;
}


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

    this.state = {
      limit: props.limit,
      sortedBy: this.props.sortedBy,
      sortedDesc: this.props.sortedDesc
    };
    this.focusState = {};

    this.updateSorted = this.updateSorted.bind(this);
    this.handleLimitClick = this.handleLimitClick.bind(this);
    this.onMouseLeaveRow = this.onMouseLeaveRow.bind(this);
    this.onMouseEnterRow = this.onMouseEnterRow.bind(this);
    this.saveTableContentRef = this.saveTableContentRef.bind(this);
    this.saveTableHeadRef = this.saveTableHeadRef.bind(this);
    // Use debouncing to prevent event flooding when e.g. crossing fast with mouse cursor
    // over the whole table. That would be expensive as each focus causes table to rerender.
    this.debouncedFocusRow = debounce(this.focusRow, TABLE_ROW_FOCUS_DEBOUNCE_INTERVAL);
    this.debouncedBlurRow = debounce(this.blurRow, TABLE_ROW_FOCUS_DEBOUNCE_INTERVAL);
  }

  updateSorted(sortedBy, sortedDesc) {
    this.setState({ sortedBy, sortedDesc });
    this.props.onSortChange(sortedBy, sortedDesc);
  }

  handleLimitClick() {
    this.setState(prevState => ({
      limit: prevState.limit ? 0 : this.props.limit
    }));
  }

  focusRow(rowIndex, node) {
    // Remember the focused row index, the node that was focused and
    // the table content height so that we can keep the node row fixed
    // without auto-scrolling happening.
    // NOTE: It would be ideal to modify the real component state here,
    // but that would cause whole table to rerender, which becomes to
    // expensive with the current implementation if the table consists
    // of 1000+ nodes.
    this.focusState = {
      focusedNode: node,
      focusedRowIndex: rowIndex,
      tableContentMinHeightConstraint: this.tableContentRef && this.tableContentRef.scrollHeight,
    };
  }

  blurRow() {
    // Reset the focus state
    this.focusState = {};
  }

  onMouseEnterRow(rowIndex, node) {
    this.debouncedBlurRow.cancel();
    this.debouncedFocusRow(rowIndex, node);
  }

  onMouseLeaveRow() {
    this.debouncedFocusRow.cancel();
    this.debouncedBlurRow();
  }

  saveTableContentRef(ref) {
    this.tableContentRef = ref;
  }

  saveTableHeadRef(ref) {
    this.tableHeadRef = ref;
  }

  getColumnHeaders() {
    const columns = this.props.columns || [];
    return [{ id: 'label', label: this.props.label }].concat(columns);
  }

  componentDidMount() {
    const scrollbarWidth = this.tableContentRef.offsetWidth - this.tableContentRef.clientWidth;
    this.tableHeadRef.style.paddingRight = `${scrollbarWidth}px`;
  }

  render() {
    const {
      nodeIdKey, columns, topologyId, onClickRow,
      onMouseEnter, onMouseLeave, timestamp
    } = this.props;

    const sortedBy = this.state.sortedBy || getDefaultSortedBy(columns, this.props.nodes);
    const sortedByHeader = this.getColumnHeaders().find(h => h.id === sortedBy);
    const sortedDesc = (this.state.sortedDesc === null)
      ? defaultSortDesc(sortedByHeader) : this.state.sortedDesc;

    let nodes = getSortedNodes(this.props.nodes, sortedByHeader, sortedDesc);

    const { focusedNode, focusedRowIndex, tableContentMinHeightConstraint } = this.focusState;
    if (Number.isInteger(focusedRowIndex) && focusedRowIndex < nodes.length) {
      const nodeRowIndex = nodes.findIndex(node => node.id === focusedNode.id);
      if (nodeRowIndex >= 0) {
        // If the focused node still exists in the table, we move it
        // to the hovered row, keeping the rest of the table sorted.
        nodes = moveElement(nodes, nodeRowIndex, focusedRowIndex);
      } else {
        // Otherwise we insert the dead focused node there, pretending
        // it's still alive. That enables the users to read off all the
        // info they want and perhaps even open the details panel. Also,
        // only if we do this, we can guarantee that mouse hover will
        // always freeze the table row until we focus out.
        nodes = insertElement(nodes, focusedRowIndex, focusedNode);
      }
    }

    // If we are 1 over the limit, we still show the row. We never display
    // "+1" but only "+2" and up.
    const limit = this.state.limit > 0 && nodes.length === this.state.limit + 1
      ? nodes.length
      : this.state.limit;
    const limited = nodes && limit > 0 && nodes.length > limit;
    const expanded = limit === 0;
    const notShown = nodes.length - limit;
    if (nodes && limited) {
      nodes = nodes.slice(0, limit);
    }

    const className = classNames('node-details-table-wrapper-wrapper', this.props.className);
    const headers = this.getColumnHeaders();
    const styles = getTableColumnsStyles(headers);

    return (
      <div className={className} style={this.props.style}>
        <div className="node-details-table-wrapper">
          <table className="node-details-table">
            <thead ref={this.saveTableHeadRef}>
              {this.props.nodes && this.props.nodes.length > 0 && (
                <NodeDetailsTableHeaders
                  headers={headers}
                  sortedBy={sortedBy}
                  sortedDesc={sortedDesc}
                  onClick={this.updateSorted}
                />
              )}
            </thead>
            <tbody
              style={this.props.tbodyStyle}
              ref={this.saveTableContentRef}
              onMouseEnter={onMouseEnter}
              onMouseLeave={onMouseLeave}>
              {nodes && nodes.map((node, index) => (
                <NodeDetailsTableRow
                  key={node.id}
                  renderIdCell={this.props.renderIdCell}
                  selected={this.props.selectedNodeId === node.id}
                  node={node}
                  index={index}
                  nodeIdKey={nodeIdKey}
                  colStyles={styles}
                  columns={columns}
                  onClick={onClickRow}
                  onMouseEnter={this.onMouseEnterRow}
                  onMouseLeave={this.onMouseLeaveRow}
                  timestamp={timestamp}
                  topologyId={topologyId} />
              ))}
              {minHeightConstraint(tableContentMinHeightConstraint)}
            </tbody>
          </table>
          <ShowMore
            handleClick={this.handleLimitClick}
            collection={nodes}
            expanded={expanded}
            notShown={notShown} />
        </div>
      </div>
    );
  }
}


NodeDetailsTable.defaultProps = {
  limit: NODE_DETAILS_DATA_ROWS_DEFAULT_LIMIT,
  // key to identify a node in a row (used for topology links)
  nodeIdKey: 'id',
  onSortChange: () => { },
  sortedBy: null,
  sortedDesc: null,
};

function mapStateToProps(state) {
  return {
    timestamp: state.get('pausedAt'),
  };
}

export default connect(mapStateToProps)(NodeDetailsTable);
