//! Valve's in-binary NAME tables — the one place a stripped Source-2 module names its own functions.
//!
//! Two static tables sit in writable data, and they give different things:
//!
//! - **Entity-IO datadesc** (112-byte stride, in the game library) pairs a NAME with the FUNCTION: the
//!   C++ handler name (`InputKill`), the map-facing input name (`Kill`), and the handler's address.
//! - **Pulse bindings** (80-byte stride, in every module registering Pulse cells or an entity API)
//!   pairs a fully-qualified `Class::Method` with the author-facing display name and description, two
//!   metadata words — and two code pointers that are DESCRIPTOR ACCESSORS, not the bound function
//!   (see [`names`]). It documents the callable surface; it does not locate it.
//!
//! Both are ground truth from the shipped binary — the names are what Valve compiled in, not a transfer
//! from another game and not an inference — so they outrank every other naming source, and extracting
//! them belongs beside the RTTI and schema readers rather than in a side-channel: the source travels
//! with the binary, so it re-derives on every build for free and needs no genesis input.
//!
//! Both readers are deliberately shape-driven, not offset-driven: a record is accepted only when its
//! name pointer resolves to a plausibly-shaped string AND its function pointer lands in executable
//! code. A layout change therefore yields FEWER records, never wrong ones.

use crate::elf::CodeImage;
use std::collections::HashMap;

/// Bytes of one Pulse binding record. The scan steps 8 bytes rather than by stride (a table's first
/// record is not aligned to any section-relative boundary), so this only bounds the read.
const PULSE_STRIDE: usize = 80;
/// Bytes of one entity-IO datadesc record — a `typedescription_t`. The array interleaves plain field
/// descriptors, inputs and outputs at this one stride; the `Input` name prefix selects the inputs.
const DATADESC_STRIDE: usize = 112;

/// Longest string either table is expected to hold. Descriptions are prose, names are identifiers; a
/// pointer that resolves to anything longer is not a record field, so the cap doubles as a validity gate.
const MAX_STR: usize = 256;

/// One Pulse scripting binding, as the module registers it.
#[derive(Clone, Debug)]
pub struct PulseBinding {
    /// Fully-qualified `Class::Method`.
    pub name: String,
    /// The author-facing label Valve shows in the Pulse graph editor ("Get Abs Origin").
    pub display: Option<String>,
    /// The author-facing documentation string ("The entity origin (absolute).").
    pub description: Option<String>,
    /// Accessor returning the binding's static descriptor — a lazy-init singleton, NOT the bound
    /// function. Useful as the anchor a runtime walks to reach the descriptor; useless as a locator.
    pub descriptor: u64,
    /// A second accessor of the same shape, for the binding's argument descriptor.
    pub arg_descriptor: u64,
    /// The binding's own INVOCATION shim — the one code pointer in the record that is an entry point
    /// rather than a descriptor accessor. Zero when the slot holds no executable code (8 of 485 on CS2).
    ///
    /// Measured as a fixed-signature marshalling stub: seven integer arguments returning int, where the
    /// fifth is an array of pointers to the argument values (element *k* at `+8+8k`) and the seventh is an
    /// output sink read by exactly the bindings that declare a return.
    ///
    /// It IS emitted, and the measurement above is not the reason to trust it — the live oracle is. The
    /// address and its measured read-set ship as `surfaces.pulse[].shim` / `.call`; `verify_pulse_shims`
    /// actually CALLS every `args-only` shim on a running server each build (CS2 186 of 193 clean, Dota
    /// 211 of 211); and `GameProfile::min_pulse_callable` floors the population that survives. A locator
    /// nobody has exercised would be a claim — this one is exercised every derive.
    pub shim: u64,
    pub flags: PulseFlags,
}

