<script>
	import { createEventDispatcher } from "svelte";

	/** @type {Array<Object>} */
	export let columns;

	/** @type {Array<Object>} */
	export let rows;

	/** @type {Array<Object>} */
	export let c_rows;

	/** @type {Array<number>} */
	export let sortOrders = [1, -1];

	// READ AND WRITE

	/** @type {string} */
	export let sortBy = "";

	/** @type {number} */
	export let sortOrder = sortOrders?.[0] || 1;

	/** @type {Object} */
	export let filterSelections = {};

	// expand
	/** @type {Array.<string|number>} */
	export let expanded = [];

	// READ ONLY

	/** @type {string} */
	export let expandRowKey = null;

	/** @type {string} */
	export let expandSingle = false;

	/** @type {string} */
	export let iconAsc = "▲";

	/** @type {string} */
	export let iconDesc = "▼";

	/** @type {string} */
	export let iconSortable = "";

	/** @type {string} */
	export let iconExpand = "▼";

	/** @type {string} */
	export let iconExpanded = "▲";

	/** @type {boolean} */
	export let showExpandIcon = false;

	/** @type {string} */
	export let classNameTable = "";

	/** @type {string} */
	export let classNameThead = "";

	/** @type {string} */
	export let classNameTbody = "";

	/** @type {string} */
	export let classNameSelect = "";

	/** @type {string} */
	export let classNameInput = "";

	/** @type {string} */
	export let classNameRow = "";

	/** @type {string} */
	export let classNameCell = "";

	/** @type {string} class added to the expanded row*/
	export let classNameRowExpanded = "";

	/** @type {string} class added to the expanded row*/
	export let classNameExpandedContent = "";

	/** @type {string} class added to the cell that allows expanding/closing */
	export let classNameCellExpand = "";

	const dispatch = createEventDispatcher();

	let sortFunction = () => "";

	// Validation
	if (!Array.isArray(expanded)) throw "'expanded' needs to be an array";

	let showFilterHeader = columns.some(c => {
		// check if there are any filter or search headers
		return c.filterOptions !== undefined || c.searchValue !== undefined;
	});
	let filterValues = {};
	let columnByKey;
	$: {
		columnByKey = {};
		columns.forEach(col => {
			columnByKey[col.key] = col;
		});
	}

	$: colspan = (showExpandIcon ? 1 : 0) + columns.length;

	console.log(rows);

	$: c_rows = rows
		.filter(r => {
			// get search and filter results/matches
			return Object.keys(filterSelections).every(f => {
				// check search (text input) matches
				let resSearch =
					filterSelections[f] === "" ||
					(columnByKey[f].searchValue &&
						(columnByKey[f].searchValue(r) + "")
							.toLocaleLowerCase()
							.indexOf((filterSelections[f] + "").toLocaleLowerCase()) >= 0);

				// check filter (dropdown) matches
				let resFilter =
					resSearch ||
					filterSelections[f] === undefined ||
					// default to value() if filterValue() not provided in col
					filterSelections[f] ===
					(typeof columnByKey[f].filterValue === "function"
						? columnByKey[f].filterValue(r)
						: columnByKey[f].value(r));
				return resFilter;
			});
		})
		.map(r =>
			Object.assign({}, r, {
				// internal row property for sort order
				$sortOn: sortFunction(r),
				// internal row property for expanded rows
				$expanded:
					expandRowKey !== null && expanded.indexOf(r[expandRowKey]) >= 0,
			})
		)
		.sort((a, b) => {
			if (!sortBy) return 0;
			else if (a.$sortOn > b.$sortOn) return sortOrder;
			else if (a.$sortOn < b.$sortOn) return -sortOrder;
			return 0;
		});

	const asStringArray = v =>
		[]
			.concat(v)
			.filter(v => typeof v === "string" && v !== "")
			.join(" ");

	const calculateFilterValues = () => {
		filterValues = {};
		columns.forEach(c => {
			if (typeof c.filterOptions === "function") {
				filterValues[c.key] = c.filterOptions(rows);
			} else if (Array.isArray(c.filterOptions)) {
				// if array of strings is provided, use it for name and value
				filterValues[c.key] = c.filterOptions.map(val => ({
					name: val,
					value: val,
				}));
			}
		});
	};

	$: {
		let col = columnByKey[sortBy];
		if (
			col !== undefined &&
			col.sortable === true &&
			typeof col.value === "function"
		) {
			sortFunction = r => col.value(r);
		}
	}

	$: {
		// if filters are enabled, watch rows and columns
		if (showFilterHeader && columns && rows) {
			calculateFilterValues();
		}
	}

	const updateSortOrder = colKey => {
		return colKey === sortBy
			? sortOrders[
			(sortOrders.findIndex(o => o === sortOrder) + 1) % sortOrders.length
				]
			: sortOrders[0];
	};

	const handleClickCol = (event, col) => {
		if (col.sortable) {
			sortOrder = updateSortOrder(col.key);
			sortBy = sortOrder ? col.key : undefined;
		}
		dispatch("clickCol", { event, col, key: col.key });
	};

	const handleClickRow = (event, row) => {
		dispatch("clickRow", { event, row });
	};

	const handleClickExpand = (event, row) => {
		row.$expanded = !row.$expanded;
		const keyVal = row[expandRowKey];
		if (expandSingle && row.$expanded) {
			expanded = [keyVal];
		} else if (expandSingle) {
			expanded = [];
		} else if (!row.$expanded) {
			expanded = expanded.filter(r => r != keyVal);
		} else {
			expanded = [...expanded, keyVal];
		}
		dispatch("clickExpand", { event, row });
	};

	const handleClickCell = (event, row, key) => {
		dispatch("clickCell", { event, row, key });
	};
