//! Ask the runtime capture whether two resources that our decoder gives the
//! **same geometry** really are the same geometry.
//!
//! Our XBG7 anchor scan sometimes lands two different resource names on one
//! vertex buffer. Statics cannot separate "the container genuinely reuses a
//! buffer" from "the scan picked the wrong candidate" — but a capture can: the
//! engine uploads a buffer per resource and reuses one only 3.4 % of the time
//! (see docs/re/structures/xbg7-mesh.md), so a group of `k` resources our
//! decoder collapses onto one buffer should show up as `k` distinct `vbase`s
//! carrying that same vertex count and those same positions. Fewer means at
//! most one member of the group is really that geometry.
//!
//! Usage:
//!   cargo run --release --example shared_vbase_check -- \
//!       <Stage_SNN.xpr> <capture.log>...
use sylpheed_formats::mesh::Xbg7Model;
use sylpheed_formats::ship_capture::{parse_capture, parse_drawlog, CapturedDraw};
use std::collections::{BTreeMap, BTreeSet};

/// Quantised position key — the logs print 4 decimals, so compare at that scale.
fn key(p: [f32; 3]) -> (i64, i64, i64) {
    (
        (p[0] as f64 * 1e4).round() as i64,
        (p[1] as f64 * 1e4).round() as i64,
        (p[2] as f64 * 1e4).round() as i64,
    )
}

/// Where in the container does a captured buffer live? POSITION is `f32×3` big
/// endian at vertex offset 0, so a draw's dumped positions are a literal byte
/// pattern: find the first one, then confirm the next few at a fixed stride.
/// This turns a capture into ground truth for a resource we mis-anchored.
fn locate_run(bytes: &[u8], pos: &[[f32; 3]]) -> Vec<(usize, usize)> {
    if pos.len() < 4 {
        return Vec::new();
    }
    // The log prints 4 decimals, so match on value with the printing tolerance
    // rather than on bytes.
    let be = |b: &[u8], at: usize| f32::from_be_bytes(b[at..at + 4].try_into().unwrap());
    let same = |b: &[u8], at: usize, p: [f32; 3]| {
        at + 12 <= b.len() && (0..3).all(|c| (be(b, at + c * 4) - p[c]).abs() <= 1e-4)
    };
    let mut out = Vec::new();
    for o in (0..bytes.len().saturating_sub(12)).step_by(4) {
        if !same(bytes, o, pos[0]) {
            continue;
        }
        for stride in (12..=64).step_by(4) {
            if (1..4).all(|k| same(bytes, o + k * stride, pos[k])) {
                out.push((o, stride));
                break;
            }
        }
    }
    out
}

