/* eslint-disable no-unused-expressions, jsx-a11y/click-events-have-key-events */
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import ReactTable from 'react-table';
import 'react-table/react-table.css';
import _ from 'lodash';
import Moment from 'moment';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {
	Dropdown as SemanticDropdown,
	Icon as SemanticIcon
} from 'semantic-ui-react';
import Numbro from '../Numbro';
import * as styles from './Style.less';

const SELECTION_STATE = {
	NONE: 0,
	COLUMNS: 1,
	ROWS: 2,
	CELLS: 3
};

const FIND_REPLACE_STATE = {
	NONE: 0,
	FIND: 1,
	REPLACE: 2
};

export default class Spreadsheet extends Component {
	static MODE = {
		EDIT: 'edit',
		READ_ONLY: 'read_only',
		TOGGLE: 'toggle'
	};

	static TYPE = {
		CURRENCY: 'currency',
		ELEMENT: 'element',
		ENUM: 'enum',
		DATE: 'date',
		TOGGLE: 'toggle'
	};

	static propTypes = {
		canAddRows: PropTypes.bool,
		canDeleteRows: PropTypes.bool,
		columns: PropTypes.array.isRequired,
		counts: PropTypes.object.isRequired,
		data: PropTypes.array.isRequired,
		enabledProperty: PropTypes.string,
		mode: PropTypes.oneOf(Object.values(Spreadsheet.MODE)),
		name: PropTypes.object.isRequired,
		options: PropTypes.object,
		pagination: PropTypes.object,
		sort: PropTypes.object,
		onFetchData: PropTypes.func,
		onChange: PropTypes.func
	};

	static defaultProps = {
		canAddRows: false,
		canDeleteRows: false,
		enabledProperty: 'enabled',
		mode: Spreadsheet.MODE.EDIT,
		options: {},
		pagination: null,
		sort: null,
		onFetchData: null,
		onChange: null
	};

	static hasFocus(element) {
		return element === window.document.activeElement;
	}

	constructor(props) {
		super(props);

		this.container = null;
		this.containerRef = (element) => {
			this.container = element;
		};

		this.cellInput = null;
		this.cellInputRef = (element) => {
			this.cellInput = element;
		};

		this.findInput = null;
		this.findInputRef = (element) => {
			this.findInput = element;
		};

		this.replaceInput = null;
		this.replaceInputRef = (element) => {
			this.replaceInput = element;
		};

		let toggleRows = props.mode === Spreadsheet.MODE.TOGGLE;

		const columns = [{
			maxWidth: 50,
			resizable: false,
			Header: () => this.renderIndexHeader(),
			Cell: cell => this.renderIndexCell(cell)
		}];
		props.columns.forEach((column, index) => {
			const columProps = {};
			if (column.type === Spreadsheet.TYPE.TOGGLE) {
				columProps.maxWidth = 80;
				columProps.align = 'center';
				toggleRows = true;
			}
			columns.push({
				...column,
				...columProps,
				index,
				Header: column.type === Spreadsheet.TYPE.TOGGLE ? p => this.renderToggleColumnHeader(p) : p => this.renderColumnHeader(p),
				Cell: column.type === Spreadsheet.TYPE.TOGGLE ? cell => this.renderToggleCell(cell) : cell => this.renderCell(cell)
			});
		});

		this.snapshots = [];
		this.selectionState = SELECTION_STATE.NONE;

		const data = props.mode === Spreadsheet.MODE.TOGGLE ? props.data.map(row => ({ enabled: true, ...row })) : props.data;

		this.dataModel = props.columns.map(column => column.accessor);

		this.selectionEnabled = props.mode === Spreadsheet.MODE.EDIT || props.mode === Spreadsheet.MODE.READ_ONLY;

		this.state = {
			columns,
			data,
			finding: {
				state: FIND_REPLACE_STATE.NONE,
				query: '',
				matches: [],
				current: {},
				replace: ''
			},
			selected: null,
			toggleRows
		};
	}

	componentDidMount() {
		this.updateSelection(this.state.selected);

		this.keyPressHandler = this.onKeyPress.bind(this);
		window.document.body.addEventListener('keydown', this.keyPressHandler);

		if (this.selectionEnabled) {
			this.copyHandler = this.onCopy.bind(this);
			window.document.body.addEventListener('copy', this.copyHandler);

			if (this.props.mode === Spreadsheet.MODE.EDIT) {
				this.pasteHandler = this.onPaste.bind(this);
				window.document.body.addEventListener('paste', this.pasteHandler);
			}
		}

		if (this.props.sort) {
			this.applySort(this.props.sort);
		}

		this.updateOptions();
	}

