#!/usr/bin/env python3
"""
Unified Linter - Modular Linting Framework
Company: Copyright (c) 2025  BTA Design Services  
         Licensed under the MIT License.
Description: Flexible, plugin-based linting system supporting multiple linters
Usage:
    python3 tb_lint.py [options] <file.sv> [<file2.sv> ...]
    
Options:
    --help              Show this help message
    --config FILE       Configuration file (JSON)
    --linter NAME       Run specific linter (default: all)
    --list-linters      List available linters
    --strict            Treat warnings as errors
    --json              Output in JSON format
    --color             Enable colored output
    -f FILE_LIST        File containing list of files (one per line)
    -o OUTPUT_FILE      Output file for results
    
Examples:
    # Run all linters
    python3 tb_lint.py -f file_list.txt
    
    # Run only NaturalDocs linter
    python3 tb_lint.py --linter naturaldocs file.sv
    
    # Use custom config
    python3 tb_lint.py --config my_config.json -f files.txt
"""
import sys
import os
import json
import argparse
from pathlib import Path
from typing import List, Optional
# Add script directory to path
script_dir = Path(__file__).parent
sys.path.insert(0, str(script_dir))
from core import (
    ConfigManager,
    LinterRegistry,
    get_registry,
    BaseLinter,
    LinterResult,
    RuleSeverity
)
# Import linters to register them
from linters import NaturalDocsLinter, VeribleLinter
[docs]
class Colors:
    """ANSI color codes"""
    RED = '\033[0;31m'
    GREEN = '\033[0;32m'
    YELLOW = '\033[1;33m'
    BLUE = '\033[0;34m'
    CYAN = '\033[0;36m'
    BOLD = '\033[1m'
    NC = '\033[0m' 
[docs]
class UnifiedLinter:
    """
    Unified linting orchestrator
    
    Manages multiple linters and aggregates results
    """
    
[docs]
    def __init__(self, config_file: Optional[str] = None, use_color: bool = False,
                 strict_mode: bool = False):
        """
        Initialize unified linter
        
        Args:
            config_file: Path to configuration file
            use_color: Enable colored output
            strict_mode: Treat warnings as errors
        """
        self.config_manager = ConfigManager(config_file)
        self.registry = get_registry()
        self.use_color = use_color and sys.stdout.isatty()
        self.strict_mode = strict_mode 
    
    def _color(self, color: str, text: str) -> str:
        """Apply color if enabled"""
        if self.use_color:
            return f"{color}{text}{Colors.NC}"
        return text
    
[docs]
    def list_linters(self) -> List[str]:
        """Get list of available linters"""
        return self.registry.list_linters() 
    
[docs]
    def run_linter(self, linter_name: str, file_paths: List[str]) -> LinterResult:
        """
        Run a specific linter on files
        
        Args:
            linter_name: Name of linter to run
            file_paths: List of files to check
        
        Returns:
            LinterResult with violations found
        """
        # Get linter configuration
        linter_config = self.config_manager.get_linter_config(linter_name)
        
        # Get linter instance
        linter = self.registry.get_linter(linter_name, linter_config)
        
        if not linter:
            print(f"ERROR: Linter '{linter_name}' not found", file=sys.stderr)
            return LinterResult(linter_name=linter_name)
        
        # Run linter
        return linter.lint_files(file_paths) 
    
[docs]
    def run_all_linters(self, file_paths: List[str]) -> dict:
        """
        Run all enabled linters on files
        
        Args:
            file_paths: List of files to check
        
        Returns:
            Dictionary mapping linter names to results
        """
        results = {}
        
        for linter_name in self.list_linters():
            # Check if linter is enabled in configuration
            if not self.config_manager.is_linter_enabled(linter_name):
                print(f"\n{self._color(Colors.YELLOW, f'Skipping {linter_name} linter (disabled in config)')}")
                continue
            
            print(f"\n{self._color(Colors.CYAN, '='*80)}")
            print(f"{self._color(Colors.CYAN, f'Running {linter_name} linter...')}")
            print(f"{self._color(Colors.CYAN, '='*80)}\n")
            
            result = self.run_linter(linter_name, file_paths)
            results[linter_name] = result
        
        return results 
    
