Source: renderer/managers/SearchManager.js

/**
 * Search Manager - Handles search functionality
 */
class SearchManager {
  constructor(editorManager, notificationManager) {
    this.editorManager = editorManager;
    this.notificationManager = notificationManager;
    this.currentSearchMatches = [];
    this.currentMatchIndex = -1;
    this.searchTimeout = null;
    this.init();
  }

  init() {
    this.setupSearchWidget();
    this.setupGoToLineDialog();
    this.setupSidebarSearch();
  }

  /**
   * Setup search widget functionality
   */
  setupSearchWidget() {
    const widgetSearchInput = document.getElementById('widget-search-input');
    if (widgetSearchInput) {
      widgetSearchInput.addEventListener('input', (e) => {
        this.performLiveSearch(e.target.value);
      });
    }
  }

  /**
   * Setup go to line dialog
   */
  setupGoToLineDialog() {
    // Event handlers will be attached by the UI controller
  }

  /**
   * Setup sidebar search
   */
  setupSidebarSearch() {
    const searchInput = document.getElementById('sidebar-search-input');
    if (searchInput) {
      searchInput.addEventListener('input', (e) => {
        const searchTerm = e.target.value.trim();
        
        if (this.searchTimeout) {
          clearTimeout(this.searchTimeout);
        }
        
        if (searchTerm.length >= 2 && this.currentWorkspacePath) {
          this.searchTimeout = setTimeout(() => {
            this.performWorkspaceSearch(searchTerm);
          }, 300);
        } else {
          this.clearSearchResults();
        }
      });
    }
  }

  /**
   * Show find dialog
   */
  showFindDialog() {
    const widget = document.getElementById('search-widget');
    const input = document.getElementById('widget-search-input');
    widget.classList.add('visible');
    input.focus();
    input.select();
  }

  /**
   * Close search widget
   */
  closeSearchWidget() {
    const widget = document.getElementById('search-widget');
    widget.classList.remove('visible');
    this.clearSearchHighlights();
    this.currentSearchMatches = [];
    this.currentMatchIndex = -1;
    this.updateSearchResults();
  }

