//! RE diagnostic: emit every `weapon\Weapon_*.tbl` in a pak as machine-readable
//! records, split at the sub-object boundaries the schema declares.
//!
//! An IDXD `.tbl` for a weapon holds `count` sub-records — a `Weapon` and its
//! `Shell` — flattened into one string pool. `sylpheed-cli pak dump` shows them
//! merged, which is ambiguous (both declare `ID`/`Name`). The title's schema
//! name pool gives the split point: the pool contains the literal type names
//! (`Weapon`, `Shell`), and each sub-record's fields follow its type name.
//!
//! Here we split on a type-name token appearing as a *key* position, so each
//! record is emitted with its own ID and field set:
//!
//! ```text
//! REC <tbl-hash> <index> <TypeName> <ID>
//! F   <key> <value>
//! ```
//!
//! Run: cargo run --release -p sylpheed-formats --example idxd_tokens -- <pak> [types...]

use sylpheed_formats::idxd::IdxdObject;
use sylpheed_formats::pak::PakArchive;

fn is_number(s: &str) -> bool {
    let s = s.strip_suffix(['f', 'F']).unwrap_or(s);
    let t = s.strip_prefix(['-', '+']).unwrap_or(s);
    !t.is_empty() && t.chars().all(|c| c.is_ascii_digit() || c == '.')
}

/// `Yes`/`No`-style scalar literals are values, not field names, even though
/// they are identifier-shaped.
fn is_enum_value(s: &str) -> bool {
    matches!(s, "Yes" | "No" | "YES" | "NO" | "On" | "Off" | "ON" | "OFF")
}

fn is_key_like(s: &str) -> bool {
    !s.is_empty()
        && s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
        && s.chars().next().is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
        && !is_number(s)
        && !is_enum_value(s)
}

fn main() {
    let mut args = std::env::args().skip(1);
    let pak_path = args.next().expect("usage: idxd_tokens <pak> [TypeName...]");
    let types: Vec<String> = {
        let v: Vec<String> = args.collect();
        if v.is_empty() {
            vec!["Weapon".into(), "Shell".into()]
        } else {
            v
        }
    };

    let pak = PakArchive::open(&pak_path).expect("open pak");
    for entry in pak.entries() {
        let Ok(bytes) = pak.read(entry) else { continue };
        let Ok(obj) = IdxdObject::parse(&bytes) else { continue };
        let toks = obj.tokens();
        // Only entries that actually declare one of the requested sub-record
        // types (the pool-start heuristic can prefix one stray byte, so match a
        // short suffix rather than equality -- same rule as the splitter below).
        if !toks
            .iter()
            .any(|t| types.iter().any(|ty| t == ty || (t.ends_with(ty.as_str()) && t.len() <= ty.len() + 2)))
        {
            continue;
        }

        if std::env::var("SYLPH_RAW").is_ok() {
            println!("RAW {:08x}", entry.name_hash);
            for t in toks {
                println!("T {t}");
            }
        }

        // Sub-record boundaries: a bare type-name token. The first one is the
        // schema's own declaration (`EnumWeapon`-style header lives in title
        // code, not here), so we take every occurrence in order.
        let mut bounds: Vec<(usize, &str)> = Vec::new();
        for (i, t) in toks.iter().enumerate() {
            // The pool-start heuristic can glue one stray binary byte onto the
            // first token ("YWeapon"), so match a short suffix, not equality.
            if let Some(ty) = types
                .iter()
                .find(|ty| t == *ty || (t.ends_with(ty.as_str()) && t.len() <= ty.len() + 2))
            {
                bounds.push((i, ty.as_str()));
            }
        }
        for (n, &(start, ty)) in bounds.iter().enumerate() {
            let end = bounds.get(n + 1).map(|b| b.0).unwrap_or(toks.len());
            let slice = &toks[start..end];
            // The record's own ID value is the token straight after its type
            // name (`Shell` -> `Shell_DSaber_P_wep_01_Beam`). The `ID`/`Name`
            // *keys* are interned once in the pool, so only the first record
            // shows them -- position-of-key lookup would miss the rest.
            let id = slice.get(1).map(|s| s.as_str()).unwrap_or("?");
            println!("REC {:08x} {n} {ty} {id}", entry.name_hash);
            for i in 1..slice.len() {
                let (k, v) = (slice[i].as_str(), slice[i - 1].as_str());
                if is_key_like(k) && (!is_key_like(v) || is_number(v)) {
                    println!("F {k} {v}");
                }
            }
        }
    }
}
