<template>
<table
	ref="rootEl"
	tabindex="-1"
	:class="$style.grid"
	@mousedown="onMouseDown"
	@keydown="onKeyDown"
>
	<thead>
		<MkHeaderRow
			:columns="columns"
			:bus="bus"
			@operation:beginWidthChange="onHeaderCellWidthBeginChange"
			@operation:endWidthChange="onHeaderCellWidthEndChange"
			@operation:widthLargest="onHeaderCellWidthLargest"
			@change:width="onHeaderCellChangeWidth"
			@change:contentSize="onHeaderCellChangeContentSize"
		/>
	</thead>
	<tbody>
		<MkDataRow
			v-for="row in rows"
			:key="row.index"
			:row="row"
			:cells="cells[row.index]"
			:bus="bus"
			@operation:beginEdit="onCellEditBegin"
			@operation:endEdit="onCellEditEnd"
			@change:value="onChangeCellValue"
			@change:contentSize="onChangeCellContentSize"
		/>
	</tbody>
</table>
</template>

<script setup lang="ts">
import { computed, ref, toRefs, watch } from 'vue';
import {
	CellValueChangedEvent,
	ColumnSetting,
	DataSource,
	GridColumn,
	GridEventEmitter,
	GridRow,
	GridState,
	Size,
} from '@/components/grid/grid.js';
import MkDataRow from '@/components/grid/MkDataRow.vue';
import MkHeaderRow from '@/components/grid/MkHeaderRow.vue';
import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { cellValidation, ValidateViolation } from '@/components/grid/cell-validators.js';
import { CELL_ADDRESS_NONE, CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
import { calcCellWidth, equalCellAddress, getCellAddress } from '@/components/grid/utils.js';

const props = defineProps<{
	columnSettings: ColumnSetting[],
	data: DataSource[]
}>();

const emit = defineEmits<{
	(ev: 'operation:cellValidation', violation: ValidateViolation): void;
	(ev: 'operation:rowDeleting', rows: GridRow[]): void;
	(ev: 'change:cellValue', event: CellValueChangedEvent): void;
}>();

const bus = new GridEventEmitter();

const { columnSettings, data } = toRefs(props);

const rootEl = ref<InstanceType<typeof HTMLTableElement>>();
const columns = ref<GridColumn[]>([]);
const rows = ref<GridRow[]>([]);
const cells = ref<GridCell[][]>([]);
const previousCellAddress = ref<CellAddress>(CELL_ADDRESS_NONE);
const editingCellAddress = ref<CellAddress>(CELL_ADDRESS_NONE);
const firstSelectionColumnIdx = ref<number>(CELL_ADDRESS_NONE.col);
const firstSelectionRowIdx = ref<number>(CELL_ADDRESS_NONE.row);
const state = ref<GridState>('normal');

const selectedCell = computed(() => {
	const selected = cells.value.flat().filter(it => it.selected);
	return selected.length > 0 ? selected[0] : undefined;
});
const rangedCells = computed(() => cells.value.flat().filter(it => it.ranged));
const rangedBounds = computed(() => {
	const _cells = rangedCells.value;
	const leftTop = {
		col: Math.min(..._cells.map(it => it.address.col)),
		row: Math.min(..._cells.map(it => it.address.row)),
	};
	const rightBottom = {
		col: Math.max(..._cells.map(it => it.address.col)),
		row: Math.max(..._cells.map(it => it.address.row)),
	};
	return {
		leftTop,
		rightBottom,
	};
});
const availableBounds = computed(() => {
	const leftTop = {
		col: 0,
		row: 0,
	};
	const rightBottom = {
		col: Math.max(...columns.value.map(it => it.index)),
		row: Math.max(...rows.value.map(it => it.index)),
	};
	return { leftTop, rightBottom };
});
const rangedRows = computed(() => rows.value.filter(it => it.ranged));

watch(columnSettings, refreshColumnsSetting);
watch(data, refreshData);

if (_DEV_) {
	watch(state, (value) => {
		console.log(`state: ${value}`);
	});
}

function onKeyDown(ev: KeyboardEvent) {
	if (_DEV_) {
		console.log('[Grid]', `ctrl: ${ev.ctrlKey}, shift: ${ev.shiftKey}, code: ${ev.code}`);
	}

	switch (state.value) {
		case 'normal': {
			ev.preventDefault();

			if (ev.ctrlKey) {
				if (ev.shiftKey) {
					// ctrl + shiftキーが押されている場合は選択セルの範囲拡大(最大範囲)
					const selectedCellAddress = requireSelectionCell();
					const max = availableBounds.value;
					const bounds = rangedBounds.value;

					let newBounds: { leftTop: CellAddress, rightBottom: CellAddress };
					switch (ev.code) {
						case 'ArrowRight': {
							newBounds = {
								leftTop: { col: selectedCellAddress.col, row: bounds.leftTop.row },
								rightBottom: { col: max.rightBottom.col, row: bounds.rightBottom.row },
							};
							break;
						}
						case 'ArrowLeft': {
							newBounds = {
								leftTop: { col: max.leftTop.col, row: bounds.leftTop.row },
								rightBottom: { col: selectedCellAddress.col, row: bounds.rightBottom.row },
							};
							break;
						}
						case 'ArrowUp': {
							newBounds = {
								leftTop: { col: bounds.leftTop.col, row: max.leftTop.row },
								rightBottom: { col: bounds.rightBottom.col, row: selectedCellAddress.row },
							};
							break;
						}
						case 'ArrowDown': {
							newBounds = {
								leftTop: { col: bounds.leftTop.col, row: selectedCellAddress.row },
								rightBottom: { col: bounds.rightBottom.col, row: max.rightBottom.row },
							};
							break;
						}
						default: {
							return;
						}
					}

					unSelectionOutOfRange(newBounds.leftTop, newBounds.rightBottom);
					expandCellRange(newBounds.leftTop, newBounds.rightBottom);
				} else {
					switch (ev.code) {
						case 'KeyC': {
							rangeCopyToClipboard();
							break;
						}
						case 'KeyV': {
							pasteFromClipboard();
							break;
						}
					}
				}
			} else {
				if (ev.shiftKey) {
					// shiftキーが押されている場合は選択セルの範囲拡大(隣のセルまで)
					const selectedCellAddress = requireSelectionCell();
					const bounds = rangedBounds.value;
					let newBounds: { leftTop: CellAddress, rightBottom: CellAddress };
					switch (ev.code) {
						case 'ArrowRight': {
							newBounds = {
								leftTop: {
									col: bounds.leftTop.col < selectedCellAddress.col
										? bounds.leftTop.col + 1
										: selectedCellAddress.col,
									row: bounds.leftTop.row,
								},
								rightBottom: {
									col: (bounds.rightBottom.col > selectedCellAddress.col || bounds.leftTop.col === selectedCellAddress.col)
										? bounds.rightBottom.col + 1
										: selectedCellAddress.col,
									row: bounds.rightBottom.row,
								},
							};
							break;
						}
						case 'ArrowLeft': {
							newBounds = {
								leftTop: {
									col: (bounds.leftTop.col < selectedCellAddress.col || bounds.rightBottom.col === selectedCellAddress.col)
										? bounds.leftTop.col - 1
										: selectedCellAddress.col,
									row: bounds.leftTop.row,
								},
								rightBottom: {
									col: bounds.rightBottom.col > selectedCellAddress.col
										? bounds.rightBottom.col - 1
										: selectedCellAddress.col,
									row: bounds.rightBottom.row,
								},
							};
							break;
						}
						case 'ArrowUp': {
							newBounds = {
								leftTop: {
									col: bounds.leftTop.col,
									row: (bounds.leftTop.row < selectedCellAddress.row || bounds.rightBottom.row === selectedCellAddress.row)
										? bounds.leftTop.row - 1
										: selectedCellAddress.row,
								},
								rightBottom: {
									col: bounds.rightBottom.col,
									row: bounds.rightBottom.row > selectedCellAddress.row
										? bounds.rightBottom.row - 1
										: selectedCellAddress.row,
								},
							};
							break;
						}
						case 'ArrowDown': {
							newBounds = {
								leftTop: {
									col: bounds.leftTop.col,
									row: bounds.leftTop.row < selectedCellAddress.row
										? bounds.leftTop.row + 1
										: selectedCellAddress.row,
								},
								rightBottom: {
									col: bounds.rightBottom.col,
									row: (bounds.rightBottom.row > selectedCellAddress.row || bounds.leftTop.row === selectedCellAddress.row)
										? bounds.rightBottom.row + 1
										: selectedCellAddress.row,
								},
							};
							break;
						}
						default: {
							return;
						}
					}

					unSelectionOutOfRange(newBounds.leftTop, newBounds.rightBottom);
					expandCellRange(newBounds.leftTop, newBounds.rightBottom);
				} else {
					// shiftキーもctrlキーが押されていない場合
					switch (ev.code) {
						case 'ArrowRight': {
							const selectedCellAddress = requireSelectionCell();
							selectionCell({ col: selectedCellAddress.col + 1, row: selectedCellAddress.row });
							break;
						}
						case 'ArrowLeft': {
							const selectedCellAddress = requireSelectionCell();
							selectionCell({ col: selectedCellAddress.col - 1, row: selectedCellAddress.row });
							break;
						}
						case 'ArrowUp': {
							const selectedCellAddress = requireSelectionCell();
							selectionCell({ col: selectedCellAddress.col, row: selectedCellAddress.row - 1 });
							break;
						}
						case 'ArrowDown': {
							const selectedCellAddress = requireSelectionCell();
							selectionCell({ col: selectedCellAddress.col, row: selectedCellAddress.row + 1 });
							break;
						}
						case 'Delete': {
							if (rangedRows.value.length > 0) {
								emit('operation:rowDeleting', [...rangedRows.value]);
							} else {
								const ranges = rangedCells.value;
								for (const range of ranges) {
									range.value = undefined;
								}
							}
							break;
						}
						default: {
							return;
						}
					}
				}
			}
			break;
		}
	}
}

function onMouseDown(ev: MouseEvent) {
	const cellAddress = getCellAddress(ev.target as HTMLElement);
	switch (state.value) {
		case 'cellEditing': {
			if (availableCellAddress(cellAddress) && !equalCellAddress(editingCellAddress.value, cellAddress)) {
				selectionCell(cellAddress);
			}
			break;
		}
		case 'normal': {
			if (availableCellAddress(cellAddress)) {
				selectionCell(cellAddress);

				registerMouseUp();
				registerMouseMove();
				state.value = 'cellSelecting';
			} else if (isColumnHeaderCellAddress(cellAddress)) {
				unSelectionRange();

				const colCells = cells.value.map(row => row[cellAddress.col]);
				selectionRange(...colCells.map(cell => cell.address));

				registerMouseUp();
				registerMouseMove();
				firstSelectionColumnIdx.value = cellAddress.col;
				state.value = 'colSelecting';

				rootEl.value?.focus();
			} else if (isRowNumberCellAddress(cellAddress)) {
				unSelectionRange();

				const rowCells = cells.value[cellAddress.row];
				selectionRange(...rowCells.map(cell => cell.address));

				registerMouseUp();
				registerMouseMove();
				firstSelectionRowIdx.value = cellAddress.row;
				state.value = 'rowSelecting';

				rootEl.value?.focus();
			}
			break;
		}
	}
}

function onMouseMove(ev: MouseEvent) {
	ev.preventDefault();
	switch (state.value) {
		case 'cellSelecting': {
			const selectedCellAddress = selectedCell.value?.address;
			const targetCellAddress = getCellAddress(ev.target as HTMLElement);
			if (equalCellAddress(previousCellAddress.value, targetCellAddress) || !availableCellAddress(targetCellAddress) || !selectedCellAddress) {
				return;
			}

			const leftTop = {
				col: Math.min(targetCellAddress.col, selectedCellAddress.col),
				row: Math.min(targetCellAddress.row, selectedCellAddress.row),
			};

			const rightBottom = {
				col: Math.max(targetCellAddress.col, selectedCellAddress.col),
				row: Math.max(targetCellAddress.row, selectedCellAddress.row),
			};

			unSelectionOutOfRange(leftTop, rightBottom);
			expandCellRange(leftTop, rightBottom);
			previousCellAddress.value = targetCellAddress;

			break;
		}
		case 'colSelecting': {
			const targetCellAddress = getCellAddress(ev.target as HTMLElement);
			if (!isColumnHeaderCellAddress(targetCellAddress) || previousCellAddress.value.col === targetCellAddress.col) {
				return;
			}

			const leftTop = {
				col: Math.min(targetCellAddress.col, firstSelectionColumnIdx.value),
				row: 0,
			};

			const rightBottom = {
				col: Math.max(targetCellAddress.col, firstSelectionColumnIdx.value),
				row: cells.value.length - 1,
			};

			unSelectionOutOfRange(leftTop, rightBottom);
			expandCellRange(leftTop, rightBottom);
			previousCellAddress.value = targetCellAddress;

			break;
		}
		case 'rowSelecting': {
			const targetCellAddress = getCellAddress(ev.target as HTMLElement);
			if (!isRowNumberCellAddress(targetCellAddress) || previousCellAddress.value.row === targetCellAddress.row) {
				return;
			}

			const leftTop = {
				col: 0,
				row: Math.min(targetCellAddress.row, firstSelectionRowIdx.value),
			};

			const rightBottom = {
				col: Math.min(...cells.value.map(it => it.length - 1)),
				row: Math.max(targetCellAddress.row, firstSelectionRowIdx.value),
			};

			unSelectionOutOfRange(leftTop, rightBottom);
			expandCellRange(leftTop, rightBottom);

			rows.value[targetCellAddress.row].ranged = true;

			const rangedRowIndexes = rangedRows.value.map(it => it.index);
			expandRowRange(Math.min(...rangedRowIndexes), Math.max(...rangedRowIndexes));

			previousCellAddress.value = targetCellAddress;

			break;
		}
	}
}

function onMouseUp(ev: MouseEvent) {
	ev.preventDefault();
	switch (state.value) {
		case 'rowSelecting':
		case 'colSelecting':
		case 'cellSelecting': {
			unregisterMouseUp();
			unregisterMouseMove();
			state.value = 'normal';
			previousCellAddress.value = CELL_ADDRESS_NONE;
			break;
		}
	}
}

function onCellEditBegin(sender: GridCell) {
	state.value = 'cellEditing';
	editingCellAddress.value = sender.address;
	for (const cell of cells.value.flat()) {
		if (cell.address.col !== sender.address.col || cell.address.row !== sender.address.row) {
			// 編集状態となったセル以外は全部選択解除
			cell.selected = false;
		}
	}
}

function onCellEditEnd() {
	editingCellAddress.value = CELL_ADDRESS_NONE;
	state.value = 'normal';
}

function onChangeCellValue(sender: GridCell, newValue: CellValue) {
	setCellValue(sender, newValue);
}

function onChangeCellContentSize(sender: GridCell, contentSize: Size) {
	cells.value[sender.address.row][sender.address.col].contentSize = contentSize;
	if (sender.column.setting.width === 'auto') {
		largestCellWidth(sender.column);
	}
}

function onHeaderCellWidthBeginChange() {
	switch (state.value) {
		case 'normal': {
			state.value = 'colResizing';
			break;
		}
	}
}

function onHeaderCellWidthEndChange() {
	switch (state.value) {
		case 'colResizing': {
			state.value = 'normal';
			break;
		}
	}
}

function onHeaderCellChangeWidth(sender: GridColumn, width: string) {
	switch (state.value) {
		case 'colResizing': {
			const column = columns.value[sender.index];
			column.width = width;
			break;
		}
	}
}

function onHeaderCellChangeContentSize(sender: GridColumn, newSize: Size) {
	switch (state.value) {
		case 'normal': {
			columns.value[sender.index].contentSize = newSize;
			if (sender.setting.width === 'auto') {
				largestCellWidth(sender);
			}
			break;
		}
	}
}

function onHeaderCellWidthLargest(sender: GridColumn) {
	switch (state.value) {
		case 'normal': {
			largestCellWidth(sender);
			break;
		}
	}
}

function largestCellWidth(column: GridColumn) {
	const _cells = cells.value;
	const largestColumnWidth = columns.value[column.index].contentSize.width;

	const largestCellWidth = (_cells.length > 0)
		? _cells
			.map(row => row[column.index])
			.reduce(
				(acc, value) => Math.max(acc, value.contentSize.width),
				0,
			)
		: 0;

	console.log(`largestCellWidth: ${largestColumnWidth}, ${largestCellWidth}`);

	column.width = `${Math.max(largestColumnWidth, largestCellWidth)}px`;
}

function setCellValue(sender: GridCell | CellAddress, newValue: CellValue) {
	const cellAddress = 'address' in sender ? sender.address : sender;
	const cell = cells.value[cellAddress.row][cellAddress.col];

	const violation = cellValidation(cell, newValue);
	emit('operation:cellValidation', violation);

	cell.validation = {
		valid: violation.valid,
		violations: violation.violations.filter(it => !it.valid),
	};
	cell.value = newValue;

	emit('change:cellValue', {
		column: cell.column,
		row: cell.row,
		value: newValue,
	});
}

function selectionCell(target: CellAddress) {
	if (!availableCellAddress(target)) {
		return;
	}

	unSelectionRange();

	const _cells = cells.value;
	_cells[target.row][target.col].selected = true;
	_cells[target.row][target.col].ranged = true;
}

function requireSelectionCell(): CellAddress {
	const selected = selectedCell.value;
	if (!selected) {
		throw new Error('No selected cell');
	}

	return selected.address;
}

function selectionRange(...targets: CellAddress[]) {
	const _cells = cells.value;
	for (const target of targets) {
		_cells[target.row][target.col].ranged = true;
	}
}

function unSelectionRange() {
	const _cells = rangedCells.value;
	for (const cell of _cells) {
		cell.selected = false;
		cell.ranged = false;
	}

	const _rows = rows.value;
	for (const row of _rows) {
		row.ranged = false;
	}
}

function unSelectionOutOfRange(leftTop: CellAddress, rightBottom: CellAddress) {
	const _cells = rangedCells.value;
	for (const cell of _cells) {
		const outOfRangeCol = cell.address.col < leftTop.col || cell.address.col > rightBottom.col;
		const outOfRangeRow = cell.address.row < leftTop.row || cell.address.row > rightBottom.row;
		if (outOfRangeCol || outOfRangeRow) {
			cell.ranged = false;
		}
	}

	const outOfRangeRows = rows.value.filter((_, index) => index < leftTop.row || index > rightBottom.row);
	for (const row of outOfRangeRows) {
		row.ranged = false;
	}
}

function expandCellRange(leftTop: CellAddress, rightBottom: CellAddress) {
	const targetRows = cells.value.slice(leftTop.row, rightBottom.row + 1);
	for (const row of targetRows) {
		for (const cell of row.slice(leftTop.col, rightBottom.col + 1)) {
			cell.ranged = true;
		}
	}
}

function expandRowRange(top: number, bottom: number) {
	const targetRows = rows.value.slice(top, bottom + 1);
	for (const row of targetRows) {
		row.ranged = true;
	}
}

function availableCellAddress(cellAddress: CellAddress): boolean {
	return cellAddress.row >= 0 && cellAddress.col >= 0 && cellAddress.row < rows.value.length && cellAddress.col < columns.value.length;
}

function isColumnHeaderCellAddress(cellAddress: CellAddress): boolean {
	return cellAddress.row === -1 && cellAddress.col >= 0;
}

function isRowNumberCellAddress(cellAddress: CellAddress): boolean {
	return cellAddress.row >= 0 && cellAddress.col === -1;
}

function rangeCopyToClipboard() {
	const lines = Array.of<string>();
	const bounds = rangedBounds.value;
	for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) {
		const items = Array.of<string>();
		for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) {
			const cell = cells.value[row][col];
			items.push(cell.value?.toString() ?? '');
		}
		lines.push(items.join('\t'));
	}

	const text = lines.join('\n');
	copyToClipboard(text);
}

