#!/usr/bin/env python3
# Copyright (c) 2025 Patrick Motsch
# All rights reserved.
"""
Script to count code lines in a folder and its subfolders.
Reports lines per file for Python, JavaScript, and CSS files,
and calculates the total lines of code.
"""

import os
import argparse
from pathlib import Path
from typing import Dict, List, Tuple

# File extensions to count
FILE_TYPES = {
    'python': ['.py'],
    'javascript': ['.js', '.jsx', '.ts', '.tsx'],
    'css': ['.css', '.scss', '.sass', '.less'],
    'html': ['.htm','.html']
}

def count_lines(file_path: str) -> int:
    """Count the number of non-empty lines in a file."""
    try:
        with open(file_path, 'r', encoding='utf-8') as file:
            # Count non-empty lines (strip whitespace)
            return sum(1 for line in file if line.strip())
    except UnicodeDecodeError:
        # Skip files that can't be decoded as UTF-8
        print(f"Warning: Could not read {file_path} as text. Skipping.")
        return 0
    except Exception as e:
        print(f"Error reading {file_path}: {e}")
        return 0

def get_file_type(file_path: str) -> str:
    """Determine the file type based on extension."""
    extension = os.path.splitext(file_path)[1].lower()
    
    for file_type, extensions in FILE_TYPES.items():
        if extension in extensions:
            return file_type
    
    return "other"

def scan_directory(directory: str, exclude_dirs: List[str] = None) -> Tuple[Dict[str, List[Tuple[str, int]]], Dict[str, int]]:
    """
    Scan a directory and its subdirectories for code files and count lines.
    
    Args:
        directory: The directory to scan
        exclude_dirs: List of directory names to exclude (e.g., ['node_modules', '.git'])
        
    Returns:
        Tuple containing:
        - Dictionary mapping file types to lists of (file_path, line_count) tuples
        - Dictionary mapping file types to total line counts
    """
    if exclude_dirs is None:
        exclude_dirs = ['node_modules', '.git', 'venv', 'env', '__pycache__', '.vscode', '.idea']
    
    # Initialize results
    files_by_type = {file_type: [] for file_type in FILE_TYPES.keys()}
    files_by_type["other"] = []
    
    totals_by_type = {file_type: 0 for file_type in FILE_TYPES.keys()}
    totals_by_type["other"] = 0
    
    # Walk through directory
    for root, dirs, files in os.walk(directory):
        # Skip excluded directories
        dirs[:] = [d for d in dirs if d not in exclude_dirs]
        
        for file in files:
            file_path = os.path.join(root, file)
            file_type = get_file_type(file_path)
            
            # Count lines if it's a type we're interested in
            if file_type in files_by_type:
                line_count = count_lines(file_path)
                
                # Add to files list
                files_by_type[file_type].append((file_path, line_count))
                
                # Add to total
                totals_by_type[file_type] += line_count
    
    return files_by_type, totals_by_type

def main():
    """Main function."""
    parser = argparse.ArgumentParser(description='Count code lines in a directory.')
    parser.add_argument('directory', type=str, nargs='?', default='.',
                        help='Directory to scan (default: current directory)')
    parser.add_argument('--exclude', type=str, nargs='+', 
                        default=['node_modules', '.git', 'venv', 'env', '__pycache__', '.vscode', '.idea'],
                        help='Directories to exclude')
    args = parser.parse_args()
    
    directory = args.directory
    exclude_dirs = args.exclude
    
    print(f"Scanning directory: {os.path.abspath(directory)}")
    print(f"Excluding directories: {', '.join(exclude_dirs)}")
    print("\nCounting lines of code...\n")
    
    # Scan directory
    files_by_type, totals_by_type = scan_directory(directory, exclude_dirs)
    
    # Calculate total lines
    total_lines = sum(totals_by_type.values())
    
    # Print results by file type
    for file_type in sorted(files_by_type.keys()):
        if file_type == "other" and not files_by_type["other"]:
            continue
            
        files = files_by_type[file_type]
        if not files:
            continue
            
        print(f"\n{file_type.upper()} FILES ({totals_by_type[file_type]} total lines):")
        print("-" * 80)
        
        # Sort files by line count (descending)
        for file_path, line_count in sorted(files, key=lambda x: x[1], reverse=True):
            relative_path = os.path.relpath(file_path, directory)
            print(f"{relative_path}: {line_count} lines")
    
    # Print summary
    print("\nSUMMARY:")
    print("-" * 80)
    for file_type in sorted(totals_by_type.keys()):
        if totals_by_type[file_type] > 0:
            print(f"{file_type.upper()}: {totals_by_type[file_type]} lines")
    
    print("-" * 80)
    print(f"TOTAL LINES OF CODE: {total_lines}")

if __name__ == '__main__':
    main()