use criterion::{criterion_group, criterion_main, Criterion};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::time::{Duration, Instant};

mod data_source;

#[derive(Copy, Clone, Debug)]
struct BgpReader<'s> {
    name: &'s str,
    executable: &'s str,
    args: &'s [&'s str],
}

/// Alternate BGP reading programs to test against listed as the executable name and additional
/// command line arguments. When executed the specified arguments will be added before the input
/// file.
const BGP_READERS: &[BgpReader<'static>] = &[
    BgpReader {
        name: "bgpkit",
        executable: "bgpkit-parser",
        args: &[],
    },
    BgpReader {
        name: "bgpdump",
        executable: "bgpdump",
        args: &["-M"],
    },
    // The remaining items have not been tested. While I attempted to find the correct executable
    // names and arguments, some may not be correct.
    BgpReader {
        name: "libparsebgp",
        executable: "libparsebgp",
        args: &[],
    },
    // Micro BGP Suite also provides a legacy wrapper under bgpscanner's name, so it may be
    // difficult to determine if which version is being used.
    BgpReader {
        name: "bgpscanner",
        executable: "bgpscanner",
        args: &[],
    },
    BgpReader {
        name: "Micro BGP Suite",
        executable: "bgpgrep",
        args: &[],
    },
    #[cfg(unix)]
    BgpReader {
        name: "mrt-parser",
        executable: "mrt",
        args: &["-f"],
    },
];

fn perform_run(executable: &PathBuf, args: &[&str], input_file: &PathBuf) -> Duration {
    // Setup execution of the command
    let mut command_setup = Command::new(executable);
    command_setup
        .args(args)
        .arg(input_file)
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        // As much as I would like inherit the stderr, some of these programs will just spam the
        // console with useless logging information.
        .stderr(Stdio::null());

    let start_time = Instant::now();
    let status = command_setup.status();
    let elapsed = start_time.elapsed();

    // Check to ensure command was successful
    match status {
        Ok(a) if a.success() => {}
        Ok(status) => panic!(
            "{} exited with failing status code {}",
            executable.display(),
            status
        ),
        Err(err) => panic!(
            "An error occurred while attempting to run {}: {}",
            executable.display(),
            err
        ),
    }

    elapsed
}

fn benchmark(c: &mut Criterion) {
    let mut programs_to_test = Vec::new();

    for &program in BGP_READERS {
        let BgpReader {
            name,
            executable,
            args,
        } = program;

        let path_result = match executable {
            // Ensure we always use the latest build of the project. Optimized binary targets are
            // compiled before benchmarks to allow for integration tests so it will always be found
            // at this path.
            "bgpkit-parser" => Ok(data_source::locate_target_dir().join("release/bgpkit-parser")),
            // For remaining programs use which crate to locate their executables on the system
            name => which::which(name),
        };

        match path_result {
            Ok(path) => {
                println!(
                    "Benchmarking {} using: {} {} [input file]",
                    name,
                    path.display(),
                    args.join(" ")
                );
                programs_to_test.push((program, path));
            }
            Err(err) => {
                println!("Unable to locate executable for {}: {}", name, err);
            }
        }
    }

    let update_data = data_source::test_data_file("update-example.gz");
    let mut update_group = c.benchmark_group("update-data.gz");

    for (program, path) in &programs_to_test {
        update_group.bench_function(program.name, |b| {
            b.iter_custom(|n| {
                let mut total_elapsed = Duration::default();

                for _ in 0..n {
                    total_elapsed += perform_run(path, program.args, &update_data);
                }

                total_elapsed
            })
        });
    }

    update_group.finish();

    let rib_data = data_source::test_data_file("rib-example-small.bz2");
    let mut rib_group = c.benchmark_group("rib-data.bz2");

    // The file is fairly large when decompressed, so we only do a couple iterations
    rib_group.sample_size(10);

    for (program, path) in &programs_to_test {
        rib_group.bench_function(program.name, |b| {
            b.iter_custom(|n| {
                let mut total_elapsed = Duration::default();

                for _ in 0..n {
                    total_elapsed += perform_run(path, program.args, &rib_data);
                }

                total_elapsed
            })
        });
    }

    rib_group.finish();
}

criterion_group!(benches, benchmark);
criterion_main!(benches);
