//! The definition-object layout must keep agreeing with the disc records.
//!
//! `unit_layout` was read out of the game's loader and checked against objects
//! dumped from a running Stage 02. This pins that check: every mapped float
//! field of every identified object must equal the value its disc record sets.
//! It needs no emulator — the dump is checked in under `docs/re/captures/`.

use std::collections::BTreeMap;

use sylpheed_formats::idxd::IdxdObject;
use sylpheed_formats::pak::PakArchive;
use sylpheed_formats::unit_layout::{fields, Kind};

/// Live objects identified in the dump, with the disc record each one is.
/// `bf001` is here because full-record agreement is what identified it: the
/// four-value signature also fitted `UN_be005_ADAN_SpaceFortress`, which
/// disagrees on `Color_R`/`Color_G`.
const IDENTIFIED: &[(&str, &str)] = &[
    ("UN_f106_TCAF_Destroyer", "0xbd3ee300"),
    ("UN_f105_TCAF_Cruiser", "0xbd40e200"),
    ("UN_e105_ADAN_Cruiser", "0xbd3fd800"),
    ("UN_e106_ADAN_Destroyer", "0xbd3e6f80"),
    ("UN_f101_TCAF_Acropolis", "0xbd3e1f00"),
    ("UN_bf001_TCAF_SchlosBase", "0xbd3b6a00"),
    ("UN_e007_ADAN_Turret", "0xbd3dea80"),
    ("UN_e010_ADAN_Attacker_S", "0xbd3faa80"),
    ("UN_f003_TCAF_ArrowHead", "0xbd3dbd00"),
    ("UN_f001_TCAF_DeltaSaber_T", "0xbd3dfc00"),
    ("UN_f001_TCAF_DeltaSaber_T_Player", "0xbd3da100"),
];

const DUMP: &str = include_str!("../../../docs/re/captures/stage02-live-unit-definitions-deep.txt");

fn disc_root() -> Option<String> {
    if let Ok(p) = std::env::var("SYLPHEED_DISC") {
        if std::path::Path::new(&p).join("dat").is_dir() {
            return Some(p);
        }
    }
    let d = "/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja)";
    std::path::Path::new(d).join("dat").is_dir().then(|| d.to_string())
}

/// `va -> offset -> value`, from the dump's `addr +off hex u32 f32` columns.
fn live() -> BTreeMap<String, BTreeMap<usize, f32>> {
    let mut out: BTreeMap<String, BTreeMap<usize, f32>> = BTreeMap::new();
    let mut cur = String::new();
    for line in 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() >= 5 && f[1].starts_with('+') {
            if let (Ok(off), Ok(val)) = (usize::from_str_radix(&f[1][1..], 16), f[4].parse::<f32>())
            {
                out.entry(cur.clone()).or_default().insert(off, val);
            }
        }
    }
    out
}

#[test]
fn mapped_fields_match_the_disc_records() {
    let Some(disc) = disc_root() else {
        eprintln!("SKIP: extracted disc not found (set SYLPHEED_DISC to enable)");
        return;
    };
    let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).expect("main pak");
    let live = live();
    let floats: Vec<_> = fields().into_iter().filter(|f| f.kind == Kind::F32).collect();

    let (mut agree, mut bad) = (0usize, Vec::new());
    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)) = IDENTIFIED.iter().find(|(i, _)| *i == id) else { continue };
        let Some(words) = live.get(*va) else { continue };
        for f in &floats {
            let (Some(want), Some(got)) = (obj.get_f32(f.name), words.get(&f.offset)) else {
                continue;
            };
            // Angle fields are degrees on disc and radians in the object.
            let rad = want * std::f32::consts::PI / 180.0;
            if (want - got).abs() <= want.abs() * 1e-4 || (rad - got).abs() <= rad.abs() * 1e-4 {
                agree += 1;
            } else {
                bad.push(format!("{id}.{} disc {want} vs live +{} = {got}", f.name, f.offset));
            }
        }
    }
    assert!(bad.is_empty(), "{} fields disagree: {bad:?}", bad.len());
    assert!(agree >= 400, "expected ≥400 agreeing fields, got {agree}");
}
