import React from 'react';
import { Table, Input, Pagination, Dropdown, Popup, Icon, Button } from 'semantic-ui-react';
import { isMobile } from 'react-device-detect';
import { Link } from 'gatsby';

import { IConfigSortData, IResultSortDataBy, sortDataBy } from '../utils/datasort';
import { useStateWithStorage } from '../utils/storage';

import * as SearchString from 'search-string';
import * as localForage from 'localforage';

const filterTypeOptions = [
    { key : '0', value : 'Exact', text : 'Exact match only' },
    { key : '1', value : 'Whole word', text : 'Whole word only' },
    { key : '2', value : 'Any match', text : 'Match any text' }
];

const pagingOptions = [
	{ key: '0', value: '10', text: '10' },
	{ key: '1', value: '25', text: '25' },
	{ key: '2', value: '50', text: '50' },
	{ key: '3', value: '100', text: '100' }
];

export interface ITableConfigRow {
	width: number;
	column: string;
	title: string | JSX.Element;
	pseudocolumns?: string[];
	reverse?: boolean;
	tiebreakers?: string[];
}

type SearchableTableProps = {
	id?: string;
	data: any[];
	explanation?: React.ReactNode;
	config: ITableConfigRow[];
	renderTableRow: (row: any, idx?: number) => JSX.Element;
	filterRow: (crew: any, filter: any, filterType?: string) => boolean;
	initOptions?: any;
    showFilterOptions: boolean;
	showPermalink: boolean;
	lockable?: any[];
};

