use std::{
    sync::{Arc, Barrier},
    thread,
    time::{Duration, Instant},
};

struct BenchmarkResult {
    runs: u64,
    duration: Duration,
}

pub fn benchmark<F>(name: &str, runs: u64, warmup: u64, parallel_count: u64, mut func: F)
where
    F: FnMut(),
{
    for _ in 0..warmup {
        std::hint::black_box(func());
    }
    let start = Instant::now();
    for _ in 0..runs {
        std::hint::black_box(func());
    }
    let duration = start.elapsed();
    let result = BenchmarkResult { runs, duration };
    let hashes_per_second =
        (result.runs as f64 / result.duration.as_secs_f64()) * parallel_count as f64;
    print_report(name, hashes_per_second);
}

pub fn benchmark_parallel<F>(
    name: &str,
    runs: u64,
    parallel_count: u64,
    thread_count: usize,
    mut func: F,
) where
    F: FnMut() + Send + Clone + 'static,
{
    let barrier = Arc::new(Barrier::new(thread_count));
    let mut handles = Vec::with_capacity(thread_count);

    let start_time = Instant::now();

    for _ in 0..thread_count {
        let mut func_clone = func.clone();
        let c_barrier = Arc::clone(&barrier);

        handles.push(thread::spawn(move || {
            c_barrier.wait();

            for _ in 0..runs {
                func_clone();
            }
        }));
    }

    for handle in handles {
        handle.join().unwrap();
    }

    let duration = start_time.elapsed();

    let result = BenchmarkResult { runs, duration };
    let hashes_per_second = (result.runs as f64 / result.duration.as_secs_f64())
        * parallel_count as f64
        * thread_count as f64;

    print_report(name, hashes_per_second);
}

fn print_report(name: &str, hashes_per_second: f64) {
    println!(
        "{} ran at {}",
        name,
        format_hashes_per_second(hashes_per_second)
    );
}

fn format_hashes_per_second(hashes_per_second: f64) -> String {
    let gh_delimiter = 1_000_000_000.0;
    let mh_delimiter = 1_000_000.0;
    let kh_delimiter = 1_000.0;
    if hashes_per_second > gh_delimiter {
        format!("{:.2}GH/s", hashes_per_second / gh_delimiter)
    } else if hashes_per_second > mh_delimiter {
        format!("{:.2}MH/s", hashes_per_second / mh_delimiter)
    } else if hashes_per_second > kh_delimiter {
        format!("{:.2}KH/s", hashes_per_second / kh_delimiter)
    } else {
        format!("{:.2}H/s", hashes_per_second)
    }
}