	componentWillUnmount() {
		window.document.body.removeEventListener('keydown', this.keyPressHandler);

		if (this.selectionEnabled) {
			window.document.body.removeEventListener('copy', this.copyHandler);

			if (this.props.mode === Spreadsheet.MODE.EDIT) {
				window.document.body.removeEventListener('paste', this.pasteHandler);
			}
		}
	}

	onSortedChange(newSorted) {
		const sort = {
			value: newSorted[0].value,
			property: newSorted[0].id
		};

		this.applySort(sort);
	}

	onKeyPress(e) {
		if (this.handleKeyPress(e)) {
			e.preventDefault();
			e.stopPropagation();
		}
	}

	onMouseLeave() {
		this.selectionState = SELECTION_STATE.NONE;
	}

	onCopy(e) {
		const columns = this.state.selected.columns.map(i => this.state.columns.find(c => c.index === i));
		const data = [];
		this.state.data.forEach((row, index) => {
			if (this.state.selected.rows.includes(index)) {
				const rowObj = {};

				columns.forEach((column) => {
					const value = column.accessor.split('.').reduce((previous, current) => { // handle keypath
						return previous[current];
					}, row);
					rowObj[column.accessor] = value;
				});

				data.push(rowObj);
			}
		});

		const toCopy = data.map(o => Object.keys(o).map(p => o[p]).join('\t')).join('\n');
		e.clipboardData.setData('text/plain', toCopy);

		e.preventDefault();
		e.stopPropagation();
	}

	onPaste(e) {
		const { selected } = this.state;
		const clipboardData = e.clipboardData.getData('text');

		// get columns from t-l of selection
		const firstColumnIndex = selected.columns[0];
		const properties = this.state.columns.filter(column => column.index >= firstColumnIndex).map(column => column.accessor);

		// get row index of selection
		const firstRowIndex = selected.rows[0];

		// deep copy of data
		this.saveSnapshot();
		const data = JSON.parse(JSON.stringify(this.state.data));
		const newData = [];
		const rows = clipboardData.split('\n');
		const numRows = rows.length;
		const numSelectedRows = selected.rows.length;

		// fill rows to match current selection if larger
		if (numSelectedRows > numRows) {
			let i = numRows;
			while (rows.length < numSelectedRows) {
				rows.push(rows[i % numRows]);
				i += 1;
			}
		}

		rows.forEach((row, index) => {
			const rowIndex = firstRowIndex + index;
			const obj = rowIndex < data.length ? { ...data[rowIndex] } : {};

			const values = row.split('\t');
			values.forEach((value, valueIndex) => {
				const property = properties[valueIndex];
				const column = this.state.columns.find(c => c.accessor === property);
				if (property && column.editable) {
					obj[property] = value;
				}
			});

			newData.push(obj);
		});

		data.splice(firstRowIndex, rows.length, ...newData);

		this.setState(() => ({
			data
		}), () => {
			this.props.onChange && this.props.onChange(this.state.data);
		});

		e.preventDefault();
		e.stopPropagation();
	}

	onCellDoubleClick(column, row) {
		this.beginEditing(column, row);
	}

	onCellValueChange(type, value) {
		const data = JSON.parse(JSON.stringify(this.state.data));
		data[this.state.editing.row][this.state.editing.accessor] = value;
		this.setState(() => ({
			data
		}), () => {
			if (type === Spreadsheet.TYPE.ENUM) {
				this.props.onChange && this.props.onChange(this.state.data);
				this.updateOptions();
			}
		});
	}

	onIndexHeaderClick(e) {
		this.selectionState = SELECTION_STATE.NONE;

		this.updateSelection({
			start: { column: 0, row: 0 },
			end: { column: this.props.columns.length - 1, row: this.state.data.length - 1 }
		});

		e.preventDefault();
		e.stopPropagation();
	}

	onColumnHeaderMouseDown(e) {
		this.selectionState = SELECTION_STATE.COLUMNS;
		this.resetSelection();
		this.mouseDownPoint = { x: e.pageX, y: e.pageY };
		// this.updateSelection({
		// 	start: { column, row: 0 },
		// 	end: { column, row: this.state.data.length - 1 }
		// });
	}