[docs]
    def print_result(self, result: LinterResult, output_file=None):
        """
        Print linter results in human-readable format
        
        Args:
            result: LinterResult to print
            output_file: File handle for output (default: stdout)
        """
        out = output_file if output_file else sys.stdout
        
        # Print violations grouped by file
        violations_by_file = {}
        for violation in result.violations:
            if violation.file not in violations_by_file:
                violations_by_file[violation.file] = []
            violations_by_file[violation.file].append(violation)
        
        # Print each file's violations
        for file_path, violations in violations_by_file.items():
            print(f"\n{self._color(Colors.CYAN, f'File: {file_path}')}", file=out)
            
            for violation in sorted(violations, key=lambda v: v.line):
                if violation.severity == RuleSeverity.ERROR:
                    color = Colors.RED
                    level = "ERROR"
                elif violation.severity == RuleSeverity.WARNING:
                    color = Colors.YELLOW
                    level = "WARNING"
                else:
                    color = Colors.BLUE
                    level = "INFO"
                
                print(self._color(color, 
                    f"  {file_path}:{violation.line}:{violation.column}: "
                    f"{violation.rule_id} {level}: {violation.message}"),
                    file=out)
        
        # Print file errors
        for file_path, error_msg in result.errors.items():
            print(self._color(Colors.RED, f"\n✗ {file_path}: {error_msg}"), file=out)
        
        # Print summary
        print(f"\n{self._color(Colors.CYAN, '='*80)}", file=out)
        print(f"{self._color(Colors.CYAN, f'{result.linter_name} Summary')}", file=out)
        print(f"{self._color(Colors.CYAN, '='*80)}", file=out)
        print(f"Files checked: {result.files_checked}", file=out)
        if result.files_failed > 0:
            print(self._color(Colors.RED, f"Files failed: {result.files_failed}"), file=out)
        print(self._color(Colors.RED, f"Errors: {result.error_count}"), file=out)
        print(self._color(Colors.YELLOW, f"Warnings: {result.warning_count}"), file=out)
        print(self._color(Colors.BLUE, f"Info: {result.info_count}"), file=out) 
    
[docs]
    def print_json(self, results: dict, output_file=None):
        """
        Print results in JSON format
        
        Args:
            results: Dictionary of linter results
            output_file: File handle for output (default: stdout)
        """
        out = output_file if output_file else sys.stdout
        
        output = {
            'linters': {},
            'summary': {
                'total_files_checked': 0,
                'total_files_failed': 0,
                'total_errors': 0,
                'total_warnings': 0,
                'total_info': 0
            }
        }
        
        for linter_name, result in results.items():
            output['linters'][linter_name] = {
                'files_checked': result.files_checked,
                'files_failed': result.files_failed,
                'errors': result.error_count,
                'warnings': result.warning_count,
                'info': result.info_count,
                'violations': [
                    {
                        'file': v.file,
                        'line': v.line,
                        'column': v.column,
                        'severity': v.severity.value,
                        'message': v.message,
                        'rule_id': v.rule_id
                    }
                    for v in result.violations
                ],
                'errors': result.errors
            }
            
            output['summary']['total_files_checked'] += result.files_checked
            output['summary']['total_files_failed'] += result.files_failed
            output['summary']['total_errors'] += result.error_count
            output['summary']['total_warnings'] += result.warning_count
            output['summary']['total_info'] += result.info_count
        
        print(json.dumps(output, indent=2), file=out) 
    
