use std::{
    hint::black_box,
    sync::{
        Arc,
        atomic::{AtomicU32, Ordering},
    },
};

use criterion::{Bencher, BenchmarkId, Criterion, criterion_group, criterion_main};

use battlesnake::types::{
    Coord,
    simulation::Game as Board,
    wire::{Battlesnake, Board as WireBoard, Game, Request, RoyaleSettings, Ruleset, Settings},
};
use rand::{SeedableRng, rngs::SmallRng};

fn create_start_snake(coord: Coord) -> Battlesnake {
    let id: Arc<str> = format!("{coord:?}").into();
    Battlesnake {
        id: id.clone(),
        name: id.clone(),
        health: 100,
        body: vec![coord; 3],
        latency: "0".into(),
        head: coord,
        length: 3,
        shout: None,
        squad: id,
    }
}

fn create_standard_start_request(starts: [Coord; 4]) -> Request {
    Request {
        game: Game {
            id: "test".into(),
            ruleset: Ruleset {
                name: "standard".into(),
                version: "0".into(),
                settings: Settings {
                    food_spawn_chance: 15,
                    minimum_food: 1,
                    hazard_damage_per_turn: 30,
                    royale: RoyaleSettings {
                        shrink_every_n_turns: 20,
                    },
                },
            },
            map: "standard".into(),
            timeout: 500,
            source: "other".into(),
        },
        turn: 0,
        board: WireBoard {
            height: 11,
            width: 11,
            food: vec![Coord { x: 5, y: 5 }],
            hazards: vec![],
            snakes: vec![
                create_start_snake(starts[0]),
                create_start_snake(starts[1]),
                create_start_snake(starts[2]),
                create_start_snake(starts[3]),
            ],
        },
        you: create_start_snake(starts[0]),
    }
}

fn standard(c: &mut Criterion) {
    let turns_min = AtomicU32::new(u32::MAX);
    let turns_max = AtomicU32::new(u32::MIN);
    let turns_sum = AtomicU32::new(0);
    let turns_total = AtomicU32::new(0);
    let mut group = c.benchmark_group("standard");
    group.sample_size(10000);

    let benchmark = |b: &mut Bencher, board: &Board| {
        b.iter(|| {
            let mut board = board.clone();
            board.simulate_random(&mut SmallRng::from_os_rng());
            let turn = board.board.turn();

            if turn < turns_min.load(Ordering::Relaxed) {
                turns_min.store(turn, Ordering::Relaxed);
            }
            if turn > turns_max.load(Ordering::Relaxed) {
                turns_max.store(turn, Ordering::Relaxed);
            }
            turns_sum.fetch_add(turn, Ordering::Relaxed);
            turns_total.fetch_add(1, Ordering::Relaxed);
        });
    };

    let request = create_standard_start_request([
        Coord { x: 1, y: 1 },
        Coord { x: 9, y: 1 },
        Coord { x: 1, y: 9 },
        Coord { x: 9, y: 9 },
    ]);
    let board = Board::from(&request);
    group.bench_with_input(
        BenchmarkId::from_parameter("start x"),
        black_box(&board),
        benchmark,
    );
    {
        let max = turns_max.load(Ordering::Relaxed);
        let min = turns_min.load(Ordering::Relaxed);
        let sum = turns_sum.load(Ordering::Relaxed);
        let total = turns_total.load(Ordering::Relaxed);
        let avg = sum / total;
        println!("turns: [{min}, {max}] avg {avg} @ {total} samples");
        turns_max.store(u32::MIN, Ordering::Relaxed);
        turns_min.store(u32::MAX, Ordering::Relaxed);
        turns_sum.store(0, Ordering::Relaxed);
        turns_total.store(0, Ordering::Relaxed);
    }

    let request = create_standard_start_request([
        Coord { x: 5, y: 1 },
        Coord { x: 1, y: 5 },
        Coord { x: 5, y: 9 },
        Coord { x: 9, y: 5 },
    ]);
    let board = Board::from(&request);
    group.bench_with_input(
        BenchmarkId::from_parameter("start +"),
        black_box(&board),
        benchmark,
    );
    {
        let max = turns_max.load(Ordering::Relaxed);
        let min = turns_min.load(Ordering::Relaxed);
        let sum = turns_sum.load(Ordering::Relaxed);
        let total = turns_total.load(Ordering::Relaxed);
        let avg = sum / total;
        println!("turns: [{min}, {max}] avg {avg} @ {total} samples");
    }
}

