//! Offline Itanium C++ RTTI: locate a class's vtable in an ELF `.so` and read its slot array.
//!
//! Chain (Itanium ABI, LP64): the class name is stored length-prefixed+mangled (e.g.
//! "11CBaseEntity") as a `_ZTS` string in `.rodata`; the `_ZTI` typeinfo points to that string
//! at +8; the `_ZTV` vtable points to the typeinfo at +8, with `offset-to-top` at +0, so virtual
//! slots start at vtable+16. Those slot pointers live in `.data.rel.ro` and are supplied by
//! relocations, which `CodeImage::read_ptr` already resolves.
//!
//! This is the ELF/Itanium half; a Windows fork would add an MSVC-RTTI sibling behind the same
//! `find_vtable` shape (COL at vftable-8, TypeDescriptor `.?AV<name>@@`).

use crate::elf::{CodeImage, KindTag};

pub struct VTable {
    pub slot0: u64,      // vaddr of virtual slot index 0
    pub slots: Vec<u64>, // function vaddrs; gamedata offset of a method == its index here
}

/// One vtable discovered by the whole-binary sweep — the class inventory row.
pub struct ClassVtable {
    pub name: String,          // demangled, e.g. "CBaseEntity"
    pub vtable_va: u64,        // vaddr of slot index 0
    pub offset_to_top: i64, // 0 for the primary (complete-object) vtable; <0 for sub-object tables
    pub slots: Vec<u64>,    // method vaddrs; a method's gamedata offset == its index here
    pub bases: Vec<BaseClass>, // direct base classes (the is-a graph edges)
}

/// A direct base class of a type, from its Itanium typeinfo.
pub struct BaseClass {
    pub name: String,       // demangled base class name
    pub offset: i64, // this-pointer adjustment to the base subobject (0 for the primary base)
    pub virtual_base: bool, // true if inherited virtually
}

/// The three Itanium `type_info` "kind" vtables (their in-object `+16` slot0 pointers). libc++abi is
/// statically bundled in CS2 libraries, so these resolve as WEAK symbols and let us classify each
/// typeinfo *exactly* — no heuristic guess of `__class` vs `__si` vs `__vmi`.
struct RttiKinds {
    class: u64, // __class_type_info  — no bases
    si: u64,    // __si_class_type_info — single public base at offset 0
    vmi: u64,   // __vmi_class_type_info — multiple / virtual / non-public bases
}

impl RttiKinds {
    fn detect(img: &CodeImage) -> Self {
        let k = |n: &str| img.symbol_addr(n).map_or(0, |a| a.wrapping_add(16));
        Self {
            class: k("_ZTVN10__cxxabiv117__class_type_infoE"),
            si: k("_ZTVN10__cxxabiv120__si_class_type_infoE"),
            vmi: k("_ZTVN10__cxxabiv121__vmi_class_type_infoE"),
        }
    }
    /// Is `p` (a typeinfo's `+0` field) one of the three kind vtables? When the kind symbols are
    /// stripped (all zero) we can't tell, so accept any pointer the caller already range-checked.
    fn is_kind(&self, p: u64) -> bool {
        if self.class == 0 && self.si == 0 && self.vmi == 0 {
            return true;
        }
        p == self.class || p == self.si || p == self.vmi
    }
}

/// Itanium length-prefixed name for a flat class, e.g. `CBaseEntity` -> `11CBaseEntity`.
/// (Namespaced/templated names need full mangling; our targets are flat class names.)
fn mangle(class: &str) -> String {
    format!("{}{}", class.len(), class)
}