/// The metadata words a Pulse binding carries, decoded.
///
/// Pulse is Source 2's TYPED graph VM, so a binding cannot be registered without the engine knowing how
/// it may be called — and it records that as data. Each decoded flag below is named for what it was
/// measured to separate across the 1,217 CS2 bindings, not for what its field is presumed to mean:
/// every one of them partitions the table almost perfectly along a naming convention that is
/// independent of the bytes.
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub struct PulseFlags {
    /// Free-function binding on a library class — no receiver. Set for every `CPulse*lib` / `*Funcs`
    /// binding and, being the complement of `instance`, never together with it.
    pub library: bool,
    /// Binding requires an entity receiver. Separates the `*API::` classes from everything else
    /// exactly: 345 of 353 `*API::` bindings set it, and 0 of the other 864 do.
    pub instance: bool,
    /// Binding writes state — the const-correctness bit. Across the entity APIs it is set on 100% of
    /// `Set`/`Add`/`Remove`/`Destroy`/`Turn`/`Toggle`/`Play`/`Stop` bindings and on ~0% of
    /// `Get`/`Is`/`Has`/`Find` ones.
    pub mutating: bool,
    /// Binding may suspend the calling cursor rather than returning within the frame — every record
    /// carrying it is a `Wait` / `Yield` / `Pause` / timer-`Start` / long-running-sequence binding.
    pub blocking: bool,
    /// The two undecoded words as read, so a consumer can re-derive meaning if a later build repurposes
    /// a bit rather than silently inheriting today's reading.
    pub raw: (u32, u32),
}

impl PulseFlags {
    // Both words hold their booleans one-per-BYTE rather than packed one-per-bit, which is what a
    // plain `bool` struct member compiles to — so the flags are read as byte tests, not masks.
    fn decode(w0: u32, w1: u32) -> Self {
        PulseFlags {
            library: w0 & 0xff != 0,
            mutating: w0 >> 8 & 0xff != 0,
            instance: w0 >> 16 & 0xff != 0,
            blocking: w1 >> 3 & 1 != 0,
            raw: (w0, w1),
        }
    }
}

/// The byte offset a FIELD descriptor records its member at. Established against the SchemaSystem at
/// **601/601** on records genuinely inside a datadesc array — the qualification that matters, because a
/// sweep of all writable data mostly finds the schema's OWN field tables, which carry a name pointer and
/// an offset too and therefore match themselves.
const FIELD_OFFSET_SLOT: u64 = 8;

/// One entity-IO input handler: the C++ method name, the input name a map fires, and the handler.
#[derive(Clone, Debug)]
pub struct DatadescInput {
    /// The C++ handler name, e.g. `InputKill`. Not class-qualified BY THE RECORD — the descriptor
    /// carries no owning class. [`datadesc_arrays`] recovers it from the array instead; see `class`.
    pub handler: String,
    /// The class that owns this handler, where the array it sits in identified one. `InputEnable` is a
    /// distinct handler on 48 classes, and this is what tells them apart.
    pub class: Option<String>,
    /// The entity-IO input name a map or another entity fires, e.g. `Kill`.
    pub io_name: String,
    pub func: u64,
}

/// One entity-IO output: an event an entity fires, and the member holding its subscriber list.
///
/// The mirror of [`DatadescInput`] in the same array — an input is something you SEND an entity, an output
/// is something it TELLS you, which is what a mod hooks to react to gameplay. Carries a member OFFSET
/// rather than a function pointer: an output is data on the instance, not code.
#[derive(Clone, Debug)]
pub struct DatadescOutput {
    /// The member holding the output, e.g. `m_OnStartTouch`.
    pub member: String,
    /// The entity-IO name a map wires to, e.g. `OnStartTouch`. Usually the member minus `m_`, but NOT
    /// reliably so (`m_OnBombExplode` fires `BombExplode`), which is why both are kept.
    pub output: String,
    /// Byte offset of the member within its entity.
    pub offset: u32,
}