fn main() {
    let args: Vec<String> = std::env::args().collect();
    if args.len() < 3 {
        eprintln!("usage: shared_vbase_check <Stage_SNN.xpr> <capture.log>...");
        std::process::exit(2);
    }
    let bytes = std::fs::read(&args[1]).expect("read container");

    // Every draw from every log, keyed by vertex count.
    // Keep the logs apart: each is its own emulator run, so a `vbase` only
    // means something within one log.
    let mut logs: Vec<(String, Vec<CapturedDraw>)> = Vec::new();
    for log in args[2..].iter().filter(|a| !a.starts_with("--")) {
        let text = std::fs::read_to_string(log).expect("read log");
        let mut d = parse_capture(&text);
        if d.is_empty() {
            d = parse_drawlog(&text);
        }
        eprintln!("{log}: {} draws", d.len());
        logs.push((log.rsplit('/').next().unwrap_or(log).to_string(), d));
    }

    // `--map`: is a draw's guest `vbase` just the container file offset plus a
    // constant? If the container is uploaded contiguously it is — and then a
    // capture names the exact offset of every buffer the engine drew, which is
    // ground truth the anchor scan currently has to guess at.
    if args.iter().any(|a| a == "--map") {
        for (log, draws) in &logs {
            let mut seen: BTreeSet<u32> = BTreeSet::new();
            let mut delta: BTreeMap<i64, usize> = BTreeMap::new();
            let mut unfound = 0usize;
            for d in draws {
                if d.pos.len() < 8 || d.vcount < 20 || !seen.insert(d.vbase) {
                    continue;
                }
                let at = locate_run(&bytes, &d.pos);
                if at.is_empty() {
                    unfound += 1;
                    continue;
                }
                for (o, _) in at {
                    *delta.entry(d.vbase as i64 - o as i64).or_default() += 1;
                }
            }
            let mut top: Vec<_> = delta.iter().collect();
            top.sort_by_key(|(_, n)| std::cmp::Reverse(**n));
            println!("{log}: {} distinct vbases located, {unfound} not in this container", seen.len() - unfound);
            for (d, n) in top.iter().take(5) {
                println!("   vbase - offset = 0x{:X}  ×{n}", d);
            }
        }
        return;
    }

    // Decode the container and group resources by the exact geometry they got.
    let models = Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false);

    // `--truth <base>`: with the container's guest load address (from `--map`),
    // every draw names a file offset. Print it against the offset our anchor
    // scan chose for each resource — a direct read-out of what we got wrong.
    if let Some(a) = args.iter().find_map(|a| a.strip_prefix("--truth=")) {
        let base = u32::from_str_radix(a.trim_start_matches("0x"), 16).expect("base");
        // Where the anchor scan actually put each sub-mesh — exact, from the
        // decoder, not inferred by searching for its leading vertices (the same
        // leading run occurs at several offsets in a container, so a search
        // cannot tell where a resource was anchored).
        let mut ours: BTreeMap<usize, Vec<(String, usize)>> = BTreeMap::new();
        for m in &models {
            for sub in &m.meshes {
                if let Some(o) = sub.vbuf_offset {
                    ours.entry(o).or_default().push((m.name.clone(), sub.positions.len()));
                }
            }
        }
        if args.iter().any(|a| a == "--anchors") {
            println!("{:<12}  where our decode put each resource", "file offset");
            for (o, v) in &ours {
                for (n, c) in v {
                    println!("0x{o:<10x}  {n} ({c} verts)");
                }
            }
            return;
        }
        let mut drawn: BTreeMap<usize, u32> = BTreeMap::new();
        for (_, draws) in &logs {
            for d in draws {
                let off = d.vbase.wrapping_sub(base) as usize;
                if off < bytes.len() && d.vcount >= 20 {
                    drawn.insert(off, d.vcount);
                }
            }
        }
        // Which resources have the drawn vertex count, wherever we put them?
        // Right size + wrong place is a different bug from never finding it.
        let mut by_count: BTreeMap<usize, Vec<String>> = BTreeMap::new();
        for m in &models {
            let n: usize = m.meshes.iter().map(|s| s.positions.len()).sum();
            by_count.entry(n).or_default().push(m.name.clone());
        }
        // Is a capture-proven offset even a candidate the scan considers?
        // Absent ⇒ the run scan misses it; present ⇒ selection picked another.
        let starts: BTreeSet<usize> =
            sylpheed_formats::mesh::debug_vertex_run_starts(&bytes, 24).into_iter().collect();
        eprintln!("{} stride-24 candidate starts in this container", starts.len());
        println!("{:<12} {:>7} {:>9}  {:<44}  our resources with that vcount", "file offset", "vcount", "candidate", "claimed by our decode");
        for (off, vcount) in &drawn {
            let who = ours
                .get(off)
                .map(|v| {
                    v.iter().map(|(n, c)| format!("{n}({c})")).collect::<Vec<_>>().join(", ")
                })
                .unwrap_or_else(|| "— NOBODY".into());
            let same = by_count
                .get(&(*vcount as usize))
                .map(|v| v.join(", "))
                .unwrap_or_else(|| "— none".into());
            // Nearest resource we anchored at or before this offset — the
            // likely owner of a buffer nobody claims.
            let near = ours
                .range(..=*off)
                .next_back()
                .map(|(o, v)| format!("{} @ -0x{:x}", v[0].0, off - o))
                .unwrap_or_default();
            let cand = if starts.contains(off) { "yes" } else { "NO" };
            println!("0x{off:<10x} {vcount:>7} {cand:>9}  {who:<44}  {same:<34}  {near}");
        }
        return;
    }
    let mut groups: BTreeMap<Vec<(i64, i64, i64)>, Vec<String>> = BTreeMap::new();
    for m in &models {
        let pos: Vec<(i64, i64, i64)> =
            m.meshes.iter().flat_map(|s| s.positions.iter().copied()).map(key).collect();
        if pos.is_empty() {
            continue;
        }
        groups.entry(pos).or_default().push(m.name.clone());
    }
    let shared: Vec<_> = groups.iter().filter(|(_, n)| n.len() > 1).collect();
    eprintln!(
        "{} models, {} distinct geometries, {} shared by >1 resource",
        models.len(),
        groups.len(),
        shared.len()
    );

    for (pos, names) in shared {
        let vcount = pos.len() as u32;
        // A draw belongs to this geometry if every dumped position is one of
        // the decoded ones (the log dumps at most the first 64).
        let want: BTreeSet<(i64, i64, i64)> = pos.iter().copied().collect();
        println!("\n{}  ({vcount} verts, {} resources)", names.join(" ≡ "), names.len());
        for (log, draws) in &logs {
            let hits: Vec<&CapturedDraw> = draws.iter().filter(|d| d.vcount == vcount).collect();
            let all: BTreeSet<u32> = hits.iter().map(|d| d.vbase).collect();
            let matching: Vec<&&CapturedDraw> = hits
                .iter()
                .filter(|d| !d.pos.is_empty() && d.pos.iter().all(|p| want.contains(&key(*p))))
                .collect();
            let ok: BTreeSet<u32> = matching.iter().map(|d| d.vbase).collect();
            // A buffer we do NOT match may still be the mirrored twin: same
            // geometry with x negated. That is the case our assembler papers
            // over with `apply_twin_mirrors`.
            let mirrored: BTreeSet<u32> = hits
                .iter()
                .filter(|d| !ok.contains(&d.vbase))
                .filter(|d| {
                    !d.pos.is_empty()
                        && d.pos.iter().all(|p| want.contains(&key([-p[0], p[1], p[2]])))
                })
                .map(|d| d.vbase)
                .collect();
            println!(
                "  {log:32} draws={:<5} vbases@vcount={:<3} ours={} mirrored={} other={}",
                hits.len(),
                all.len(),
                ok.len(),
                mirrored.len(),
                all.len() - ok.len() - mirrored.len()
            );
            // Where does each captured buffer live in the container? One
            // representative draw per vbase is enough.
            let mut done: BTreeSet<u32> = BTreeSet::new();
            for d in &hits {
                if d.pos.len() < 8 || !done.insert(d.vbase) {
                    continue;
                }
                let kind = if ok.contains(&d.vbase) {
                    "ours"
                } else if mirrored.contains(&d.vbase) {
                    "mirror"
                } else {
                    "other"
                };
                let at = locate_run(&bytes, &d.pos);
                let shown: Vec<String> =
                    at.iter().take(4).map(|(o, s)| format!("0x{o:x}/stride{s}")).collect();
                println!(
                    "      vbase=0x{:08X} [{kind:6}] in container at: {}",
                    d.vbase,
                    if shown.is_empty() { "NOT FOUND".into() } else { shown.join(" ") }
                );
            }
        }
    }
}