</script>

<table class={asStringArray(classNameTable)}>
	<thead class={asStringArray(classNameThead)}>
	{#if showFilterHeader}
		<tr>
			{#each columns as col}
				<th class={asStringArray([col.headerFilterClass])}>
					{#if col.searchValue !== undefined}
						<input
							bind:value={filterSelections[col.key]}
							class={asStringArray(classNameInput)}
						/>
					{:else if filterValues[col.key] !== undefined}
						<select
							bind:value={filterSelections[col.key]}
							class={asStringArray(classNameSelect)}
						>
							<option value={undefined} />
							{#each filterValues[col.key] as option}
								<option value={option.value}>{option.name}</option>
							{/each}
						</select>
					{/if}
				</th>
			{/each}
			{#if showExpandIcon}
				<th />
			{/if}
		</tr>
	{/if}
	<slot name="header" {sortOrder} {sortBy}>
		<tr>
			{#each columns as col}
				<th
					on:click={e => handleClickCol(e, col)}
					class={asStringArray([
              col.sortable ? "isSortable" : "",
              col.headerClass,
            ])}
				>
					{col.title}
					{#if sortBy === col.key}
						{@html sortOrder === 1 ? iconAsc : iconDesc}
					{:else if col.sortable}
						{@html iconSortable}
					{/if}
				</th>
			{/each}
			{#if showExpandIcon}
				<th />
			{/if}
		</tr>
	</slot>
	</thead>

	<tbody class={asStringArray(classNameTbody)}>
	{#each c_rows as row, n}
		<slot name="row" {row} {n}>
			<tr on:click={e => { handleClickRow(e, row); }} class={asStringArray([classNameRow, row.$expanded && classNameRowExpanded])}>
				{#each columns as col}
					<td on:click={e => {handleClickCell(e, row, col.key);}} class={asStringArray([col.class, classNameCell])}>
						{#if col.renderComponent}
							<svelte:component
								this={col.renderComponent.component || col.renderComponent}
								{...col.renderComponent.props || {}}
								{row}
								{col}
							/>
						{:else}
							{@html col.renderValue ? col.renderValue(row) : col.value(row)}
						{/if}
					</td>
				{/each}
				{#if showExpandIcon}
					<td on:click={e => handleClickExpand(e, row)} class={asStringArray(["isClickable", classNameCellExpand])}>
						{@html row.$expanded ? iconExpand : iconExpanded}
					</td>
				{/if}
			</tr>
			{#if row.$expanded}
				<tr class={asStringArray(classNameExpandedContent)}>
					<td {colspan}>
						<slot name="expanded" {row} {n} />
					</td>
				</tr>
			{/if}
		</slot>
	{/each}
	</tbody>
</table>

<style>
    table {
        width: 100%;
    }
    .isSortable {
        cursor: pointer;
    }

    .isClickable {
        cursor: pointer;
    }

    tr th select {
        width: 100%;
    }
</style>
