use std::fs;
use std::hint::black_box;
use std::num::NonZeroUsize;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};

use codspeed_criterion_compat::{BenchmarkId, Criterion, criterion_group, criterion_main};

use lru::LruCache;
use sqpack_rs::SqPack;
use sqpack_rs::hash::hash_index1_key;

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 = 50;
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
}

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

fn build_fixture() -> Fixture {
    let root = common::unique_tempdir("lru_whole_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/lru_whole_{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"))
            .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 = make_v1_index_clean(&entries);
    fs::write(sqpack_ffxiv.join(format!("{ARCHIVE_STEM}.index")), &index).expect("write index");
    fs::write(sqpack_ffxiv.join(format!("{ARCHIVE_STEM}.dat0")), &dat).expect("write dat");

    Fixture { root, paths }
}

struct LruWholeFile<'a> {
    sq: &'a SqPack,
    cache: Mutex<LruCache<u64, Arc<Vec<u8>>>>,
}

impl<'a> LruWholeFile<'a> {
    fn new(sq: &'a SqPack, capacity: usize) -> Self {
        Self {
            sq,
            cache: Mutex::new(LruCache::new(NonZeroUsize::new(capacity).expect("nonzero"))),
        }
    }

    fn read_file(&self, path: &str) -> Arc<Vec<u8>> {
        let key = hash_index1_key(path);
        if let Some(hit) = self.cache.lock().expect("cache poisoned").get(&key) {
            return Arc::clone(hit);
        }
        let bytes = Arc::new(self.sq.read_file(path).expect("read_file"));
        self.cache
            .lock()
            .expect("cache poisoned")
            .put(key, Arc::clone(&bytes));
        bytes
    }
}

fn unique_workload(paths: &[String]) -> Vec<String> {
    paths.to_vec()
}

fn mixed_workload(paths: &[String]) -> Vec<String> {
    let mut out = Vec::with_capacity(paths.len() * 2);
    for (i, p) in paths.iter().enumerate() {
        out.push(p.clone());
        if i >= 1 {
            out.push(paths[i - 1].clone());
        }
    }
    for p in paths.iter().take(paths.len() - 1) {
        out.push(p.clone());
    }
    out
}

fn hot_loop_workload(paths: &[String]) -> Vec<String> {
    let hot = &paths[..5];
    let mut out = Vec::with_capacity(100);
    for _ in 0..20 {
        for p in hot {
            out.push(p.clone());
        }
    }
    out
}

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

fn run_lru(lru: &LruWholeFile<'_>, workload: &[String]) -> usize {
    let mut total = 0usize;
    for p in workload {
        let bytes = lru.read_file(p);
        total += bytes.len();
    }
    total
}

fn bench_one(c: &mut Criterion, group_name: &str, workload_fn: fn(&[String]) -> Vec<String>) {
    let fixture = build_fixture();
    let mut g = c.benchmark_group(group_name);
    g.sample_size(20);

    let sq_baseline = SqPack::open(&fixture.root).expect("open baseline");
    let sq_lru_inner = SqPack::open(&fixture.root).expect("open lru");
    let workload = workload_fn(&fixture.paths);

    g.bench_with_input(
        BenchmarkId::new("baseline", workload.len()),
        &(),
        |b, ()| {
            b.iter(|| {
                black_box(run_baseline(&sq_baseline, &workload));
            });
        },
    );

    g.bench_with_input(BenchmarkId::new("lru", workload.len()), &(), |b, ()| {
        b.iter(|| {
            let lru = LruWholeFile::new(&sq_lru_inner, 256);
            black_box(run_lru(&lru, &workload));
        });
    });

    g.finish();
}

fn unique_only(c: &mut Criterion) {
    bench_one(c, "unique_only", unique_workload);
}

fn mixed(c: &mut Criterion) {
    bench_one(c, "mixed", mixed_workload);
}

fn hot_loop(c: &mut Criterion) {
    bench_one(c, "hot_loop", hot_loop_workload);
}

criterion_group!(benches, unique_only, mixed, hot_loop);
criterion_main!(benches);