/// Find the class's primary (complete-object) vtable and read its function-pointer slots.
pub fn find_vtable(img: &CodeImage, class: &str, max_slots: usize) -> Option<VTable> {
    let mut candidates: Vec<u64> = Vec::new();

    // Fast path: an exported `_ZTV` symbol (uncommon for gameplay classes, but cheap).
    if let Some(ztv) = img.symbol_addr(&format!("_ZTV{}", mangle(class))) {
        candidates.push(ztv.wrapping_add(16));
    }

    // General path: name string -> typeinfo (points to name at +8) -> vtable (points to TI at +8).
    let mut needle = mangle(class).into_bytes();
    needle.push(0);
    for name_str in img.find_bytes(&needle) {
        for &ti_name_slot in img.ptrs_to(name_str) {
            if ti_name_slot < 8 {
                continue;
            }
            let typeinfo = ti_name_slot - 8;
            for &vt_ti_slot in img.ptrs_to(typeinfo) {
                candidates.push(vt_ti_slot.wrapping_add(8));
            }
        }
    }
    candidates.sort_unstable();
    candidates.dedup();

    for slot0 in candidates {
        // primary vtable has offset-to-top == 0 at slot0-16; filters typeinfo base-class lists
        if img.read_ptr(slot0.wrapping_sub(16)) != Some(0) {
            continue;
        }
        let slots = read_slots(img, slot0, max_slots);
        // Higher floor than `enumerate_vtables` (which admits `>= 2`): a 2-slot stub is too thin to trust
        // as the TARGET's real vtable when matching by name. A class whose primary vtable has exactly 2
        // code slots is still catalogued in the model but not re-located here, so its offsets flag
        // unresolved — a missed derivation for a rare class, never a wrong value.
        if slots.len() >= 3 {
            return Some(VTable { slot0, slots });
        }
    }
    None
}

/// Consecutive slot pointers that land in executable code; stops at the first that doesn't.
///
/// Returning exactly `max` slots is AMBIGUOUS — the vtable may genuinely end there, or may continue past
/// the cap with the tail silently dropped. Callers that care (the ones recording slot counts into the
/// model) should compare `len() == max` and warn; see `GameProfile::max_vtable_slots`.
fn read_slots(img: &CodeImage, slot0: u64, max: usize) -> Vec<u64> {
    let mut out = Vec::new();
    for i in 0..max {
        match img.read_ptr(slot0.wrapping_add((i as u64).wrapping_mul(8))) {
            Some(v) if img.is_code(v) => out.push(v),
            _ => break,
        }
    }
    out
}

/// Demangle an Itanium *type* name (the bare `_ZTS` payload, e.g. "11CBaseEntity") to a readable
/// class name. cpp_demangle wants a whole symbol, so we re-attach the `_ZTS` prefix and strip the
/// "typeinfo name for " decoration it produces. Falls back to the mangled form.
fn demangle_type(mangled: &str) -> String {
    let sym = format!("_ZTS{mangled}");
    cpp_demangle::Symbol::new(sym.as_bytes())
        .ok()
        .and_then(|s| s.demangle().ok())
        .map(|d| {
            d.strip_prefix("typeinfo name for ")
                .unwrap_or(&d)
                .to_string()
        })
        .unwrap_or_else(|| mangled.to_string())
}

/// If `ti` addresses a valid Itanium typeinfo, return its DEMANGLED class name.
/// A typeinfo is `[kind_vtable_ptr][name_ptr][ base-class data … ]`: `+0` points at one of the
/// C++ runtime's type_info-kind vtables, `+8` at the `_ZTS` name string.
fn typeinfo_name(img: &CodeImage, ti: u64, kinds: &RttiKinds) -> Option<String> {
    // +0 must be one of the three kind vtables. Prefer the symbol-name-derived tag (the only signal that
    // survives a DYNAMICALLY-linked C++ runtime, where the three kinds all resolve to the same offline
    // value); else fall back to the in-image value check (statically-linked / stripped builds).
    if img.kind_at(ti).is_none() {
        let kind = img.read_ptr(ti)?;
        if kind == 0 || !img.contains(kind) || !kinds.is_kind(kind) {
            return None;
        }
    }
    let name_ptr = img.read_ptr(ti.wrapping_add(8))?;
    let mangled = img.read_c_string(name_ptr)?;
    // Itanium type names start with a length digit (flat class) or a mangling sigil.
    let c0 = *mangled.as_bytes().first()?;
    if !(c0.is_ascii_digit() || matches!(c0, b'N' | b'I' | b'P' | b'K' | b'S')) {
        return None;
    }
    Some(demangle_type(&mangled))
}

