//! Where does each declared field sit inside the engine's live definition object?
//!
//! A live object carries no names, so the mapping has to be inferred: take a unit
//! whose disc record SETS a field, look for that value in the dumped words of the
//! matching live object, and keep the offsets that agree across several units.
//!
//! Run: live_offsets <disc-root> <live-dump.txt> <ID=0xVA> ...
//!   e.g. … UN_f106_TCAF_Destroyer=0xbd3ee300 UN_f105_TCAF_Cruiser=0xbd40e200
use std::collections::BTreeMap;
use sylpheed_formats::idxd::IdxdObject;
use sylpheed_formats::pak::PakArchive;

fn main() {
    let mut a = std::env::args().skip(1);
    let disc = a.next().expect("disc root");
    let dump = a.next().expect("live dump");
    let pairs: Vec<(String, String)> = a
        .filter_map(|s| s.split_once('=').map(|(i, v)| (i.to_string(), v.to_string())))
        .collect();

    // live: va -> offset -> f32
    let mut live: BTreeMap<String, BTreeMap<usize, f32>> = BTreeMap::new();
    let mut cur = String::new();
    for line in std::fs::read_to_string(&dump).expect("dump").lines() {
        if let Some(rest) = line.strip_prefix("=== ") {
            cur = rest.trim().to_string();
            continue;
        }
        let f: Vec<&str> = line.split_whitespace().collect();
        if f.len() >= 4 && f[1].starts_with('+') {
            if let (Ok(off), Ok(val)) =
                (usize::from_str_radix(&f[1][1..], 16), f[3].parse::<f32>())
            {
                live.entry(cur.clone()).or_default().insert(off, val);
            }
        }
    }

    let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).expect("pak");
    // key -> offset -> how many units agree
    let mut votes: BTreeMap<String, BTreeMap<usize, usize>> = BTreeMap::new();
    let mut units = 0usize;
    for entry in pak.entries() {
        let Ok(bytes) = pak.read(entry) else { continue };
        let Ok(obj) = IdxdObject::parse(&bytes) else { continue };
        let Some(id) = obj.get_raw("ID") else { continue };
        let Some((_, va)) = pairs.iter().find(|(i, _)| i == id) else { continue };
        let Some(words) = live.get(va) else { continue };
        units += 1;
        let mut numeric = 0usize;
        let mut hit = 0usize;
        for key in obj.tokens() {
            let Some(v) = obj.get_f32(key) else { continue };
            if !v.is_finite() || v == 0.0 {
                continue; // zero matches everywhere and says nothing
            }
            numeric += 1;
            let mut found = false;
            for (off, w) in words {
                if (*w - v).abs() <= v.abs() * 1e-6 {
                    *votes.entry(key.clone()).or_default().entry(*off).or_default() += 1;
                    found = true;
                }
            }
            if found {
                hit += 1;
            }
        }
        eprintln!("  {id}: {numeric} numeric fields set on disc, {hit} found in the dumped window");
    }
    println!("{units} units correlated against their live objects\n");
    println!("{:<28} {:>8}  offsets agreeing (votes)", "field", "unique?");
    for (key, offs) in &votes {
        let best = offs.iter().max_by_key(|(_, n)| **n).unwrap();
        if *best.1 < units.max(2) {
            continue; // needs every correlated unit to agree
        }
        let list: Vec<String> = offs
            .iter()
            .filter(|(_, n)| **n == *best.1)
            .map(|(o, n)| format!("+{o:03x}×{n}"))
            .collect();
        println!(
            "{:<28} {:>8}  {}",
            key,
            if list.len() == 1 { "unique" } else { "ambig" },
            list.join(" ")
        );
    }
}