async function pasteFromClipboard() {
	function parseValue(value: string, type: ColumnSetting['type']): CellValue {
		switch (type) {
			case 'number': {
				return Number(value);
			}
			case 'boolean': {
				return value === 'true';
			}
			default: {
				return value;
			}
		}
	}

	const clipBoardText = await navigator.clipboard.readText();

	const bounds = rangedBounds.value;
	const lines = clipBoardText.replace(/\r/g, '')
		.split('\n')
		.map(it => it.split('\t'));

	if (lines.length === 1 && lines[0].length === 1) {
		// 単独文字列の場合は選択範囲全体に同じテキストを貼り付ける
		const ranges = rangedCells.value;
		for (const cell of ranges) {
			setCellValue(cell, parseValue(lines[0][0], cell.column.setting.type));
		}
	} else {
		// 表形式文字列の場合は表形式にパースし、選択範囲に合うように貼り付ける
		const offsetRow = bounds.leftTop.row;
		const offsetCol = bounds.leftTop.col;
		for (let row = bounds.leftTop.row; row <= bounds.rightBottom.row; row++) {
			const rowIdx = row - offsetRow;
			if (lines.length <= rowIdx) {
				// クリップボードから読んだ二次元配列よりも選択範囲の方が大きい場合、貼り付け操作を打ち切る
				break;
			}

			const items = lines[rowIdx];
			for (let col = bounds.leftTop.col; col <= bounds.rightBottom.col; col++) {
				const colIdx = col - offsetCol;
				if (items.length <= colIdx) {
					// クリップボードから読んだ二次元配列よりも選択範囲の方が大きい場合、貼り付け操作を打ち切る
					break;
				}

				setCellValue(cells.value[row][col], parseValue(items[colIdx], cells.value[row][col].column.setting.type));
			}
		}
	}
}

