Source: main/ipc/fileHandlers.js

/**
 * @fileoverview IPC handlers for file operations in the main process.
 * 
 * This module provides all IPC handlers for file system operations including
 * opening files/folders, saving files, reading file content, and managing
 * file system watching for automatic UI updates.
 * 
 * @author CTrace GUI Team
 * @version 1.0.0
 */

const { ipcMain, dialog } = require('electron');
const fs = require('fs').promises;
const path = require('path');
const chokidar = require('chokidar');
const { detectFileEncoding, buildFileTree, searchInDirectory, FILE_SIZE_LIMIT } = require('../utils/fileUtils');

/**
 * File watcher instance for monitoring workspace changes
 * @type {chokidar.FSWatcher|null}
 * @private
 */
let fileWatcher = null;

/**
 * Currently watched workspace path
 * @type {string|null}
 * @private
 */
let currentWatchPath = null;

/**
 * Sets up all IPC handlers for file operations.
 * 
 * This function registers all IPC handlers that the renderer process can invoke
 * for file operations. It handles folder dialogs, file dialogs, saving files,
 * reading files, and file tree operations.
 * 
 * @function setupFileHandlers
 * @param {BrowserWindow} mainWindow - Main window reference for dialogs
 * 
 * @example
 * setupFileHandlers(mainWindow);
 */
function setupFileHandlers(mainWindow) {
  // Open folder dialog
  ipcMain.handle('open-folder-dialog', async () => {
    const result = await dialog.showOpenDialog(mainWindow, {
      properties: ['openDirectory']
    });
    
    if (!result.canceled && result.filePaths.length > 0) {
      const folderPath = result.filePaths[0];
      try {
        const fileTree = await buildFileTree(folderPath);
        
        // Start watching the workspace for changes
        startWatchingWorkspace(folderPath, mainWindow);
        
        return {
          success: true,
          folderPath,
          fileTree
        };
      } catch (error) {
        return {
          success: false,
          error: error.message
        };
      }
    }
    
    return { success: false, canceled: true };
  });

  // Get file tree for refresh
  ipcMain.handle('get-file-tree', async (event, folderPath) => {
    try {
      const fileTree = await buildFileTree(folderPath);
      return {
        success: true,
        fileTree
      };
    } catch (error) {
      return {
        success: false,
        error: error.message
      };
    }
  });

  // Open file dialog
  ipcMain.handle('open-file-dialog', async () => {
    const result = await dialog.showOpenDialog(mainWindow, {
      properties: ['openFile'],
      filters: [
        { name: 'All Files', extensions: ['*'] },
        { name: 'Text Files', extensions: ['txt', 'md', 'json', 'js', 'ts', 'html', 'css', 'py', 'cpp', 'c', 'h'] }
      ]
    });
    
    if (!result.canceled && result.filePaths.length > 0) {
      const filePath = result.filePaths[0];
      try {
        const fileInfo = await detectFileEncoding(filePath);
        
        if (!fileInfo.isUTF8) {
          // File is not UTF-8, return warning info
          return {
            success: true,
            warning: 'encoding',
            filePath,
            fileName: path.basename(filePath),
            message: 'This file appears to contain non-UTF8 characters. Opening it may cause display issues or data corruption.'
          };
        }
        
        let content;
        let isPartial = false;
        
        if (fileInfo.size > FILE_SIZE_LIMIT) {
          // File is large, load only first part
          const partialBuffer = fileInfo.buffer.slice(0, FILE_SIZE_LIMIT);
          content = partialBuffer.toString('utf8');
          isPartial = true;
        } else {
          // File is small enough, load entirely
          content = fileInfo.buffer.toString('utf8');
        }
        
        return {
          success: true,
          filePath,
          content,
          fileName: path.basename(filePath),
          isPartial,
          totalSize: fileInfo.size,
          loadedSize: isPartial ? FILE_SIZE_LIMIT : fileInfo.size
        };
      } catch (error) {
        return {
          success: false,
          error: error.message
        };
      }
    }
    
    return { success: false, canceled: true };
  });

  // Select local LLM (GGUF) model file
  ipcMain.handle('select-llm-file', async () => {
    const result = await dialog.showOpenDialog(mainWindow, {
      properties: ['openFile'],
      filters: [
        { name: 'GGUF / Model Files', extensions: ['gguf', 'bin', 'pt', 'ggml', 'onnx'] },
        { name: 'All Files', extensions: ['*'] }
      ]
    });

    if (!result.canceled && result.filePaths.length > 0) {
      return { success: true, filePath: result.filePaths[0] };
    }

    return { success: false, canceled: true };
  });

  // Save file
  ipcMain.handle('save-file', async (event, filePath, content) => {
    try {
      await fs.writeFile(filePath, content, 'utf8');
      return { success: true };
    } catch (error) {
      return { success: false, error: error.message };
    }
  });

  // Save file as
  ipcMain.handle('save-file-as', async (event, content) => {
    const result = await dialog.showSaveDialog(mainWindow, {
      filters: [
        { name: 'All Files', extensions: ['*'] },
        { name: 'Text Files', extensions: ['txt'] },
        { name: 'JavaScript', extensions: ['js'] },
        { name: 'TypeScript', extensions: ['ts'] },
        { name: 'HTML', extensions: ['html'] },
        { name: 'CSS', extensions: ['css'] }
      ]
    });
    
    if (!result.canceled) {
      try {
        await fs.writeFile(result.filePath, content, 'utf8');
        return {
          success: true,
          filePath: result.filePath,
          fileName: path.basename(result.filePath)
        };
      } catch (error) {
        return { success: false, error: error.message };
      }
    }
    
    return { success: false, canceled: true };
  });

  // Read file content
  ipcMain.handle('read-file', async (event, filePath) => {
    try {
      const fileInfo = await detectFileEncoding(filePath);
      
      if (!fileInfo.isUTF8) {
        // File is not UTF-8, return warning info
        return {
          success: true,
          warning: 'encoding',
          filePath,
          fileName: path.basename(filePath),
          message: 'This file appears to contain non-UTF8 characters. Opening it may cause display issues or data corruption.'
        };
      }
      
      let content;
      let isPartial = false;
      
      if (fileInfo.size > FILE_SIZE_LIMIT) {
        // File is large, load only first part
        const partialBuffer = fileInfo.buffer.slice(0, FILE_SIZE_LIMIT);
        content = partialBuffer.toString('utf8');
        isPartial = true;
      } else {
        // File is small enough, load entirely
        content = fileInfo.buffer.toString('utf8');
      }
      
      return {
        success: true,
        content,
        fileName: path.basename(filePath),
        isPartial,
        totalSize: fileInfo.size,
        loadedSize: isPartial ? FILE_SIZE_LIMIT : fileInfo.size
      };
    } catch (error) {
      return { success: false, error: error.message };
    }
  });

  // Load complete file (for large files that were partially loaded)
  ipcMain.handle('load-complete-file', async (event, filePath) => {
    try {
      const fileInfo = await detectFileEncoding(filePath);
      
      if (!fileInfo.isUTF8) {
        return {
          success: false,
          error: 'File contains non-UTF8 characters and cannot be safely loaded.'
        };
      }
      
      const content = fileInfo.buffer.toString('utf8');
      
      return {
        success: true,
        content,
        fileName: path.basename(filePath),
        isPartial: false,
        totalSize: fileInfo.size,
        loadedSize: fileInfo.size
      };
    } catch (error) {
      return { success: false, error: error.message };
    }
  });

  // Force open file (ignore encoding warnings)
  ipcMain.handle('force-open-file', async (event, filePath) => {
    try {
      const fileInfo = await detectFileEncoding(filePath);
      
      let content;
      let isPartial = false;
      
      // Try to read as UTF-8, may have some garbled characters
      if (fileInfo.size > FILE_SIZE_LIMIT) {
        const partialBuffer = fileInfo.buffer.slice(0, FILE_SIZE_LIMIT);
        try {
          content = partialBuffer.toString('utf8');
        } catch (error) {
          // If UTF-8 fails, try latin1 as fallback
          content = partialBuffer.toString('latin1');
        }
        isPartial = true;
      } else {
        try {
          content = fileInfo.buffer.toString('utf8');
        } catch (error) {
          // If UTF-8 fails, try latin1 as fallback
          content = fileInfo.buffer.toString('latin1');
        }
      }
      
      return {
        success: true,
        content,
        fileName: path.basename(filePath),
        isPartial,
        totalSize: fileInfo.size,
        loadedSize: isPartial ? FILE_SIZE_LIMIT : fileInfo.size,
        encodingWarning: !fileInfo.isUTF8
      };
    } catch (error) {
      return { success: false, error: error.message };
    }
  });

  // Search in files
  ipcMain.handle('search-in-files', async (event, searchTerm, folderPath) => {
    try {
      const results = await searchInDirectory(folderPath, searchTerm);
      return { success: true, results };
    } catch (error) {
      return { success: false, error: error.message };
    }
  });

  // Handler to force load full file (bypass size limits)
  ipcMain.handle('force-load-full-file', async (event, filePath) => {
    try {
      const buffer = await fs.readFile(filePath);
      const content = buffer.toString('utf8');
      
      return {
        success: true,
        content,
        totalSize: buffer.length,
        fileName: path.basename(filePath)
      };
    } catch (error) {
      console.error('Error force loading full file:', error);
      return {
        success: false,
        error: error.message
      };
    }
  });
}

