use std::fs;
use std::path::PathBuf;
use std::sync::Arc;

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::{MmapFs, SqPack, SqPackHashManifestBuilder, SqPackHashVerifier, Vfs};

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 = 500;
const ARCHIVE_STEM: &str = "0a0000.win32";

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() -> Fixture {
    let root = common::unique_tempdir("mmap_vs_file");
    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 entries: Vec<(u64, u32)> = Vec::with_capacity(ENTRY_COUNT);
    let mut paths: Vec<String> = Vec::with_capacity(ENTRY_COUNT);

    for i in 0..ENTRY_COUNT {
        let path = format!("exd/mmap_bench_{i:04}.dat");
        let payload_len = 128 + (i % 8) * 32;
        let payload: Vec<u8> = (0..payload_len)
            .map(|j| u8::try_from((i + j) & 0xff).expect("masked to 0..=255"))
            .collect();
        let off = append_standard_entry(&mut dat, &payload);
        let hash = hash_index1_key(&path);
        entries.push((hash, data_field(off)));
        paths.push(path);
    }
    entries.sort_by_key(|&(h, _)| h);

    let index_bytes = make_v1_index_clean(&entries);

    fs::write(
        sqpack_ffxiv.join(format!("{ARCHIVE_STEM}.index")),
        &index_bytes,
    )
    .expect("write index");
    fs::write(sqpack_ffxiv.join(format!("{ARCHIVE_STEM}.dat0")), &dat).expect("write dat");

    let mut sample = Vec::with_capacity(100);
    let mut i = 0usize;
    while sample.len() < 100 {
        sample.push(paths[i % ENTRY_COUNT].clone());
        i += 7;
    }

    Fixture {
        root,
        random_paths: sample,
    }
}

struct Fixture {
    root: PathBuf,
    random_paths: Vec<String>,
}

fn run_random_reads(sq: &SqPack, paths: &[String]) -> usize {
    let mut bytes_read = 0usize;
    for p in paths {
        let bytes = sq.read_file(p).expect("read_file");
        bytes_read += bytes.len();
    }
    bytes_read
}

fn run_verify(sq: &SqPack, manifest: &sqpack_rs::SqPackHashManifest) -> usize {
    let mut verifier = SqPackHashVerifier::new(sq);
    let report = verifier.verify(manifest);
    report.verified_count
}

fn run_manifest_build(sq: &SqPack) -> usize {
    let mut builder = SqPackHashManifestBuilder::new(sq);
    let report = builder.build().expect("manifest build");
    report.manifest.len()
}

fn random_reads(c: &mut Criterion) {
    let fixture = build_fixture();
    let mut g = c.benchmark_group("random_reads");
    g.sample_size(20);

    let std_vfs: Arc<dyn Vfs> = Arc::new(sqpack_rs::StdFs::new());
    let sq_file =
        SqPack::open_with_vfs(&fixture.root, Arc::clone(&std_vfs)).expect("open with StdFs");

    g.bench_with_input(BenchmarkId::new("file", ENTRY_COUNT), &(), |b, ()| {
        b.iter(|| {
            black_box(run_random_reads(&sq_file, &fixture.random_paths));
        });
    });

    let mmap_vfs: Arc<dyn Vfs> = Arc::new(MmapFs::new());
    let sq_mmap =
        SqPack::open_with_vfs(&fixture.root, Arc::clone(&mmap_vfs)).expect("open with MmapFs");

    g.bench_with_input(BenchmarkId::new("mmap", ENTRY_COUNT), &(), |b, ()| {
        b.iter(|| {
            black_box(run_random_reads(&sq_mmap, &fixture.random_paths));
        });
    });

    g.finish();
}

fn verify_scan(c: &mut Criterion) {
    let fixture = build_fixture();
    let mut g = c.benchmark_group("verify_scan");
    g.sample_size(20);

    let std_vfs: Arc<dyn Vfs> = Arc::new(sqpack_rs::StdFs::new());
    let sq_file =
        SqPack::open_with_vfs(&fixture.root, Arc::clone(&std_vfs)).expect("open with StdFs");
    let manifest_file = SqPackHashManifestBuilder::new(&sq_file)
        .build()
        .expect("build manifest (file)")
        .manifest;

    g.bench_with_input(BenchmarkId::new("file", ENTRY_COUNT), &(), |b, ()| {
        b.iter(|| {
            black_box(run_verify(&sq_file, &manifest_file));
        });
    });

    let mmap_vfs: Arc<dyn Vfs> = Arc::new(MmapFs::new());
    let sq_mmap =
        SqPack::open_with_vfs(&fixture.root, Arc::clone(&mmap_vfs)).expect("open with MmapFs");
    let manifest_mmap = SqPackHashManifestBuilder::new(&sq_mmap)
        .build()
        .expect("build manifest (mmap)")
        .manifest;

    g.bench_with_input(BenchmarkId::new("mmap", ENTRY_COUNT), &(), |b, ()| {
        b.iter(|| {
            black_box(run_verify(&sq_mmap, &manifest_mmap));
        });
    });

    g.finish();
}

fn manifest_build(c: &mut Criterion) {
    let fixture = build_fixture();
    let mut g = c.benchmark_group("manifest_build");
    g.sample_size(20);

    let std_vfs: Arc<dyn Vfs> = Arc::new(sqpack_rs::StdFs::new());
    let sq_file =
        SqPack::open_with_vfs(&fixture.root, Arc::clone(&std_vfs)).expect("open with StdFs");

    g.bench_with_input(BenchmarkId::new("file", ENTRY_COUNT), &(), |b, ()| {
        b.iter(|| {
            black_box(run_manifest_build(&sq_file));
        });
    });

    let mmap_vfs: Arc<dyn Vfs> = Arc::new(MmapFs::new());
    let sq_mmap =
        SqPack::open_with_vfs(&fixture.root, Arc::clone(&mmap_vfs)).expect("open with MmapFs");

    g.bench_with_input(BenchmarkId::new("mmap", ENTRY_COUNT), &(), |b, ()| {
        b.iter(|| {
            black_box(run_manifest_build(&sq_mmap));
        });
    });

    g.finish();
}

criterion_group!(benches, random_reads, verify_scan, manifest_build);
criterion_main!(benches);