	onColumnHeaderMouseMove(column, e) {
		if (this.selectionState === SELECTION_STATE.COLUMNS) {
			// const column = this.state.columns.find(c => c.accessor === accessor).index;

			// select column if nothing is selected (i.e. move in same column clicked on)
			if (!this.state.selected) {
				const dX = Math.abs(e.pageX - this.mouseDownPoint.x);
				const dY = Math.abs(e.pageY - this.mouseDownPoint.y);
				if (Math.sqrt((dX * dX) + (dY * dY)) >= 10) {
					this.updateSelection({
						start: { column, row: 0 },
						end: { column, row: this.state.data.length - 1 }
					});
				}
			}
		}
	}

	onColumnHeaderMouseUp() {
		this.selectionState = SELECTION_STATE.NONE;
	}

	onColumnHeaderMouseEnter(column) {
		if (this.selectionState === SELECTION_STATE.COLUMNS) {
			this.updateSelection({
				...this.state.selected,
				end: { column, row: this.state.data.length - 1 }
			});
		}
	}

	onColumnHeaderClick(e) {
		// cancel default action (sort) if column has been selected
		if (this.state.selected) {
			e.preventDefault();
			e.stopPropagation();
		}
	}

	onIndexCellMouseDown(row) {
		this.selectionState = SELECTION_STATE.ROWS;

		this.updateSelection({
			start: { column: 0, row },
			end: { column: this.props.columns.length - 1, row }
		});
	}

	onIndexCellMouseUp() {
		this.selectionState = SELECTION_STATE.NONE;
	}

	onIndexCellMouseEnter(row) {
		if (this.selectionState === SELECTION_STATE.ROWS) {
			this.updateSelection({
				...this.state.selected,
				end: { column: this.props.columns.length - 1, row }
			});
		}
	}

	onCellMouseDown(column, row) {
		this.selectionState = SELECTION_STATE.CELLS;

		this.updateSelection({
			start: { column, row },
			end: { column, row }
		});
	}

	onCellMouseUp() {
		this.selectionState = SELECTION_STATE.NONE;
	}

	onCellMouseEnter(column, row) {
		if (this.selectionState === SELECTION_STATE.CELLS) {
			this.updateSelection({
				...this.state.selected,
				end: { column, row }
			});
		}
	}

	onToggleAllRows() {
		this.saveSnapshot();
		const enabled = this.state.data.every(row => row[this.props.enabledProperty]);
		const data = this.state.data.map((row) => {
			return {
				...row,
				[this.props.enabledProperty]: !enabled
			};
		});

		this.setState(() => ({
			data
		}), () => {
			this.props.onChange && this.props.onChange(this.state.data);
		});
	}

	onToggleRow(row, property = this.props.enabledProperty) {
		const data = JSON.parse(JSON.stringify(this.state.data));
		data[row][property] = !data[row][property];

		this.setState(() => ({
			data
		}), () => {
			this.props.onChange && this.props.onChange(this.state.data);
		});
	}

	onAddRow() {
		this.saveSnapshot();
		this.setState(state => ({
			data: state.data.concat(Object.assign(...this.dataModel.map(k => ({ [k]: '' }))))
		}));
	}

	onDeleteRow(row) {
		this.saveSnapshot();
		const data = JSON.parse(JSON.stringify(this.state.data));
		data.splice(row, 1);
		this.setState(() => ({
			data
		}), () => {
			this.props.onChange && this.props.onChange(this.state.data);
		});
	}

	onDeleteSelectedRows() {
		this.saveSnapshot();

		const { selected } = this.state;
		const data = JSON.parse(JSON.stringify(this.state.data));
		data.splice(selected.rows[0], selected.rows.length);
		this.setState(() => ({
			data
		}), () => {
			this.resetSelection();
			this.props.onChange && this.props.onChange(this.state.data);
		});
	}

	onUndo(callback) {
		if (this.snapshots.length > 0) {
			this.setState({
				data: this.snapshots.pop()
			}, () => {
				this.props.onChange && this.props.onChange(this.state.data);

				if (this.state.finding.state !== FIND_REPLACE_STATE.NONE) {
					this.find(this.state.finding.query);
				}
				callback && callback();
			});
		}
	}

	onFindValueChange(value) {
		this.find(value, this.state.selected.columns[0], this.state.selected.rows[0]);
	}

	onReplaceValueChange(value) {
		this.setState(state => ({
			finding: {
				...state.finding,
				replace: value
			}
		}));
	}

	onReplaceCurrent() {
		this.saveSnapshot();
		const data = JSON.parse(JSON.stringify(this.state.data));
		const { finding } = this.state;
		data[finding.current.row][finding.current.accessor] = finding.replace;
		this.setState(() => ({
			data
		}), () => {
			this.props.onChange && this.props.onChange(this.state.data);
			this.find(finding.query, finding.current.column, finding.current.row);
		});
	}