/**
 * Start watching workspace for file changes
 * @param {string} workspacePath - Path to watch
 * @param {BrowserWindow} mainWindow - Main window reference
 */
function startWatchingWorkspace(workspacePath, mainWindow) {
  // Stop existing watcher if any
  stopWatchingWorkspace();
  
  currentWatchPath = workspacePath;
  
  // Create new watcher
  fileWatcher = chokidar.watch(workspacePath, {
    ignoreInitial: true,
    ignored: [
      /(^|[\/\\])\../, // ignore hidden files
      /node_modules/,  // ignore node_modules
      /My Music/,      // ignore Windows system folders
      /My Pictures/,
      /My Videos/,
      /\$RECYCLE\.BIN/,
      /System Volume Information/
    ],
    depth: 10, // limit recursion depth
    ignorePermissionErrors: true // ignore permission errors
  });
  
  // Debounce function to avoid too many updates
  let updateTimeout;
  const debouncedUpdate = () => {
    clearTimeout(updateTimeout);
    updateTimeout = setTimeout(async () => {
      try {
        const fileTree = await buildFileTree(workspacePath);
        mainWindow.webContents.send('workspace-changed', {
          success: true,
          fileTree,
          folderPath: workspacePath
        });
      } catch (error) {
        console.error('Error updating file tree:', error);
      }
    }, 300); // 300ms debounce
  };
  
  // Listen for file system events
  fileWatcher
    .on('add', debouncedUpdate)
    .on('unlink', debouncedUpdate)
    .on('addDir', debouncedUpdate)
    .on('unlinkDir', debouncedUpdate)
    .on('error', error => console.error('File watcher error:', error));
  
  console.log('Started watching workspace:', workspacePath);
}

/**
 * Stop watching current workspace
 */
function stopWatchingWorkspace() {
  if (fileWatcher) {
    fileWatcher.close();
    fileWatcher = null;
    console.log('Stopped watching workspace:', currentWatchPath);
  }
  currentWatchPath = null;
}

module.exports = { setupFileHandlers };