/// Direct base classes of the typeinfo at `ti`, dispatched on its exact Itanium kind.
fn typeinfo_bases(img: &CodeImage, ti: u64, kinds: &RttiKinds) -> Vec<BaseClass> {
    // Classify the kind: prefer the symbol-name tag (dynamically-linked runtime), else compare the resolved
    // +0 pointer to the in-image kind vtables (statically-linked). Without the tag, an old build can't tell
    // __si from __vmi at all, and the base graph would silently come back empty.
    let tag = img.kind_at(ti).or_else(|| {
        let kind = img.read_ptr(ti).unwrap_or(0);
        if kind == 0 {
            None
        } else if kind == kinds.si {
            Some(KindTag::Si)
        } else if kind == kinds.vmi {
            Some(KindTag::Vmi)
        } else {
            None
        }
    });
    match tag {
        Some(KindTag::Si) => {
            // __si_class_type_info: one public, non-virtual base at offset 0; its typeinfo ptr at +16.
            if let Some(bp) = img.read_ptr(ti.wrapping_add(16))
                && let Some(name) = typeinfo_name(img, bp, kinds)
            {
                return vec![BaseClass {
                    name,
                    offset: 0,
                    virtual_base: false,
                }];
            }
            Vec::new()
        }
        Some(KindTag::Vmi) => {
            // __vmi_class_type_info: flags@+16, base_count@+20, then 16-byte {typeinfo_ptr, offset_flags}.
            let Some(count) = img.read_u32(ti.wrapping_add(20)) else {
                return Vec::new();
            };
            if count == 0 || count > 128 {
                return Vec::new();
            }
            let mut bases = Vec::new();
            for i in 0..count as u64 {
                let e = ti.wrapping_add(24).wrapping_add(i.wrapping_mul(16));
                let Some(bp) = img.read_ptr(e) else {
                    break;
                };
                if let Some(name) = typeinfo_name(img, bp, kinds) {
                    let of = img.read_i64(e.wrapping_add(8)).unwrap_or(0);
                    bases.push(BaseClass {
                        name,
                        offset: of >> 8, // Itanium: high bits = this-pointer adjustment
                        virtual_base: of & 0x1 != 0, // low byte: 0x1 = virtual, 0x2 = public
                    });
                }
            }
            bases
        }
        _ => Vec::new(), // __class_type_info (no bases) or a kind we can't classify
    }
}

/// Enumerate EVERY class vtable in the image via Itanium RTTI — the whole-binary class inventory.
///
/// Reloc-driven (not a raw byte sweep): each vtable's typeinfo field at `vtable-8` is a relocation,
/// so we walk the reloc map, keep slots that point at a valid typeinfo, and recover the vtable just
/// above. Every pointer is read through the `.rela.dyn`-resolved `read_ptr`, so `.data.rel.ro` slots
/// (zero on disk) come back as their true as-loaded values.
pub fn enumerate_vtables(img: &CodeImage, max_slots: usize) -> Vec<ClassVtable> {
    let kinds = RttiKinds::detect(img);
    let mut out = Vec::new();
    for (slot, val) in img.reloc_slots() {
        if slot < 8 {
            continue;
        }
        let Some(name) = typeinfo_name(img, val, &kinds) else {
            continue;
        };
        // No de-dup guard: `reloc_slots` iterates a map KEYED by slot vaddr, so every slot — and hence
        // every `slot + 8` — is already unique. A `seen` set here can never reject a candidate.
        let vtable_va = slot.wrapping_add(8);
        // offset-to-top sits at vtable-16 (just below the typeinfo field): a plain, non-relocated,
        // pointer-aligned int, 0 for a primary table and a small negative for sub-object tables.
        let Some(ott) = img.read_i64(slot.wrapping_sub(8)) else {
            continue;
        };
        if !(-(1 << 24)..=0).contains(&ott) || ott % 8 != 0 {
            continue;
        }
        let slots = read_slots(img, vtable_va, max_slots);
        if slots.len() < 2 {
            continue;
        }
        let bases = typeinfo_bases(img, val, &kinds);
        out.push(ClassVtable {
            name,
            vtable_va,
            offset_to_top: ott,
            slots,
            bases,
        });
    }
    out.sort_by_key(|c| c.vtable_va);
    out
}
