panasyn 0.1.0

A lightweight GPU-accelerated terminal emulator for macOS and Linux.
#![allow(dead_code)]
#![allow(clippy::module_inception, clippy::single_match)]
pub mod benchmark;
pub mod format;
pub mod fuzz;
pub mod headless;
pub mod perf;
pub mod profiling;
pub mod recorder;
pub mod regression;
pub mod replay;

/// Run replay/benchmark/fuzz commands from raw args.
pub fn run() -> Result<(), Box<dyn std::error::Error>> {
    let args: Vec<String> = std::env::args().collect();
    let subcommand = args.get(1).map(|s| s.as_str());

    match subcommand {
        Some("replay") => handle_replay(&args),
        Some("fuzz") => handle_fuzz(&args),
        Some("convert") => handle_convert(&args),
        Some("profile") => handle_profile(&args),
        Some("bench") => handle_bench(&args),
        Some("list") => handle_list(),
        Some("help") | None => {
            print_usage();
            Ok(())
        }
        _ => {
            print_usage();
            Err(format!("unknown subcommand: {}", subcommand.unwrap_or("")).into())
        }
    }
}

fn print_usage() {
    println!("Panasyn Replay System");
    println!();
    println!("USAGE:");
    println!("  panasyn replay <path>          Replay a recording");
    println!("  panasyn replay <path> --benchmark      Benchmark mode");
    println!("  panasyn replay <path> --headless        Headless mode with metrics");
    println!("  panasyn replay <path> --machine         Machine-readable output");
    println!("  panasyn replay <path> --regression      Regression testing mode");
    println!("  panasyn fuzz [n]               Run fuzzer (default: 10000 iterations)");
    println!("  panasyn fuzz --categories      Run categorized fuzzing");
    println!("  panasyn convert <jsonl> <ltr>  Convert JSONL to LTR format");
    println!("  panasyn profile <path>         Profile allocations during replay");
    println!(
        "  panasyn bench [path]           Benchmark render, shaping, latency, PTY backpressure"
    );
    println!("  panasyn list                   List local replay files, if present");
    println!();
    println!("FLAGS:");
    println!("  --headless        Parse-only mode, no window/renderer");
    println!("  --benchmark       Extended performance measurements");
    println!("  --machine         Machine-readable key=value output");
    println!("  --regression      Compare against stored snapshot hashes");
    println!("  --store           Store benchmark result to benchmarks/");
    println!("  --fuzz N          Run N fuzz iterations");
    println!("  --categories      Run categorized fuzzing");
    println!("  --compress        Write compressed LTR format");
    println!("  --frames N        Number of offscreen render frames for bench mode");
    println!("  --pty-bytes N     Bytes emitted by the PTY backpressure benchmark");
    println!();
    println!("FILE FORMATS:");
    println!("  .jsonl   JSON Lines (legacy)");
    println!("  .ltr     Panasyn Replay (binary, recommended)");
    println!("  .ltr.zst Panasyn Replay (zstd-compressed)");
    println!();
    println!("REPLAY FILES:");
    println!("  .jsonl/.ltr files are local developer artifacts and are not shipped");
}

