import React, { PureComponent, Children, isValidElement } from 'react';
import PropTypes from 'prop-types';
import orderBy from 'lodash/orderBy';
import chunk from 'lodash/chunk';
import { createRow } from './TableRow';
import './table.scss';

function sortChildren({ children, sort, order }) {
  if (sort instanceof Array) {
    return orderBy(children, item => item.props.value[sort[0]], order);
  }
  return children;
}

function filterChildren({ children, filterOptions: { filter, filterValues } }) {
  const result = children && children.filter(item => filterValues.includes(item.props.value[filter]));
  return result.length ? result : children;
}

function createFilteredRows({ children, isRowSelected, props, sort, order, filterOptions }) {
  let resultChildren = Children
    .toArray(children);

  const filteredChildren = filterOptions && filterOptions.filter ?
    filterChildren({ children: resultChildren, filterOptions }) : resultChildren;
  const sortedChildren = sortChildren({ children: filteredChildren, sort, order });

  resultChildren = filteredChildren.reduce((arrayChildren, child, index) => {
    const resultChild = createRow({ child, isRowSelected, index, props, rowNumber: sortedChildren.indexOf(child) });

    if (resultChild) {
      const { props: { value } } = resultChild;
      arrayChildren.push({ ...value, child: resultChild, index });
    }

    return arrayChildren;
  }, []) || [];
  if (sort instanceof Array) {
    resultChildren = orderBy(resultChildren, sort, order);
  }
  return resultChildren.map(({ child }) => child);
}

function getNewSelectedRows({ children, selectedChildren, sort, order }) {
  const sortedChildren = sortChildren({ children, sort, order });
  return sortedChildren.reduce((acc, child, index) => {
    if (selectedChildren.indexOf(child.props.index) > -1) {
      acc.push(index);
    }
    return acc;
  }, []);
}

function isValueInRange(value, range) {
  return !!range && (
    (range.start <= value && value <= range.end)
    || (range.end <= value && value <= range.start)
  );
}

function genRangeOfValues(start, offset) {
  let off = offset;
  const values = [];
  const dir = (offset > 0) ? -1 : 1; // This forces offset to approach 0 from either direction.
  while (off !== 0) {
    values.push(start + off);
    off += dir;
  }

  return values;
}

function flattenRanges(selectedRows) {
  return selectedRows.reduce(
    (rows, selection) => {
      if (typeof selection === 'object') {
        const values = genRangeOfValues(selection.end, selection.start - selection.end);
        values.push(selection.end);
        values.forEach(selectionRow => {
          if (rows.indexOf(selectionRow) === -1) {
            rows.push(selectionRow);
          }
        });
      } else if (rows.indexOf(selection) === -1) {
        rows.push(selection);
      }
      return rows;
    }, []
  ).sort();
}

function calculateSelectedRows(props) {
  // Determine what rows are 'pre-selected'.
  const preSelectedRows = [];
  const { selectable, multiSelectable, children } = props;
  if (selectable) {
    let index = 0;
    Children.forEach(children, child => {
      if (isValidElement(child)) {
        if (child.props.selected && (preSelectedRows.length === 0 || multiSelectable)) {
          preSelectedRows.push(index);
        }
        index += 1;
      }
    });
  }

  return preSelectedRows;
}


class TableBody extends PureComponent {
  static componentName = 'TableBody';

  static propTypes = {
    /**
     * Children passed to table body.
     */
    children: PropTypes.node, // eslint-disable-line react/no-unused-prop-types
    /**
     * Controls the display of the row checkbox.
     */
    displayRowCheckbox: PropTypes.bool,
    /**
     * @ignore
     * Callback function for when a cell is clicked.
     */
    onCellClick: PropTypes.func,
    /**
     * @ignore
     * Called when a row is selected. selectedRows is an
     * array of all row selections. IF all rows have been selected,
     * the string "all" will be returned instead to indicate that
     * all rows have been selected.
     */
    onChange: PropTypes.func,
    /**
     * @ignore
     * If true, multiple table rows can be selected.
     * CTRL/CMD+Click and SHIFT+Click are valid actions.
     */
    multiSelectable: PropTypes.bool,
    /**
     * @ignore
     * If true, table rows can be selected. If multiple
     * row selection is desired, enable multiSelectable.
     */
    selectable: PropTypes.bool,
    sort: PropTypes.array, // eslint-disable-line react/no-unused-prop-types
    order: PropTypes.string, // eslint-disable-line react/no-unused-prop-types
    /**
     * @ignore
     * if true on 'componentWillReceiveProps' children will be calculate
     */
    reCalculateSelection: PropTypes.bool
  };

  static defaultProps = {
    children: undefined,
    displayRowCheckbox: false,
    reCalculateSelection: true,
    multiSelectable: false,
    onCellClick: null,
    onChange: null,
    selectable: true,
    sort: null,
    order: 'asc'
  };

  state = {
    selectedRows: [],
    children: []
  };

  componentWillMount() {
    const selectedRows = calculateSelectedRows(this.props);
    this.setState({
      selectedRows,
      selectedChildren: selectedRows,
      children: this.buildChildren(selectedRows)
    });
  }

