Source: renderer/managers/EditorManager.js

/**
 * Editor Manager - Handles editor functionality like gutter, status bar, formatting
 */

// Import syntax highlighter
const { highlightCppSyntax, applySyntaxHighlight, shouldHighlight } = require('../utils/syntaxHighlighter');
const { detectFileType } = require('../utils/fileTypeUtils');

class EditorManager {
  constructor() {
    this.editor = document.getElementById('editor');
    this.gutter = document.getElementById('gutter');
    this.lineCounter = document.getElementById('lineCounter');
    this.currentFileType = 'Plain Text';
    this.highlightEnabled = false;
    this.updateTimer = null;
    this.isUpdating = false;
    this.init();
  }

  init() {
    if (!this.editor || !this.gutter || !this.lineCounter) return;

    // Maintain visual selection when editor loses focus
    this.setupPersistentSelection();

    // Make TAB key insert tab character in editor
    this.editor.addEventListener('keydown', (e) => {
      if (e.key === 'Tab') {
        e.preventDefault();
        document.execCommand('insertText', false, '\t');
      } else if (e.key === 'Enter') {
        e.preventDefault();
        document.execCommand('insertLineBreak');
      }
    });

    // Enhanced input handler with debounced syntax highlighting
    this.editor.addEventListener('input', (e) => {
      if (!this.isUpdating) {
        this.updateGutter();
        this.updateStatusBar();
        this.debouncedHighlightUpdate();
      }
    });

    this.editor.addEventListener('scroll', () => {
      this.syncScroll();
    });
    this.editor.addEventListener('click', () => {
      this.updateStatusBar();
      this.updateGutter();
    });
    this.editor.addEventListener('keyup', () => {
      this.updateStatusBar();
      this.updateGutter();
    });
    
    // Initialize
    this.updateGutter();
    this.updateStatusBar();
  }

  /**
   * Save current cursor position
   */
  saveCursorPosition() {
    const selection = window.getSelection();
    if (!selection.rangeCount) return null;
    
    const range = selection.getRangeAt(0);
    const preCaretRange = range.cloneRange();
    preCaretRange.selectNodeContents(this.editor);
    preCaretRange.setEnd(range.endContainer, range.endOffset);
    
    return preCaretRange.toString().length;
  }

  /**
   * Restore cursor position
   */
  restoreCursorPosition(offset) {
    if (offset === null || offset === undefined) return;
    
    const selection = window.getSelection();
    const range = document.createRange();
    
    let currentOffset = 0;
    const walker = document.createTreeWalker(
      this.editor,
      NodeFilter.SHOW_TEXT,
      null,
      false
    );
    
    let node;
    while (node = walker.nextNode()) {
      const nodeLength = node.textContent.length;
      if (currentOffset + nodeLength >= offset) {
        const targetOffset = offset - currentOffset;
        range.setStart(node, Math.min(targetOffset, node.textContent.length));
        range.collapse(true);
        selection.removeAllRanges();
        selection.addRange(range);
        return;
      }
      currentOffset += nodeLength;
    }
    
    // If we didn't find the position, place cursor at end
    if (this.editor.lastChild) {
      range.selectNodeContents(this.editor);
      range.collapse(false);
      selection.removeAllRanges();
      selection.addRange(range);
    }
  }

  /**
   * Update syntax highlighting with debouncing
   */
  debouncedHighlightUpdate() {
    if (this.updateTimer) {
      clearTimeout(this.updateTimer);
    }
    this.updateTimer = setTimeout(() => {
      this.updateSyntaxHighlight();
    }, 150);
  }

  /**
   * Update syntax highlighting directly in editor
   */
  updateSyntaxHighlight() {
    if (!this.editor || this.isUpdating) return;

    this.isUpdating = true;
    const savedOffset = this.saveCursorPosition();
    
    // Get plain text content
    const text = this.getContent();
    
    // Apply syntax highlighting if enabled
    if (this.highlightEnabled && shouldHighlight(this.currentFileType)) {
      const highlighted = applySyntaxHighlight(text, this.currentFileType);
      this.editor.innerHTML = highlighted;
    } else {
      this.editor.textContent = text;
    }
    
    // Restore cursor position
    this.restoreCursorPosition(savedOffset);
    this.isUpdating = false;
  }

