//! Benchmark suite for roto — themed after the 1995 film *Hackers*.
//!
//! Proto schema: `proto/hackers.proto`
//! Generated types: `src/hackers.rs`  (via `protoc-gen-roto`)
//!
//! # Setup
//!
//! Generate the data files once before running benchmarks:
//!
//! ```sh
//! cargo run --release --bin gen_bench_data -- --preset tiny
//! cargo run --release --bin gen_bench_data -- --preset small
//! cargo run --release --bin gen_bench_data -- --preset medium
//! cargo run --release --bin gen_bench_data -- --preset large
//! ```
//!
//! Then run:
//!
//! ```sh
//! cargo bench --bench hackers_bench
//! ```
//!
//! Benchmark groups:
//! - `shallow_parse`  — `Campaign::new(data)`, one scan of the whole blob
//! - `deep_parse`     — Campaign → Operations → Hackers (a `::new()` per level)
//! - `field_access`   — individual field reads on pre-parsed messages (O(1))
//! - `iterate`        — counting repeated fields at different nesting depths

use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use roto_benches::hackers::{Campaign, CampaignBuilder, Hacker, Operation, Worm};
use std::hint::black_box;

// =============================================================================
// Data loading
// =============================================================================

/// Load a pre-generated data file from `data/bench/<name>.pb`.
/// Returns `None` (and prints a hint) if the file does not exist.
fn load(name: &str) -> Option<Vec<u8>> {
    let path = format!("data/bench/{name}.pb");
    match std::fs::read(&path) {
        Ok(data) => Some(data),
        Err(_) => {
            eprintln!(
                "[skip] {path} not found — \
                 run `cargo run --release --bin gen_bench_data -- --preset {name}` first"
            );
            None
        }
    }
}

// =============================================================================
// Benchmarks
// =============================================================================

/// `Campaign::new()` — one linear scan to record field offsets, no allocation.
/// Throughput reported in MB/s so different sizes are directly comparable.
fn bench_shallow_parse(c: &mut Criterion) {
    let cases = [
        ("tiny", load("tiny")),
        ("small", load("small")),
        ("medium", load("medium")),
        ("large", load("large")),
    ];

    let mut group = c.benchmark_group("shallow_parse");
    for (label, maybe_data) in &cases {
        let Some(data) = maybe_data else { continue };
        group.throughput(Throughput::Bytes(data.len() as u64));
        group.bench_with_input(BenchmarkId::new("Campaign::new", label), data, |b, data| {
            b.iter(|| Campaign::new(black_box(data)).unwrap())
        });
    }
    group.finish();
}

/// Walk every level of the tree: Campaign → Operations → Hackers.
/// Each `::new()` is an additional linear scan of that sub-message's bytes.
fn bench_deep_parse(c: &mut Criterion) {
    let cases = [
        ("tiny", load("tiny")),
        ("small", load("small")),
        ("medium", load("medium")),
    ];

    let mut group = c.benchmark_group("deep_parse");
    for (label, maybe_data) in &cases {
        let Some(data) = maybe_data else { continue };
        group.throughput(Throughput::Bytes(data.len() as u64));
        group.bench_with_input(
            BenchmarkId::new("Campaign+Ops+Hackers", label),
            data,
            |b, data| {
                b.iter(|| {
                    let campaign = Campaign::new(data).unwrap();
                    let mut hacker_count = 0usize;
                    for op_res in campaign.operations() {
                        let (op_bytes, _) = op_res.unwrap();
                        let op = Operation::new(op_bytes).unwrap();
                        for crew_res in op.crew() {
                            let (hacker_bytes, _) = crew_res.unwrap();
                            let hacker = Hacker::new(hacker_bytes).unwrap();
                            let _ = black_box(hacker.handle().unwrap());
                            hacker_count += 1;
                        }
                    }
                    black_box(hacker_count)
                })
            },
        );
    }
    group.finish();
}

