<script lang="ts" setup>
import type { SpreadsheetProps, CellPosition } from './types'
import { Btn, CheckInput, TextInput, ToggleInput, useLocalStore, ListItem, Dropdown, useI18n } from '@bagelink/vue'
import { computed, ref, watch, nextTick } from 'vue'
import { useSpreadsheetClipboard } from './composables/useSpreadsheetClipboard'
import { useSpreadsheetColumns } from './composables/useSpreadsheetColumns'
import { useSpreadsheetSelection } from './composables/useSpreadsheetSelection'
import { useSpreadsheetSort } from './composables/useSpreadsheetSort'
import { useSpreadsheetUndo } from './composables/useSpreadsheetUndo'
import SpreadsheetTable from './SpreadsheetTable.vue'
import { flattenObject, unflattenObject, parseValueForFormat } from './utils'

const { modelValue, columnConfig, label, allowAddRow = true } = defineProps<SpreadsheetProps>()

const emit = defineEmits(['update:modelValue'])
const { $t } = useI18n()
const { wrapText } = useLocalStore({ wrapText: false })

const localRows = ref(
	modelValue ? modelValue.map(row => flattenObject({ ...row })) : []
)

watch(() => modelValue, (newVal) => {
	localRows.value = newVal ? newVal.map(row => flattenObject({ ...row })) : []
})

function emitUpdate() {
	emit('update:modelValue', localRows.value.map(row => unflattenObject({ ...row })))
}

const {
	columns,
	fixedColumns,
	scrollableColumns,
	columnOptions,
	visibleColumns,
	columnWidths,
	isCellEditable,
	createEmptyRow,
	handleResizeStart,
} = useSpreadsheetColumns(localRows, () => columnConfig)

const { sortColumn, sortDirection, sortByColumn } = useSpreadsheetSort(
	localRows,
	columns,
	visibleColumns,
	emitUpdate
)

const {
	selectionStart,
	selectionEnd,
	handleMouseDown,
	handleMouseOver,
	handleMouseUp,
	selectEntireRow,
} = useSpreadsheetSelection(columns)

const { canUndo, canRedo, saveState, undo, redo } = useSpreadsheetUndo(
	localRows,
	selectionStart,
	selectionEnd,
	emitUpdate
)

const { copySelection, pasteSelection } = useSpreadsheetClipboard(
	localRows,
	columns,
	selectionStart,
	selectionEnd,
	saveState,
	isCellEditable,
	createEmptyRow,
	emitUpdate
)

// ─── Editing ─────────────────────────────────────────────────────────────────

const editingCell = ref<CellPosition | null>(null)
const editingOriginalValue = ref<string | null>(null)
const editInputRef = ref(new Map<string, HTMLInputElement>())

function setInputRef(el: any, key: string) {
	if (el?.$el instanceof HTMLInputElement) {
		editInputRef.value.set(key, el.$el)
	} else if (el instanceof HTMLInputElement) {
		editInputRef.value.set(key, el)
	} else {
		editInputRef.value.delete(key)
	}
}

function startEditing(row: number, col: number, initialKey?: string) {
	const columnKey = columns.value[col]?.key
	if (!columnKey) { return }
	if (initialKey !== undefined && !isCellEditable(columnKey)) { return }

	editingCell.value = { row, col }
	editingOriginalValue.value = localRows.value[row][columnKey]?.toString() ?? ''

	if (initialKey !== undefined) {
		updateCell(row, columnKey, initialKey)
	}

	nextTick(() => {
		const input = editInputRef.value.get(`cell-${row}-${col}`)
		if (input) {
			input.focus()
			if (initialKey !== undefined) { input.value = initialKey }
		}
	})
}

function stopEditing(cancelled = false) {
	if (cancelled && editingCell.value && editingOriginalValue.value !== null) {
		const { row, col } = editingCell.value
		const columnKey = columns.value[col]?.key
		if (columnKey) { localRows.value[row][columnKey] = editingOriginalValue.value }
	}
	editingCell.value = null
	editingOriginalValue.value = null
}

function handleStopEditingAndBlur(cancelled = false) {
	nextTick(() => {
		stopEditing(cancelled)
		nextTick(() => {
			(document.querySelector('.spreadsheet') as HTMLElement)?.focus()
		})
	})
}

function updateCell(rowIndex: number, key: string, newValue: string | boolean) {
	const column = columns.value.find(col => col.key === key)
	if (column?.locked) { return }
	saveState('cell')
	const parsed = parseValueForFormat(newValue, column?.format)
	localRows.value[rowIndex][key] = parsed ?? column?.defaultValue ?? (column?.format === 'boolean' ? false : '')
	emitUpdate()
}

// ─── Search ───────────────────────────────────────────────────────────────────

const search = ref('')
const searchInputRef = ref<any>(null)

function isSearchFocused(): boolean {
	const input = searchInputRef.value?.$el?.querySelector('input')
	return document.activeElement === input
}

const filteredRows = computed(() => {
	if (!search.value) { return localRows.value }
	const term = search.value.toLowerCase()
	return localRows.value.filter(row => Object.values(row).some(v => v != null && String(v).toLowerCase().includes(term))
	)
})

// ─── Row management ───────────────────────────────────────────────────────────

function addRow() {
	if (!columns.value.length) { return }
	saveState('row')
	localRows.value.push(createEmptyRow())
	emitUpdate()
}