  /**
   * Sync gutter scroll with editor
   */
  syncScroll() {
    if (this.gutter && this.editor) {
      this.gutter.scrollTop = this.editor.scrollTop;
    }
  }

  /**
   * Enhanced gutter line numbers with proper formatting (like VS Code)
   */
  updateGutter() {
    if (!this.editor || !this.gutter) return;
    
    const text = this.getContent();
    // Count lines properly - if text ends with newline, don't count it as extra line
    let lines = 1;
    if (text) {
      const splitLines = text.split('\n');
      lines = splitLines.length;
      // If last element is empty string (trailing newline), don't count it
      if (splitLines[splitLines.length - 1] === '') {
        lines = Math.max(1, lines - 1);
      }
    }
    
    // Get current line number for highlighting
    const currentLine = this.getCurrentLineNumber();
    
    const maxDigits = Math.max(2, lines.toString().length);
    
    // Clear gutter and rebuild with styled line numbers
    this.gutter.innerHTML = '';
    
    // Create line numbers with current line highlighted
    for (let i = 1; i <= lines; i++) {
      const lineSpan = document.createElement('div');
      lineSpan.textContent = i.toString().padStart(maxDigits, ' ');
      lineSpan.style.lineHeight = '20px';
      
      if (i === currentLine) {
        lineSpan.style.color = '#f0f6fc';
        lineSpan.style.fontWeight = 'bold';
        lineSpan.style.fontSize = '13px';
      } else {
        lineSpan.style.color = '#6e7681';
        lineSpan.style.fontSize = '12px';
      }
      
      this.gutter.appendChild(lineSpan);
    }
    
    // Update gutter width based on content
    const charWidth = 8; // Approximate character width in monospace font
    this.gutter.style.width = Math.max(60, (maxDigits + 2) * charWidth) + 'px';
  }
  
  /**
   * Get current line number where cursor is
   */
  getCurrentLineNumber() {
    if (!this.editor) return 1;
    
    const selection = window.getSelection();
    if (!selection.rangeCount) return 1;
    
    const range = selection.getRangeAt(0);
    const preCaretRange = range.cloneRange();
    preCaretRange.selectNodeContents(this.editor);
    preCaretRange.setEnd(range.endContainer, range.endOffset);
    
    const textBeforeCursor = preCaretRange.toString();
    const lines = textBeforeCursor.split('\n');
    return lines.length;
  }

  /**
   * Enhanced status bar with cursor position tracking
   */
  updateStatusBar() {
    if (!this.editor || !this.lineCounter) return;
    
    const selection = window.getSelection();
    if (!selection.rangeCount) return;
    
    const range = selection.getRangeAt(0);
    const preCaretRange = range.cloneRange();
    preCaretRange.selectNodeContents(this.editor);
    preCaretRange.setEnd(range.endContainer, range.endOffset);
    
    const textBeforeCursor = preCaretRange.toString();
    const lines = textBeforeCursor.split('\n');
    const line = lines.length;
    const col = lines[lines.length - 1].length + 1;
    
    if (range.toString().length > 0) {
      const selectedText = range.toString();
      const selectedLines = selectedText.split('\n').length;
      this.lineCounter.textContent = `Ln ${line}, Col ${col} (${selectedText.length} chars, ${selectedLines} lines selected)`;
    } else {
      this.lineCounter.textContent = `Ln ${line}, Col ${col}`;
    }
  }

