use std::fmt::Write as _;
use std::hint::black_box;
use std::num::NonZeroUsize;
use std::sync::{Arc, Mutex};

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

use lru::LruCache;
use sqpack_rs::Language;
use sqpack_rs::excel::{ExcelData, ExcelHeader};

const COL_INT32: u16 = 0x0006;
const COL_UINT32: u16 = 0x0007;
const COL_UINT16: u16 = 0x0005;
const COL_UINT8: u16 = 0x0003;
const COL_BOOL: u16 = 0x0001;
const COL_STRING: u16 = 0x0000;

fn build_item_like_exh(pages: &[(u32, u32)], languages: &[u8]) -> Vec<u8> {
    let columns: &[(u16, u16)] = &[
        (COL_INT32, 0),
        (COL_INT32, 4),
        (COL_INT32, 8),
        (COL_INT32, 12),
        (COL_INT32, 16),
        (COL_INT32, 20),
        (COL_INT32, 24),
        (COL_INT32, 28),
        (COL_INT32, 32),
        (COL_INT32, 36),
        (COL_UINT32, 40),
        (COL_UINT32, 44),
        (COL_UINT32, 48),
        (COL_UINT32, 52),
        (COL_UINT16, 56),
        (COL_UINT16, 58),
        (COL_UINT16, 60),
        (COL_UINT16, 62),
        (COL_UINT8, 64),
        (COL_UINT8, 65),
        (COL_BOOL, 66),
        (COL_STRING, 68),
        (0x0019, 72),
        (0x001A, 72),
        (0x001B, 72),
        (0x001C, 72),
        (0x001D, 72),
        (0x001E, 72),
        (0x001F, 72),
        (0x0020, 72),
    ];

    let column_count = u16::try_from(columns.len()).expect("fits u16");
    let page_count = u16::try_from(pages.len()).expect("fits u16");
    let language_count = u16::try_from(languages.len()).expect("fits u16");
    let total_rows: u32 = pages.iter().map(|(_, count)| count).sum();

    let mut buf = Vec::new();
    buf.extend_from_slice(b"EXHF");
    buf.extend_from_slice(&3u16.to_be_bytes());
    buf.extend_from_slice(&76u16.to_be_bytes());
    buf.extend_from_slice(&column_count.to_be_bytes());
    buf.extend_from_slice(&page_count.to_be_bytes());
    buf.extend_from_slice(&language_count.to_be_bytes());
    buf.extend_from_slice(&0u16.to_be_bytes());
    buf.push(0u8);
    buf.push(1u8);
    buf.extend_from_slice(&0u16.to_be_bytes());
    buf.extend_from_slice(&total_rows.to_be_bytes());
    buf.extend_from_slice(&0u32.to_be_bytes());
    buf.extend_from_slice(&0u32.to_be_bytes());
    for &(ty, off) in columns {
        buf.extend_from_slice(&ty.to_be_bytes());
        buf.extend_from_slice(&off.to_be_bytes());
    }
    for &(start, count) in pages {
        buf.extend_from_slice(&start.to_be_bytes());
        buf.extend_from_slice(&count.to_be_bytes());
    }
    for &lang in languages {
        buf.push(lang);
        buf.push(0u8);
    }
    buf
}

fn build_item_like_exd(row_count: usize) -> Vec<u8> {
    const ROW_SIZE: usize = 76;
    const TRAILING_LEN: usize = 20;
    const PAYLOAD_SIZE: usize = ROW_SIZE + TRAILING_LEN;
    const ROW_WIRE_SIZE: usize = 6 + PAYLOAD_SIZE;
    const EXD_HEADER_SIZE: usize = 0x20;
    const INDEX_ENTRY_SIZE: usize = 8;

    let index_size = row_count * INDEX_ENTRY_SIZE;
    let data_region_start = EXD_HEADER_SIZE + index_size;
    let total_size = data_region_start + row_count * ROW_WIRE_SIZE;

    let mut buf = vec![0u8; total_size];
    buf[0..4].copy_from_slice(b"EXDF");
    buf[4..6].copy_from_slice(&2u16.to_be_bytes());
    buf[8..12].copy_from_slice(&u32::try_from(index_size).expect("fits u32").to_be_bytes());

    for i in 0..row_count {
        let row_id = u32::try_from(i).expect("fits u32");
        let row_offset = data_region_start + i * ROW_WIRE_SIZE;
        let idx_off = EXD_HEADER_SIZE + i * INDEX_ENTRY_SIZE;
        buf[idx_off..idx_off + 4].copy_from_slice(&row_id.to_be_bytes());
        buf[idx_off + 4..idx_off + 8]
            .copy_from_slice(&u32::try_from(row_offset).expect("fits u32").to_be_bytes());
        let data_size = u32::try_from(PAYLOAD_SIZE).expect("fits u32");
        buf[row_offset..row_offset + 4].copy_from_slice(&data_size.to_be_bytes());
        buf[row_offset + 4..row_offset + 6].copy_from_slice(&1u16.to_be_bytes());

        let struct_start = row_offset + 6;
        buf[struct_start + 68..struct_start + 72].copy_from_slice(&0u32.to_be_bytes());
        let trailing_start = struct_start + ROW_SIZE;
        let mut label = String::new();
        write!(&mut label, "Item_{i:07}").expect("write");
        let lb = label.as_bytes();
        let copy_len = lb.len().min(12);
        buf[trailing_start..trailing_start + copy_len].copy_from_slice(&lb[..copy_len]);
    }
    buf
}