	onReplaceAll() {
		this.saveSnapshot();
		const data = JSON.parse(JSON.stringify(this.state.data));
		const { finding } = this.state;
		finding.matches.forEach((match) => {
			data[match.row][match.accessor] = finding.replace;
		});
		this.setState(() => ({
			data
		}), () => {
			this.props.onChange && this.props.onChange(this.state.data);
			this.find(finding.query, finding.current.column, finding.current.row);
		});
	}

	onPreviousMatch() {
		let currentIndex = this.state.finding.current.index - 1;
		if (currentIndex < 0) {
			currentIndex = this.state.finding.matches.length - 1;
		}

		const current = { ...this.state.finding.matches[currentIndex], index: currentIndex };

		this.setState(state => ({
			finding: {
				...state.finding,
				current
			}
		}));
	}

	onNextMatch() {
		let currentIndex = this.state.finding.current.index + 1;
		if (currentIndex >= this.state.finding.matches.length) {
			currentIndex = 0;
		}

		const current = { ...this.state.finding.matches[currentIndex], index: currentIndex };

		this.setState(state => ({
			finding: {
				...state.finding,
				current
			}
		}));
	}

	handleKeyPress({
		ctrlKey,
		key,
		metaKey,
		shiftKey
	}) {
		// console.log(key);
		const { columns, mode } = this.props;
		const {
			data,
			editing,
			finding,
			selected
		} = this.state;
		let { column, row } = selected ? selected.start : { column: 0, row: 0 };
		let handled = false;

		if (metaKey || ctrlKey) {
			// undo
			if (key === 'z') {
				handled = true;

				this.onUndo(() => {
					if (editing) {
						this.saveSnapshot();
					}
				});
			// find
			} else if (key === 'f') {
				handled = true;

				this.beginFinding();
			}
		} else if (key === ' ') {
			if (mode === Spreadsheet.MODE.TOGGLE) {
				handled = true;
				this.onToggleRow(selected.start.row);
				this.handleKeyPress({ key: 'ArrowDown' });
			}
		} else if (key === 'Backspace' || key === 'Delete') {
			if (this.props.mode === Spreadsheet.MODE.EDIT && !this.state.editing) {
				// can delete row and row is selected
				if (this.props.canDeleteRows && selected.columns.includes(0) && selected.columns.includes(this.props.columns.length - 1)) {
					handled = true;
					this.onDeleteSelectedRows();
				// clear content
				} else {
					this.clearSelectedCells();
				}
			}
		} else if (key === 'Enter' && this.props.mode === Spreadsheet.MODE.EDIT) {
			handled = true;

			if (!editing) {
				this.beginEditing(column, row);
			} else {
				this.endEditing();
			}
		} else if (key === 'Escape') {
			if (editing && Spreadsheet.hasFocus(this.cellInput)) {
				this.onUndo();
				this.endEditing();
			} else if (finding.state !== FIND_REPLACE_STATE.NONE
						&& (Spreadsheet.hasFocus(this.findInput) || Spreadsheet.hasFocus(this.replaceInput))) {
				this.endFinding();
			} else {
				this.resetSelection();
			}
			handled = true;
		} else {
			handled = true;

			if (key === 'ArrowUp' && row > 0) { // up
				row -= 1;
			} else if (key === 'ArrowDown' && row < data.length - 1) { // down
				row += 1;
			} else if (key === 'ArrowLeft' && column > 0) { // left
				column -= 1;
			} else if ((key === 'ArrowRight' || key === 'Tab') && column < columns.length - 1) { // right or tab
				column += 1;
			} else {
				return false;
			}

			const newSelected = { ...selected, start: { column, row } };
			if (!shiftKey) {
				newSelected.end = { column, row };
			}

			this.updateSelection(newSelected);
		}

		return handled;
	}

	updateSelection(selected) {
		let start;
		let end;

		if (selected) {
			({ start, end } = selected);
		}

		if (!start || !end) {
			this.resetSelection();
			return;
		}

		const columns = [];
		const rows = [];

		const columnStart = Math.max(Math.min(start.column, end.column), 0);
		const columnEnd = Math.max(start.column, end.column, 0);
		for (let i = columnStart; i <= columnEnd; i += 1) {
			columns.push(i);
		}

		const rowStart = Math.max(Math.min(start.row, end.row), 0);
		const rowEnd = Math.max(start.row, end.row, 0);
		for (let i = rowStart; i <= rowEnd; i += 1) {
			rows.push(i);
		}

		this.setState({
			selected: {
				start,
				end,
				columns,
				rows
			},
			editing: false
		});
	}