/// O(1) field accesses on pre-parsed messages.
/// Measures only the decode step at a known offset — not the scan.
fn bench_field_access(c: &mut Criterion) {
    let Some(data) = load("small") else { return };

    let campaign = Campaign::new(&data).unwrap();
    let (op_bytes, _) = campaign.operations().next().unwrap().unwrap();
    let op = Operation::new(op_bytes).unwrap();
    let (hacker_bytes, _) = op.crew().next().unwrap().unwrap();
    let hacker = Hacker::new(hacker_bytes).unwrap();
    let worm = Worm::new(op.worm().unwrap()).unwrap();

    let mut group = c.benchmark_group("field_access");

    group.bench_function("campaign::name", |b| {
        b.iter(|| black_box(campaign.name().unwrap()))
    });
    group.bench_function("campaign::total_bytes_stolen", |b| {
        b.iter(|| black_box(campaign.total_bytes_stolen().unwrap()))
    });
    group.bench_function("operation::codename", |b| {
        b.iter(|| black_box(op.codename().unwrap()))
    });
    group.bench_function("operation::timestamp", |b| {
        b.iter(|| black_box(op.timestamp().unwrap()))
    });
    group.bench_function("operation::successful", |b| {
        b.iter(|| black_box(op.successful().unwrap()))
    });
    group.bench_function("hacker::handle", |b| {
        b.iter(|| black_box(hacker.handle().unwrap()))
    });
    group.bench_function("hacker::skill_level (f32)", |b| {
        b.iter(|| black_box(hacker.skill_level().unwrap()))
    });
    group.bench_function("hacker::is_elite (bool)", |b| {
        b.iter(|| black_box(hacker.is_elite().unwrap()))
    });
    group.bench_function("worm::polymorphic (bool)", |b| {
        b.iter(|| black_box(worm.polymorphic().unwrap()))
    });
    group.bench_function("worm::payload (bytes)", |b| {
        b.iter(|| black_box(worm.payload().unwrap()))
    });

    group.finish();
}

/// Iterate repeated fields at different depths.
fn bench_iterate(c: &mut Criterion) {
    let cases = [
        ("tiny", load("tiny")),
        ("small", load("small")),
        ("medium", load("medium")),
    ];

    let mut group = c.benchmark_group("iterate");
    for (label, maybe_data) in &cases {
        let Some(data) = maybe_data else { continue };

        // Top-level repeated field — walk Operation blobs, no inner parse.
        group.bench_with_input(
            BenchmarkId::new("count_operations", label),
            data,
            |b, data| {
                b.iter(|| {
                    let campaign = Campaign::new(data).unwrap();
                    black_box(campaign.operations().count())
                })
            },
        );

        // Nested repeated field — parse each Operation to reach its crew.
        group.bench_with_input(
            BenchmarkId::new("count_all_crew", label),
            data,
            |b, data| {
                b.iter(|| {
                    let campaign = Campaign::new(data).unwrap();
                    let mut n = 0usize;
                    for op_res in campaign.operations() {
                        let (op_bytes, _) = op_res.unwrap();
                        n += Operation::new(op_bytes).unwrap().crew().count();
                    }
                    black_box(n)
                })
            },
        );
    }
    group.finish();
}

/// Parse, update a field, and serialize back to a buffer.
fn bench_read_update_write(c: &mut Criterion) {
    let cases = [
        ("tiny", load("tiny")),
        ("small", load("small")),
        ("medium", load("medium")),
    ];

    let mut group = c.benchmark_group("read_update_write");
    for (label, maybe_data) in &cases {
        let Some(data) = maybe_data else { continue };
        group.throughput(Throughput::Bytes(data.len() as u64));
        group.bench_with_input(BenchmarkId::new("Campaign", label), data, |b, data| {
            b.iter(|| {
                let campaign = Campaign::new(data).unwrap();
                let mut buf = vec![0u8; data.len() * 2];
                let res = CampaignBuilder::builder(&mut buf)
                    .name("updated")
                    .unwrap()
                    .with(&campaign)
                    .unwrap()
                    .finish();
                black_box(res.unwrap().len())
            })
        });
    }
    group.finish();
}

criterion_group!(
    benches,
    bench_shallow_parse,
    bench_deep_parse,
    bench_field_access,
    bench_iterate,
    bench_read_update_write
);
criterion_main!(benches);