struct LruPages {
    header: ExcelHeader,
    cache: Mutex<LruCache<u32, Arc<ExcelData>>>,
}

impl LruPages {
    fn new(header: ExcelHeader, capacity: usize) -> Self {
        Self {
            header,
            cache: Mutex::new(LruCache::new(NonZeroUsize::new(capacity).expect("nonzero"))),
        }
    }

    fn get(&self, page_start_id: u32, bytes: Vec<u8>) -> Arc<ExcelData> {
        if let Some(hit) = self.cache.lock().expect("poisoned").get(&page_start_id) {
            return Arc::clone(hit);
        }
        let parsed =
            Arc::new(ExcelData::parse(&bytes, &self.header, Language::None).expect("parse"));
        self.cache
            .lock()
            .expect("poisoned")
            .put(page_start_id, Arc::clone(&parsed));
        parsed
    }
}

const ROWS_PER_PAGE: usize = 8_000;

fn unique_pages(c: &mut Criterion) {
    const N: usize = 8;
    let pages: Vec<(u32, u32)> = (0..N)
        .map(|i| {
            (
                u32::try_from(i * ROWS_PER_PAGE).unwrap(),
                u32::try_from(ROWS_PER_PAGE).expect("ROWS_PER_PAGE fits u32"),
            )
        })
        .collect();
    let exh_bytes = build_item_like_exh(&pages, &[0]);
    let header = ExcelHeader::parse(&exh_bytes).expect("parse exh");
    let exds: Vec<Vec<u8>> = (0..N).map(|_| build_item_like_exd(ROWS_PER_PAGE)).collect();

    let mut g = c.benchmark_group("unique_pages");
    g.sample_size(20);

    g.bench_with_input(BenchmarkId::new("baseline", N), &(), |b, ()| {
        b.iter(|| {
            for blob in &exds {
                let data = ExcelData::parse(black_box(blob.as_slice()), &header, Language::None)
                    .expect("parse");
                black_box(data);
            }
        });
    });

    g.bench_with_input(BenchmarkId::new("lru", N), &(), |b, ()| {
        b.iter(|| {
            let lru = LruPages::new(header.clone(), N);
            for (i, blob) in exds.iter().enumerate() {
                black_box(lru.get(u32::try_from(i * ROWS_PER_PAGE).unwrap(), blob.clone()));
            }
        });
    });

    g.finish();
}

fn same_page(c: &mut Criterion) {
    const REPEATS: usize = 32;
    let pages: &[(u32, u32)] = &[(
        0,
        u32::try_from(ROWS_PER_PAGE).expect("ROWS_PER_PAGE fits u32"),
    )];
    let exh_bytes = build_item_like_exh(pages, &[0]);
    let header = ExcelHeader::parse(&exh_bytes).expect("parse exh");
    let blob = build_item_like_exd(ROWS_PER_PAGE);

    let mut g = c.benchmark_group("same_page");
    g.sample_size(20);

    g.bench_with_input(BenchmarkId::new("baseline", REPEATS), &(), |b, ()| {
        b.iter(|| {
            for _ in 0..REPEATS {
                let data = ExcelData::parse(black_box(blob.as_slice()), &header, Language::None)
                    .expect("parse");
                black_box(data);
            }
        });
    });

    g.bench_with_input(BenchmarkId::new("lru", REPEATS), &(), |b, ()| {
        b.iter(|| {
            let lru = LruPages::new(header.clone(), 8);
            for _ in 0..REPEATS {
                black_box(lru.get(0, blob.clone()));
            }
        });
    });

    g.finish();
}