function refreshColumnsSetting() {
	const bindToList = columnSettings.value.map(it => it.bindTo);
	if (new Set(bindToList).size !== columnSettings.value.length) {
		throw new Error(`Duplicate bindTo setting : [${bindToList.join(',')}]}]`);
	}

	refreshData();
}

function refreshData() {
	const _data: DataSource[] = data.value;
	const _rows: GridRow[] = _data.map((_, index) => ({
		index,
		ranged: false,
	}));
	const _columns: GridColumn[] = columnSettings.value.map((setting, index) => ({
		index,
		setting,
		width: calcCellWidth(setting.width),
		contentSize: { width: 0, height: 0 },
	}));
	const _cells = Array.of<GridCell[]>();

	for (const [rowIndex, row] of _rows.entries()) {
		const rowCells = Array.of<GridCell>();
		for (const [colIndex, column] of _columns.entries()) {
			const value = (column.setting.bindTo in _data[rowIndex])
				? _data[rowIndex][column.setting.bindTo]
				: undefined;

			const cell: GridCell = {
				address: { col: colIndex, row: rowIndex },
				value,
				column: column,
				row: row,
				selected: false,
				ranged: false,
				contentSize: { width: 0, height: 0 },
				validation: {
					valid: true,
					violations: [],
				},
			};

			rowCells.push(cell);
		}

		_cells.push(rowCells);
	}

	rows.value = _rows;
	columns.value = _columns;
	cells.value = _cells;
}

function registerMouseMove() {
	unregisterMouseMove();
	addEventListener('mousemove', onMouseMove);
}

function unregisterMouseMove() {
	removeEventListener('mousemove', onMouseMove);
}

function registerMouseUp() {
	unregisterMouseUp();
	addEventListener('mouseup', onMouseUp);
}

function unregisterMouseUp() {
	removeEventListener('mouseup', onMouseUp);
}

refreshColumnsSetting();
refreshData();

</script>

<style module lang="scss">
.grid {
	overflow: scroll;
	table-layout: fixed;
	width: fit-content;
	user-select: none;

	border: solid 0.5px var(--divider);
	border-spacing: 0;
	border-radius: var(--radius);
}
</style>