use std::hint::black_box;
use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use std::sync::atomic::Ordering::{Acquire, Release};
use std::thread;
use std::time::{Duration, Instant};

use criterion::async_executor::FuturesExecutor;
use criterion::{Criterion, criterion_group, criterion_main};
use saa::{Barrier, Lock};

fn exclusive_unlock_async(c: &mut Criterion) {
    async fn test() {
        let lock = Lock::default();
        lock.lock_async().await;
        assert!(lock.release_lock());
    }
    c.bench_function("lock-exclusive-unlock-async", |b| {
        b.to_async(FuturesExecutor).iter(test);
    });
}

fn exclusive_unlock_sync(c: &mut Criterion) {
    c.bench_function("lock-exclusive-unlock-sync", |b| {
        b.iter(|| {
            let lock = Lock::default();
            lock.lock_sync();
            assert!(lock.release_lock());
        });
    });
}

fn exclusive_unlock_async_with(c: &mut Criterion) {
    async fn test() {
        let lock = Lock::default();
        let mut waited = false;
        lock.lock_async_with(|| {
            waited = true;
            std::thread::yield_now();
        })
        .await;
        assert!(!waited);
        assert!(lock.release_lock());
    }
    c.bench_function("lock-exclusive-unlock-async-with", |b| {
        b.to_async(FuturesExecutor).iter(test);
    });
}

fn exclusive_unlock_sync_with(c: &mut Criterion) {
    c.bench_function("lock-exclusive-unlock-sync-with", |b| {
        b.iter(|| {
            let lock = Lock::default();
            let mut waited = false;
            lock.lock_sync_with(|| {
                waited = true;
                std::thread::yield_now();
            });
            assert!(!waited);
            assert!(lock.release_lock());
        });
    });
}

fn shared_shared_unlock_unlock(c: &mut Criterion) {
    c.bench_function("share-shared-unlock-unlock", |b| {
        b.iter(|| {
            let lock = Lock::default();
            lock.share_sync();
            lock.share_sync();
            assert!(lock.release_share());
            assert!(lock.release_share());
        });
    });
}

fn wait_awake(c: &mut Criterion) {
    c.bench_function("wait_awake", |b| {
        b.iter_custom(|iters| {
            let lock = Arc::new(Lock::default());
            let entered = Arc::new(AtomicBool::new(false));
            let mut acc = Duration::from_secs(0);
            for _ in 0..iters {
                assert!(lock.lock_sync());
                let lock_clone = lock.clone();
                let entered_clone = entered.clone();
                let thread = thread::spawn(move || {
                    let mut start = Instant::now();
                    lock_clone.lock_sync_with(|| {
                        entered_clone.swap(true, Release);
                        start = Instant::now();
                    });
                    start.elapsed()
                });
                while !entered.load(Acquire) {}
                assert!(lock.release_lock());
                acc += thread.join().unwrap();
                assert!(lock.release_lock());
                assert!(entered.swap(false, Release));
            }
            acc
        })
    });
}

fn multi_threaded_workload(iters: u64, num_threads: usize, num_cycles: usize) -> Duration {
    let lock = Arc::new(Lock::default());
    let mut threads = Vec::with_capacity(num_threads);
    let barrier = Arc::new(Barrier::with_count(num_threads));
    for _ in 0..num_threads {
        let barrier = barrier.clone();
        let lock = lock.clone();
        let join = thread::spawn(move || {
            barrier.wait_sync();
            let start = Instant::now();
            for _ in 0..iters {
                assert!(lock.lock_sync());
                let acc = black_box({
                    (black_box(0)..black_box(num_cycles)).fold(black_box(0), |acc, v| acc + v)
                });
                assert_eq!(acc, (0..num_cycles).sum::<usize>());
                assert!(lock.release_lock());
            }
            start.elapsed()
        });
        threads.push(join);
    }
    threads
        .into_iter()
        .fold(Duration::from_nanos(0), |acc, t| acc.max(t.join().unwrap()))
}

fn multi_threaded_workload_std(iters: u64, num_threads: usize, num_cycles: usize) -> Duration {
    let lock = Arc::new(std::sync::Mutex::<()>::default());
    let mut threads = Vec::with_capacity(num_threads);
    let barrier = Arc::new(Barrier::with_count(num_threads));
    for _ in 0..num_threads {
        let barrier = barrier.clone();
        let lock = lock.clone();
        let join = thread::spawn(move || {
            barrier.wait_sync();
            let start = Instant::now();
            for _ in 0..iters {
                let guard = lock.lock().unwrap();
                let acc = black_box({
                    (black_box(0)..black_box(num_cycles)).fold(black_box(0), |acc, v| acc + v)
                });
                assert_eq!(acc, (0..num_cycles).sum::<usize>());
                drop(guard);
            }
            start.elapsed()
        });
        threads.push(join);
    }
    threads
        .into_iter()
        .fold(Duration::from_nanos(0), |acc, t| acc.max(t.join().unwrap()))
}

macro_rules! multi_threaded {
    ($name:ident, $num_threads:expr, $num_cycles:expr) => {
        fn $name(c: &mut Criterion) {
            c.bench_function(stringify!($name), |b| {
                b.iter_custom(|iters| multi_threaded_workload(iters, $num_threads, $num_cycles))
            });
        }
    };
}

macro_rules! multi_threaded_std {
    ($name:ident, $num_threads:expr, $num_cycles:expr) => {
        fn $name(c: &mut Criterion) {
            c.bench_function(stringify!($name), |b| {
                b.iter_custom(|iters| multi_threaded_workload_std(iters, $num_threads, $num_cycles))
            });
        }
    };
}

multi_threaded!(multi_threaded_2_256, 2, 256);
multi_threaded!(multi_threaded_2_65536, 2, 65536);
multi_threaded!(multi_threaded_8_256, 8, 256);
multi_threaded!(multi_threaded_8_65536, 8, 65536);
multi_threaded_std!(multi_threaded_std_2_256, 2, 256);
multi_threaded_std!(multi_threaded_std_2_65536, 2, 65536);
multi_threaded_std!(multi_threaded_std_8_256, 8, 256);
multi_threaded_std!(multi_threaded_std_8_65536, 8, 65536);

criterion_group!(
    lock,
    exclusive_unlock_async,
    exclusive_unlock_sync,
    exclusive_unlock_async_with,
    exclusive_unlock_sync_with,
    shared_shared_unlock_unlock,
    wait_awake,
    multi_threaded_2_256,
    multi_threaded_2_65536,
    multi_threaded_8_256,
    multi_threaded_8_65536,
    multi_threaded_std_2_256,
    multi_threaded_std_2_65536,
    multi_threaded_std_8_256,
    multi_threaded_std_8_65536,
);
criterion_main!(lock);