[docs]
    def print_command_info(self, args, files_to_check: List[str], output_file=None):
        """
        Print command line information for tb_lint and each enabled linter
        
        Args:
            args: Command line arguments
            files_to_check: List of files to check
            output_file: File handle for output (default: stdout)
        """
        out = output_file if output_file else sys.stdout
        
        # Print header
        print(f"{self._color(Colors.CYAN, '='*80)}", file=out)
        print(f"{self._color(Colors.CYAN, 'TB_LINT - Unified Linter Framework')}", file=out)
        print(f"{self._color(Colors.CYAN, '='*80)}", file=out)
        
        # Print unified linter command
        cmd_parts = ["python3 tb_lint.py"]
        if args.config:
            cmd_parts.append(f"--config {args.config}")
        if args.linter:
            cmd_parts.append(f"--linter {args.linter}")
        if args.strict:
            cmd_parts.append("--strict")
        if args.json:
            cmd_parts.append("--json")
        if args.color:
            cmd_parts.append("--color")
        if args.file_list:
            cmd_parts.append(f"-f {args.file_list}")
        if args.output:
            cmd_parts.append(f"-o {args.output}")
        if args.files:
            cmd_parts.extend(args.files)
        
        print(f"\n{self._color(Colors.BOLD, 'Unified Linter Command:')}", file=out)
        print(f"  {' '.join(cmd_parts)}", file=out)
        
        # Print config file
        config_display = self.config_manager.config_file or 'configs/lint_config.json (default)'
        print(f"\n{self._color(Colors.BOLD, 'Configuration:')}", file=out)
        print(f"  {config_display}", file=out)
        
        # Print enabled linters and their equivalent commands
        print(f"\n{self._color(Colors.BOLD, 'Enabled Linters:')}", file=out)
        for linter_name in self.list_linters():
            if self.config_manager.is_linter_enabled(linter_name):
                linter_config = self.config_manager.get_linter_config(linter_name)
                linter = self.registry.get_linter(linter_name, linter_config)
                
                # Generate linter-specific command
                if linter_name == "verible":
                    verible_bin = linter.verible_bin if hasattr(linter, 'verible_bin') else 'verible-verilog-lint'
                    cmd = f"{verible_bin}"
                    
                    # Add config file if present
                    config_file_path = linter_config.get('config_file', '')
                    if config_file_path:
                        cmd += f" [config: {config_file_path}]"
                    
                    # Add file list
                    if args.file_list:
                        cmd += f" $(cat {args.file_list})"
                    elif len(files_to_check) <= 3:
                        cmd += f" {' '.join(files_to_check)}"
                    else:
                        cmd += f" {files_to_check[0]} ... ({len(files_to_check)} files)"
                    
                    print(f"  • {self._color(Colors.GREEN, linter_name)}:", file=out)
                    print(f"    {cmd}", file=out)
                    
                elif linter_name == "naturaldocs":
                    verible_bin = linter.verible_bin if hasattr(linter, 'verible_bin') else 'verible-verilog-syntax'
                    cmd = f"{verible_bin} --export_json"
                    
                    # Add config file if present
                    config_file_path = linter_config.get('config_file', '')
                    if config_file_path:
                        cmd += f" [config: {config_file_path}]"
                    
                    # Add file list
                    if args.file_list:
                        cmd += f" $(cat {args.file_list})"
                    elif len(files_to_check) <= 3:
                        cmd += f" {' '.join(files_to_check)}"
                    else:
                        cmd += f" {files_to_check[0]} ... ({len(files_to_check)} files)"
                    
                    print(f"  • {self._color(Colors.GREEN, linter_name)}:", file=out)
                    print(f"    {cmd}", file=out)
                    print(f"    Note: AST-based linting using Verible parser", file=out)
                else:
                    print(f"  • {self._color(Colors.GREEN, linter_name)}", file=out)
        
        print(f"{self._color(Colors.CYAN, '='*80)}", file=out)
        print("", file=out) 
    
[docs]
    def print_final_summary(self, results: dict, output_file=None):
        """
        Print final TB_LINT summary
        
        Args:
            results: Dictionary of linter results
            output_file: File handle for output (default: stdout)
        """
        out = output_file if output_file else sys.stdout
        
        # Calculate total errors
        total_errors = sum(r.error_count for r in results.values())
        
        # Print separator line
        print("", file=out)
        print("=" * 80, file=out)
        
        # Determine pass/fail status
        if total_errors > 0:
            status_msg = self._color(Colors.RED, "TB_LINT : FAILED")
        else:
            status_msg = self._color(Colors.GREEN, "TB_LINT : PASSED")
        
        print(status_msg, file=out)
        print("=" * 80, file=out) 
    
[docs]
    def get_exit_code(self, results: dict) -> int:
        """
        Determine exit code based on results
        
        Args:
            results: Dictionary of linter results
        
        Returns:
            0 if all passed, 1 if violations found
        """
        total_errors = sum(r.error_count for r in results.values())
        total_warnings = sum(r.warning_count for r in results.values())
        
        if total_errors > 0:
            return 1
        elif total_warnings > 0 and self.strict_mode:
            return 1
        return 0 
 