	resetSelection() {
		this.setState({ selected: null });
	}

	clearSelectedCells() {
		const { columns, rows } = this.state.selected;

		this.saveSnapshot();

		const data = JSON.parse(JSON.stringify(this.state.data));
		const properties = this.state.columns.filter(column => columns.includes(column.index) && column.editable).map(column => column.accessor);

		for (let [row] = rows; row <= rows[rows.length - 1]; row += 1) {
			properties.forEach((property) => {
				data[row][property] = '';
			});
		}

		this.setState(() => ({
			data
		}), () => {
			this.props.onChange && this.props.onChange(this.state.data);
			this.updateOptions();
		});
	}

	beginEditing(column, row) {
		this.selectionState = SELECTION_STATE.NONE;
		const { accessor } = this.props.columns[column];
		this.previousValue = this.state.data[row][accessor];

		this.saveSnapshot();

		this.setState({
			editing: {
				accessor,
				column,
				row
			}
		});
	}

	endEditing() {
		if (this.state.data[this.state.editing.row][this.state.editing.accessor] === this.previousValue) {
			this.snapshots.pop();
		}
		this.setState({
			editing: null
		}, () => {
			this.props.onChange && this.props.onChange(this.state.data);
		});
		this.container.focus();
	}

	beginFinding() {
		this.setState(state => ({
			finding: {
				...state.finding,
				state: FIND_REPLACE_STATE.FIND,
				query: '',
				replace: '',
				matches: [],
				current: {}
			}
		}));
	}

	endFinding() {
		this.setState(state => ({
			finding: {
				...state.finding,
				state: FIND_REPLACE_STATE.NONE
			}
		}));
	}

	find(query, currentColumn, currentRow) {
		const matches = [];
		let current;
		if (query.length >= 1) {
			const regExp = new RegExp(`${query}`, 'i');
			this.state.data.forEach((obj, row) => {
				this.state.columns.forEach((column) => {
					if (column.accessor) {
						const value = obj[column.accessor];
						if (regExp.test(value)) {
							matches.push({
								row,
								column: column.index,
								accessor: column.accessor
							});
							if (!current
								&& (row > currentRow
									|| (row === currentRow && column.index >= currentColumn))) {
								current = {
									row,
									column: column.index,
									accessor: column.accessor,
									index: matches.length - 1
								};
							}
						}
					}
				});
			});

			if (matches.length > 0 && !current) {
				current = { ...matches[0], index: 0 };
			}
		}

		this.setState(state => ({
			finding: {
				...state.finding,
				current,
				matches,
				query,
				state: FIND_REPLACE_STATE.FIND
			}
		}));
	}

	updateOptions() {
		const options = {};
		Object.keys(this.props.options).forEach((property) => {
			const column = this.props.columns.find(c => c.accessor === property);
			if (column && column.exclusive) {
				options[property] = this.props.options[property].filter(option => this.state.data.find(obj => obj[property] === option.value) == null);
			} else {
				options[property] = this.props.options[property];
			}
		});

		this.setState({
			options
		});
	}

	applySort(sort) {
		const data = _.orderBy(JSON.parse(JSON.stringify(this.state.data)), sort.property, sort.value);

		this.setState(() => ({ data, sort }));
	}

	saveSnapshot() {
		this.snapshots.push(JSON.parse(JSON.stringify(this.state.data)));
	}

	renderIndexHeader() {
		const classes = [styles.Header];

		if (this.selectionEnabled) {
			const { selected } = this.state;
			const isSelected = selected
								&& selected.columns.includes(0)
								&& selected.columns.includes(this.props.columns.length - 1)
								&& selected.rows.includes(0)
								&& selected.rows.includes(this.state.data.length - 1);
			if (isSelected) {
				classes.push('selected');
			}
		}

		return (
			<div
				className={classes.join(' ')}
				onClick={e => this.onIndexHeaderClick(e)}
				role="button"
				tabIndex="0"
			/>
		);
	}