export const SearchableTable = (props: SearchableTableProps) => {
	let data = [...props.data];
	const tableId = props.id ?? '';

	const hasDate = data.length > 0 && data[0].date_added;
	const defaultSort = {
		column: data.length > 0 && hasDate ? 'date_added' : 'name',
		direction: data.length > 0 && hasDate ? 'descending' : 'ascending'
	};

	const [searchFilter, setSearchFilter] = useStateWithStorage(tableId+'searchFilter', '');
	const [filterType, setFilterType] = useStateWithStorage(tableId+'filterType', 'Any match');
	const [column, setColumn] = useStateWithStorage(tableId+'column', defaultSort.column);
	const [direction, setDirection] = useStateWithStorage(tableId+'direction', defaultSort.direction);
	const [pagination_rows, setPaginationRows] = useStateWithStorage(tableId+'paginationRows', 10);
	const [pagination_page, setPaginationPage] = useStateWithStorage(tableId+'paginationPage', 1);

	const [activeLock, setActiveLock] = React.useState(undefined);

	// Override stored values with custom initial options and reset all others to defaults
	//	Previously stored values will be rendered before an override triggers a re-render
	React.useEffect(() => {
		if (props.initOptions) {
			setSearchFilter(props.initOptions['search'] ?? '');
			setFilterType(props.initOptions['filter'] ?? 'Any match');
			setColumn(props.initOptions['column'] ?? defaultSort.column);
			setDirection(props.initOptions['direction'] ?? defaultSort.direction);
			setPaginationRows(props.initOptions['rows'] ?? 10);
			setPaginationPage(props.initOptions['page'] ?? 1);
		}
	}, [props.initOptions]);

	// Activate lock by default if only 1 lockable
	React.useEffect(() => {
		setActiveLock(props.lockable?.length === 1 ? props.lockable[0] : undefined);
	}, [props.lockable]);

	// Update column and/or toggle direction, and store new values in state
	//	Actual sorting of full dataset will occur on next render before filtering and pagination
	function onHeaderClick(newColumn) {
		if (!newColumn.column) return;

		const lastColumn = column, lastDirection = direction;

		const sortConfig = {
			field: newColumn.column,
			direction: lastDirection === 'ascending' ? 'descending' : 'ascending'
		};
		if (newColumn.pseudocolumns && newColumn.pseudocolumns.includes(lastColumn)) {
			if (direction === 'descending') {
				const nextIndex = newColumn.pseudocolumns.indexOf(lastColumn) + 1; // Will be 0 if previous column was not a pseudocolumn
				sortConfig.field = newColumn.pseudocolumns[nextIndex === newColumn.pseudocolumns.length ? 0 : nextIndex];
				sortConfig.direction = 'ascending';
			}
			else {
				sortConfig.field = lastColumn;
				sortConfig.direction = 'descending';
			}
		}
		else if (newColumn.column !== lastColumn) {
			sortConfig.direction = newColumn.reverse ? 'descending' : 'ascending';
		}

		setColumn(sortConfig.field);
		setDirection(sortConfig.direction);
		setPaginationPage(1);
	}

	function onChangeFilter(value) {
		setSearchFilter(value);
		setPaginationPage(1);
	}

	function renderTableHeader(column: any, direction: 'descending' | 'ascending' | null): JSX.Element {
		return (
			<Table.Row>
				{props.config.map((cell, idx) => (
					<Table.HeaderCell
						key={idx}
						width={cell.width as any}
						sorted={((cell.pseudocolumns && cell.pseudocolumns.includes(column)) || (column === cell.column)) ? direction : null}
						onClick={() => onHeaderClick(cell)}
						textAlign={cell.width === 1 ? 'center' : 'left'}
					>
						{cell.title}{cell.pseudocolumns?.includes(column) && <><br/><small>{column.replace('_',' ').replace('.length', '')}</small></>}
					</Table.HeaderCell>
				))}
			</Table.Row>
		);
	}

	function renderPermalink(): JSX.Element {
		// Will not catch custom options (e.g. highlight)
		const params = new URLSearchParams();
		if (searchFilter != '') params.append('search', searchFilter);
		if (filterType != 'Any match') params.append('filter', filterType);
		if (column != defaultSort.column) params.append('column', column);
		if (direction != defaultSort.direction) params.append('direction', direction);
		if (pagination_rows != 10) params.append('rows', pagination_rows);
		if (pagination_page != 1) params.append('page', pagination_page);
		let permalink = window.location.protocol + '//' + window.location.host + window.location.pathname;
		if (params.toString() != '') permalink += '?' + params.toString();
		return (
			<Link to={permalink}>
				<Icon name='linkify' /> Permalink
			</Link>
		);
	}

	function onLockableClick(lock: any): void {
		if (lock) {
			setActiveLock(lock);
		}
		else {
			setActiveLock(undefined);
			// Remember active page after removing lock
			setPaginationPage(activePage);
		}
	}

	function isRowActive(row: any, highlight: any): boolean {
		if (!highlight) return false;
		let isMatch = true;
		Object.keys(highlight).forEach(key => {
			if (row[key] !== highlight[key]) isMatch = false;
		});
		return isMatch;
	}

	// Sorting
	if (column) {
		const sortConfig: IConfigSortData = {
			field: column,
			direction: direction,
			keepSortOptions: true
		};

		// Define tiebreaker rules with names in alphabetical order as default
		//	Hack here to sort rarity in the same direction as max_rarity
		let subsort = [];
		const columnConfig = props.config.find(col => col.column === column);
		if (columnConfig && columnConfig.tiebreakers) {
			subsort = columnConfig.tiebreakers.map(subfield => {
				const subdirection = subfield.substr(subfield.length-6) === 'rarity' ? direction : 'ascending';
				return { field: subfield, direction: subdirection };
			});
		}
		if (column != 'name') subsort.push({ field: 'name', direction: 'ascending' });
		sortConfig.subsort = subsort;

		// Use original dataset for sorting
		const sorted: IResultSortDataBy = sortDataBy([...props.data], sortConfig);
		data = sorted.result;

		// Sorting by pre-calculated ranks should filter out crew without matching skills
		//	Otherwise crew without skills show up first (because 0 comes before 1)
		if (column.substr(0, 5) === 'ranks') {
			const rank = column.split('.')[1];
			data = data.filter(row => row.ranks[rank] > 0);
		}
	}

	// Filtering
	let filters = [];
	if (searchFilter) {
		let grouped = searchFilter.split(/\s+OR\s+/i);
		grouped.forEach(group => {
			filters.push(SearchString.parse(group));
		});
	}
	data = data.filter(row => props.filterRow(row, filters, filterType));

	// Pagination
	let activePage = pagination_page;
	if (activeLock) {
		const index = data.findIndex(row => isRowActive(row, activeLock));
		// Locked crew is not viewable in current filter
		if (index < 0) {
			setActiveLock(undefined);
			return (<></>);
		}
		activePage = Math.floor(index / pagination_rows) + 1;
	}
	let totalPages = Math.ceil(data.length / pagination_rows);
	if (activePage > totalPages) activePage = totalPages;
	data = data.slice(pagination_rows * (activePage - 1), pagination_rows * activePage);

	return (
		<div>
			<Input
				style={{ width: isMobile ? '100%' : '50%' }}
				iconPosition="left"
				placeholder="Search..."
				value={searchFilter}
				onChange={(e, { value }) => onChangeFilter(value)}>
					<input />
					<Icon name='search' />
					<Button icon onClick={() => onChangeFilter('')} >
						<Icon name='delete' />
					</Button>
			</Input>

			{props.showFilterOptions && (
				<span style={{ paddingLeft: '2em' }}>
					<Dropdown inline
								options={filterTypeOptions}
								value={filterType}
								onChange={(event, {value}) => setFilterType(value as number)}
					/>
				</span>
			)}

			<Popup wide trigger={<Icon name="help" />}
				header={'Advanced search'}
				content={props.explanation ? props.explanation : renderDefaultExplanation()}
			/>

			{props.lockable && <LockButtons lockable={props.lockable} activeLock={activeLock} setLock={onLockableClick} />}

			<Table sortable celled selectable striped collapsing unstackable compact="very">
				<Table.Header>{renderTableHeader(column, direction)}</Table.Header>
				<Table.Body>{data.map((row, idx) => props.renderTableRow(row, idx, isRowActive(row, activeLock)))}</Table.Body>
				<Table.Footer>
					<Table.Row>
						<Table.HeaderCell colSpan={props.config.length}>
							<Pagination
								totalPages={totalPages}
								activePage={activePage}
								onPageChange={(event, { activePage }) => {
									setPaginationPage(activePage as number);
									setActiveLock(undefined);	// Remove lock when changing pages
								}}
							/>
							<span style={{ paddingLeft: '2em'}}>
								Rows per page:{' '}
								<Dropdown
									inline
									options={pagingOptions}
									value={pagination_rows}
									onChange={(event, {value}) => {
										setPaginationPage(1);
										setPaginationRows(value as number);
									}}
								/>
							</span>
							{props.showPermalink && (<span style={{ paddingLeft: '5em'}}>{renderPermalink()}</span>)}
						</Table.HeaderCell>
					</Table.Row>
				</Table.Footer>
			</Table>
		</div>
	);
};