  componentWillReceiveProps(nextProps) {
    const { reCalculateSelection, sort, order } = nextProps;
    const { children } = this.state;

    if (reCalculateSelection) {
      const selectedChildren = calculateSelectedRows(nextProps);
      this.setState({
        selectedChildren,
        children: this.buildChildren(selectedChildren, nextProps)
      });
    } else {
      this.setState({
        children: this.buildChildren(this.state.selectedChildren, nextProps)
      });
    }
    if (sort !== this.props.sort || order !== this.props.order) {
      this.setState({
        selectedRows: getNewSelectedRows({ children, selectedChildren: this.state.selectedChildren, sort, order })
      });
    }
  }

  onCellClick = (event, rowNumber, columnNumber) => {
    event.stopPropagation();
    if (this.props.onCellClick) {
      this.props.onCellClick(rowNumber, this.getColumnId(columnNumber), event);
    }
  };

  onRowClick = (event, rowNumber) => {
    const { selectable } = this.props;

    if (selectable) {
      // Prevent text selection while selecting rows.
      this.processRowSelection(event, rowNumber);
    }
  };

  onChange(selectedRows) {
    const { onChange, multiSelectable } = this.props;
    const { children } = this.state;
    const values = children
      .filter(({ props: { rowNumber } }) => selectedRows.indexOf(rowNumber) !== -1)
      .map(({ props: { value } }) => value);
    onChange(multiSelectable ? values : values[0]);
  }

  getColumnId = columnNumber => (
    this.props.displayRowCheckbox ? columnNumber - 1 : columnNumber
  );

  buildPaginatedChildren = (resultChildren, paginate, pageCount, pageSelected, handlePageChange, setPageCount) => {
    const chunkedChildren = chunk(resultChildren, paginate);
    if (chunkedChildren.length !== pageCount) {
      handlePageChange({ selected: 0 });
    }
    setPageCount(chunkedChildren.length);
    return chunkedChildren[pageSelected];
  }

  buildChildren(selectedChildren, props) {
    const { children, displayRowCheckbox, sort, order,
      multiSelectable, selectable, filterOptions, paginate,
      pageSelected, setPageCount, pageCount, handlePageChange } = props || this.props;
    const safePaginate = Math.abs(parseInt(paginate, 10));
    const resultChildren = children ? createFilteredRows({
      children,
      sort,
      order,
      filterOptions,
      isRowSelected: (currentIndex => selectedChildren && selectedChildren.indexOf(currentIndex) > -1),
      props: {
        multiSelectable,
        displayRowCheckbox,
        onCellClick: this.onCellClick,
        onRowClick: this.onRowClick,
        selectable
      }
    }) : null;
    return paginate ?
      this.buildPaginatedChildren(resultChildren, safePaginate, pageCount, pageSelected, handlePageChange, setPageCount)
      :
      resultChildren;
  }

  isIndexInSelection = (rowNumber, rows) => {
    const selectedRows = rows || this.state.selectedRows;
    const selectedRow = selectedRows.find(
      selection => {
        if (typeof selection === 'object') {
          return isValueInRange(rowNumber, selection);
        }
        return (selection === rowNumber);
      }
    );
    return typeof selectedRow !== 'undefined';
  };

  processRowSelection(event, rowNumber) {
    let selectedRows = this.state.selectedRows ? [...this.state.selectedRows] : [];
    // let { selectedRows: [...selectedRows] } = this.state;
    const { multiSelectable, onChange } = this.props;

    if (event.shiftKey && multiSelectable && selectedRows.length) {
      const lastIndex = selectedRows.length - 1;
      const lastSelection = selectedRows[lastIndex];

      if (typeof lastSelection === 'object') {
        lastSelection.end = rowNumber;
      } else {
        selectedRows.splice(lastIndex, 1, { start: lastSelection, end: rowNumber });
      }
    } else if (
      multiSelectable && ((event.ctrlKey && !event.metaKey) || (event.metaKey && !event.ctrlKey) || event.shiftKey)
    ) {
      selectedRows = flattenRanges(selectedRows);
      const selectedIndexRow = selectedRows.indexOf(rowNumber);
      if (selectedIndexRow !== -1) {
        selectedRows.splice(selectedIndexRow, 1);
      } else {
        selectedRows.push(rowNumber);
      }
    } else {
      selectedRows = selectedRows.length === 1 && selectedRows[0] === rowNumber ? [] : [rowNumber];
    }
    const selectedChildren = this.state.children.reduce((acc, child, i) => {
      if (this.isIndexInSelection(i, selectedRows)) {
        acc.push(child.props.index);
      }
      return acc;
    }, []);
    this.setState({
      selectedRows,
      selectedChildren,
      children: this.buildChildren(selectedChildren)
    });
    if (onChange) {
      this.onChange(flattenRanges(selectedRows));
    }
  }

  render() {
    const { children } = this.state;
    return (
      <tbody styleName="table-body">
        {children}
      </tbody>
    );
  }
}


export default TableBody;