  /**
   * Jump to specific line in editor
   * @param {number} lineNumber - Line number to jump to
   */
  jumpToLine(lineNumber) {
    console.log('Jumping to line:', lineNumber);
    
    if (!this.editor) {
      console.error('Editor not found');
      return;
    }
    
    const text = this.getContent();
    const lines = text.split('\n');
    console.log('Total lines in editor:', lines.length);
    
    if (lineNumber > lines.length) {
      console.warn('Line number exceeds file length');
      return;
    }
    
    // Calculate character position
    let position = 0;
    for (let i = 0; i < lineNumber - 1 && i < lines.length; i++) {
      position += lines[i].length + 1; // +1 for newline
    }
    
    console.log('Calculated position:', position);
    
    // Create range and set cursor
    const range = document.createRange();
    const sel = window.getSelection();
    
    let currentPos = 0;
    let targetNode = null;
    let targetOffset = 0;
    
    // Find the text node and offset
    const walker = document.createTreeWalker(
      this.editor,
      NodeFilter.SHOW_TEXT,
      null,
      false
    );
    
    let node;
    while (node = walker.nextNode()) {
      const nodeLength = node.textContent.length;
      if (currentPos + nodeLength >= position) {
        targetNode = node;
        targetOffset = position - currentPos;
        break;
      }
      currentPos += nodeLength;
    }
    
    if (targetNode) {
      range.setStart(targetNode, targetOffset);
      range.collapse(true);
      sel.removeAllRanges();
      sel.addRange(range);
      
      // Scroll into view
      const lineHeight = 20;
      const targetScrollTop = Math.max(0, (lineNumber - 10) * lineHeight);
      this.editor.scrollTop = targetScrollTop;
    }
    
    this.editor.focus();
    this.updateStatusBar();
    
    console.log('Successfully jumped to line:', lineNumber);
  }

  /**
   * Enhanced formatting
   */
  formatCode() {
    if (!this.editor) return;
    
    const text = this.getContent();
    const lines = text.split('\n');
    let indentLevel = 0;
    const formatted = lines.map(line => {
      const trimmed = line.trim();
      if (trimmed.endsWith('{') || trimmed.endsWith(':')) {
        const result = '  '.repeat(indentLevel) + trimmed;
        indentLevel++;
        return result;
      } else if (trimmed.startsWith('}')) {
        indentLevel = Math.max(0, indentLevel - 1);
        return '  '.repeat(indentLevel) + trimmed;
      } else {
        return '  '.repeat(indentLevel) + trimmed;
      }
    }).join('\n');
    
    this.setContent(formatted);
    
    return formatted;
  }

  /**
   * Toggle word wrap
   */
  toggleWordWrap() {
    if (!this.editor) return;
    
    this.editor.style.whiteSpace = this.editor.style.whiteSpace === 'pre-wrap' ? 'pre' : 'pre-wrap';
  }

  /**
   * Get editor content
   * @returns {string} - Current editor content
   */
  getContent() {
    if (!this.editor) return '';
    // Replace <br> tags with newlines and get text content
    return this.editor.textContent || '';
  }

  /**
   * Set editor content
   * @param {string} content - Content to set
   */
  setContent(content) {
    if (this.editor) {
      this.editor.textContent = content;
      this.updateGutter();
      this.updateStatusBar();
      this.updateSyntaxHighlight();
    }
  }

  /**
   * Set file type (placeholder for future use)
   * @param {string} filename - The filename to detect type from
   */
  setFileType(filename) {
    this.currentFileType = detectFileType(filename);
    
    // Enable syntax highlighting for C/C++ files
    this.highlightEnabled = shouldHighlight(this.currentFileType);
    
    // Update highlighting immediately
    this.updateSyntaxHighlight();
  }

