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::hash::hash_index1_key;
use sqpack_rs::{
    SqPack, SqPackHashManifestBuilder, SqPackHashVerifier, SqPackPathManifestBuilder,
    SqPackPathVerifier,
};

mod common;
use common::{
    BLOCK_HEADER_SIZE, BLOCK_TYPE_UNCOMPRESSED, INDEX_ENTRY_SIZE, SQPACK_HEADER_SIZE,
    make_index_header, make_sqpack_header,
};

const ENTRY_COUNT: usize = 64;

fn bench_path(i: usize) -> String {
    format!("exd/parallel_bench_{i:04}.dat")
}

fn append_standard_entry(buf: &mut Vec<u8>, payload: &[u8]) -> u64 {
    let aligned = (buf.len() + 127) & !127;
    buf.resize(aligned, 0);
    let info_off = u64::try_from(buf.len()).unwrap();

    let raw_file_size = u32::try_from(payload.len()).unwrap();
    buf.extend_from_slice(&32u32.to_le_bytes());
    buf.extend_from_slice(&2u32.to_le_bytes());
    buf.extend_from_slice(&raw_file_size.to_le_bytes());
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.extend_from_slice(&1u32.to_le_bytes());

    let block_total =
        u16::try_from(BLOCK_HEADER_SIZE as usize + payload.len()).expect("fits in u16");
    let uncompressed = u16::try_from(payload.len()).expect("fits in u16");
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.extend_from_slice(&block_total.to_le_bytes());
    buf.extend_from_slice(&uncompressed.to_le_bytes());

    buf.extend_from_slice(&BLOCK_HEADER_SIZE.to_le_bytes());
    buf.extend_from_slice(&0u32.to_le_bytes());
    buf.extend_from_slice(&BLOCK_TYPE_UNCOMPRESSED.to_le_bytes());
    buf.extend_from_slice(&u32::from(uncompressed).to_le_bytes());
    buf.extend_from_slice(payload);

    info_off
}

fn data_field(info_off: u64) -> u32 {
    u32::try_from(info_off / 8).expect("fits in u32")
}

fn make_v1_index_clean(entries: &[(u64, u32)]) -> Vec<u8> {
    let mut buf = Vec::new();
    buf.extend_from_slice(&make_sqpack_header(2));
    let index_data_offset = SQPACK_HEADER_SIZE + 1024;
    let index_data_size = u32::try_from(entries.len()).unwrap() * INDEX_ENTRY_SIZE;
    buf.extend_from_slice(&make_index_header(index_data_offset, index_data_size, 1));
    for (hash, data) in entries {
        buf.extend_from_slice(&hash.to_le_bytes());
        buf.extend_from_slice(&data.to_le_bytes());
        buf.extend_from_slice(&0u32.to_le_bytes());
    }
    buf
}

fn build_fixture(tag: &str) -> PathBuf {
    let root = common::unique_tempdir(tag);
    let sqpack_ffxiv = root.join("sqpack").join("ffxiv");
    fs::create_dir_all(&sqpack_ffxiv).expect("mkdir sqpack/ffxiv");

    let mut dat = make_sqpack_header(1);
    let mut v1_entries: Vec<(u64, u32)> = Vec::new();
    for i in 0..ENTRY_COUNT {
        let payload = format!("parallel-bench-payload-{i:04}");
        let off = append_standard_entry(&mut dat, payload.as_bytes());
        let hash = hash_index1_key(&bench_path(i));
        v1_entries.push((hash, data_field(off)));
    }
    v1_entries.sort_by_key(|&(h, _)| h);
    let index_bytes = make_v1_index_clean(&v1_entries);

    fs::write(sqpack_ffxiv.join("0a0000.win32.index"), &index_bytes).expect("write index");
    fs::write(sqpack_ffxiv.join("0a0000.win32.dat0"), &dat).expect("write dat");
    root
}

fn bench_path_keyed_verify(c: &mut Criterion) {
    let root = build_fixture("verify_parallel_path");
    let manifest = {
        let sq = SqPack::open(&root).expect("open install");
        let mut builder = SqPackPathManifestBuilder::new(&sq);
        for i in 0..ENTRY_COUNT {
            builder.add(&bench_path(i)).expect("add path");
        }
        builder.finish()
    };

    let mut group = c.benchmark_group("sqpack.verify_parallel.path_keyed");

    group.bench_with_input(
        BenchmarkId::from_parameter("eager"),
        &(&root, &manifest),
        |b, (root, manifest)| {
            b.iter(|| {
                let sq = SqPack::open(black_box(root)).expect("open install");
                let mut v = SqPackPathVerifier::new(&sq);
                let report = v.verify(black_box(manifest));
                black_box(report);
            });
        },
    );

    group.bench_with_input(
        BenchmarkId::from_parameter("parallel"),
        &(&root, &manifest),
        |b, (root, manifest)| {
            b.iter(|| {
                let sq = SqPack::open(black_box(root)).expect("open install");
                let mut v = SqPackPathVerifier::new(&sq);
                let report = v.verify_parallel(black_box(manifest));
                black_box(report);
            });
        },
    );

    group.finish();
    let _ = fs::remove_dir_all(&root);
}

fn bench_hash_keyed_verify(c: &mut Criterion) {
    let root = build_fixture("verify_parallel_hash");
    let manifest = {
        let sq = SqPack::open(&root).expect("open install");
        let mut builder = SqPackHashManifestBuilder::new(&sq);
        let report = builder.build().expect("build manifest");
        report.manifest
    };

    let mut group = c.benchmark_group("sqpack.verify_parallel.hash_keyed");

    group.bench_with_input(
        BenchmarkId::from_parameter("eager"),
        &(&root, &manifest),
        |b, (root, manifest)| {
            b.iter(|| {
                let sq = SqPack::open(black_box(root)).expect("open install");
                let mut v = SqPackHashVerifier::new(&sq);
                let report = v.verify(black_box(manifest));
                black_box(report);
            });
        },
    );

    group.bench_with_input(
        BenchmarkId::from_parameter("parallel"),
        &(&root, &manifest),
        |b, (root, manifest)| {
            b.iter(|| {
                let sq = SqPack::open(black_box(root)).expect("open install");
                let mut v = SqPackHashVerifier::new(&sq);
                let report = v.verify_parallel(black_box(manifest));
                black_box(report);
            });
        },
    );

    group.finish();
    let _ = fs::remove_dir_all(&root);
}

fn bench_manifest_build(c: &mut Criterion) {
    let root = build_fixture("manifest_build_parallel");

    let mut group = c.benchmark_group("sqpack.verify_parallel.manifest_build");

    group.bench_with_input(BenchmarkId::from_parameter("eager"), &root, |b, root| {
        b.iter(|| {
            let sq = SqPack::open(black_box(root)).expect("open install");
            let mut builder = SqPackHashManifestBuilder::new(&sq);
            let report = builder.build().expect("build");
            black_box(report);
        });
    });

    group.bench_with_input(BenchmarkId::from_parameter("parallel"), &root, |b, root| {
        b.iter(|| {
            let sq = SqPack::open(black_box(root)).expect("open install");
            let mut builder = SqPackHashManifestBuilder::new(&sq);
            let report = builder.build_parallel().expect("build_parallel");
            black_box(report);
        });
    });

    group.finish();
    let _ = fs::remove_dir_all(&root);
}

criterion_group!(
    benches,
    bench_path_keyed_verify,
    bench_hash_keyed_verify,
    bench_manifest_build,
);
criterion_main!(benches);