/// Every entity-IO output the game library declares.
///
/// Shares the array and the stride with [`datadesc_inputs`]; the discriminator is structural rather than
/// lexical — an output has NO handler at `+40` (measured: zero for all 226 CS2 outputs, non-zero for every
/// input), because it is a subscriber list rather than a function.
pub fn datadesc_outputs(img: &CodeImage) -> Vec<DatadescOutput> {
    let mut out = Vec::new();
    scan_records(img, DATADESC_STRIDE, |at| {
        let Some(member) = img.read_ptr(at).and_then(|p| table_string(img, p)) else {
            return;
        };
        // Valve's output convention, the counterpart of the `Input` prefix the input reader keys on.
        if !member.starts_with("m_On") || member.len() <= 4 {
            return;
        }
        let (Some(output), Some(handler)) = (
            img.read_ptr(at + 24).and_then(|p| table_string(img, p)),
            img.read_ptr(at + 40),
        ) else {
            return;
        };
        // A handler here means this is an INPUT record that happens to be named `m_On…`.
        if handler != 0 {
            return;
        }
        let Some(offset) = img.read_u32(at + 8).filter(|&o| o > 0 && o < (1 << 20)) else {
            return;
        };
        out.push(DatadescOutput {
            member,
            output,
            offset,
        });
    });
    out.sort_by(|a, b| (&a.output, &a.member, a.offset).cmp(&(&b.output, &b.member, b.offset)));
    out.dedup_by(|a, b| a.member == b.member && a.output == b.output && a.offset == b.offset);
    out
}

/// One entity-factory record: the classname a map spawns by, and the C++ class it constructs.
///
/// Carries no function pointer — this is the vocabulary a level designer writes (`func_door`) bound to
/// the class the schema describes (`CBaseDoor`), which is the join a consumer needs to spawn or identify
/// an entity by name. It is NOT a naming source.
#[derive(Clone, Debug)]
pub struct EntityClass {
    /// The map-facing classname, e.g. `func_door`.
    pub classname: String,
    /// The C++ class it constructs, e.g. `CBaseDoor` — a schema-registered class.
    pub class: String,
}

/// Every entity classname the game library binds to a class.
///
/// Two adjacent string pointers, gated on their SHAPES being complementary: a map classname is
/// lowercase-with-underscores by Valve's own convention, and a C++ class name starts uppercase. That
/// asymmetry is what separates these records from the many other adjacent string pairs in `.data`.
pub fn entity_classes(img: &CodeImage) -> Vec<EntityClass> {
    let map_name = |s: &str| {
        s.len() >= 3
            && s.starts_with(|c: char| c.is_ascii_lowercase())
            && s.bytes()
                .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'_')
    };
    // Source's universal class prefix: a capital `C` followed by another capital. Plain
    // uppercase-first is far too loose — `.data` is full of adjacent string pairs, and it accepted
    // `chicken_server` -> `GameSessionManifest_server`, two unrelated neighbours.
    let cpp_name = |s: &str| {
        s.len() >= 3
            && s.starts_with('C')
            && s.as_bytes()[1].is_ascii_uppercase()
            && s.bytes().all(|c| c.is_ascii_alphanumeric() || c == b'_')
    };
    let mut out = Vec::new();
    let mut seen = HashMap::new();
    scan_records(img, 16, |at| {
        let (Some(classname), Some(class)) = (
            img.read_ptr(at).and_then(|p| table_string(img, p)),
            img.read_ptr(at + 8).and_then(|p| table_string(img, p)),
        ) else {
            return;
        };
        if !map_name(&classname) || !cpp_name(&class) {
            return;
        }
        // One binding per classname; a duplicate that disagrees is dropped rather than guessed at.
        match seen.entry(classname.clone()) {
            std::collections::hash_map::Entry::Occupied(e) => {
                if *e.get() != class {
                    out.retain(|x: &EntityClass| x.classname != classname);
                }
            }
            std::collections::hash_map::Entry::Vacant(e) => {
                e.insert(class.clone());
                out.push(EntityClass { classname, class });
            }
        }
    });
    out.sort_by(|a, b| a.classname.cmp(&b.classname));
    out
}

/// A name the binary vouches for, at an address it can be trusted to LOCATE.
#[derive(Clone, Debug)]
pub struct ValveName {
    pub name: String,
    pub addr: u64,
}

/// Read the NUL-terminated string at `vaddr`, rejecting anything that is not a short printable-ASCII
/// run. Every table field this validates is a C identifier or a UI string, so a pointer that lands on
/// binary data fails here rather than being carried forward as a garbage name.
fn table_string(img: &CodeImage, vaddr: u64) -> Option<String> {
    if vaddr == 0 {
        return None;
    }
    let s = img.read_c_string(vaddr)?;
    (!s.is_empty() && s.len() <= MAX_STR && s.bytes().all(|c| (0x20..0x7f).contains(&c)))
        .then_some(s)
}

