import React, { Component } from 'react';
import PropTypes from 'prop-types';
import Immutable from 'immutable';
import { defineMessages, intlShape, FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import get from 'lodash/get';
import { Table } from 'cspace-layout';
import dimensions from '../../../styles/dimensions.css';
import styles from '../../../styles/cspace-ui/SearchResultTable.css';
import emptyResultStyles from '../../../styles/cspace-ui/SearchResultEmpty.css';
import { getListTypeFromResult } from '../../helpers/searchHelpers';

const rowHeight = parseInt(dimensions.inputHeight, 10);

const messages = defineMessages({
  searchPending: {
    id: 'searchResultTable.searchPending',
    defaultMessage: '⋯',
  },
  rowLabel: {
    id: 'searchResultTable.rowLabel',
    description: 'The aria-label for a row',
    defaultMessage: '{primary} selected row {index} of {total}',
  },
});

/**
 * Determines if a column is sortable for a given search. A column is sortable if sortBy is truthy,
 * and the search is not constrained by a related record, or if it is, the field to sort by is not
 * complex. This is here to deal with CSPACE-5366 (searches with related record constraints are
 * done using CMIS, which can't see into complex fields). If that bug is ever fixed, then it will
 * suffice just to check sortBy.
 */
const isSortable = (column, searchDescriptor) => {
  const { sortBy } = column;

  return (sortBy && (!searchDescriptor.getIn(['searchQuery', 'rel']) || sortBy.indexOf('/0/') === -1));
};

const rowRenderer = (params, location, ariaLabel) => {
  // This is a fork of react-virtualized's default row renderer:
  // https://github.com/bvaughn/react-virtualized/blob/master/source/Table/defaultRowRenderer.js

  const {
    className,
    columns,
    index,
    key,
    onRowClick,
    // onRowDoubleClick,
    // onRowMouseOut,
    // onRowMouseOver,
    // onRowRightClick,
    rowData,
    style,
  } = params;

  const a11yProps = {};

  if (
    onRowClick // ||
    // onRowDoubleClick ||
    // onRowMouseOut ||
    // onRowMouseOver ||
    // onRowRightClick
  ) {
    a11yProps['aria-label'] = ariaLabel;
    a11yProps.tabIndex = 0;

    if (onRowClick) {
      a11yProps.onClick = (event) => onRowClick({ event, index, rowData });
    }

    // if (onRowDoubleClick) {
    //   a11yProps.onDoubleClick = event =>
    //     onRowDoubleClick({ event, index, rowData });
    // }

    // if (onRowMouseOut) {
    //   a11yProps.onMouseOut = event => onRowMouseOut({ event, index, rowData });
    // }

    // if (onRowMouseOver) {
    //   a11yProps.onMouseOver = event => onRowMouseOver({ event, index, rowData });
    // }

    // if (onRowRightClick) {
    //   a11yProps.onContextMenu = event =>
    //     onRowRightClick({ event, index, rowData });
    // }
  }

  if (location) {
    return (
      <Link
        {...a11yProps}
        className={className}
        key={key}
        role="row"
        style={style}
        to={location}
      >
        {columns}
      </Link>
    );
  }

  return (
    <div
      {...a11yProps}
      className={className}
      data-index={index}
      key={key}
      role="row"
      style={style}
    >
      {columns}
    </div>
  );
};

const propTypes = {
  columnSetName: PropTypes.string,
  config: PropTypes.shape({
    listTypes: PropTypes.object,
    recordTypes: PropTypes.object,
    subresources: PropTypes.object,
  }).isRequired,
  formatCellData: PropTypes.func,
  formatColumnLabel: PropTypes.func,
  intl: intlShape,
  isSearchPending: PropTypes.bool,
  linkItems: PropTypes.bool,
  // eslint-disable-next-line react/forbid-prop-types
  linkState: PropTypes.object,
  listType: PropTypes.string,
  perms: PropTypes.instanceOf(Immutable.Map),
  searchDescriptor: PropTypes.instanceOf(Immutable.Map),
  searchError: PropTypes.instanceOf(Immutable.Map),
  searchResult: PropTypes.instanceOf(Immutable.Map),
  showCheckboxColumn: PropTypes.bool,
  renderCheckbox: PropTypes.func,
  renderHeader: PropTypes.func,
  renderFooter: PropTypes.func,
  renderSelectBar: PropTypes.func,
  getItemLocation: PropTypes.func,
  onItemClick: PropTypes.func,
  onSortChange: PropTypes.func,
};

const defaultProps = {
  columnSetName: 'default',
  formatCellData: (column, data) => data,
  formatColumnLabel: (column) => get(column, ['messages', 'label', 'defaultMessage']),
  linkItems: true,
  listType: 'common',
  renderHeader: () => null,
  renderFooter: () => null,
  renderSelectBar: () => null,
};

export default class SearchResultTable extends Component {
  constructor() {
    super();

    this.getColumnConfig = this.getColumnConfig.bind(this);
    this.getItemLocation = this.getItemLocation.bind(this);
    this.handleKeyDown = this.handleKeyDown.bind(this);
    this.handleRowClick = this.handleRowClick.bind(this);
    this.renderNoItems = this.renderNoItems.bind(this);
    this.renderRow = this.renderRow.bind(this);
    this.renderRowLabel = this.renderRowLabel.bind(this);
    this.sort = this.sort.bind(this);
  }

  handleKeyDown(event) {
    if (event.key === 'Enter') {
      const index = get(event, ['target', 'dataset', 'index']);

      if (typeof index !== 'undefined') {
        this.handleRowClick(event.target.dataset.index);
      }
    }
  }

  handleRowClick(index) {
    const {
      config,
      searchResult,
      onItemClick,
    } = this.props;

    if (onItemClick) {
      const listType = getListTypeFromResult(config, searchResult);
      const listTypeConfig = config.listTypes[listType];
      const { listNodeName, itemNodeName } = listTypeConfig;

      const items = searchResult.getIn([listNodeName, itemNodeName]);
      const item = Immutable.List.isList(items) ? items.get(index) : items;

      onItemClick(item, index);
    }
  }

  getColumnConfig() {
    const {
      columnSetName,
      config,
      searchDescriptor,
    } = this.props;

    const recordType = searchDescriptor.get('recordType');
    const subresource = searchDescriptor.get('subresource');

    const columnConfigurer = subresource
      ? config.subresources[subresource]
      : config.recordTypes[recordType];

    let columnConfig = get(columnConfigurer, ['columns', columnSetName]);

    if (!columnConfig && columnSetName !== defaultProps.columnSetName) {
      // Fall back to the default column set if the named one doesn't exist.

      columnConfig = get(columnConfigurer, ['columns', defaultProps.columnSetName]);
    }

    if (!columnConfig) {
      columnConfig = [];
    }

    return columnConfig;
  }

  getItemLocation(item) {
    const {
      config,
      linkState,
      perms,
      searchResult,
      searchDescriptor,
    } = this.props;

    const listType = getListTypeFromResult(config, searchResult);
    const getItemLocationPath = get(config, ['listTypes', listType, 'getItemLocationPath']);

    if (!getItemLocationPath) {
      return undefined;
    }

    const itemContext = { config, perms, searchDescriptor };
    const itemLocationPath = getItemLocationPath(item, itemContext);

    if (!itemLocationPath) {
      return undefined;
    }

    // Create a location with the item location path, along with enough state to reproduce this
    // search. The search descriptor is converted to an object in order to reliably store it in
    // location state. Also merge in any object that was passed in via the linkState prop.

    const state = {
      searchDescriptor: searchDescriptor.toJS(),
      // The search traverser on records will always link to the search result page, so use
      // its search name.
      searchName: 'searchResultPage',
      ...linkState,
    };

    return {
      state,
      pathname: itemLocationPath,
    };
  }

  sort({ sortBy, sortDirection }) {
    const {
      onSortChange,
    } = this.props;

    if (onSortChange) {
      onSortChange(sortBy + (sortDirection === Table.SortDirection.DESC ? ' desc' : ''));
    }
  }

  renderNoItems() {
    const {
      isSearchPending,
    } = this.props;

    const message = isSearchPending ? <FormattedMessage {...messages.searchPending} /> : null;

    return <div className={emptyResultStyles.common}>{message}</div>;
  }

  renderRowLabel(params, totalItems) {
    const {
      intl,
    } = this.props;

    const {
      index,
      rowData,
    } = params;

    const columnConfig = this.getColumnConfig();
    const primaryCol = Object.keys(columnConfig)
      .filter((col) => col !== 'workflowState')
      .at(0);

    const primaryData = rowData.get(primaryCol);
    const label = primaryData
      ? intl.formatMessage(messages.rowLabel,
        { primary: primaryData, index: index + 1, total: totalItems })
      : 'row';

    return label;
  }

  renderRow(params, totalItems) {
    const {
      getItemLocation,
      linkItems,
    } = this.props;

    const {
      rowData,
    } = params;

    let location;

    if (linkItems) {
      const locationGetter = getItemLocation || this.getItemLocation;

      location = locationGetter(rowData);
    }

    const ariaLabel = this.renderRowLabel(params, totalItems);

    return rowRenderer(params, location, ariaLabel);
  }

  renderTable() {
    const {
      config,
      formatCellData,
      formatColumnLabel,
      searchDescriptor,
      searchResult,
      showCheckboxColumn,
      renderCheckbox,
    } = this.props;

    if (searchResult) {
      const listType = getListTypeFromResult(config, searchResult);
      const searchQuery = searchDescriptor.get('searchQuery');

      const listTypeConfig = config.listTypes[listType];
      const { listNodeName, itemNodeName } = listTypeConfig;

      let sortColumnName = null;
      let sortDir = null;

      const sortSpec = searchQuery.get('sort');

      if (sortSpec) {
        [sortColumnName, sortDir] = sortSpec.split(' ');
      }

      const list = searchResult.get(listNodeName);
      const pageSize = parseInt(list.get('pageSize'), 10);
      const totalItems = parseInt(list.get('totalItems'), 10);
      const itemsInPage = parseInt(list.get('itemsInPage'), 10);

      let items = list.get(itemNodeName);

      if (!items) {
        items = Immutable.List();
      }

      if (!Immutable.List.isList(items)) {
        // If there's only one result, it won't be returned as a list.
        items = Immutable.List.of(items);
      }

      const columnConfig = this.getColumnConfig();

      const columns = Object.keys(columnConfig)
        .filter((name) => !columnConfig[name].disabled)
        .sort((nameA, nameB) => {
          const orderA = columnConfig[nameA].order;
          const orderB = columnConfig[nameB].order;

          return orderA - orderB;
        })
        .map((name) => {
          const column = columnConfig[name];

          return {
            cellDataGetter: ({ dataKey, rowData }) => {
              let data = null;

              if (rowData) {
                const keys = dataKey.split('|');

                for (let i = 0; i < keys.length; i += 1) {
                  const candidateValue = rowData.get(keys[i]);

                  if (candidateValue) {
                    data = candidateValue;
                    break;
                  }
                }
              }

              return formatCellData(column, data, rowData);
            },
            disableSort: !isSortable(column, searchDescriptor),
            flexGrow: column.flexGrow,
            flexShrink: column.flexShrink,
            label: formatColumnLabel(column),
            dataKey: column.dataKey || name,
            width: column.width,
          };
        });

      let heightBasis;

      if (Number.isNaN(totalItems)) {
        // We don't yet know how many items are found by the search. Set the height to one item, so
        // an ellipsis (or other calculating indicator) can be shown.

        heightBasis = 1;
      } else {
        // If all of the search results fit on one page, shrink the table to fit the number of
        // results. Otherwise, size the table to fit the desired page size, even if there aren't
        // that many results on this page. This keeps the pager from jumping up on the last page
        // and while page/sorting changes are in progress.

        heightBasis = (totalItems <= pageSize && !Number.isNaN(itemsInPage))
          ? itemsInPage
          : pageSize;

        if (heightBasis === 0) {
          // If there are no items, set the height to one, because it looks weird when the footer
          // is mashed up against the header. This also leaves room to display a "no records found"
          // message if desired.

          heightBasis = 1;
        }
      }

      const height = (heightBasis * rowHeight) + rowHeight;
      const renderRowWithTotal = (params) => this.renderRow(params, totalItems);

      return (
        <div style={{ height }}>
          <Table
            columns={columns}
            rowCount={items.size}
            rowGetter={({ index }) => items.get(index)}
            showCheckboxColumn={showCheckboxColumn}
            onRowClick={this.handleRowClick}
            renderCheckbox={renderCheckbox}
            sort={this.sort}
            sortBy={sortColumnName}
            sortDirection={sortDir === 'desc' ? Table.SortDirection.DESC : Table.SortDirection.ASC}
            noRowsRenderer={this.renderNoItems}
            rowRenderer={renderRowWithTotal}
          />
        </div>
      );
    }

    return null;
  }

  render() {
    const {
      isSearchPending,
      searchError,
      searchResult,
      renderHeader,
      renderSelectBar,
      renderFooter,
    } = this.props;

    return (
      // eslint-disable-next-line jsx-a11y/no-static-element-interactions
      <div
        className={styles.common}
        role="presentation"
        onKeyDown={this.handleKeyDown}
      >
        {renderHeader({ isSearchPending, searchError, searchResult })}
        {renderSelectBar()}
        {this.renderTable()}
        {renderFooter({ isSearchPending, searchError, searchResult })}
      </div>
    );
  }
}

SearchResultTable.propTypes = propTypes;
SearchResultTable.defaultProps = defaultProps;