	renderColumnHeader({ column }) {
		const classes = [styles.Header];

		if (this.selectionEnabled) {
			const { selected } = this.state;
			const isSelected = selected && selected.columns && selected.columns.includes(column.index);
			if (isSelected) {
				classes.push('selected');
			}
			if (column.align) {
				classes.push(column.align);
			}
		}

		return (
			<div
				className={classes.join(' ')}
				onMouseDown={this.selectionEnabled ? e => this.onColumnHeaderMouseDown(e) : undefined}
				onMouseMove={this.selectionEnabled ? e => this.onColumnHeaderMouseMove(column.index, e) : undefined}
				onMouseUp={this.selectionEnabled ? () => this.onColumnHeaderMouseUp(column.index) : undefined}
				onMouseEnter={this.selectionEnabled ? () => this.onColumnHeaderMouseEnter(column.index) : undefined}
				onClick={e => this.onColumnHeaderClick(e)}
				role="button"
				tabIndex="0"
			>
				<span>
					{column.title}
					{
						this.props.mode === Spreadsheet.MODE.EDIT && !column.editable && (
							<FontAwesomeIcon icon={['fas', 'lock']} className="lock" />
						)
					}
				</span>
				<div className="sort fa-layers fa-fw">
					<FontAwesomeIcon icon={['fas', 'sort']} className="sort" />
					{
						this.state.sort && this.state.sort.property === column.id && (
							<FontAwesomeIcon icon={['fas', this.state.sort.desc ? 'sort-down' : 'sort-up']} className="sort-direction" />
						)
					}
				</div>
			</div>
		);
	}

	renderToggleColumnHeader({ column }) {
		return (
			<div
				className={[styles.Header, 'toggle'].join(' ')}
				onClick={column.toggleAll ? () => this.onToggleAllRows() : undefined}
				role="button"
				tabIndex="0"
			>
				<span>
					{
						column.toggleAll ? (
							<input
								type="checkbox"
								checked={this.state.data.every(row => row[this.props.enabledProperty])}
								onChange={() => this.onToggleAllRows()}
							/>
						) : (
							column.title
						)
					}
				</span>
			</div>
		);
	}

	renderIndexCell(cell) {
		const classes = [styles.IndexCell];

		const { selected } = this.state;
		const isSelected = selected && selected.rows && selected.rows.includes(cell.index);
		if (isSelected && this.selectionEnabled) {
			classes.push('selected');
		}

		return (
			<div
				className={classes.join(' ')}
				onMouseDown={this.selectionEnabled ? () => this.onIndexCellMouseDown(cell.index) : undefined}
				onMouseUp={this.selectionEnabled ? () => this.onIndexCellMouseUp(cell.index) : undefined}
				onMouseEnter={this.selectionEnabled ? () => this.onIndexCellMouseEnter(cell.index) : undefined}
				onClick={this.props.mode === Spreadsheet.MODE.TOGGLE ? () => this.onToggleRow(cell.index) : undefined}
				role="button"
				tabIndex="0"
			>
				{
					this.props.mode === Spreadsheet.MODE.TOGGLE && isSelected && (
						<div className="caret"><FontAwesomeIcon icon="caret-right" /></div>
					)
				}
				{
					this.props.mode === Spreadsheet.MODE.EDIT && this.props.canDeleteRows && selected.rows[0] === cell.index && selected.columns.includes(0) && selected.columns.includes(this.props.columns.length - 1) && (
						<div className="delete"><FontAwesomeIcon icon={['fal', 'times']} onMouseDown={(e) => { e.stopPropagation(); }} onClick={() => this.onDeleteSelectedRows()} /></div>
					)
				}
				<div className="index">{cell.index + 1}</div>
			</div>
		);
	}

