//! Minority report: which container decodes a shared resource differently from
//! all the others?
//!
//! Cross-container consistency has been measured as "do the spans agree", which
//! only says a resource is inconsistent — not which copy is wrong. With three or
//! more copies the majority is the reference, and the minority names the
//! container AND the resource to look at. That is what caught the `n054`/`n056`
//! shift chain after the exact-coverage fix.
//!
//! Usage: consensus_check <resource3d_dir> [--list]
use sylpheed_formats::mesh::Xbg7Model;
use std::collections::{BTreeMap, HashMap};

fn main() {
    let dir = std::env::args().nth(1).expect("resource3d dir");
    let list = std::env::args().any(|a| a == "--list");
    let mut files: Vec<_> = std::fs::read_dir(&dir)
        .unwrap()
        .flatten()
        .map(|e| e.path())
        .filter(|p| p.extension().and_then(|s| s.to_str()) == Some("xpr"))
        .collect();
    files.sort();

    // name -> [(container, verts, tris, span)]
    let mut seen: BTreeMap<String, Vec<(String, usize, usize, [i64; 3])>> = BTreeMap::new();
    for f in &files {
        let Ok(bytes) = std::fs::read(f) else { continue };
        let where_ = f.file_name().unwrap().to_string_lossy().to_string();
        for m in Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) {
            let (mut lo, mut hi) = ([f32::MAX; 3], [f32::MIN; 3]);
            for s in &m.meshes {
                for q in &s.positions {
                    for k in 0..3 {
                        lo[k] = lo[k].min(q[k]);
                        hi[k] = hi[k].max(q[k]);
                    }
                }
            }
            if lo[0] == f32::MAX {
                continue;
            }
            let v: usize = m.meshes.iter().map(|s| s.positions.len()).sum();
            let t: usize = m.meshes.iter().map(|s| s.indices.len() / 3).sum();
            let span = [
                (hi[0] - lo[0]).round() as i64,
                (hi[1] - lo[1]).round() as i64,
                (hi[2] - lo[2]).round() as i64,
            ];
            seen.entry(m.name.clone()).or_default().push((where_.clone(), v, t, span));
        }
    }

    let (mut resources, mut minority) = (0usize, 0usize);
    let mut rows: Vec<String> = Vec::new();
    for (name, list_) in &seen {
        // Only compare decodes that agree on how much geometry they found, and
        // only where a majority can exist.
        if list_.len() < 3 || !list_.iter().all(|e| e.1 == list_[0].1 && e.2 == list_[0].2) {
            continue;
        }
        let mut votes: HashMap<[i64; 3], usize> = HashMap::new();
        for e in list_ {
            *votes.entry(e.3).or_default() += 1;
        }
        let (best, n) = votes.iter().max_by_key(|(_, n)| **n).unwrap();
        if *n * 2 <= list_.len() {
            continue; // no majority — cannot call anyone the odd one out
        }
        resources += 1;
        for e in list_.iter().filter(|e| e.3 != *best) {
            minority += 1;
            rows.push(format!(
                "{:<22} {name:<26} v={:<6} {:?} vs the other {n} containers' {:?}",
                e.0, e.1, e.3, best
            ));
        }
    }
    if list {
        for r in &rows {
            println!("{r}");
        }
    }
    println!(
        "{minority} minority decodes across {resources} resources that have a majority"
    );
}