  /**
   * Setup persistent selection highlighting (like VS Code)
   * Keeps selection visually highlighted even when editor loses focus
   */
  setupPersistentSelection() {
    // Create a canvas to measure text and draw highlights
    const editorArea = document.getElementById('editor-area');
    if (!editorArea) return;
    
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    ctx.font = '12px JetBrains Mono, monospace';
    
    // Create highlight overlay that will contain absolute positioned divs
    const highlightOverlay = document.createElement('div');
    highlightOverlay.id = 'selection-highlight-overlay';
    highlightOverlay.style.cssText = `
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      pointer-events: none;
      z-index: 0;
      overflow: hidden;
    `;
    
    // Insert overlay after gutter but before editor
    const gutter = document.getElementById('gutter');
    if (gutter && gutter.nextSibling) {
      editorArea.insertBefore(highlightOverlay, gutter.nextSibling);
    } else {
      editorArea.appendChild(highlightOverlay);
    }
    
    // Make editor background transparent so overlay shows through
    this.editor.style.position = 'relative';
    this.editor.style.zIndex = '1';
    this.editor.style.background = 'transparent';
    
    let savedSelection = null;
    
    const updateHighlight = () => {
      highlightOverlay.innerHTML = '';
      
      if (!savedSelection || !this.editor.value) {
        return;
      }
      
      const { start, end } = savedSelection;
      const text = this.editor.value;
      const lines = text.split('\n');
      
      // Get editor dimensions and position
      const editorRect = this.editor.getBoundingClientRect();
      const editorAreaRect = editorArea.getBoundingClientRect();
      const gutterWidth = 61; // gutter width + border
      const lineHeight = 20;
      const paddingLeft = 20;
      const paddingTop = 20;
      
      // Calculate which lines contain the selection
      let charCount = 0;
      let startLine = -1, startCol = -1;
      let endLine = -1, endCol = -1;
      
      for (let i = 0; i < lines.length; i++) {
        const lineLength = lines[i].length;
        
        if (startLine === -1 && charCount + lineLength >= start) {
          startLine = i;
          startCol = start - charCount;
        }
        
        if (endLine === -1 && charCount + lineLength >= end) {
          endLine = i;
          endCol = end - charCount;
          break;
        }
        
        charCount += lineLength + 1; // +1 for newline
      }
      
      if (startLine === -1 || endLine === -1) return;
      
      // Draw highlight rectangles for each line in selection
      for (let line = startLine; line <= endLine; line++) {
        const lineText = lines[line];
        let colStart = (line === startLine) ? startCol : 0;
        let colEnd = (line === endLine) ? endCol : lineText.length;
        
        // Measure text width to get exact position
        const beforeText = lineText.substring(0, colStart);
        const selectedText = lineText.substring(colStart, colEnd);
        
        const startX = ctx.measureText(beforeText).width;
        const width = ctx.measureText(selectedText).width;
        
        // Create highlight div for this line
        const highlight = document.createElement('div');
        highlight.style.cssText = `
          position: absolute;
          left: ${gutterWidth + paddingLeft + startX}px;
          top: ${paddingTop + (line * lineHeight) - this.editor.scrollTop}px;
          width: ${width + 2}px;
          height: ${lineHeight}px;
          background: #3a3d41;
          pointer-events: none;
        `;
        
        highlightOverlay.appendChild(highlight);
      }
    };
    
    // Save selection when editor loses focus
    this.editor.addEventListener('blur', () => {
      const start = this.editor.selectionStart;
      const end = this.editor.selectionEnd;
      if (start !== end) {
        savedSelection = { start, end };
        updateHighlight();
      }
    });
    
    // Clear highlight when editor gains focus
    this.editor.addEventListener('focus', () => {
      if (savedSelection) {
        // Restore the selection
        this.editor.setSelectionRange(savedSelection.start, savedSelection.end);
      }
      savedSelection = null;
      highlightOverlay.innerHTML = '';
    });
    
    // Update highlight position on scroll
    this.editor.addEventListener('scroll', () => {
      if (savedSelection) {
        updateHighlight();
      }
    });
    
    // Clear on text change
    this.editor.addEventListener('input', () => {
      if (savedSelection) {
        savedSelection = null;
        highlightOverlay.innerHTML = '';
      }
    });
  }
  
  escapeHtml(text) {
    return text
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#039;');
  }

  /**
   * Focus the editor
   */
  focus() {
    if (this.editor) {
      this.editor.focus();
    }
  }
}

module.exports = EditorManager;