//! The runtime layout of a unit / vessel definition object.
//!
//! The game parses an [`crate::idxd`] record into a fixed 880-byte object whose
//! field offsets are **not** guessable from the disc data: the record is a
//! reflective key/value pool, and the loader assigns each key to a member by
//! name. This table is that assignment, read out of the loader itself
//! (`sub_82341A20` — every key is built as `addi r4, r30, -N`, so the field name
//! for each store is a string in the executable image), and verified against
//! objects dumped from a running mission: **406 values agree with the disc
//! records, 0 disagree**, over 11 objects covering both schemas.
//!
//! Why a reimplementation wants it:
//!
//! - it names the field behind every word of a live definition object, so a
//!   memory snapshot can be read directly;
//! - it says which fields a record leaves **defaulted**, and what the loader
//!   leaves there — the float accessor returns `0.0` on a pool miss;
//! - it carries two conventions that are invisible on disc: **angles are degrees
//!   in the data and radians in the object**, and **`Size_Y` takes `Size_X`**
//!   when omitted.
//!
//! See `docs/re/live-unit-definitions.md` for the derivation and the measured
//! default values.

/// How a field is stored in the definition object.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Kind {
    /// IEEE-754 single, big-endian.
    F32,
    /// Pointer to a string.
    Str,
    /// 32-bit word (bool / enum / count / id).
    Word,
}

/// One field of the definition object.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Field {
    /// Byte offset from the start of the object.
    pub offset: usize,
    /// Field name, exactly as the disc record spells it.
    pub name: &'static str,
    /// Storage kind.
    pub kind: Kind,
}

const TABLE: &str = include_str!("../data/unit_definition_layout.txt");

/// Every mapped field, in offset order.
pub fn fields() -> Vec<Field> {
    TABLE
        .lines()
        .filter(|l| !l.trim_start().starts_with('#') && !l.trim().is_empty())
        .filter_map(|l| {
            let mut it = l.split_whitespace();
            let offset = it.next()?.parse().ok()?;
            let kind = match it.next()? {
                "f32" => Kind::F32,
                "str" => Kind::Str,
                _ => Kind::Word,
            };
            // `name` is a &'static str because TABLE is 'static.
            let name = it.next()?;
            let name: &'static str = TABLE.get(
                TABLE.find(name).map(|s| s..s + name.len())?,
            )?;
            Some(Field { offset, name, kind })
        })
        .collect()
}

/// The field at `offset`, if one is mapped there.
pub fn field_at(offset: usize) -> Option<Field> {
    fields().into_iter().find(|f| f.offset == offset)
}

/// The offset of `name`, if it is mapped.
pub fn offset_of(name: &str) -> Option<usize> {
    fields().into_iter().find(|f| f.name == name).map(|f| f.offset)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn table_parses_and_is_ordered() {
        let f = fields();
        assert!(f.len() > 150, "expected the full map, got {}", f.len());
        assert!(f.windows(2).all(|w| w[0].offset < w[1].offset), "offsets must be strictly increasing");
    }

    #[test]
    fn known_fields_sit_where_the_loader_puts_them() {
        // Spot-checks from the verified map; these four also anchor the
        // identification of a live object (see docs/re/live-unit-definitions.md).
        assert_eq!(offset_of("Size_X"), Some(48));
        assert_eq!(offset_of("Size_Y"), Some(52));
        assert_eq!(offset_of("Size_Z"), Some(56));
        assert_eq!(offset_of("HP"), Some(84));
        assert_eq!(offset_of("HQRatio"), Some(88));
        assert_eq!(field_at(96).map(|f| f.name), Some("ThrusterRatio"));
        assert_eq!(field_at(48).map(|f| f.kind), Some(Kind::F32));
    }
}
