//! Screen every ship family for a part that is wildly out of scale with its
//! siblings — the "slab" signature of a mis-anchored block.
//!
//! Rendering `e106` found such a part (`bdy_03`, 600×1600×998 beside parts of
//! ~250) that coverage, cross-container consistency, the capture oracle and the
//! twin invariant were all blind to. Eyeballing does not scale to 166 containers;
//! this does the same comparison numerically.
//!
//! Usage: slab_screen <resource3d_dir> [factor]
use sylpheed_formats::mesh::Xbg7Model;
use sylpheed_formats::ship::{is_base_part, ship_id_of};
use std::collections::BTreeMap;

fn main() {
    let dir = std::env::args().nth(1).expect("resource3d dir");
    let factor: f32 = std::env::args().nth(2).and_then(|s| s.parse().ok()).unwrap_or(4.0);
    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();

    let mut flagged = 0usize;
    for f in &files {
        let Ok(bytes) = std::fs::read(f) else { continue };
        let mut by_ship: BTreeMap<String, Vec<(String, f32)>> = BTreeMap::new();
        for m in Xbg7Model::anchor_models_cancellable(&bytes, 0.0, &|| false) {
            if !is_base_part(&m.name) {
                continue;
            }
            let Some(id) = ship_id_of(&m.name) else { continue };
            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;
            }
            // Compare the part's SMALLEST axis span, not its diagonal. A long
            // thin part (an antenna, a 100 000-unit tether on `f002`) is
            // legitimately huge in one axis and would swamp a diagonal test; a
            // mis-anchored block is bulky in all three, which is what the e106
            // slab looked like (600×1600×998 beside siblings of ~250).
            let thin = (hi[0] - lo[0]).min(hi[1] - lo[1]).min(hi[2] - lo[2]);
            by_ship.entry(id.to_string()).or_default().push((m.name.clone(), thin));
        }
        for (id, parts) in &by_ship {
            if parts.len() < 3 {
                continue; // no meaningful median
            }
            let mut d: Vec<f32> = parts.iter().map(|(_, x)| *x).collect();
            d.sort_by(|a, b| a.partial_cmp(b).unwrap());
            let median = d[d.len() / 2];
            for (name, diag) in parts {
                if *diag > median * factor {
                    flagged += 1;
                    println!(
                        "{:<22} {name:<22} min-axis {diag:>8.0}  vs ship median {median:>8.0}  ({:.1}×)",
                        f.file_name().unwrap().to_string_lossy(),
                        diag / median
                    );
                }
            }
        }
    }
    println!("{flagged} parts flagged at {factor}× the ship median");
}