/// Is `s` a fully-qualified `Class::Method` of C identifiers? The Pulse table's defining shape — and a
/// strong enough gate on its own that a false positive would have to be a deliberately-planted string
/// followed by two code pointers.
fn is_qualified(s: &str) -> bool {
    let Some((class, method)) = s.split_once("::") else {
        return false;
    };
    let ident = |p: &str| {
        let mut c = p.chars();
        c.next()
            .is_some_and(|f| f.is_ascii_alphabetic() || f == '_')
            && c.all(|ch| ch.is_ascii_alphanumeric() || ch == '_')
    };
    ident(class) && ident(method)
}

/// Walk every writable data section in 8-byte steps, calling `f` with each candidate record address.
/// `stride` reserves the record's own length so a reader never runs off the end of its section.
fn scan_records(img: &CodeImage, stride: usize, mut f: impl FnMut(u64)) {
    for (va, len) in img.data_blocks() {
        let Some(last) = len.checked_sub(stride) else {
            continue;
        };
        for off in (0..=last).step_by(8) {
            f(va + off as u64);
        }
    }
}

/// Every Pulse binding `img` registers.
pub fn pulse_bindings(img: &CodeImage) -> Vec<PulseBinding> {
    let mut out = Vec::new();
    scan_records(img, PULSE_STRIDE, |at| {
        let Some(name) = img.read_ptr(at).and_then(|p| table_string(img, p)) else {
            return;
        };
        if !is_qualified(&name) {
            return;
        }
        // Both code pointers are required: one alone also matches a plain `{name, …, fnptr}` pair,
        // whereas a second in the very next slot is specific to a binding record.
        let (Some(descriptor), Some(arg_descriptor)) =
            (img.read_ptr(at + 24), img.read_ptr(at + 32))
        else {
            return;
        };
        if !img.is_code(descriptor) || !img.is_code(arg_descriptor) {
            return;
        }
        out.push(PulseBinding {
            name,
            display: img.read_ptr(at + 8).and_then(|p| table_string(img, p)),
            description: img.read_ptr(at + 16).and_then(|p| table_string(img, p)),
            descriptor,
            arg_descriptor,
            shim: img
                .read_ptr(at + 72)
                .filter(|&p| img.is_code(p))
                .unwrap_or(0),
            flags: PulseFlags::decode(
                img.read_u32(at + 56).unwrap_or(0),
                img.read_u32(at + 60).unwrap_or(0),
            ),
        });
    });
    out
}

/// Every entity-IO input handler `img` declares.
pub fn datadesc_inputs(img: &CodeImage) -> Vec<DatadescInput> {
    let mut out = Vec::new();
    scan_records(img, DATADESC_STRIDE, |at| {
        let Some(handler) = img.read_ptr(at).and_then(|p| table_string(img, p)) else {
            return;
        };
        // `Input` + at least one more character: the naming convention Valve's entity-IO macros emit,
        // and what separates the input records from the field and output descriptors sharing the array.
        if !handler.starts_with("Input") || handler.len() <= 5 {
            return;
        }
        let (Some(io_name), Some(func)) = (
            img.read_ptr(at + 24).and_then(|p| table_string(img, p)),
            img.read_ptr(at + 40),
        ) else {
            return;
        };
        if !img.is_code(func) {
            return;
        }
        out.push(DatadescInput {
            handler,
            class: None,
            io_name,
            func,
        });
    });
    out
}

/// One datadesc array: the FIELD descriptors that identify its owning class, and the input handlers
/// that class owns.
pub struct DatadescArray {
    /// `(member name, byte offset)` for each field descriptor, which the SchemaSystem states
    /// independently — the fingerprint that names the array's class.
    pub fields: Vec<(String, i32)>,
    pub inputs: Vec<DatadescInput>,
}

