import { useCallback, useEffect, useRef, useState } from 'react' import { CellUtils, CellValue, Workbook, } from '../feature/excelEngine/excelEngine' interface CellData { value: string isSelected: boolean } interface UseSpreadsheetEngineProps { rows?: number cols?: number sheetName?: string initialData?: string[][] } export function useSpreadsheetEngine({ rows = 50, cols = 20, sheetName = 'Sheet1', initialData, }: UseSpreadsheetEngineProps = {}) { const workbookRef = useRef(null) const [cells, setCells] = useState(() => { return Array(rows) .fill(null) .map(() => Array(cols) .fill(null) .map(() => ({ value: '', isSelected: false })) ) }) const [isCalculating, setIsCalculating] = useState(false) // Инициализация workbook useEffect(() => { if (!workbookRef.current) { workbookRef.current = new Workbook() workbookRef.current.addSheet(sheetName) // addCustomFunctions(workbookRef.current) } // Загружаем начальные данные если они есть if (initialData) { loadDataFromArray(initialData) } }, [sheetName, initialData]) // Конвертация данных из движка в React состояние const syncFromEngine = useCallback(() => { if (!workbookRef.current) return const sheet = workbookRef.current.getSheet(sheetName) if (!sheet) return const newCells = Array(rows) .fill(null) .map(() => Array(cols) .fill(null) .map(() => ({ value: '', isSelected: false })) ) // Получаем все ячейки из движка for (let row = 0; row < rows; row++) { for (let col = 0; col < cols; col++) { const cellName = CellUtils.createCellName(col, row + 1) const cell = sheet.getCell(cellName) // Показываем сырое значение (raw) в UI, а не вычисленное if (cell.raw !== null && cell.raw !== undefined) { newCells[row][col].value = String(cell.raw) } } } setCells(newCells) }, [rows, cols, sheetName]) // Установка значения ячейки const setCellValue = useCallback( (row: number, col: number, value: string) => { if (!workbookRef.current) return const cellName = CellUtils.createCellName(col, row + 1) const qualifiedName = `${sheetName}!${cellName}` setIsCalculating(true) try { // Конвертируем пустую строку в null const cellValue: CellValue = value === '' ? null : value // Если значение начинается с =, то это формула if (typeof cellValue === 'string' && cellValue.startsWith('=')) { workbookRef.current.setCell(qualifiedName, cellValue) } else { // Пытаемся конвертировать в число, если возможно const numValue = Number(cellValue) if (!isNaN(numValue) && cellValue !== null && cellValue !== '') { workbookRef.current.setCell(qualifiedName, numValue) } else { workbookRef.current.setCell(qualifiedName, cellValue) } } // Пересчитываем все формулы workbookRef.current.recalc() // Синхронизируем с React состоянием syncFromEngine() } catch (error) { console.error('Error setting cell value:', error) } finally { setIsCalculating(false) } }, [sheetName, syncFromEngine] ) // Получение вычисленного значения ячейки const getCellValue = useCallback( (row: number, col: number): CellValue => { if (!workbookRef.current) return null const cellName = CellUtils.createCellName(col, row + 1) const qualifiedName = `${sheetName}!${cellName}` try { return workbookRef.current.getValue(qualifiedName) } catch (error) { console.error('Error getting cell value:', error) return '#ERROR!' } }, [sheetName] ) // Получение сырого значения ячейки (для отображения в строке формул) const getCellRawValue = useCallback( (row: number, col: number): string => { if (!workbookRef.current) return '' const cellName = CellUtils.createCellName(col, row + 1) const sheet = workbookRef.current.getSheet(sheetName) if (!sheet) return '' const cell = sheet.getCell(cellName) return cell.raw !== null && cell.raw !== undefined ? String(cell.raw) : '' }, [sheetName] ) // Загрузка данных из массива const loadDataFromArray = useCallback( (data: string[][]) => { if (!workbookRef.current) return const sheet = workbookRef.current.getSheet(sheetName) if (!sheet) return setIsCalculating(true) try { // Очищаем лист sheet.cells.clear() // Загружаем данные for (let row = 0; row < data.length && row < rows; row++) { for (let col = 0; col < data[row].length && col < cols; col++) { const value = data[row][col] if (value !== null && value !== undefined && value !== '') { const cellName = CellUtils.createCellName(col, row + 1) sheet.setCell(cellName, value) } } } // Пересчитываем все формулы workbookRef.current.recalc() // Синхронизируем с React состоянием syncFromEngine() } catch (error) { console.error('Error loading data:', error) } finally { setIsCalculating(false) } }, [rows, cols, sheetName, syncFromEngine] ) // Экспорт данных в массив (для сохранения) const exportToArray = useCallback((): string[][] => { if (!workbookRef.current) return [] const sheet = workbookRef.current.getSheet(sheetName) if (!sheet) return [] const result: string[][] = Array(rows) .fill(null) .map(() => Array(cols).fill('')) for (let row = 0; row < rows; row++) { for (let col = 0; col < cols; col++) { const cellName = CellUtils.createCellName(col, row + 1) const cell = sheet.getCell(cellName) if (cell.raw !== null && cell.raw !== undefined) { result[row][col] = String(cell.raw) } } } return result }, [rows, cols, sheetName]) // Экспорт в JSON для бэкенда const exportToJSON = useCallback((): string => { if (!workbookRef.current) return '{}' return JSON.stringify(workbookRef.current.toJSON()) }, []) // Импорт из JSON с бэкенда const importFromJSON = useCallback( (jsonData: string) => { if (!workbookRef.current) return setIsCalculating(true) try { const data = JSON.parse(jsonData) workbookRef.current.fromJSON(data) // addCustomFunctions(workbookRef.current) workbookRef.current.recalc() syncFromEngine() } catch (error) { console.error('Error importing from JSON:', error) } finally { setIsCalculating(false) } }, [syncFromEngine] ) // Добавление пользовательской функции const addFunction = useCallback( (name: string, func: (...args: any[]) => any) => { if (!workbookRef.current) return workbookRef.current.addFunction(name, func) }, [] ) // Пересчёт всех формул const recalculate = useCallback(() => { if (!workbookRef.current) return setIsCalculating(true) try { workbookRef.current.recalc() syncFromEngine() } catch (error) { console.error('Error recalculating:', error) } finally { setIsCalculating(false) } }, [syncFromEngine]) // Получение информации о зависимостях ячейки const getCellDependencies = useCallback( (row: number, col: number) => { if (!workbookRef.current) return { precedents: [], dependents: [] } const cellName = CellUtils.createCellName(col, row + 1) const sheet = workbookRef.current.getSheet(sheetName) if (!sheet) return { precedents: [], dependents: [] } const cell = sheet.getCell(cellName) return { precedents: Array.from(cell.precedents), dependents: Array.from(cell.dependents), } }, [sheetName] ) // Получение статистики workbook const getWorkbookStats = useCallback(() => { if (!workbookRef.current) return { sheets: 0, cells: 0, formulas: 0 } let totalCells = 0 let totalFormulas = 0 for (const sheet of workbookRef.current.sheets.values()) { totalCells += sheet.cells.size for (const cell of sheet.cells.values()) { if (typeof cell.raw === 'string' && cell.raw.startsWith('=')) { totalFormulas++ } } } return { sheets: workbookRef.current.sheets.size, cells: totalCells, formulas: totalFormulas, } }, []) return { cells, setCellValue, getCellValue, getCellRawValue, loadDataFromArray, exportToArray, exportToJSON, importFromJSON, addFunction, recalculate, getCellDependencies, getWorkbookStats, isCalculating, workbook: workbookRef.current, } }