type LockButtonsProps = {
	lockable: any[];
	activeLock: any;
	setLock: (lock: any) => void;
};

const LockButtons = (props: LockButtonsProps) => {
	const { lockable, activeLock, setLock } = props;

	if (lockable?.length == 0) return (<></>);

	return (
		<div style={{ margin: '.5em 0' }}>
			<span style={{ marginRight: '.5em' }}>Lock view on:</span>
			{lockable.map((lock, lockNum) => (
				<Button key={lockNum} compact toggle active={JSON.stringify(lock) === JSON.stringify(activeLock)} onClick={() => handleClick(lock)}>
					{lock.name}
				</Button>
			))}
		</div>
	);

	function handleClick(lock: any): void {
		const isActive = JSON.stringify(lock) === JSON.stringify(activeLock);
		setLock(isActive ? undefined : lock);
	}
};

// Check for custom initial table options from URL or <Link state>
export const initSearchableOptions = (location: any) => {
	let initOptions = false;
	const OPTIONS = ['search', 'filter', 'column', 'direction', 'rows', 'page'];

	const urlParams = location.search ? new URLSearchParams(location.search) : undefined;
	const linkState = location.state;

	OPTIONS.forEach((option) => {
		let value = undefined;
		// Always use URL parameters if found
		if (urlParams?.has(option)) value = urlParams.get(option);
		// Otherwise check <Link state>
		if (!value && linkState && linkState[option]) value = JSON.parse(JSON.stringify(linkState[option]));
		if (value) {
			if (!initOptions) initOptions = {};
			initOptions[option] = value;
		}
	});

	return initOptions;
};

// Check for other initial option from URL or <Link state> by custom name
export const initCustomOption = (location: any, option: string, defaultValue: any) => {
	let value = undefined;
	// Always use URL parameters if found
	if (location?.search) {
		const urlParams = new URLSearchParams(location.search);
		if (urlParams.has(option)) value = Array.isArray(defaultValue) ? urlParams.getAll(option) : urlParams.get(option);
	}
	// Otherwise check <Link state>
	if (!value && location?.state) {
		const linkState = location.state;
		if (linkState[option]) value = JSON.parse(JSON.stringify(linkState[option]));
	}
	return value ?? defaultValue;
};

export const prettyCrewColumnTitle = (column: string) => {
	if (column.substr(0, 6) == 'ranks.') {
		let title = column.replace('ranks.', '');
		if (title.substr(-4) == 'Rank') {
			title = title.replace('Rank', '');
			title = title.substr(0, 1).toUpperCase() + title.substr(1);
			return title;
		}
		else {
			const vars = title.split('_');
			let score = vars.shift();
			switch (score) {
				case 'G': score = 'Gauntlet'; break;
				case 'V': score = 'Voyage'; break;
			}
			const skills = vars.reduce((prev, curr) => prev != '' ? prev + ' / ' + curr : curr, '');
			return (
				<span style={{ fontSize: '.95em' }}>
					{score}<br/>{skills}
				</span>
			);
		}
		return title;
	}
	return column;
};

function renderDefaultExplanation(): JSX.Element {
	return (
		<div>
			<p>
				Search for crew by name or trait (with optional '-' for exclusion). For example, this returns all Rikers
				that are not romantic:
			</p>
			<p>
				<code>riker -romantic</code>
			</p>

			<p>
				Search for multiple crew by separating terms with <b>OR</b>. This returns any Tuvok or T'Pol:
			</p>
			<p>
				<code>tuvok OR tpol</code>
			</p>

			<p>
				Specify <b>name</b>, <b>trait</b>, <b>rarity</b> or <b>skill</b> fields for more advanced searches. This
				returns all female crew of rarity 4 or 5 with science skill and the Q Continuum trait:
			</p>
			<p>
				<code>trait:female rarity:4,5 skill:sci trait:"q continuum"</code>
			</p>

			<p>
				Search for all crew that are in the game portal (<b>true</b>) or not (any other value):
			</p>
			<p>
				<code>in_portal:true</code>
			</p>
		</div>
	);
}