fn character_sheet(c: &mut Criterion) {
    let pages: Vec<(u32, u32)> = (0..3)
        .map(|i| {
            (
                u32::try_from(i * ROWS_PER_PAGE).unwrap(),
                u32::try_from(ROWS_PER_PAGE).expect("ROWS_PER_PAGE fits u32"),
            )
        })
        .collect();
    let exh_bytes = build_item_like_exh(&pages, &[0]);
    let header = ExcelHeader::parse(&exh_bytes).expect("parse exh");
    let blobs: Vec<Vec<u8>> = (0..3).map(|_| build_item_like_exd(ROWS_PER_PAGE)).collect();

    let mut access_order: Vec<usize> = Vec::with_capacity(60);
    access_order.extend(std::iter::repeat_n(0usize, 40));
    access_order.extend(std::iter::repeat_n(1usize, 15));
    access_order.extend(std::iter::repeat_n(2usize, 5));
    let shuffled: Vec<usize> = {
        let mut out = vec![0usize; access_order.len()];
        let mut idx = 0usize;
        for v in &access_order {
            out[idx] = *v;
            idx = (idx + 7) % access_order.len();
        }
        out
    };

    let mut g = c.benchmark_group("character_sheet");
    g.sample_size(20);

    g.bench_with_input(
        BenchmarkId::new("baseline", shuffled.len()),
        &(),
        |b, ()| {
            b.iter(|| {
                for &page_idx in &shuffled {
                    let data = ExcelData::parse(
                        black_box(blobs[page_idx].as_slice()),
                        &header,
                        Language::None,
                    )
                    .expect("parse");
                    black_box(data);
                }
            });
        },
    );

    g.bench_with_input(BenchmarkId::new("lru", shuffled.len()), &(), |b, ()| {
        b.iter(|| {
            let lru = LruPages::new(header.clone(), 8);
            for &page_idx in &shuffled {
                let page_start = u32::try_from(page_idx * ROWS_PER_PAGE).unwrap();
                black_box(lru.get(page_start, blobs[page_idx].clone()));
            }
        });
    });

    g.finish();
}

fn probe_unique_pages(c: &mut Criterion) {
    const N: usize = 8;
    let pages: Vec<(u32, u32)> = (0..N)
        .map(|i| {
            (
                u32::try_from(i * ROWS_PER_PAGE).unwrap(),
                u32::try_from(ROWS_PER_PAGE).expect("ROWS_PER_PAGE fits u32"),
            )
        })
        .collect();
    let exh_bytes = build_item_like_exh(&pages, &[0]);
    let header = ExcelHeader::parse(&exh_bytes).expect("parse exh");
    let exds: Vec<Vec<u8>> = (0..N).map(|_| build_item_like_exd(ROWS_PER_PAGE)).collect();

    let mut g = c.benchmark_group("probe_unique_pages");
    g.sample_size(20);

    g.bench_function("a_parse_drop", |b| {
        b.iter(|| {
            for blob in &exds {
                let data = ExcelData::parse(black_box(blob.as_slice()), &header, Language::None)
                    .expect("parse");
                black_box(data);
            }
        });
    });

    g.bench_function("b_parse_hold_vec", |b| {
        b.iter(|| {
            let mut held: Vec<ExcelData> = Vec::with_capacity(N);
            for blob in &exds {
                let data = ExcelData::parse(black_box(blob.as_slice()), &header, Language::None)
                    .expect("parse");
                held.push(data);
            }
            black_box(held);
        });
    });

    g.bench_function("c_arc_hold_vec", |b| {
        b.iter(|| {
            let mut held: Vec<Arc<ExcelData>> = Vec::with_capacity(N);
            for blob in &exds {
                let data = Arc::new(
                    ExcelData::parse(black_box(blob.as_slice()), &header, Language::None)
                        .expect("parse"),
                );
                held.push(data);
            }
            black_box(held);
        });
    });

    g.bench_function("d_mutex_hashmap", |b| {
        use std::collections::HashMap;
        b.iter(|| {
            let map: Mutex<HashMap<u32, Arc<ExcelData>>> = Mutex::new(HashMap::with_capacity(N));
            for (i, blob) in exds.iter().enumerate() {
                let data = Arc::new(
                    ExcelData::parse(black_box(blob.as_slice()), &header, Language::None)
                        .expect("parse"),
                );
                map.lock()
                    .unwrap()
                    .insert(u32::try_from(i * ROWS_PER_PAGE).unwrap(), data);
            }
            black_box(map);
        });
    });

    g.bench_function("e_full_lru", |b| {
        b.iter(|| {
            let lru = LruPages::new(header.clone(), N);
            for (i, blob) in exds.iter().enumerate() {
                black_box(lru.get(u32::try_from(i * ROWS_PER_PAGE).unwrap(), blob.clone()));
            }
        });
    });

    g.bench_function("f_lru_cap_2", |b| {
        b.iter(|| {
            let lru = LruPages::new(header.clone(), 2);
            for (i, blob) in exds.iter().enumerate() {
                black_box(lru.get(u32::try_from(i * ROWS_PER_PAGE).unwrap(), blob.clone()));
            }
        });
    });

    g.finish();
}

criterion_group!(
    benches,
    unique_pages,
    same_page,
    character_sheet,
    probe_unique_pages
);
criterion_main!(benches);
