import { useState, useCallback, useEffect } from 'react'; import type { BoardManager } from '../useBoard/types'; import type { CursorManager, CursorMode, Coord, VimMotionSystem, GameKeyManager, KeyLogEntry } from './types'; import { useVimMotions } from './useVimMotions'; export function useCursor(board: BoardManager): CursorManager & { keyManager: GameKeyManager } { const [position, setPosition] = useState({ row: 0, col: 0 }); const [mode, setMode] = useState('normal'); const [keyLog, setKeyLog] = useState([]); const vimMotions = useVimMotions(); const addKeyToLog = useCallback((key: string) => { setKeyLog(prev => [...prev, { key, timestamp: Date.now() }]); }, []); const move = useCallback((dRows: number, dCols: number, count: number) => { setPosition(prev => { const newRow = prev.row + (dRows * count); const newCol = prev.col + (dCols * count); if (newRow < 0 || newRow > 8 || newCol < 0 || newCol > 8) { flashInvalid(); return prev; } return { row: newRow, col: newCol }; }); }, []); const moveLeft = useCallback((count = 1) => { move(0, -1, count); }, [move]); const moveRight = useCallback((count = 1) => { move(0, 1, count); }, [move]); const moveUp = useCallback((count = 1) => { move(-1, 0, count); }, [move]); const moveDown = useCallback((count = 1) => { move(1, 0, count); }, [move]); const moveToStart = useCallback(() => { setPosition(prev => ({ ...prev, col: 0 })); }, []); const moveToEnd = useCallback(() => { setPosition(prev => ({ ...prev, col: 8 })); }, []); const moveToTop = useCallback(() => { setPosition({ row: 0, col: 0 }); }, []); const moveToBottom = useCallback(() => { setPosition({ row: 8, col: 0 }); }, []); const repeatLastMotion = useCallback(() => { const lastMotion = vimMotions.repeatLastMotion(); if (lastMotion) { move(lastMotion.dr, lastMotion.dc, lastMotion.steps); } }, [vimMotions, move]); const resetCount = useCallback(() => { vimMotions.resetCount(); }, [vimMotions]); const getCount = useCallback(() => { return vimMotions.getCount().toString(); }, [vimMotions]); const hasCount = useCallback(() => { return vimMotions.hasCount(); }, [vimMotions]); const setCount = useCallback((digit: string) => { vimMotions.processKey(digit); }, [vimMotions]); const setLastKey = useCallback((key: string) => { addKeyToLog(key); }, [addKeyToLog]); const getLastKey = useCallback(() => { return keyLog[keyLog.length - 1]?.key || ''; }, [keyLog]); const flashInvalid = useCallback(() => { const container = board.containerRef.current; if (!container) return; const cellEl = container.querySelector(`.sudoku-cell[data-row="${position.row}"][data-col="${position.col}"]`) as HTMLElement; if (cellEl) { cellEl.classList.add('invalid-move'); setTimeout(() => cellEl.classList.remove('invalid-move'), 180); } }, [board, position]); const flashValid = useCallback(() => { const container = board.containerRef.current; if (!container) return; const cellEl = container.querySelector(`.sudoku-cell[data-row="${position.row}"][data-col="${position.col}"]`) as HTMLElement; if (cellEl) { cellEl.classList.add('valid-move'); setTimeout(() => cellEl.classList.remove('valid-move'), 300); } }, [board, position]); const flashLocked = useCallback(() => { const container = board.containerRef.current; if (!container) return; const cellEl = container.querySelector(`.sudoku-cell[data-row="${position.row}"][data-col="${position.col}"]`) as HTMLElement; if (cellEl) { cellEl.classList.add('locked-cell'); setTimeout(() => cellEl.classList.remove('locked-cell'), 300); } }, [board, position]); const highlightConflicts = useCallback((conflictIndices: number[]) => { const container = board.containerRef.current; if (!container) return; conflictIndices.forEach((idx) => { const row = Math.floor(idx / 9); const col = idx % 9; const cellEl = container.querySelector(`.sudoku-cell[data-row="${row}"][data-col="${col}"]`) as HTMLElement; if (cellEl) { cellEl.classList.add('conflict'); } }); setTimeout(() => { conflictIndices.forEach((idx) => { const row = Math.floor(idx / 9); const col = idx % 9; const cellEl = container.querySelector(`.sudoku-cell[data-row="${row}"][data-col="${col}"]`) as HTMLElement; if (cellEl) { cellEl.classList.remove('conflict'); } }); }, 2000); }, [board]); const clearKeyLog = useCallback(() => { setKeyLog([]); }, []); const getKeyLog = useCallback(() => { return [...keyLog]; }, [keyLog]); useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { const key = e.key; const result = vimMotions.processKey(key); if (!result) return; addKeyToLog(key); switch (result.type) { case 'move': move(result.data.dr, result.data.dc, result.data.steps); break; case 'anchor': switch (result.data) { case 'start': moveToStart(); break; case 'end': moveToEnd(); break; case 'top': moveToTop(); break; case 'bottom': moveToBottom(); break; } break; case 'repeat': if (result.data) { move(result.data.dr, result.data.dc, result.data.steps); } break; case 'mode': setMode(result.data); break; case 'digit': if (mode === 'edit') { const success = board.setCell(position.row, position.col, result.data); if (success) { flashValid(); } else if (!board.canEditCell(position.row, position.col)) { flashLocked(); } else { const conflicts = board.getConflicts(position.row, position.col); if (conflicts.length > 0) { highlightConflicts(conflicts); } flashInvalid(); } } break; case 'erase': if (mode === 'edit') { const success = board.setCell(position.row, position.col, '.'); if (success) { flashValid(); } else if (!board.canEditCell(position.row, position.col)) { flashLocked(); } else { flashInvalid(); } } break; } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [vimMotions, addKeyToLog, move, moveToStart, moveToEnd, moveToTop, moveToBottom, mode, board, position, flashValid, flashLocked, flashInvalid, highlightConflicts]); const cursorManager: CursorManager = { position: () => position, mode: () => mode, move, moveLeft, moveRight, moveUp, moveDown, moveToStart, moveToEnd, moveToTop, moveToBottom, repeatLastMotion, resetCount, getCount, hasCount, setCount, setLastKey, getLastKey, setMode, flashInvalid, flashValid, flashLocked, highlightConflicts }; const keyManager: GameKeyManager = { keyLog, clearKeyLog, getKeyLog, editMode: mode, setEditMode: setMode }; return { ...cursorManager, keyManager }; }