  /**
   * Perform live search in current document
   * @param {string} searchText - Text to search for
   */
  performLiveSearch(searchText) {
    this.clearSearchHighlights();
    this.currentSearchMatches = [];
    this.currentMatchIndex = -1;

    if (!searchText.trim()) {
      this.updateSearchResults();
      return;
    }

    const text = this.editorManager.getContent();
    
    if (!text) {
      this.updateSearchResults();
      return;
    }

    try {
      const regex = new RegExp(searchText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
      let match;
      
      while ((match = regex.exec(text)) !== null) {
        this.currentSearchMatches.push({
          start: match.index,
          end: match.index + match[0].length,
          text: match[0]
        });
        
        // Prevent infinite loops with zero-length matches
        if (match.index === regex.lastIndex) {
          regex.lastIndex++;
        }
      }

      if (this.currentSearchMatches.length > 0) {
        this.currentMatchIndex = 0;
        this.highlightAllMatches();
      }
    } catch (e) {
      console.warn('Invalid search regex:', e);
    }

    this.updateSearchResults();
  }

  /**
   * Search next match
   */
  searchNext() {
    if (this.currentSearchMatches.length === 0) return;
    
    this.currentMatchIndex = (this.currentMatchIndex + 1) % this.currentSearchMatches.length;
    this.highlightAllMatches();
    this.focusOnCurrentMatch();
    this.updateSearchResults();
  }

  /**
   * Search previous match
   */
  searchPrev() {
    if (this.currentSearchMatches.length === 0) return;
    
    this.currentMatchIndex = this.currentMatchIndex <= 0 
      ? this.currentSearchMatches.length - 1 
      : this.currentMatchIndex - 1;
    this.highlightAllMatches();
    this.focusOnCurrentMatch();
    this.updateSearchResults();
  }

  /**
   * Update search results display
   */
  updateSearchResults() {
    const resultsText = document.getElementById('search-results-text');
    const prevBtn = document.getElementById('search-prev');
    const nextBtn = document.getElementById('search-next');

    if (!resultsText || !prevBtn || !nextBtn) return;

    if (this.currentSearchMatches.length === 0) {
      resultsText.textContent = 'No results';
      prevBtn.disabled = true;
      nextBtn.disabled = true;
    } else {
      resultsText.textContent = `${this.currentMatchIndex + 1} of ${this.currentSearchMatches.length}`;
      prevBtn.disabled = false;
      nextBtn.disabled = false;
    }
  }

  /**
   * Focus on current match
   */
  focusOnCurrentMatch() {
    if (this.currentMatchIndex >= 0 && this.currentMatchIndex < this.currentSearchMatches.length) {
      const editor = this.editorManager.editor;
      const match = this.currentSearchMatches[this.currentMatchIndex];
      
      editor.focus();
      editor.setSelectionRange(match.start, match.end);
      
      // Scroll to make the match visible
      const lines = editor.value.substring(0, match.start).split('\n');
      const lineNumber = lines.length;
      const approximateLineHeight = 20;
      const targetScrollTop = (lineNumber - 5) * approximateLineHeight;
      
      editor.scrollTop = Math.max(0, targetScrollTop);
      
      setTimeout(() => {
        if (editor.setSelectionRange) {
          editor.setSelectionRange(match.start, match.end);
        }
      }, 10);
    }
  }

  /**
   * Highlight all matches
   */
  highlightAllMatches() {
    // For now, we'll keep it simple and just highlight the current match when navigating
  }

  /**
   * Clear search highlights
   */
  clearSearchHighlights() {
    // Clear any visual highlights if implemented
  }

  /**
   * Show go to line dialog
   */
  showGoToLineDialog() {
    const dialog = document.getElementById('goto-dialog');
    const input = document.getElementById('goto-input');
    
    // Set max value based on current content
    const text = this.editorManager.getContent();
    if (text) {
      const lineCount = text.split('\n').length;
      input.max = lineCount;
    }
    
    dialog.classList.add('visible');
    input.focus();
    input.select();
  }

  /**
   * Close go to line dialog
   */
  closeGoToLineDialog() {
    const dialog = document.getElementById('goto-dialog');
    dialog.classList.remove('visible');
  }

  /**
   * Perform go to line
   */
  performGoToLine() {
    const input = document.getElementById('goto-input');
    const lineNumber = parseInt(input.value);
    
    if (!lineNumber || lineNumber < 1) {
      this.notificationManager.showError('Please enter a valid line number');
      return;
    }
    
    const text = this.editorManager.getContent();
    if (text) {
      const lines = text.split('\n');
      
      if (lineNumber > lines.length) {
        this.notificationManager.showError(`Line ${lineNumber} does not exist. Maximum line is ${lines.length}`);
        return;
      }
      
      this.editorManager.jumpToLine(lineNumber);
      this.notificationManager.showSuccess(`Jumped to line ${lineNumber}`);
    } else {
      this.notificationManager.showError('No file is currently open');
    }
    
    this.closeGoToLineDialog();
  }

  /**
   * Perform workspace search
   * @param {string} searchTerm - Term to search for
   */
  async performWorkspaceSearch(searchTerm) {
    if (!this.currentWorkspacePath) {
      this.displaySearchResults([], searchTerm);
      return;
    }

    try {
      const searchResults = document.getElementById('search-results');
      if (searchResults) {
        searchResults.innerHTML = '<div style="color: #7d8590; padding: 12px; text-align: center;">Searching...</div>';
      }
      
      const result = await window.ipcRenderer.invoke('search-in-files', searchTerm, this.currentWorkspacePath);
      
      if (result.success) {
        this.displaySearchResults(result.results, searchTerm);
      } else {
        const searchResults = document.getElementById('search-results');
        if (searchResults) {
          searchResults.innerHTML = '<div style="color: #f85149; padding: 12px;">Search failed: ' + result.error + '</div>';
        }
      }
    } catch (error) {
      const searchResults = document.getElementById('search-results');
      if (searchResults) {
        searchResults.innerHTML = '<div style="color: #f85149; padding: 12px;">Search error: ' + error.message + '</div>';
      }
    }
  }

  /**
   * Display search results in sidebar
   * @param {Array} results - Search results
   * @param {string} searchTerm - Search term
   */
  displaySearchResults(results, searchTerm) {
    const searchResults = document.getElementById('search-results');
    if (!searchResults) return;

    if (results.length === 0) {
      searchResults.innerHTML = '<div style="color: #7d8590; padding: 12px; text-align: center;">No results found</div>';
      return;
    }

    const groupedResults = {};
    results.forEach(result => {
      if (!groupedResults[result.file]) {
        groupedResults[result.file] = [];
      }
      groupedResults[result.file].push(result);
    });

    let html = '<div style="display: flex; flex-direction: column; gap: 8px;">';
    
    Object.keys(groupedResults).forEach(file => {
      const fileResults = groupedResults[file];
      const fileName = file.split(/[/\\]/).pop();
      const relativePath = file.replace(this.currentWorkspacePath, '').replace(/^[/\\]/, '');
      
      // Escape the file path properly for onclick
      const escapedPath = file.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
      
      html += `<div class="search-result-item" style="display: flex; flex-direction: column;">`;
      html += `<div class="search-result-file" onclick="window.searchManager.openSearchResult('${escapedPath}', ${fileResults[0].line})" style="margin-bottom: 4px;">${fileName}</div>`;
      html += `<div class="search-result-line" style="margin-bottom: 6px;">${relativePath} • ${fileResults.length} result${fileResults.length > 1 ? 's' : ''}</div>`;
      
      // Show each line result individually in vertical layout
      html += '<div style="display: flex; flex-direction: column; gap: 2px;">';
      fileResults.forEach(result => {
        const highlightedContent = result.content.replace(
          new RegExp(searchTerm.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi'),
          match => `<span class="search-highlight">${match}</span>`
        );
        html += `<div class="search-result-content" onclick="window.searchManager.openSearchResult('${escapedPath}', ${result.line}); event.stopPropagation();" style="display: block; margin: 2px 0; padding: 4px 6px; cursor: pointer; border-radius: 3px; background: #161b22; border-left: 2px solid #1f6feb;" onmouseover="this.style.background='#30363d'" onmouseout="this.style.background='#161b22'">`;
        html += `<div style="color: #7d8590; font-size: 10px; margin-bottom: 2px;">Line ${result.line}:</div>`;
        html += `<div style="font-family: monospace; font-size: 11px;">${highlightedContent}</div>`;
        html += `</div>`;
      });
      html += '</div>';
      
      html += '</div>';
    });
    
    html += '</div>';
    searchResults.innerHTML = html;
  }

  /**
   * Clear search results
   */
  clearSearchResults() {
    const searchResults = document.getElementById('search-results');
    if (searchResults) {
      searchResults.innerHTML = '';
    }
  }

  /**
   * Open search result (to be implemented by parent controller)
   * @param {string} filePath - File path
   * @param {number} lineNumber - Line number
   */
  async openSearchResult(filePath, lineNumber) {
    // This will be implemented by the main controller
    console.log('Open search result requested:', filePath, 'at line', lineNumber);
  }

  /**
   * Set current workspace path
   * @param {string} workspacePath - Workspace path
   */
  setWorkspacePath(workspacePath) {
    this.currentWorkspacePath = workspacePath;
  }
}

module.exports = SearchManager;