use std::fs;
use std::path::PathBuf;

use codspeed_criterion_compat::{BenchmarkId, Criterion, criterion_group, criterion_main};
use std::hint::black_box;

use sqpack_rs::SqPack;
use sqpack_rs::hash::hash_index1_key;

mod common;

const GAME_PATH: &str = "exd/root.exl";

const INDEX_ENTRY_COUNT: usize = 1000;

struct Fixture {
    root: PathBuf,
}

impl Fixture {
    fn new() -> Self {
        let root = common::unique_tempdir("read_file");
        let sqpack_ffxiv = root.join("sqpack").join("ffxiv");
        fs::create_dir_all(&sqpack_ffxiv).expect("mkdir sqpack/ffxiv");

        let a = common::build_compressible_payload(2048);
        let b: Vec<u8> = vec![0xABu8; 2048];
        let c = common::build_compressible_payload(3072);
        let d: Vec<u8> = vec![0xCDu8; 3072];
        let blocks: [(&[u8], bool); 4] = [
            (a.as_slice(), true),
            (b.as_slice(), false),
            (c.as_slice(), true),
            (d.as_slice(), false),
        ];
        let (dat_buf, file_info_offset) = common::make_standard_dat(&blocks);

        let dat_path = sqpack_ffxiv.join("0a0000.win32.dat0");
        fs::write(&dat_path, &dat_buf).expect("write .dat0 fixture");

        assert!(
            file_info_offset % 8 == 0,
            "file_info_offset must be a multiple of 8 to fit in the v1 data field",
        );
        let raw_offset = u32::try_from(file_info_offset / 8).expect("offset fits in u32");
        assert_eq!(
            raw_offset & 0xF,
            0,
            "low nibble must be clear for data_file_id=0"
        );
        let real_data = raw_offset;
        let real_hash = hash_index1_key(GAME_PATH);

        let mut entries: Vec<(u64, u32)> = Vec::with_capacity(INDEX_ENTRY_COUNT);
        entries.push((real_hash, real_data));
        for i in 0..(INDEX_ENTRY_COUNT - 1) {
            let synth_hash = 0xF000_0000_0000_0000_u64 + u64::try_from(i).unwrap();
            let synth_data: u32 = 0;
            entries.push((synth_hash, synth_data));
        }
        let index_buf = common::make_index_file(&entries);
        let index_path = sqpack_ffxiv.join("0a0000.win32.index");
        fs::write(&index_path, &index_buf).expect("write .index fixture");

        let sq = SqPack::open(&root).expect("fixture install must open");
        let bytes = sq
            .read_file(GAME_PATH)
            .expect("fixture path must be readable");
        let expected_len: usize = a.len() + b.len() + c.len() + d.len();
        assert_eq!(
            bytes.len(),
            expected_len,
            "fixture payload length mismatch — bench would be measuring the wrong thing",
        );

        Fixture { root }
    }
}

impl Drop for Fixture {
    fn drop(&mut self) {
        let _ = fs::remove_dir_all(&self.root);
    }
}

fn bench_read_file(c: &mut Criterion) {
    let fixture = Fixture::new();
    let mut group = c.benchmark_group("sqpack.read_file");

    group.bench_with_input(
        BenchmarkId::from_parameter("cold_cache"),
        &fixture.root,
        |b, root| {
            b.iter(|| {
                let sq = SqPack::open(black_box(root)).expect("open install");
                let bytes = sq.read_file(black_box(GAME_PATH)).expect("read fixture");
                black_box(bytes);
            });
        },
    );

    group.finish();
    drop(fixture);
}

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