/// Segment the datadesc into ARRAYS, so an input handler can be attributed to the class that owns it.
///
/// The record carries no owning class, which is why `names` has to drop a handler name the table gives
/// more than one address. But the array does: it also holds FIELD descriptors, and a field is a
/// `(member, offset)` pair the SchemaSystem states from a completely different table. The class whose
/// schema contains EVERY pair in an array owns the array, and therefore owns its handlers.
///
/// An array is found from a confirmed input outward — a record with a name at `+0` continues it — so a
/// run is anchored on something already known to be a datadesc record rather than on a shape guess.
pub fn datadesc_arrays(img: &CodeImage) -> Vec<DatadescArray> {
    let named = |at: u64| img.read_ptr(at).and_then(|p| table_string(img, p));
    let input_at = |at: u64| -> Option<DatadescInput> {
        let handler = named(at).filter(|h| h.starts_with("Input") && h.len() > 5)?;
        let io_name = img.read_ptr(at + 24).and_then(|p| table_string(img, p))?;
        let func = img.read_ptr(at + 40).filter(|&f| img.is_code(f))?;
        Some(DatadescInput {
            handler,
            class: None,
            io_name,
            func,
        })
    };

    let mut out = Vec::new();
    let mut claimed: Vec<(u64, u64)> = Vec::new();
    for (va, len) in img.data_blocks() {
        let Some(last) = len.checked_sub(DATADESC_STRIDE) else {
            continue;
        };
        let end = va + last as u64;
        let mut at = va;
        while at <= end {
            if input_at(at).is_none() {
                at += 8;
                continue;
            }
            if claimed.iter().any(|&(s, e)| at >= s && at <= e) {
                at += DATADESC_STRIDE as u64;
                continue;
            }
            let stride = DATADESC_STRIDE as u64;
            let (mut lo, mut hi) = (at, at);
            while lo >= va + stride && named(lo - stride).is_some() {
                lo -= stride;
            }
            while hi + stride <= end && named(hi + stride).is_some() {
                hi += stride;
            }
            claimed.push((lo, hi));
            let (mut fields, mut inputs) = (Vec::new(), Vec::new());
            let mut cur = lo;
            while cur <= hi {
                if let Some(i) = input_at(cur) {
                    inputs.push(i);
                } else if let Some(n) = named(cur).filter(|n| n.starts_with("m_") && n.len() > 3)
                    && let Some(o) = img.read_u32(cur + FIELD_OFFSET_SLOT)
                {
                    fields.push((n, o as i32));
                }
                cur += stride;
            }
            if !inputs.is_empty() {
                out.push(DatadescArray { fields, inputs });
            }
            at = hi + stride;
        }
    }
    out
}