	renderCell(cell) {
		const {
			columns, selected, editing, finding, toggleRows
		} = this.state;
		const classes = [styles.Cell];
		const isMatch = finding.state !== FIND_REPLACE_STATE.NONE && finding.matches.find(match => match.column === cell.column.index && match.row === cell.index);
		let isEditing = false;

		const isSelected = selected && selected.rows.includes(cell.index) && (selected.columns.includes(cell.column.index) || this.props.mode === Spreadsheet.MODE.TOGGLE);
		if (isSelected) {
			classes.push('selected');
		}

		if (this.selectionEnabled && isSelected) {
			isEditing = editing && editing.column === cell.column.index && editing.row === cell.index;

			if (cell.column.index === selected.columns[0]) {
				classes.push('left');
			}
			if (cell.column.index === selected.columns[selected.columns.length - 1]
				|| (toggleRows && isSelected && cell.column.index === columns[columns.length - 2].index)) {
				classes.push('right');
			}
			if (cell.index === selected.rows[0]) {
				classes.push('top');
			}
			if (cell.index === selected.rows[selected.rows.length - 1]) {
				classes.push('bottom');
			}
		}

		if (this.props.mode === Spreadsheet.MODE.EDIT) {
			if (!cell.column.editable) {
				classes.push('not-editable');
			} else if (isEditing) {
				classes.push('editing');
			}
		}

		if (isMatch) {
			classes.push('match');

			if (cell.column.index === finding.current.column && cell.index === finding.current.row) {
				classes.push('current');
			}
		}
		if (cell.column.align) {
			classes.push(`align-${cell.column.align}`);
		}
		if (toggleRows && !cell.original[this.props.enabledProperty]) {
			classes.push('disabled');
		}

		if (this.props.mode === Spreadsheet.MODE.TOGGLE) {
			classes.push('togglable');
		}

		let options;
		let { value } = cell;
		if (cell.column.type === Spreadsheet.TYPE.ELEMENT) {
			value = cell.column.cell(cell.original);
		} else if (cell.column.type === Spreadsheet.TYPE.ENUM) {
			options = this.props.options[cell.column.id];

			if (cell.column.multiple) {
				if (cell.value && cell.value.length) {
					const values = [];
					cell.value.split(', ').forEach((v) => {
						const option = options.find(o => o.value === v);
						if (option) {
							values.push(option);
						}
					});

					value = values.length ? values.map(v => v.text).join(', ') : [];
				}
			} else if (cell.value && cell.value.toString().length > 0) {
				const option = options.find(o => o.value === cell.value);
				value = option ? option.text : '';
			}
		} else if (cell.column.type === Spreadsheet.TYPE.DATE) {
			value = cell.value === '0' ? `No ${cell.column.title}` : Moment.utc(value, 'x').format(cell.column.format || 'll');
			classes.push('align-right');
		} else if (cell.column.type === Spreadsheet.TYPE.CURRENCY) {
			value = Numbro.formatCurrency(value);
		} else if (cell.column.format) {
			value = Numbro.format(value, cell.column.format);
		}

		if (!value) {
			value = '';
		}

		return (
			<div
				className={classes.join(' ')}
				onMouseDown={this.selectionEnabled && !isEditing ? () => this.onCellMouseDown(cell.column.index, cell.index) : undefined}
				onMouseUp={this.selectionEnabled && !isEditing ? () => this.onCellMouseUp(cell.column.index, cell.index) : undefined}
				onMouseEnter={this.selectionEnabled && !isEditing ? () => this.onCellMouseEnter(cell.column.index, cell.index) : undefined}
				onDoubleClick={this.props.mode === Spreadsheet.MODE.EDIT && !isEditing && cell.column.editable ? () => this.onCellDoubleClick(cell.column.index, cell.index) : undefined}
				onClick={this.props.mode === Spreadsheet.MODE.TOGGLE ? () => this.onToggleRow(cell.index) : undefined}
			>
				{
					isEditing ? cell.column.type === Spreadsheet.TYPE.ENUM ? (
						<SemanticDropdown
							defaultOpen
							search
							selection
							multiple={cell.column.multiple}
							options={this.state.options[cell.column.id]}
							placeholder={cell.column.title}
							value={cell.column.multiple ? cell.value ? cell.value.split(', ') : [] : cell.value}
							onChange={(__, { value: val }) => this.onCellValueChange(cell.column.type, cell.column.multiple ? val.filter(v => v.length).map(v => v).join(', ') : val)}
							onClose={() => this.endEditing()}
							// onKeyUp={evt => this.onKeyUp(evt)}
						/>
					) : (
						<input
							autoFocus // eslint-disable-line jsx-a11y/no-autofocus
							type="text"
							value={cell.value}
							ref={this.cellInputRef}
							onChange={e => this.onCellValueChange(cell.column.type, e.target.value)}
						/>
					) : (
						value
					)
				}
			</div>
		);
	}

	renderToggleCell(cell) {
		const { selected } = this.state;
		const classes = [styles.Cell, 'align-center', 'toggle'];

		if (this.props.mode === Spreadsheet.MODE.TOGGLE && selected.rows.includes(cell.index)) {
			classes.push('selected');
		}

		if (!cell.value) {
			classes.push('disabled');
		}

		return (
			<div
				className={classes.join(' ')}
				onClick={() => this.onToggleRow(cell.index, cell.column.id)}
				role="button"
				tabIndex="0"
			>
				{
					cell.column.iconEnabled && cell.column.iconDisabled ? (
						<span>
							<FontAwesomeIcon
								icon={['fal', cell.value ? cell.column.iconEnabled : cell.column.iconDisabled]}
								onChange={() => this.onToggleRow(cell.index, cell.column.id)}
							/>
						</span>
					) : (
						<span><input type="checkbox" checked={cell.value} onChange={() => this.onToggleRow(cell.index, cell.column.id)} /></span>
					)
				}
			</div>
		);
	}

