//! Check the code-derived offset→field map against the live dump, then read out
//! the runtime value of every field the disc record leaves defaulted.
//! Run: verify_fieldmap <disc-root> <live-dump> <ID=0xVA>...
use std::collections::BTreeMap;
use sylpheed_formats::idxd::IdxdObject;
use sylpheed_formats::pak::PakArchive;

// Offsets read out of sub_82341A20's own key strings (stfs stores only).
// Offsets and names recovered from `sub_82341A20` itself: each key is built as
// `addi r4, r30, -N` (r30 = 0x82088f94, so the name is a string in the image),
// and the value lands in the first store into the object after the accessor
// call. 125 float-typed fields.
const MAP: &[(usize, &str)] = &[(48, "Size_X"), (52, "Size_Y"), (56, "Size_Z"), (64, "Color_R"), (68, "Color_G"), (72, "Color_B"), (80, "Size_Radius"), (84, "HP"), (88, "HQRatio"), (92, "ShieldRatio"), (96, "ThrusterRatio"), (116, "ResistanceToOptics"), (120, "ResistanceToShell"), (124, "ResistanceToExplosion"), (128, "ResistanceToPlayer"), (132, "ResistanceParalyze"), (156, "MinimumVelocity"), (160, "MaximumVelocity"), (164, "CruisingVelocity"), (168, "Acceleration"), (172, "Deceleration"), (176, "AV_PitchPlus_Max"), (180, "AV_PitchPlus_Min"), (184, "AA_PitchPlus_Max"), (188, "AA_PitchPlus_Min"), (192, "AV_PitchMinus_Max"), (196, "AV_PitchMinus_Min"), (200, "AA_PitchMinus_Max"), (204, "AA_PitchMinus_Min"), (208, "AV_Yaw_Max"), (212, "AV_Yaw_Min"), (216, "AA_Yaw_Max"), (220, "AA_Yaw_Min"), (224, "AV_Roll_Max"), (228, "AV_Roll_Min"), (232, "AA_Roll_Max"), (236, "AA_Roll_Min"), (248, "SideThrustVelocity_Max"), (252, "SideThrustAcceleration"), (256, "MaximumBank_Normal"), (260, "YawDragFactor"), (264, "PitchDragFactor"), (268, "RollDragFactor"), (272, "DragFactorThreshold"), (276, "ArterBurner_Vc"), (280, "ReverseThrust_Vc"), (284, "ArterBurner_Acc"), (288, "ReverseThrust_Acc"), (292, "AccPitchFactor"), (296, "DecPitchFactor"), (300, "AV_AxisMode_Max"), (304, "AV_AxisMode_Min"), (308, "AA_AxisMode_Max"), (312, "AA_AxisMode_Min"), (316, "PowerCutConsumeShield"), (320, "PowerCutDeceleration"), (324, "AB_ConsumeShield_Begin"), (328, "AB_ConsumeShield"), (332, "AB_AV_PitchPlus"), (336, "AB_AA_PitchPlus"), (340, "AB_AV_PitchMinus"), (344, "AB_AA_PitchMinus"), (348, "AB_AV_Yaw"), (352, "AB_AA_Yaw"), (356, "AB_AV_Roll"), (360, "AB_AA_Roll"), (368, "SideRoll_Time"), (372, "SideRoll_Length"), (380, "BarrelRoll_CountMinimum"), (384, "BarrelRoll_CountMaximum"), (388, "BarrelRoll_Time"), (392, "BarrelRoll_Radius"), (400, "TurnAttack_CutoffRatio"), (404, "TurnAttack_DoubleRatio"), (408, "CutoffTimeMin"), (412, "CutoffTimeMax"), (416, "TurnAttack_DoubleTimeMin"), (420, "TurnAttack_DoubleTimeMax"), (428, "Turn_AngularVelocity"), (432, "TurnAway_Time_Minimum"), (436, "TurnAway_Time_Maximum"), (444, "BoostAway_Time_Minimum"), (448, "BoostAway_Time_Maximum"), (456, "HoldPosition_LengthMin"), (460, "HoldPosition_LengthMax"), (464, "HoldPosition_MinimumTime"), (468, "HoldPosition_MaximumTime"), (472, "HoldPosition_SideRatio"), (476, "HoldPosition_BackRatio"), (480, "HoldPosition_CutoffRatio"), (484, "HoldPosition_CancelTime"), (492, "Slalom_CutoffRatio"), (496, "Slalom_TurnCount_Min"), (500, "Slalom_TurnCount_Max"), (508, "Through_CutoffRatio"), (512, "Through_AngleMinimum"), (516, "Through_AngleMaximum"), (520, "Through_Time1Max"), (524, "Through_Time1Min"), (528, "Through_Time2Max"), (532, "Through_Time2Min"), (536, "Through_LengthMinimum"), (540, "Through_LengthMaximum"), (548, "SolidCutoff_Ratio"), (552, "SolidCutoff_LengthMin"), (556, "SolidCutoff_LengthMax"), (560, "HomingResistAdjustment"), (564, "UsingChaffRatio"), (568, "MaxValue"), (572, "ChargeDelay"), (576, "ChargeDelay_Break"), (580, "ChargeSpeed"), (584, "Delay"), (588, "DelayAdjustment"), (624, "DestroyMotionTime"), (628, "DryMass"), (632, "GrossMass"), (664, "LowerHPThresholdRatio"), (668, "SELength"), (672, "RadarRange"), (676, "FCSRange"), (680, "FiringRange"), (692, "AttackVesselPoint"), (696, "AttackCraftPoint"), (700, "DefencePoint")];

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

    let mut live: BTreeMap<String, BTreeMap<usize, f32>> = BTreeMap::new();
    let mut cur = String::new();
    for line in std::fs::read_to_string(&dump).unwrap().lines() {
        if let Some(r) = line.strip_prefix("=== ") { cur = r.trim().into(); continue; }
        let f: Vec<&str> = line.split_whitespace().collect();
        // dump columns: addr  +off  hex  u32  f32  -- the float is f[4], not f[3]
        if f.len() >= 5 && f[1].starts_with('+') {
            if let (Ok(o), Ok(v)) = (usize::from_str_radix(&f[1][1..], 16), f[4].parse::<f32>()) {
                live.entry(cur.clone()).or_default().insert(o, v);
            }
        }
    }

    let pak = PakArchive::open(format!("{disc}/dat/GP_MAIN_GAME_E.pak")).unwrap();
    let (mut agree, mut disagree) = (0, 0);
    let mut defaults: BTreeMap<String, Vec<(String, f32)>> = BTreeMap::new();
    for e in pak.entries() {
        let Ok(b) = pak.read(e) else { continue };
        let Ok(o) = IdxdObject::parse(&b) else { continue };
        let Some(id) = o.get_raw("ID") else { continue };
        let Some((_, va)) = pairs.iter().find(|(i, _)| i == id) else { continue };
        let Some(w) = live.get(va) else { continue };
        for (off, key) in MAP {
            let Some(got) = w.get(off) else { continue };
            match o.get_f32(key) {
                Some(want) => {
                    // Angle fields are stored in RADIANS at runtime and degrees on
                    // disc, so a match is either the raw value or its conversion.
                    let rad = want * std::f32::consts::PI / 180.0;
                    let ok = (want - got).abs() <= want.abs() * 1e-4
                        || (rad - got).abs() <= rad.abs() * 1e-4;
                    if ok { agree += 1 } else {
                        disagree += 1;
                        println!("  MISMATCH {id} {key}: disc {want}, live +{off} = {got}");
                    }
                }
                None => defaults.entry((*key).into()).or_default().push((id.into(), *got)),
            }
        }
    }
    println!("\nmap check: {agree} fields agree with the disc, {disagree} disagree\n");
    println!("runtime value where the disc record DEFAULTS the field:");
    for (key, hits) in &defaults {
        let vals: Vec<String> = hits.iter().map(|(_, v)| format!("{v}")).collect();
        let uniq: std::collections::BTreeSet<&String> = vals.iter().collect();
        println!("  {:<22} {:<28} ({} units)", key,
            uniq.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", "), hits.len());
    }
}
