sherlock-io 0.1.0

SherlockIO - Lightning-fast language detection CLI tool
Documentation
use anyhow::Result;
use colored::*;
use serde_json;

use crate::cli::OutputFormat;
use crate::detector::Analysis;

pub struct Reporter;

impl Reporter {
    pub fn new() -> Self {
        Self
    }

    pub fn generate_report(&self, analysis: &Analysis, format: OutputFormat) -> Result<()> {
        match format {
            OutputFormat::Table => self.print_table_report(analysis),
            OutputFormat::Json => self.print_json_report(analysis),
            OutputFormat::Csv => self.print_csv_report(analysis),
        }
    }

    fn print_table_report(&self, analysis: &Analysis) -> Result<()> {
        println!("{}", "Language Detection Report".bold().underline());
        println!();
        
        // Summary
        println!("📊 {}: {}", "Total Files".bold(), analysis.total_files.to_string().cyan());
        println!("💾 {}: {}", "Total Size".bold(), self.format_bytes(analysis.total_bytes).cyan());
        println!();

        if analysis.stats.is_empty() {
            println!("{}", "No languages detected!".yellow());
            return Ok(());
        }

        // Table header
        println!("{:<20} {:<8} {:<12} {:<8} {:<10}", 
            "Language".bold(), 
            "Files".bold(), 
            "Percentage".bold(), 
            "Size".bold(),
            "Bar".bold()
        );
        println!("{}", "─".repeat(70).dimmed());

        // Language stats
        for stat in &analysis.stats {
            let bar = self.create_progress_bar(stat.percentage, 20);
            let color = self.parse_hex_color(&stat.color);
            
            println!("{:<20} {:<8} {:<12} {:<8} {}", 
                stat.language.color(color),
                stat.file_count.to_string().cyan(),
                format!("{:.1}%", stat.percentage).green(),
                self.format_bytes(stat.total_bytes).magenta(),
                bar
            );
        }

        // Unknown files
        if !analysis.unknown_files.is_empty() {
            println!();
            println!("{} {} unknown files", 
                "⚠️".yellow(), 
                analysis.unknown_files.len().to_string().yellow()
            );
        }

        Ok(())
    }

    fn print_json_report(&self, analysis: &Analysis) -> Result<()> {
        let json_output = serde_json::json!({
            "summary": {
                "total_files": analysis.total_files,
                "total_bytes": analysis.total_bytes,
                "languages_detected": analysis.stats.len()
            },
            "languages": analysis.stats.iter().map(|stat| {
                serde_json::json!({
                    "name": stat.language,
                    "file_count": stat.file_count,
                    "percentage": stat.percentage,
                    "total_bytes": stat.total_bytes,
                    "color": stat.color,
                    "files": stat.files
                })
            }).collect::<Vec<_>>(),
            "unknown_files": analysis.unknown_files
        });

        println!("{}", serde_json::to_string_pretty(&json_output)?);
        Ok(())
    }

    fn print_csv_report(&self, analysis: &Analysis) -> Result<()> {
        println!("Language,Files,Percentage,Bytes");
        
        for stat in &analysis.stats {
            println!("{},{},{:.2},{}", 
                stat.language, 
                stat.file_count, 
                stat.percentage, 
                stat.total_bytes
            );
        }

        if !analysis.unknown_files.is_empty() {
            println!("Unknown,{},0,0", analysis.unknown_files.len());
        }

        Ok(())
    }

    fn create_progress_bar(&self, percentage: f64, width: usize) -> String {
        let filled = ((percentage / 100.0) * width as f64) as usize;
        let filled = filled.min(width);
        
        let bar = "█".repeat(filled) + &"░".repeat(width - filled);
        format!("{} {:.1}%", bar.green(), percentage)
    }

    fn format_bytes(&self, bytes: u64) -> String {
        const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"];
        let mut size = bytes as f64;
        let mut unit_index = 0;

        while size >= 1024.0 && unit_index < UNITS.len() - 1 {
            size /= 1024.0;
            unit_index += 1;
        }

        if unit_index == 0 {
            format!("{} {}", size as u64, UNITS[unit_index])
        } else {
            format!("{:.1} {}", size, UNITS[unit_index])
        }
    }

    fn parse_hex_color(&self, hex: &str) -> colored::Color {
        // Simple color mapping for terminal compatibility
        match hex {
            "#CE422B" => colored::Color::Red,
            "#3776AB" => colored::Color::Blue,
            "#F7DF1E" => colored::Color::Yellow,
            "#3178C6" => colored::Color::Cyan,
            "#00ADD8" => colored::Color::Cyan,
            "#ED8B00" => colored::Color::Yellow,
            "#A8B9CC" => colored::Color::White,
            "#00599C" => colored::Color::Blue,
            "#239120" => colored::Color::Green,
            "#777BB4" => colored::Color::Magenta,
            "#CC342D" => colored::Color::Red,
            "#FA7343" => colored::Color::Yellow,
            "#7F52FF" => colored::Color::Magenta,
            "#DC322F" => colored::Color::Red,
            "#89E051" => colored::Color::Green,
            "#E34F26" => colored::Color::Red,
            "#1572B6" => colored::Color::Blue,
            "#CF649A" => colored::Color::Magenta,
            "#4FC08D" => colored::Color::Green,
            "#61DAFB" => colored::Color::Cyan,
            _ => colored::Color::White,
        }
    }
}