[docs]
def main():
    """Main entry point"""
    # Parse config file argument first to get project info for epilog
    import sys
    config_file = None
    for i, arg in enumerate(sys.argv):
        if arg in ['-c', '--config'] and i + 1 < len(sys.argv):
            config_file = sys.argv[i + 1]
            break
    
    # Load config to get project info for epilog
    temp_config = ConfigManager(config_file)
    project_info = temp_config.get_project_info()
    company = project_info.get('company', '')
    project_name = project_info.get('name', '')
    epilog_text = f"{company} - {project_name}" if company and project_name else None
    
    parser = argparse.ArgumentParser(
        description='Unified Linting Framework',
        epilog=epilog_text,
        formatter_class=argparse.RawDescriptionHelpFormatter if epilog_text else argparse.HelpFormatter
    )
    
    parser.add_argument('files', nargs='*', help='Files to check')
    parser.add_argument('-f', '--file-list', help='File containing list of files')
    parser.add_argument('-o', '--output', help='Output file (default: stdout)')
    parser.add_argument('-c', '--config', help='Configuration file (JSON)')
    parser.add_argument('--linter', help='Run specific linter (default: all)')
    parser.add_argument('--list-linters', action='store_true', help='List available linters')
    parser.add_argument('--strict', action='store_true', help='Treat warnings as errors')
    parser.add_argument('--json', action='store_true', help='Output in JSON format')
    parser.add_argument('--color', action='store_true', help='Enable colored output')
    
    args = parser.parse_args()
    
    # Create unified linter
    unified = UnifiedLinter(
        config_file=args.config,
        use_color=args.color,
        strict_mode=args.strict
    )
    
    # List linters if requested
    if args.list_linters:
        print("Available linters:")
        for linter_name in unified.list_linters():
            print(f"  - {linter_name}")
        return 0
    
    # Collect files to check
    files_to_check = []
    
    if args.files:
        # Check if any positional arguments look like file lists (.txt files)
        # and provide helpful error message
        for file_arg in args.files:
            if file_arg.endswith('.txt') and os.path.exists(file_arg):
                # Check if it's a file list by looking at first line
                with open(file_arg, 'r') as f:
                    first_line = f.readline().strip()
                    if first_line.startswith('#') or first_line.endswith('.sv') or first_line.endswith('.svh'):
                        print(f"ERROR: '{file_arg}' appears to be a file list.", file=sys.stderr)
                        print(f"Use: python3 tb_lint.py -f {file_arg}", file=sys.stderr)
                        print(f"Not: python3 tb_lint.py {file_arg}", file=sys.stderr)
                        return 1
        files_to_check.extend(args.files)
    
    if args.file_list:
        if not os.path.exists(args.file_list):
            print(f"ERROR: File list '{args.file_list}' not found", file=sys.stderr)
            return 1
        
        with open(args.file_list, 'r') as f:
            for line in f:
                line = line.strip()
                if line and not line.startswith('#'):
                    files_to_check.append(line)
    
    if not files_to_check:
        parser.print_help()
        print("\nERROR: No files specified", file=sys.stderr)
        print("\nTip: Use -f flag for file lists:", file=sys.stderr)
        print("  python3 tb_lint.py -f file_list.txt", file=sys.stderr)
        return 1
    
    # Print command info (not for JSON output)
    if not args.json:
        unified.print_command_info(args, files_to_check)
    
    # Run linter(s)
    if args.linter:
        # Run specific linter
        result = unified.run_linter(args.linter, files_to_check)
        results = {args.linter: result}
    else:
        # Run all linters
        results = unified.run_all_linters(files_to_check)
    
    # Output results
    try:
        if args.output:
            with open(args.output, 'w') as out_f:
                if args.json:
                    unified.print_json(results, out_f)
                else:
                    # Print command info to file
                    unified.print_command_info(args, files_to_check, out_f)
                    for linter_name, result in results.items():
                        unified.print_result(result, out_f)
                    # Print final summary
                    unified.print_final_summary(results, out_f)
        else:
            if args.json:
                unified.print_json(results)
            else:
                for linter_name, result in results.items():
                    unified.print_result(result)
                # Print final summary
                unified.print_final_summary(results)
    except Exception as e:
        print(f"ERROR writing output: {e}", file=sys.stderr)
        return 1
    
    # Return appropriate exit code
    return unified.get_exit_code(results) 
if __name__ == '__main__':
    sys.exit(main())