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 {
	Icon as SemanticIcon,
	Button as SemanticButton
} from 'semantic-ui-react';
import Numbro from '../Numbro';
import Tooltip from '../Tooltip';
import Progress from '../Progress';
import * as styles from './Style.less';

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

export default class Table extends Component {
	static TYPE = {
		CURRENCY: 'currency',
		DATE: 'date',
		ELEMENT: 'element',
		NUMBER: 'number',
		PERCENT: 'percent',
		PERCENTILE: 'percentile',
		PROGRESS: 'progress'
	};

	static propTypes = {
		id: PropTypes.string.isRequired,
		columns: PropTypes.array.isRequired,
		counts: PropTypes.object,
		data: PropTypes.array.isRequired,
		error: PropTypes.object,
		manual: PropTypes.bool,
		noBorder: PropTypes.bool,
		onFetchData: PropTypes.func,
		pageSize: PropTypes.number,
		pagination: PropTypes.object,
		sort: PropTypes.object
	};

	static defaultProps = {
		counts: null,
		error: {
			title: 'No Data Available',
			message: 'If you have any filters applied, try removing or changing them'
		},
		manual: false,
		noBorder: false,
		onFetchData: null,
		pageSize: 20,
		pagination: null,
		sort: null,
	};

	constructor(props) {
		super(props);

		const {
			columns,
			selectionColumns
		} = this.createColumns(props.columns);

		this.selectionState = SELECTION_STATE.NONE;
		this.tableContainer = React.createRef();
		this.selectedCell = React.createRef();
		this.table = null;

		let tableStorage = localStorage.getItem('table');
		let { pageSize, sort } = props;

		if (tableStorage) {
			tableStorage = JSON.parse(tableStorage);
			sort = tableStorage.sort[this.props.id] ? tableStorage.sort[this.props.id] : props.sort;
			pageSize = tableStorage.pageSize.Default ? parseInt(tableStorage.pageSize.Default, 10) : props.pageSize;
		}

		this.state = {
			columns,
			data: props.data,
			sort,
			page: 0,
			pageSize,
			selected: null,
			selectionColumns,
			showSelectAll: false
		};
	}

	componentDidMount() {
		this.keyPressHandler = this.onKeyPress.bind(this);
		this.copyHandler = this.onCopy.bind(this);

		this.applySort(this.state.sort);
	}

	componentWillReceiveProps(nextProps) {
		const {
			columns,
			selectionColumns
		} = this.createColumns(nextProps.columns);

		this.setState({
			data: nextProps.data,
			page: 0,
			selected: null,
			showSelectAll: false,
			columns,
			selectionColumns
		}, () => {
			if (this.table) {
				this.table.setState({
					page: 0
				}, () => {
					this.applySort();
				});
			} else {
				this.applySort();
			}
		});
	}

	componentDidUpdate() {
		window.document.body.addEventListener('copy', this.copyHandler);
		window.document.body.addEventListener('keydown', this.keyPressHandler);

		if (this.selectedCell && this.selectedCell.current) {
			this.selectedCell.current.focus();
		}
	}

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

	onPageSizeChange(pageSize) {
		let tableStorage = localStorage.getItem('table');
		tableStorage = tableStorage ? JSON.parse(tableStorage) : { sort: {}, pageSize: {} };
		tableStorage.pageSize.Default = pageSize;
		localStorage.setItem('table', JSON.stringify(tableStorage));

		this.setState({
			pageSize
		});
	}

	onPageChange(page) {
		this.setState({
			page
		});
	}

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

		let tableStorage = localStorage.getItem('table');
		tableStorage = tableStorage ? JSON.parse(tableStorage) : { sort: {}, pageSize: {} };
		tableStorage.sort[this.props.id] = sort;
		localStorage.setItem('table', JSON.stringify(tableStorage));