fn constrictor(c: &mut Criterion) {
    let turns_min = AtomicU32::new(u32::MAX);
    let turns_max = AtomicU32::new(u32::MIN);
    let turns_sum = AtomicU32::new(0);
    let turns_total = AtomicU32::new(0);
    let mut group = c.benchmark_group("constrictor");
    group.sample_size(10000);

    let benchmark = |b: &mut Bencher, board: &Board| {
        b.iter(|| {
            let mut board = board.clone();
            board.simulate_random(&mut SmallRng::from_os_rng());
            let turn = board.board.turn();

            if turn < turns_min.load(Ordering::Relaxed) {
                turns_min.store(turn, Ordering::Relaxed);
            }
            if turn > turns_max.load(Ordering::Relaxed) {
                turns_max.store(turn, Ordering::Relaxed);
            }
            turns_sum.fetch_add(turn, Ordering::Relaxed);
            turns_total.fetch_add(1, Ordering::Relaxed);
        });
    };

    let mut request = create_standard_start_request([
        Coord { x: 1, y: 1 },
        Coord { x: 9, y: 1 },
        Coord { x: 1, y: 9 },
        Coord { x: 9, y: 9 },
    ]);
    request.game.ruleset.name = "constrictor".into();
    let board = Board::from(&request);
    group.bench_with_input(
        BenchmarkId::from_parameter("start x"),
        black_box(&board),
        benchmark,
    );
    {
        let max = turns_max.load(Ordering::Relaxed);
        let min = turns_min.load(Ordering::Relaxed);
        let sum = turns_sum.load(Ordering::Relaxed);
        let total = turns_total.load(Ordering::Relaxed);
        let avg = sum / total;
        println!("turns: [{min}, {max}] avg {avg} @ {total} samples");
        turns_max.store(u32::MIN, Ordering::Relaxed);
        turns_min.store(u32::MAX, Ordering::Relaxed);
        turns_sum.store(0, Ordering::Relaxed);
        turns_total.store(0, Ordering::Relaxed);
    }

    let mut request = create_standard_start_request([
        Coord { x: 5, y: 1 },
        Coord { x: 1, y: 5 },
        Coord { x: 5, y: 9 },
        Coord { x: 9, y: 5 },
    ]);
    request.game.ruleset.name = "constrictor".into();
    let board = Board::from(&request);
    group.bench_with_input(
        BenchmarkId::from_parameter("start +"),
        black_box(&board),
        benchmark,
    );
    {
        let max = turns_max.load(Ordering::Relaxed);
        let min = turns_min.load(Ordering::Relaxed);
        let sum = turns_sum.load(Ordering::Relaxed);
        let total = turns_total.load(Ordering::Relaxed);
        let avg = sum / total;
        println!("turns: [{min}, {max}] avg {avg} @ {total} samples");
    }
}

fn clone(c: &mut Criterion) {
    let request = create_standard_start_request([
        Coord { x: 1, y: 1 },
        Coord { x: 9, y: 1 },
        Coord { x: 1, y: 9 },
        Coord { x: 9, y: 9 },
    ]);
    let board = Board::from(&request);

    c.bench_function("clone", |b| b.iter(|| board.clone()));
}

criterion_group!(benches, standard, constrictor, clone);
criterion_main!(benches);