/// The names the tables can be trusted to LOCATE, with AMBIGUOUS ones dropped.
///
/// **Only the entity-IO datadesc qualifies.** The Pulse table's two code pointers are both descriptor
/// accessors, not the bound function: every one of the 485 CS2 `libserver` bindings measures the same
/// empty footprint (`int=0 float=0`), and they disassemble to a lazy-init singleton that returns a
/// pointer to a static descriptor. Folding a Pulse name would ship `CBaseEntityAPI::GetAbsOrigin`
/// pointing at a zero-argument accessor instead of the getter — a locator that resolves, passes live
/// validation as executable code, and is still the wrong function. The bindings are shipped as a
/// registry instead, where the address is labelled for what it is.
///
/// A name is ambiguous when the table gives it more than one address in this image: `InputEnable` is a
/// distinct handler on each of 48 classes and the record carries no owning class to tell them apart, so
/// picking one is a coin flip. The whole name is dropped instead. Returns `(names, dropped)`.
pub fn names(inputs: &[DatadescInput]) -> (Vec<ValveName>, usize) {
    // A handler whose array identified its class is keyed by the QUALIFIED name, which is what makes it
    // unambiguous: `InputEnable` on 48 classes is 48 distinct names once each carries its own. Only the
    // ones still unqualified can collide, and those still drop rather than guess.
    let qualified: Vec<String> = inputs
        .iter()
        .map(|i| match &i.class {
            Some(c) => format!("{c}::{}", i.handler),
            None => i.handler.clone(),
        })
        .collect();
    let mut by_name: HashMap<&str, Vec<u64>> = HashMap::new();
    for (i, q) in inputs.iter().zip(&qualified) {
        let e = by_name.entry(q.as_str()).or_default();
        if !e.contains(&i.func) {
            e.push(i.func);
        }
    }
    let dropped = by_name.values().filter(|a| a.len() > 1).count();
    let mut out: Vec<ValveName> = by_name
        .into_iter()
        .filter(|(_, a)| a.len() == 1)
        .map(|(name, a)| ValveName {
            name: name.to_string(),
            addr: a[0],
        })
        .collect();
    out.sort_by(|x, y| x.name.cmp(&y.name)); // deterministic fold order
    (out, dropped)
}

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

    #[test]
    fn qualified_names_are_two_c_identifiers() {
        assert!(is_qualified("CBaseEntityAPI::GetAbsOrigin"));
        assert!(is_qualified("_Priv::_m0"));
        assert!(!is_qualified("CBaseEntityAPI")); // unqualified
        assert!(!is_qualified("::GetAbsOrigin")); // empty class
        assert!(!is_qualified("CFoo::9Bar")); // method starts with a digit
        assert!(!is_qualified("CFoo<int>::Bar")); // a demangled template, not a table string
        assert!(!is_qualified("Get Abs Origin")); // the display name, not the qualified one
    }

    #[test]
    fn a_class_qualified_handler_is_no_longer_ambiguous() {
        // The same handler NAME on two classes is a collision only while both are unqualified. Once the
        // array's field descriptors identify the owner, they are two distinct names and BOTH survive —
        // which is the whole point of reading the array rather than the record.
        let q = |handler: &str, class: &str, func| DatadescInput {
            handler: handler.into(),
            class: Some(class.into()),
            io_name: "Enable".into(),
            func,
        };
        let (names, dropped) = names(&[
            q("InputEnable", "CBaseDoor", 0x100),
            q("InputEnable", "CBaseTrigger", 0x200),
        ]);
        assert_eq!(dropped, 0);
        assert_eq!(names.len(), 2);
        assert_eq!(names[0].name, "CBaseDoor::InputEnable");
        assert_eq!(names[1].name, "CBaseTrigger::InputEnable");
    }

    #[test]
    fn a_handler_name_at_several_addresses_is_dropped_not_guessed() {
        let input = |handler: &str, io: &str, func| DatadescInput {
            handler: handler.into(),
            class: None,
            io_name: io.into(),
            func,
        };
        let (names, dropped) = names(&[
            input("InputKill", "Kill", 0x100),
            // The same handler NAME on two classes — the record carries no owning class, so neither
            // address can be claimed for the bare name.
            input("InputEnable", "Enable", 0x200),
            input("InputEnable", "Enable", 0x300),
            // The same handler serving two inputs is ONE function, not an ambiguity.
            input("InputToggle", "Toggle", 0x400),
            input("InputToggle", "ToggleAlias", 0x400),
        ]);
        assert_eq!(dropped, 1);
        let got: Vec<(&str, u64)> = names.iter().map(|n| (n.name.as_str(), n.addr)).collect();
        assert_eq!(got, [("InputKill", 0x100), ("InputToggle", 0x400)]); // sorted, no InputEnable
    }

    #[test]
    fn flags_decode_per_byte_not_per_bit() {
        // A library getter: library byte set, mutating and instance clear.
        let lib_get = PulseFlags::decode(0x00_00_01, 0);
        assert!(lib_get.library && !lib_get.mutating && !lib_get.instance);
        // An entity setter: instance + mutating, library clear.
        let api_set = PulseFlags::decode(0x01_01_00, 0);
        assert!(api_set.instance && api_set.mutating && !api_set.library);
        // An entity getter: instance only.
        let api_get = PulseFlags::decode(0x01_00_00, 0);
        assert!(api_get.instance && !api_get.mutating);
        // The yield bit lives in the second word.
        assert!(PulseFlags::decode(0x00_01_00, 0x08).blocking);
        assert!(!PulseFlags::decode(0x00_01_00, 0x20).blocking);
        // The raw words survive decoding so a later build can be re-read.
        assert_eq!(api_set.raw, (0x01_01_00, 0));
    }
}