		if (!this.state.selected) {
			this.resetSelection();

			this.setState({
				page: 0, // page is reset but callback does not fire
				sort
			}, this.applySort);
		}
	}

	onKeyPress(e) {
		if (this.tableContainer
			&& this.tableContainer.current
			&& this.tableContainer.current.contains(window.document.activeElement)
			&& this.handleKeyPress(e)) {
			e.preventDefault();
			e.stopPropagation();
		}
	}

	onCopy(e) {
		if (this.tableContainer
			&& this.tableContainer.current
			&& this.state.selected
			&& this.tableContainer.current.contains(window.document.activeElement)) {
			const columns = this.state.selected.columns.map(i => this.state.selectionColumns[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);
						if (column.type === Table.TYPE.DATE) {
							rowObj[column.accessor] = Moment.utc(value, 'x').format('YYYY-MM-DD HH:mm');
						} else {
							rowObj[column.accessor] = value;
						}
					});

					data.push(rowObj);
				}
			});

			const headers = columns.map(col => col.title).join('\t');
			const toCopy = data.map(o => Object.keys(o).map(p => o[p]).join('\t')).join('\n');
			e.clipboardData.setData('text/plain', `${headers}\n${toCopy}`);

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

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

	onColumnHeaderMouseDown(e) {
		this.selectionState = SELECTION_STATE.COLUMNS;
		this.resetSelection();
		this.mouseDownPoint = { x: e.pageX, y: e.pageY };
	}

	onColumnHeaderMouseMove(accessor, e) {
		if (this.selectionState === SELECTION_STATE.COLUMNS) {
			const column = this.state.selectionColumns.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: this.state.pageSize * this.state.page },
						end: { column, row: Math.min((this.state.pageSize * (this.state.page + 1)), this.state.data.length) - 1 }
					});
				}
			}
		}
	}

	onColumnHeaderMouseEnter(accessor) {
		if (this.selectionState === SELECTION_STATE.COLUMNS) {
			const column = this.state.selectionColumns.find(c => c.accessor === accessor).index;

			this.updateSelection({
				...this.state.selected,
				end: { column, row: Math.min((this.state.pageSize * (this.state.page + 1)), this.state.data.length) - 1 }
			});
		}
	}

	onCellMouseDown(accessor, row) {
		this.selectionState = SELECTION_STATE.CELLS;
		const column = this.state.selectionColumns.find(c => c.accessor === accessor).index;

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

	onCellMouseEnter(accessor, row) {
		if (this.selectionState === SELECTION_STATE.CELLS) {
			const column = this.state.selectionColumns.find(c => c.accessor === accessor).index;

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

	handleKeyPress(e) {
		const {
			data,
			selected,
			selectionColumns
		} = this.state;

		let handled = false;

		if (e.key === 'Escape') {
			this.resetSelection();

			handled = true;
		} else if ((e.metaKey || e.ctrlKey) && e.key === 'a') { // Select All
			if (this.props.manual) {
				this.selectAll();
			} else {
				this.selectPage();
			}

			handled = true;
		} else if (e.key === 'PageDown') {
			this.nextPage();

			handled = true;
		} else if (e.key === 'PageUp') {
			this.previousPage();

			handled = true;
		} else if (selected && (e.key === 'Home' || e.key === 'End')) {
			const { end } = selected;
			const column = e.key === 'Home' ? 0 : selectionColumns.length - 1;

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

			handled = true;
		} else if (selected && (e.key === 'ArrowUp'
								|| e.key === 'ArrowDown'
								|| e.key === 'ArrowLeft'
								|| e.key === 'ArrowRight')) {
			let { column, row } = selected.end;

			if (e.key === 'ArrowUp' && row > 0) {
				row -= 1;
			} else if (e.key === 'ArrowDown' && row < data.length - 1) {
				row += 1;
			} else if (e.key === 'ArrowLeft' && column > 0) {
				column -= 1;
			} else if ((e.key === 'ArrowRight' || e.key === 'Tab') && column < selectionColumns.length - 1) {
				column += 1;
			}

			const newSelected = { ...selected, end: { column, row } };

			if (!e.shiftKey) {
				newSelected.start = { column, row };
			}

			this.updateSelection(newSelected, this.jumpToSelection);

			handled = true;
		}

		return handled;
	}

	previousPage() {
		const page = this.state.page - 1;

		if (page >= 0) {
			this.table.setState(() => ({ page }));
			this.setState(() => ({ page }));

			return true;
		}

		return false;
	}

	nextPage() {
		const page = this.state.page + 1;

		if (page < this.table.state.pages) {
			this.table.setState(() => ({ page }));
			this.setState(() => ({ page }));

			return true;
		}

		return false;
	}

	jumpToSelection() {
		if (this.state.selected) {
			const { row } = this.state.selected.end;
			const page = Math.floor(row / this.state.pageSize);
			this.table.setState(() => ({ page }));
			this.setState(() => ({ page }));
		}
	}

	selectPage() {
		this.updateSelection({
			start: { column: 0, row: this.state.pageSize * this.state.page },
			end: { column: this.state.selectionColumns.length - 1, row: Math.min((this.state.pageSize * (this.state.page + 1)), this.state.data.length) - 1 }
		});

		this.setState(prevState => ({
			showSelectAll: prevState.data.length > prevState.pageSize
		}));
	}

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

		this.setState(prevState => ({
			showSelectAll: prevState.data.length > prevState.pageSize && !this.props.manual
		}));
	}

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

	updateSelection(selected, callback) {
		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
			},
			showSelectAll: false
		}, () => {
			if (callback) {
				callback.call(this);
			}
		});
	}

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

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

	createColumns(columns) {
		const cols = [];
		const selectionColumns = [];
		let index = 0;

		columns.forEach((column) => {
			const signal = {};
			if (column.signal) {
				signal.accessor = `${column.signal.key}.value_current`;
				signal.align = 'right';
				signal.format = column.signal.Format;
				signal.type = Table.TYPE.NUMBER;
				signal.title = column.signal.Label;
				signal.tooltip = column.signal.Tooltip;
			}

			const tableColumn = {
				...signal,
				...column,
				index,
				Header: cell => this.renderColumnHeader(cell),
				Cell: cell => this.renderCell(cell),
			};

			if (column.columns) {
				tableColumn.grouped = true;
				tableColumn.columns = [];

				column.columns.forEach((col) => {
					if (col.signal) {
						signal.accessor = `${col.signal.key}.value_current`;
						signal.align = 'right';
						signal.format = col.signal.Format;
						signal.type = Table.TYPE.NUMBER;
						signal.title = col.signal.Label;
						signal.tooltip = col.signal.Tooltip;
					}
					tableColumn.columns.push({
						...signal,
						...col,
						index,
						Header: cell => this.renderColumnHeader(cell),
						Cell: cell => this.renderCell(cell),
					});

					selectionColumns.push({
						...signal,
						...column
					});

					index += 1;
				});
			} else {
				selectionColumns.push({
					...signal,
					...column
				});

				index += 1;
			}

			cols.push(tableColumn);
		});

		return {
			columns: cols,
			selectionColumns,
			index
		};
	}

	renderColumnHeader({ column }) {
		const classes = [styles.Header];
		const { selected } = this.state;
		const isSelected = selected && selected.columns && selected.columns.includes(column.index);

		if (isSelected && !column.grouped) {
			classes.push('selected');
		}

		if (this.state.sort && this.state.sort.property === column.id) {
			classes.push('sorted');
		}

		if (column.align) {
			classes.push(`align-${column.align}`);
		} else if (column.type === Table.TYPE.PROGRESS) {
			classes.push('align-center');
		} else if (
			column.type === Table.TYPE.CURRENCY
			|| column.type === Table.TYPE.DATE
			|| column.type === Table.TYPE.NUMBER
			|| column.type === Table.TYPE.PERCENT
			|| column.type === Table.TYPE.PERCENTILE
		) {
			classes.push('align-right');
		}

		return (
			<div
				className={classes.join(' ')}
				onMouseDown={column.grouped ? e => e.preventDefault() : e => this.onColumnHeaderMouseDown(e)}
				onMouseUp={column.grouped ? e => e.preventDefault() : () => this.onMouseUp(column.id)}
				onMouseEnter={column.grouped ? e => e.preventDefault() : () => this.onColumnHeaderMouseEnter(column.id)}
				onMouseMove={column.grouped ? e => e.preventDefault() : e => this.onColumnHeaderMouseMove(column.id, e)}
				role="button"
				tabIndex="0"
				title={column.title ? column.title : column.signal ? column.signal.Label : ''}
			>
				{
					column.tooltip ? (
						column.tooltip(<span>{column.title}</span>)
					) : (
						<span>{column.title}</span>
					)
				}
				<div className="sort">
					{
						this.state.sort && this.state.sort.property === column.id && (
							<SemanticIcon
								name={this.state.sort.value === 'desc' ? 'caret down' : 'caret up'}
							/>
						)
					}
				</div>
			</div>
		);
	}

	renderCell(cell) {
		const { selected } = this.state;
		let lastSelectedCell = false;
		const classes = [styles.Cell, cell.original.disabled ? 'disabled' : null];

		if (selected) {
			const isSelected = selected.rows.includes(cell.index) && (selected.columns.includes(cell.column.index));
			lastSelectedCell = selected.end.row === cell.index && selected.end.column === cell.column.index;

			if (isSelected) {
				classes.push('selected');
			}

			if (cell.column.index === selected.columns[0]) {
				classes.push('left');
			}

			if (cell.column.index === selected.columns[selected.columns.length - 1]) {
				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 (cell.column.align) {
			classes.push(`align-${cell.column.align}`);
		} else if (cell.column.type === Table.TYPE.PROGRESS) {
			classes.push('align-center');
		} else if (
			cell.column.type === Table.TYPE.CURRENCY
			|| cell.column.type === Table.TYPE.DATE
			|| cell.column.type === Table.TYPE.NUMBER
			|| cell.column.type === Table.TYPE.PERCENT
			|| cell.column.type === Table.TYPE.PERCENTILE
		) {
			classes.push('align-right');
		}

		let { value } = cell;

		if (cell.column.type === Table.TYPE.CURRENCY) {
			value = Numbro.formatCurrency(value, cell.column.format || '0,0[.]0a');
		} else if (cell.column.type === Table.TYPE.DATE) {
			value = Moment.utc(value, 'x').format(cell.column.format || 'll');
		} else if (cell.column.type === Table.TYPE.NUMBER) {
			value = Numbro.format(value, cell.column.format || '0,0[.]0');
		} else if (cell.column.type === Table.TYPE.PERCENT) {
			value = Numbro.format(value, cell.column.format || '0,0%');
		} else if (cell.column.type === Table.TYPE.PERCENTILE) {
			value = value === 0 ? '—' : Numbro.nth(value * 100);
		} else if (cell.column.type === Table.TYPE.PROGRESS) {
			value = (
				<Progress percent={value} />
			);
		} else if (cell.column.type === Table.TYPE.ELEMENT) {
			value = cell.column.cell(cell.original);
		}

		return (
			<div
				className={classes.join(' ')}
				onMouseDown={() => this.onCellMouseDown(cell.column.id, cell.index)}
				onMouseUp={() => this.onMouseUp(cell.column.id, cell.index)}
				onMouseEnter={() => this.onCellMouseEnter(cell.column.id, cell.index)}
				role="button"
				tabIndex="-1"
				ref={lastSelectedCell ? this.selectedCell : undefined}
				title={cell.column.cellTitle ? cell.column.cellTitle(cell.original) : undefined}
			>
				{
					(cell.original.disabled && cell.column.id === cell.original.disabled.accessor) && (
						<Tooltip
							header={cell.original.disabled.message}
							position="top left"
						>
							<SemanticIcon className="exclamation square" />
						</Tooltip>
					)
				}
				{value}
			</div>
		);
	}

	renderSelectAll() {
		return (this.state.showSelectAll && this.state.selected) ? (
			<>
				{
					this.state.selected.rows.length === this.state.data.length ? (
						<>
							All <strong>{this.state.selected.rows.length}</strong> rows selected
							<SemanticButton
								onClick={() => this.resetSelection()}
							>
								Clear selection
							</SemanticButton>
						</>
					) : (
						<>
							All <strong>{this.state.selected.rows.length}</strong> rows on this page are selected
							<SemanticButton
								onClick={() => this.selectAll()}
							>
								Select all {this.state.data.length} rows
							</SemanticButton>
						</>
					)
				}
			</>
		) : null;
	}

	render() {
		const {
			counts,
			error,
			manual,
			noBorder,
			onFetchData,
			pagination
		} = this.props;

		const {
			columns,
			data,
			pageSize,
			showSelectAll,
			sort
		} = this.state;

		if (data.length > 0) {
			return (
				<div
					ref={this.tableContainer}
					onMouseLeave={() => this.onMouseUp()}
				>
					<div
						className={`${styles.SelectAll} ${showSelectAll ? 'visible' : ''}`}
					>
						{this.renderSelectAll()}
					</div>
					<ReactTable
						ref={(element) => { this.table = element; }}
						className={`${styles.Table} -striped ${noBorder ? 'noBorder' : ''}`}
						tabIndex="0"
						columns={columns}
						data={data}
						minRows={1}
						manual={manual}
						page={manual ? pagination.page : undefined}
						pageSize={manual ? pagination.pageSize : undefined}
						pages={manual ? Math.ceil(counts.total / pagination.pageSize) : undefined}
						defaultPageSize={manual ? pagination.pageSize : pageSize}
						multiSort={false}
						sorted={manual ? [{ id: sort.property, desc: sort.value === 'desc' }] : undefined}
						defaultSortMethod={(a, b, desc) => (desc ? -1 : 1)} // return data in same order
						onSortedChange={manual ? undefined : (newSorted, column) => this.onSortedChange(newSorted, column)}
						onPageChange={manual ? undefined : page => this.onPageChange(page)}
						onPageSizeChange={manual ? undefined : newPageSize => this.onPageSizeChange(newPageSize)}
						onFetchData={manual ? onFetchData : undefined}
						previousText={(
							<SemanticIcon
								name="arrow left"
							/>
						)}
						nextText={(
							<SemanticIcon
								name="arrow right"
							/>
						)}
					/>
				</div>
			);
		}

		return (
			<div className={styles.Empty}>
				<SemanticIcon name="info circle" />
				<h2>
					{error.title}
				</h2>
				<p>
					{error.message}
				</p>
			</div>
		);
	}
}