	render() {
		const {
			canAddRows,
			counts,
			mode,
			name,
			onFetchData,
			pagination,
			sort
		} = this.props;

		const {
			columns,
			data,
			finding
		} = this.state;

		return (
			<div
				className={styles.Spreadsheet}
				ref={this.containerRef}
				onMouseLeave={() => this.onMouseLeave()}
			>
				{
					mode === Spreadsheet.MODE.TOGGLE && (
						<div className={styles.Info}>
							<strong>{data.filter(item => item[this.props.enabledProperty]).length}</strong> of {data.length} selected
						</div>
					)
				}
				{
					data.length > 0 ? (
						<>
							<ReactTable
								className={styles.Table}
								manual
								columns={columns}
								data={data}
								minRows={1}
								sorted={sort ? [{ id: sort.property, ...sort }] : []}
								showPagination={pagination != null}
								page={pagination ? pagination.page : undefined}
								pageSize={pagination ? pagination.pageSize : data.length}
								pages={pagination ? Math.ceil(counts.total / pagination.pageSize) : undefined}
								onFetchData={onFetchData}
								onSortedChange={onFetchData ? undefined : (newSorted, column) => this.onSortedChange(newSorted, column)}
								defaultSortMethod={(a, b, desc) => { desc ? -1 : 1; }} // return data in same order
								previousText={(
									<SemanticIcon
										name="arrow left"
									/>
								)}
								nextText={(
									<SemanticIcon
										name="arrow right"
									/>
								)}
							/>
							<div className={styles.Options}>
								<div className="left">
									{
										canAddRows && <button type="button" onClick={() => this.onAddRow()}><FontAwesomeIcon icon={['fal', 'plus']} /> Add a {name.singular}</button>
									}
									{
										finding.state !== FIND_REPLACE_STATE.NONE && (
											<div className="find">
												<div className="find-input">
													<input
														autoFocus // eslint-disable-line jsx-a11y/no-autofocus
														placeholder="Find in table..."
														value={finding.query}
														ref={this.findInputRef}
														onChange={e => this.onFindValueChange(e.target.value)}
													/>
													{
														finding.query.length > 0 && (
															finding.matches.length > 0 ? (
																<div className="results">{finding.current.index + 1} of {finding.matches.length}</div>
															) : (
																<div className="results">0 of 0</div>
															)
														)
													}
												</div>
												<button type="button" disabled={finding.matches.length === 0} onClick={() => this.onPreviousMatch()}>
													<FontAwesomeIcon icon={['fal', 'angle-up']} />
												</button>
												<button type="button" disabled={finding.matches.length === 0} onClick={() => this.onNextMatch()}>
													<FontAwesomeIcon icon={['fal', 'angle-down']} />
												</button>
												<button type="button" onClick={() => this.setState(state => ({ finding: { ...state.finding, state: FIND_REPLACE_STATE.REPLACE } }))}>
													<FontAwesomeIcon icon={['fal', 'exchange']} />
												</button>
												{
													finding.state === FIND_REPLACE_STATE.REPLACE && (
														<div className="replace">
															<div className="find-input">
																<input
																	placeholder="Replace"
																	value={finding.replace}
																	ref={this.replaceInputRef}
																	onChange={e => this.onReplaceValueChange(e.target.value)}
																/>
															</div>
															<button type="button" disabled={finding.matches.length === 0} onClick={() => this.onReplaceCurrent()}>
																Replace
															</button>
															<button type="button" disabled={finding.matches.length === 0} onClick={() => this.onReplaceAll()}>
																Replace All
															</button>
														</div>
													)
												}
											</div>
										)
									}
								</div>
								{
									mode !== Spreadsheet.MODE.READ_ONLY && (
										<div className="right">
											<button
												type="button"
												disabled={this.snapshots.length === 0}
												onClick={() => this.onUndo()}
											>
												<FontAwesomeIcon icon={['fal', 'undo']} />
											</button>
										</div>
									)
								}
							</div>
						</>
					) : (
						<div className="empty">
							No {name.plural} found.
							{
								canAddRows && <> Wouold you like to <span onClick={() => this.onAddRow()} role="button" tabIndex="-100">add one</span>?</>
							}
						</div>
					)
				}
				{/* <div className="selection">
					{
						selected.start && selected.end ? (
							<div>
								<code>
									{selected.columns.join(', ')} [{selected.start.row}{selected.end.row !== selected.start.row ? `..${selected.end.row}` : ''}]
								</code>
								<pre>{JSON.stringify(selected.data, null, 4)}</pre>
							</div>
						) : 'Nothing selected'
					}
				</div> */}
			</div>
		);
	}
}