// ─── Keyboard ────────────────────────────────────────────────────────────────

function handleCellKeyDown(event: KeyboardEvent, row: number, col: number) {
	const isThisCell = editingCell.value?.row === row && editingCell.value?.col === col
	if (!isThisCell && ((event.key.length === 1 && !event.ctrlKey && !event.metaKey) || event.key === 'Enter')) {
		event.preventDefault()
		startEditing(row, col, event.key.length === 1 ? event.key : undefined)
	}
}

function handleSpreadsheetKeyDown(event: KeyboardEvent) {
	if (editingCell.value || isSearchFocused()) { return }
	const isCtrlOrCmd = event.ctrlKey || event.metaKey
	if (!isCtrlOrCmd) { return }

	switch (event.key.toLowerCase()) {
		case 'c': event.preventDefault(); copySelection(); break
		case 'v': event.preventDefault(); pasteSelection(); break
		case 'z':
			event.preventDefault()
			if (event.shiftKey) redo(); else undo()
			break
		case 'y': event.preventDefault(); redo(); break
	}
}
</script>

<template>
	<div class="w-100p" tabindex="-1" @keydown="handleSpreadsheetKeyDown">
		<div class="flex gap-05 py-05 justify-content-end m_flex-wrap spreadsheet-toolbar">
			<label v-if="label" class="label me-auto">{{ label }}</label>
			<div class="flex gap-075">
				<TextInput
					ref="searchInputRef" v-model="search" icon="search" placeholder="Search"
					class="m-0 max-w200px"
				/>
				<Dropdown flat thin icon="more_vert">
					<ListItem title="Paste" icon="paste" @click="pasteSelection" />
					<ListItem title="Copy" icon="copy" @click="copySelection" />
					<ListItem title="Undo" icon="undo" :disabled="!canUndo" @click="undo" />
					<ListItem title="Redo" icon="redo" :disabled="!canRedo" @click="redo" />
					<ToggleInput v-model="wrapText" :label="$t('spreadsheet.wrapText')" />
					<Dropdown placement="bottom-end">
						<template #trigger="{ show }">
							<ListItem title="Column Visibility" icon="view_column" @click="show()" />
						</template>
						<div class="p-05">
							<div class="flex space-between">
								<Btn
									flat thin small value="$t:spreadsheet.selectAll"
									@click="visibleColumns = columnOptions.map(col => col.key)"
								/>
								<Btn flat thin sm value="$t:spreadsheet.clearAll" @click="visibleColumns = []" />
							</div>
							<CheckInput
								v-for="col in columnOptions" :key="col.key" v-model="visibleColumns"
								:value="col.key" :label="col.label || col.key"
							/>
						</div>
					</Dropdown>
				</Dropdown>
			</div>
		</div>
		<div class="spreadsheet" @mouseup="handleMouseUp">
			<div class="flex w-100p relative">
				<SpreadsheetTable
					v-if="fixedColumns.length" :columns="fixedColumns" :rows="filteredRows"
					:is-fixed="true" :show-row-numbers="true" :column-widths="columnWidths"
					:sort-column="sortColumn" :sort-direction="sortDirection"
					:selection-start="selectionStart" :selection-end="selectionEnd"
					:editing-cell="editingCell" :base-column-index="0" :wrap-text="wrapText"
					class="sticky z-2 start-0 bg-white"
					@sortColumn="sortByColumn" @resizeStart="handleResizeStart"
					@selectRow="selectEntireRow" @cellMouseDown="handleMouseDown" @cellMouseOver="handleMouseOver"
					@cellEditStart="startEditing" @cellKeyDown="handleCellKeyDown" @updateCell="updateCell"
					@stopEditing="handleStopEditingAndBlur" @setInputRef="setInputRef"
				/>
				<div class="flex-shrink flex-grow">
					<SpreadsheetTable
						:columns="scrollableColumns" :rows="filteredRows"
						:is-fixed="false" :show-row-numbers="!fixedColumns.length" :column-widths="columnWidths"
						:empty-message="search ? `No results for '${search}'` : undefined"
						:sort-column="sortColumn" :sort-direction="sortDirection"
						:selection-start="selectionStart" :selection-end="selectionEnd"
						:editing-cell="editingCell" :base-column-index="fixedColumns.length" :wrap-text="wrapText"
						@sortColumn="sortByColumn" @resizeStart="handleResizeStart"
						@selectRow="selectEntireRow" @cellMouseDown="handleMouseDown" @cellMouseOver="handleMouseOver"
						@cellEditStart="startEditing" @cellKeyDown="handleCellKeyDown" @updateCell="updateCell"
						@stopEditing="handleStopEditingAndBlur" @setInputRef="setInputRef"
					/>
				</div>
			</div>
			<Btn v-if="allowAddRow" outline thin round icon="add" value="Add Row" class="mt-05" @click="addRow" />
		</div>
	</div>
</template>

<style>

.spreadsheet {
user-select: none;
outline: 1px solid var(--bgl-border-color);
}

.spreadsheet-toolbar {
border-bottom: 1px solid var(--bgl-border-color);
padding-bottom: 0.375rem;
margin-bottom: 0;
}

body>div:has(.spreadsheet) ::-webkit-scrollbar-track {
background: var(--bgl-input-bg) !important;
}

</style>