fn handle_replay(args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
    let path = args
        .get(2)
        .ok_or_else(|| "Usage: panasyn replay <path> [flags]".to_string())?;

    let cols = 80;
    let rows = 24;

    let is_benchmark = args.contains(&"--benchmark".to_string());
    let is_machine = args.contains(&"--machine".to_string());
    let is_headless = args.contains(&"--headless".to_string());
    let is_regression = args.contains(&"--regression".to_string());
    let store = args.contains(&"--store".to_string());

    let fuzz_n: Option<usize> = args
        .iter()
        .position(|a| a == "--fuzz")
        .and_then(|i| args.get(i + 1))
        .and_then(|s| s.parse().ok());

    if let Some(n) = fuzz_n {
        let report = fuzz::fuzz_iterations(n, cols as u16, rows as u16);
        fuzz::print_fuzz_report(&report);
        return Ok(());
    }

    let events = format::read_any(path)?;

    if is_regression {
        let result = regression::run_regression(&events, cols as u16, rows as u16);
        if is_machine {
            println!(
                "REGRESSION_RESULT {}",
                serde_json::to_string(&serde_json::json!({
                    "events_processed": result.events_processed,
                    "grid_hash": format!("{:016x}", result.final_grid_hash),
                    "cursor_row": result.final_cursor.0,
                    "cursor_col": result.final_cursor.1,
                    "scrollback_hash": format!("{:016x}", result.scrollback_hash),
                    "alt_screen": result.alt_screen_state,
                }))?
            );
        } else {
            println!("=== Regression Result ===");
            println!("{}", result);
        }
        return Ok(());
    }

    if is_benchmark {
        let result = benchmark::run_benchmark(&events, cols as u16, rows as u16);
        if store {
            let label = std::path::Path::new(path)
                .file_stem()
                .and_then(|s| s.to_str())
                .unwrap_or("unknown");
            benchmark::store_benchmark(label, &result)?;
        }
        if is_machine {
            benchmark::print_machine_readable(&result);
        } else {
            benchmark::print_benchmark(&result);
        }
        return Ok(());
    }

    if is_headless {
        let result = headless::run_headless(&events, cols as u16, rows as u16);
        if is_machine {
            headless::print_headless_machine(&result);
        } else {
            headless::print_headless(&result);
        }
        return Ok(());
    }

    // Default: standard replay
    let result = replay::run_replay(&events, cols as u16, rows as u16);
    if is_machine {
        println!(
            "REPLAY_RESULT events={} bytes={} parse_ms={:.2} cursor=({},{}) scrollback={}",
            result.events_processed,
            result.total_bytes,
            result.parse_time_ms,
            result.final_cursor.0,
            result.final_cursor.1,
            result.scrollback_lines
        );
    } else {
        println!("=== Replay Summary ===");
        println!("  Events:     {}", result.events_processed);
        println!(
            "  Bytes:      {} ({:.2} KB)",
            result.total_bytes,
            result.total_bytes as f64 / 1024.0
        );
        println!("  Parse time: {:.2} ms", result.parse_time_ms);
        println!(
            "  Cursor:     ({}, {})",
            result.final_cursor.0, result.final_cursor.1
        );
        println!("  Scrollback: {} lines", result.scrollback_lines);

        if !result.snapshots.is_empty() {
            println!("\n=== Snapshots ===");
            for (label, _snap) in &result.snapshots {
                println!("  {}: {} chars", label, _snap.len());
            }
        }
        if !result.errors.is_empty() {
            println!("\n=== Errors ===");
            for e in &result.errors {
                println!("  {}", e);
            }
        }
    }
    Ok(())
}

fn handle_fuzz(args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
    let cols = 80;
    let rows = 24;

    let use_categories = args.contains(&"--categories".to_string());

    if use_categories {
        let reports = fuzz::fuzz_ansi_categories(cols, rows);
        fuzz::print_category_reports(&reports);
        let total_crashes: usize = reports.iter().map(|r| r.crashes).sum();
        if total_crashes > 0 {
            return Err(format!("{total_crashes} fuzz category crashes detected").into());
        }
        return Ok(());
    }

    let n: usize = args.get(2).and_then(|s| s.parse().ok()).unwrap_or(10000);

    println!(
        "Running {} ANSI fuzz iterations on {}x{} grid...",
        n, cols, rows
    );
    let report = fuzz::fuzz_iterations(n, cols, rows);
    fuzz::print_fuzz_report(&report);

    if report.crashes > 0 {
        return Err(format!("{} fuzz crashes detected", report.crashes).into());
    }
    Ok(())
}

fn handle_convert(args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
    let jsonl_path = args.get(2).ok_or("Usage: panasyn convert <jsonl> <ltr>")?;
    let ltr_path = args.get(3).ok_or("Usage: panasyn convert <jsonl> <ltr>")?;
    let compress = args.contains(&"--compress".to_string());

    let events = recorder::read_jsonl(jsonl_path)?;
    if compress {
        format::write_ltr_zst(ltr_path, &events)?;
        println!("Converted {} -> {} (compressed)", jsonl_path, ltr_path);
    } else {
        format::write_ltr(ltr_path, &events)?;
        println!("Converted {} -> {}", jsonl_path, ltr_path);
    }
    Ok(())
}

fn handle_profile(args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
    let path = args.get(2).ok_or("Usage: panasyn profile <path>")?;
    let events = format::read_any(path)?;
    let cols = 80;
    let rows = 24;

    // Run replay wrapped in allocation tracking
    let (result, allocs) = crate::replay::profiling::track_allocations(|| {
        replay::run_replay(&events, cols as u16, rows as u16)
    });

    println!("=== Profile Results ===");
    println!("  Events processed:  {}", result.events_processed);
    println!(
        "  Total bytes:       {} ({:.2} KB)",
        result.total_bytes,
        result.total_bytes as f64 / 1024.0
    );
    println!("  Parse time:        {:.2} ms", result.parse_time_ms);
    println!("  Allocations:       {}", allocs.alloc_count);
    println!(
        "  Bytes allocated:   {} ({:.2} KB)",
        allocs.bytes_allocated,
        allocs.bytes_allocated as f64 / 1024.0
    );
    if result.total_bytes > 0 {
        println!(
            "  Bytes/byte parsed: {:.2}",
            allocs.bytes_allocated as f64 / result.total_bytes as f64
        );
    }
    println!();
    profiling::print_profiling_guide();

    Ok(())
}

fn handle_bench(args: &[String]) -> Result<(), Box<dyn std::error::Error>> {
    let path = args
        .get(2)
        .filter(|arg| !arg.starts_with("--"))
        .map(String::as_str);
    let frames = flag_value(args, "--frames").unwrap_or(30).max(1);
    let pty_bytes = flag_value(args, "--pty-bytes").unwrap_or(512 * 1024).max(1);
    let machine = args.contains(&"--machine".to_string());
    let (label, events) = if let Some(path) = path {
        (path, format::read_any(path)?)
    } else {
        ("generated-smoke", default_bench_events())
    };
    let result = perf::run_suite(&events, 80, 24, frames, pty_bytes)?;
    perf::print_suite(label, &result, machine);
    Ok(())
}

fn default_bench_events() -> Vec<recorder::ReplayEvent> {
    let mut output = String::new();
    for index in 1..=1000 {
        output.push_str(&format!("{index:04} Panasyn replay smoke\n"));
    }
    vec![recorder::ReplayEvent::pty_bytes(output.as_bytes())]
}

fn flag_value(args: &[String], flag: &str) -> Option<usize> {
    args.iter()
        .position(|arg| arg == flag)
        .and_then(|index| args.get(index + 1))
        .and_then(|value| value.parse().ok())
}

fn handle_list() -> Result<(), Box<dyn std::error::Error>> {
    let recordings_dir = std::path::Path::new("recordings");
    if !recordings_dir.exists() {
        println!("No recordings directory found.");
        return Ok(());
    }

    println!("=== Available Recordings ===");
    let mut total = 0usize;
    for entry in std::fs::read_dir(recordings_dir)? {
        let entry = entry?;
        let path = entry.path();
        if path.is_file()
            && let Some(ext) = path.extension()
            && (ext == "ltr" || ext == "jsonl" || ext == "zst")
        {
            let size = entry.metadata()?.len();
            let Some(name) = path.file_name().map(|name| name.to_string_lossy()) else {
                continue;
            };
            let size_str = if size > 1024 * 1024 {
                format!("{:.1} MB", size as f64 / (1024.0 * 1024.0))
            } else if size > 1024 {
                format!("{:.1} KB", size as f64 / 1024.0)
            } else {
                format!("{} B", size)
            };
            println!("  {:40} {}", name, size_str);
            total += 1;
        }
    }
    if total == 0 {
        println!("  (no recordings found)");
    } else {
        println!("\nTotal: {} recordings", total);
    }
    Ok(())
}