//! The VScript binding registry — the fourth surface a Source-2 module documents about itself, and the
//! only one that states a function's PARAMETER NAMES.
//!
//! Valve exposes a subset of the C++ surface to script (Lua in Dota's custom games, and a smaller set in
//! CS2). Every exposed method is registered with the script VM through a descriptor carrying its
//! script-facing name, its C++ name, an English description, a return type, and a pointer to the
//! implementation. That is a locator AND a prototype AND documentation, all stated by Valve, which makes
//! it the same kind of find as the Pulse registry and the console-command registration.
//!
//! # Why this is not a table walk
//!
//! The obvious route — find a static array of descriptors and read it — does not work, and the reason is
//! worth stating because it costs a day to rediscover. The descriptors are built at RUNTIME: a scan of
//! Dota's `libserver.so` finds 2,268,664 `R_X86_64_RELATIVE` relocations and **not one** points at a
//! description string. On disk the descriptor array is zeroes.
//!
//! What is static is the CODE that fills it in, and every field is a constant in the instruction stream.
//! This is the same shape the Pulse parameter records turned out to have, and the same answer applies:
//! constant-propagate through the initialiser rather than read the table.
//!
//! ```text
//!     movq      xmm0, [rip+slot]      ; the script-facing name, via a relocated .data.rel.ro slot
//!     lea       rdx,  [rip+"Script_TakeDamage"]
//!     pinsrq    xmm0, rdx, 1          ; pack both names into one 16-byte store
//!     lea       rsi,  [rip+"Applies damage to this entity."]
//!     lea       rax,  [rax+rax*4]     ; index * 5
//!     shl       rax,  4               ;         * 16  -> stride 80
//!     add       rax,  [rbx+0x28]      ; base = owning class descriptor's function array
//!     mov       [rax+0x30], rbx       ; owner
//!     mov       [rax+0x10], rsi       ; description
//!     movups    [rax], xmm0           ; +0x00 script name, +0x08 C++ name
//!     mov       [rax+0x18], r11w      ; return type
//! ```
//!
//! # The record
//!
//! **80 bytes**, derived rather than assumed — the `lea r,[r+r*4]` / `shl r,4` pair states it in the
//! instruction stream, so a stride change is a decode failure rather than silent corruption.
//!
//! | offset | field |
//! |---|---|
//! | `+0x00` | script-facing name (`TakeDamage`) |
//! | `+0x08` | C++ binding name (`Script_TakeDamage`) |
//! | `+0x10` | Valve's English description |
//! | `+0x18` | return type, a `u16` |
//! | `+0x28` | a name string — the return value's, where one is given |
//! | `+0x30` | the owning class descriptor |
//! | `+0x38` | the marshalling thunk, SHARED by every binding of the same shape |
//! | `+0x40` | pointer-to-member: the implementation |
//! | `+0x48` | a `u32` count |
//!
//! `+0x40` is an Itanium pointer-to-member, which is convenient rather than awkward: a non-virtual
//! member is a plain address and a virtual one is `slot * 8 + 1`. Those are exactly the two locator
//! forms the rest of this crate already emits, so a VScript binding lands in `gamedata` as either a
//! signature or a vtable offset with no new concept.
//!
//! Do not confuse `+0x38` with `+0x40`. The thunk at `+0x38` is a compiler-generated trampoline shared
//! across every binding with the same signature; folding it would ship dozens of distinct names all
//! pointing at one address. That is the same mistake the Pulse `+24`/`+32` accessors invite, and it is
//! caught here the same way — by the sharing itself, since a real implementation is referenced once.
//!
//! # Shape-driven, so a layout change yields FEWER bindings and never wrong ones
//!
//! Nothing here is anchored on an address, a symbol or a fixed offset into the image. A record is
//! recognised by what the initialiser DOES — a 16-byte store of two plausible name strings, a
//! description or nothing at `+0x10`, a small return type, and a `+0x40` that is either executable code
//! or a small odd integer. A build that reshapes the descriptor fails those tests and produces nothing,
//! which the release floor then catches.

use crate::elf::CodeImage;
use iced_x86::{Decoder, DecoderOptions, FlowControl, Instruction, Mnemonic, OpKind, Register};
use std::collections::{BTreeMap, HashMap};

/// The record stride, in bytes. Stated by the initialiser's own `idx*5 << 4`; kept as a constant only to
/// validate what is decoded.
///
/// `pub(crate)` because the LIVE class walk in `produce` steps the same records and must step them by
/// the same number. Unlike the Pulse element stride this one is NOT derived by consensus — the
/// initialiser states it in the instruction stream, so there is nothing to vote on — and a build that
/// changes it shows up as decoded records failing validation here, not as a mis-strided live walk.
pub(crate) const STRIDE: i64 = 80;

/// Field displacements within the record.
const F_NAME: i64 = 0x00;
const F_CPP: i64 = 0x08;
const F_DESC: i64 = 0x10;
const F_RET: i64 = 0x18;
const F_IMPL: i64 = 0x40;

/// Longest accepted name/description, so a mis-decoded pointer into the middle of a blob cannot produce a
/// megabyte "name".
const MAX_NAME: usize = 128;
const MAX_DESC: usize = 512;

/// `ScriptDataType_t`, DERIVED by joining recovered bindings against Valve's own published VScript dump
/// rather than assumed from Source's historical ordering.
///
/// The distinction matters, and the first attempt at this table is the reason it is spelled out. Two
/// anchors were available by inspection — a binding returning `float` stores `1`, one returning `int`
/// stores `5` — and they fit Source 1's long-standing `FIELD_*` ordering, in which `5` is `BOOLEAN`. That
/// reading was WRONG: joined against 389 bindings whose return type Valve states, `5` is `int` and `6` is
/// `bool`. Two points are enough to fit a plausible table and not enough to check one.
///
/// Agreement on the derived table is total where a comparison is meaningful. The apparent disagreements
/// are Valve naming a SEMANTIC type over the same ABI type: `5` also covers `modifierpriority` and
/// `UnitFilterResult` (enums, which are ints), and `31` also covers `CDOTA_BaseNPC` and `CBaseEntity`
/// (entity handles, which are handles).
///
/// The raw word ships beside the decoded name regardless — the rule `flags_raw` already follows — so a
/// build that renumbers this can be re-read rather than silently mislabelled.
const RET_TYPES: [(u16, &str); 13] = [
    (0, "void"),
    (1, "float"),
    (3, "Vector"),
    (5, "int"),
    (6, "bool"),
    (13, "ehandle"),
    (14, "Vector"),
    (29, "unknown"),
    (30, "string"),
    (31, "handle"),
    (32, "table"),
    (37, "uint"),
    (39, "QAngle"),
];

/// Decode a return-type word, or `None` when the value is outside what is corroborated.
pub fn ret_type_name(raw: u16) -> Option<&'static str> {
    RET_TYPES.iter().find(|(v, _)| *v == raw).map(|(_, n)| *n)
}

/// Where a binding's implementation lives, decoded from the pointer-to-member at `+0x40`.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Impl {
    /// A non-virtual member: the address itself.
    Addr(u64),
    /// A virtual member: `(pmf - 1) / 8` is the vtable slot index.
    Slot(u64),
}

/// One registered VScript binding.
#[derive(Clone, Debug)]
pub struct VScriptFunc {
    /// The script-facing name a Lua author calls (`TakeDamage`).
    pub name: String,
    /// The C++ binding name (`Script_TakeDamage`). Often but not always the script name with a prefix.
    pub cpp_name: String,
    /// Valve's own English description, where the registration supplies one.
    pub description: Option<String>,
    /// The return type as stored, undecoded.
    pub ret_raw: u16,
    /// The return type decoded, or `None` if the value is outside the corroborated set.
    pub ret: Option<&'static str>,
    /// The implementation, as an address or a vtable slot.
    pub imp: Option<Impl>,
}

/// What a register provably holds — CONSTANTS only, which is where this parts company with `concmd`'s
/// tracker.
///
/// There is no symbolic-base variant here and none is needed: a store is credited to a record through
/// `recid`, propagated across `mov rD,rS`, rather than through a `(register, epoch)` pair. That is why
/// the base survives being copied between registers, and why this tracker needs no epoch counter.
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum V {
    Unknown,
    Const(u64),
}

impl V {
    fn konst(self) -> Option<u64> {
        match self {
            V::Const(c) => Some(c),
            V::Unknown => None,
        }
    }
}

/// The 64-bit parent register as a slot index. Thin wrapper over [`crate::abi::gp_slot`] — the mapping is a
/// fixed SysV fact, and this file only narrows it to the `u8` its `[_; 16]` arrays index by.
fn gpr(r: Register) -> Option<u8> {
    crate::abi::gp_slot(r).map(|s| s as u8)
}

fn xmm(r: Register) -> Option<u8> {
    r.is_xmm()
        .then(|| (r as usize - Register::XMM0 as usize) as u8)
        .filter(|i| *i < 16)
}

/// Registers a call clobbers, so a value cannot survive across one and be attributed to the wrong record.
const CLOBBER: [usize; 9] = [0, 1, 2, 6, 7, 8, 9, 10, 11];

/// A field of a particular record: which record, and the displacement within it.
type Slot = (u32, i64);

/// Read a NUL-terminated string, rejecting anything that is not plausibly a name.
fn text(img: &CodeImage, va: u64, max: usize) -> Option<String> {
    let s = img.read_c_string(va)?;
    if s.is_empty() || s.len() > max {
        return None;
    }
    s.chars()
        .all(|c| c.is_ascii_graphic() || c == ' ')
        .then_some(s)
}

/// An identifier-shaped string — what a script-facing or C++ name must look like. Deliberately strict:
/// a mis-decoded pointer usually lands on prose or a path, and both fail this.
fn ident(img: &CodeImage, va: u64) -> Option<String> {
    let s = text(img, va, MAX_NAME)?;
    let ok = s
        .chars()
        .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == ':')
        && s.chars()
            .next()
            .is_some_and(|c| c.is_ascii_alphabetic() || c == '_');
    ok.then_some(s)
}

/// Decode the pointer-to-member at `+0x40`.
///
/// The two forms are distinguished by the low bit, per the Itanium ABI. Both are validated: an address
/// has to land in executable code, and a slot index has to be small enough to be a real vtable position.
/// Anything else means the field is not what this reader thinks it is, and yields `None` rather than a
/// confident wrong locator.
fn decode_pmf(img: &CodeImage, pmf: u64) -> Option<Impl> {
    if pmf == 0 {
        return None;
    }
    if pmf & 1 == 1 {
        let slot = (pmf - 1) / 8;
        // 2048 is the same ceiling `rtti` reads vtables to; past it this is not a slot index.
        (slot < 2048 && (pmf - 1).is_multiple_of(8)).then_some(Impl::Slot(slot))
    } else {
        img.is_code(pmf).then_some(Impl::Addr(pmf))
    }
}

/// Recover every VScript binding the image registers.
///
/// One pass over the candidate functions, tracking what each register and XMM half provably holds and
/// collecting stores to record-relative slots. A group of stores is accepted as a binding only if it
/// presents the full shape, so partial or coincidental matches are dropped rather than guessed at.
pub fn vscript_functions(img: &CodeImage) -> Vec<VScriptFunc> {
    let entries = crate::locate::function_entries(img);

    let mut out: Vec<VScriptFunc> = Vec::new();
    let mut insn = Instruction::default();

    for (i, &start) in entries.iter().enumerate() {
        let end = entries.get(i + 1).copied().unwrap_or(u64::MAX);
        let Some(code) = img.code_range(start, end) else {
            continue;
        };

        let mut val = [V::Unknown; 16];
        // Which registers currently hold a RECORD BASE, and which hold the half-built `idx*80` on the way
        // to one. This is the structural anchor: a store is only collected when its base was computed by
        // the initialiser's own `idx*5 << 4 + [class+0x28]`. Without it the pass collects any struct with
        // two string pointers at +0x00/+0x08, and `libserver` has at least one other table of that shape
        // (the network field serialisers) which then contributes records whose "name" is a netvar.
        let mut scaled = [false; 16];
        // Which RECORD each register currently points at, not merely whether it points at one. Keying on
        // identity rather than on (register, epoch) is what lets a base survive `mov rcx,rax` — the
        // compiler routinely copies the base and then reuses the original for something else, writing
        // half a record through each. Keyed by register, those two halves land in different groups and
        // neither is complete.
        let mut recid: [Option<u32>; 16] = [None; 16];
        let mut next_rec: u32 = 0;
        // Each XMM tracked as its two 64-bit halves, which is the only way the packed name store is
        // readable: both names reach the record through one 16-byte write.
        let mut xr = [(V::Unknown, V::Unknown); 16];
        let mut stores: HashMap<Slot, u64> = HashMap::new();

        let mut dec = Decoder::with_ip(64, code, start, DecoderOptions::NONE);
        while dec.can_decode() {
            dec.decode_out(&mut insn);

            if insn.flow_control() == FlowControl::Call {
                for c in CLOBBER {
                    val[c] = V::Unknown;
                    scaled[c] = false;
                    recid[c] = None;
                }
                xr = [(V::Unknown, V::Unknown); 16];
                // MEASURED DEAD END, recorded so it is not re-attempted: treating `rax` as a speculative
                // record base after every call — on the theory that some registrations allocate a record
                // and fill it through the returned pointer — reintroduces precisely the network-field
                // serialisers the record-base anchor exists to reject (`CBaseEntity`/`m_fFlags`,
                // `CNetworkOriginCellCoordQuantizedVector`/`m_cellX`, the `*ChangedCompat` callbacks) and
                // recovers no additional binding. The structure built through a call's return here is the
                // CLASS descriptor, not a function record: its `+0x00`/`+0x08` hold the class name twice.
                continue;
            }

            match insn.mnemonic() {
                // `lea r,[rip+d]` — a string or global address. `lea rD,[rS+rS*4]` is something else
                // entirely: the first half of the record-base computation, `idx * 5`.
                Mnemonic::Lea => {
                    if let Some(d) = gpr(insn.op0_register()) {
                        let times_five = insn.memory_index() != Register::None
                            && insn.memory_base() == insn.memory_index()
                            && insn.memory_index_scale() == 4
                            && insn.memory_displacement64() == 0;
                        val[d as usize] = if insn.is_ip_rel_memory_operand() {
                            V::Const(insn.ip_rel_memory_address())
                        } else {
                            V::Unknown
                        };
                        scaled[d as usize] = times_five;
                        recid[d as usize] = None;
                    }
                }

                // `xor rD,rD` is the zeroing idiom, not an arithmetic unknown. It matters more here than
                // it looks: a `void` binding sets its return type with `xor r11d,r11d` and then stores
                // `r11w`, so treating this as an unknown loses every void-returning binding — which on
                // Dota is most of them.
                Mnemonic::Xor => {
                    if let (Some(d), Some(s)) = (gpr(insn.op0_register()), gpr(insn.op1_register()))
                    {
                        val[d as usize] = if d == s { V::Const(0) } else { V::Unknown };
                        scaled[d as usize] = false;
                        recid[d as usize] = None;
                    }
                }

                // `shl rD,4` completes `idx * 80`. Any other shift of a scaled register means this is
                // not the idiom and the candidate is dropped.
                Mnemonic::Shl => {
                    if let Some(d) = gpr(insn.op0_register()) {
                        let keep = scaled[d as usize]
                            && insn.op1_kind() == OpKind::Immediate8
                            && insn.immediate8() == 4;
                        val[d as usize] = V::Unknown;
                        scaled[d as usize] = keep;
                        recid[d as usize] = None;
                    }
                }

                // `add rD,[class+0x28]` turns `idx * 80` into the record's own address. From here every
                // store through `rD` is a field of one binding.
                Mnemonic::Add => {
                    if let Some(d) = gpr(insn.op0_register()) {
                        let base = scaled[d as usize] && insn.op1_kind() == OpKind::Memory;
                        val[d as usize] = V::Unknown;
                        scaled[d as usize] = false;
                        recid[d as usize] = base.then(|| {
                            next_rec += 1;
                            next_rec
                        });
                    }
                }

                // `movq xmm,[rip+slot]` loads a RELOCATED pointer — the script-facing name arrives this
                // way rather than as a `lea`, and reading it needs the relocation applied, which
                // `read_ptr` does. `movq xmm,r64` and the reverse also appear.
                Mnemonic::Movq | Mnemonic::Movd => {
                    if let Some(x) = xmm(insn.op0_register()) {
                        let lo = if insn.op1_kind() == OpKind::Memory {
                            if insn.is_ip_rel_memory_operand() {
                                img.read_ptr(insn.ip_rel_memory_address())
                                    .map_or(V::Unknown, V::Const)
                            } else {
                                V::Unknown
                            }
                        } else if let Some(s) = gpr(insn.op1_register()) {
                            val[s as usize]
                        } else {
                            V::Unknown
                        };
                        // `movq` zeroes the upper half; that matters because the high name is inserted
                        // afterwards and must not inherit a stale value.
                        xr[x as usize] = (lo, V::Const(0));
                    }
                }

                // `pinsrq xmm,r64,1` — the second name packed into the high half.
                Mnemonic::Pinsrq => {
                    if let (Some(x), Some(s)) = (xmm(insn.op0_register()), gpr(insn.op1_register()))
                        && insn.op2_kind() == OpKind::Immediate8
                    {
                        let v = val[s as usize];
                        if insn.immediate8() == 1 {
                            xr[x as usize].1 = v;
                        } else {
                            xr[x as usize].0 = v;
                        }
                    }
                }

                // `movddup xmm,[rip+slot]` — ONE pointer written into both halves. This is the form the
                // compiler picks when the script-facing name and the C++ name are the SAME string, which
                // is the common case: only the bindings that need a distinct C++ name (usually a
                // `Script_`-prefixed wrapper) load two pointers. Missing this mnemonic costs roughly
                // four fifths of the registry on Dota, so it is not an edge case.
                Mnemonic::Movddup => {
                    if let Some(x) = xmm(insn.op0_register()) {
                        let v = if insn.is_ip_rel_memory_operand() {
                            img.read_ptr(insn.ip_rel_memory_address())
                                .map_or(V::Unknown, V::Const)
                        } else {
                            V::Unknown
                        };
                        xr[x as usize] = (v, v);
                    }
                }

                // `punpcklqdq x0,x1` — the same pack, reached the other way.
                Mnemonic::Punpcklqdq => {
                    if let (Some(a), Some(b)) = (xmm(insn.op0_register()), xmm(insn.op1_register()))
                    {
                        xr[a as usize] = (xr[a as usize].0, xr[b as usize].0);
                    }
                }

                // The 16-byte store that lands both names.
                Mnemonic::Movups | Mnemonic::Movaps | Mnemonic::Movdqu | Mnemonic::Movdqa => {
                    if insn.op0_kind() == OpKind::Memory
                        && let Some(x) = xmm(insn.op1_register())
                        && let Some(b) = gpr(insn.memory_base())
                        && insn.memory_index() == Register::None
                        && let Some(rec) = recid[b as usize]
                    {
                        let d = insn.memory_displacement64() as i64;
                        if let Some(v) = xr[x as usize].0.konst() {
                            stores.insert((rec, d), v);
                        }
                        if let Some(v) = xr[x as usize].1.konst() {
                            stores.insert((rec, d + 8), v);
                        }
                    }
                }

                Mnemonic::Mov => {
                    // Store to a record-relative slot: `mov [base+d], reg` or `mov [base+d], imm`.
                    if insn.op0_kind() == OpKind::Memory
                        && insn.memory_index() == Register::None
                        && let Some(b) = gpr(insn.memory_base())
                        && let Some(rec) = recid[b as usize]
                    {
                        let d = insn.memory_displacement64() as i64;
                        let v = match insn.op1_kind() {
                            // A 16-bit store carries the return type. The tracker follows full
                            // registers, so `mov [rec+0x18], r11w` reads back through `r11`.
                            OpKind::Register => {
                                gpr(insn.op1_register()).and_then(|s| val[s as usize].konst())
                            }
                            OpKind::Immediate8 | OpKind::Immediate16 | OpKind::Immediate32 => {
                                Some(insn.immediate32to64() as u64)
                            }
                            OpKind::Immediate32to64 | OpKind::Immediate8to64 => {
                                Some(insn.immediate64())
                            }
                            _ => None,
                        };
                        if let Some(v) = v {
                            stores.insert((rec, d), v);
                        }
                        continue;
                    }
                    // Register-to-register and immediate loads feed the tracker.
                    if let Some(d) = gpr(insn.op0_register()) {
                        // A plain `mov rD,rS` carries the RECORD IDENTITY across, not just the value.
                        // This is the whole reason identity is tracked instead of a per-register flag:
                        // the compiler routinely computes the base in one register, copies it to a
                        // second, and then reuses the first — writing half the record through each.
                        // Without this the second half is attributed to no record and the binding is
                        // lost. `CBaseEntity::AddNewModifier` is the case that exposed it.
                        recid[d as usize] = match insn.op1_kind() {
                            OpKind::Register => {
                                gpr(insn.op1_register()).and_then(|s| recid[s as usize])
                            }
                            _ => None,
                        };
                        scaled[d as usize] = false;
                        val[d as usize] = match insn.op1_kind() {
                            OpKind::Register => {
                                gpr(insn.op1_register()).map_or(V::Unknown, |s| val[s as usize])
                            }
                            OpKind::Immediate8
                            | OpKind::Immediate16
                            | OpKind::Immediate32
                            | OpKind::Immediate32to64
                            | OpKind::Immediate8to64 => V::Const(insn.immediate64()),
                            _ => V::Unknown,
                        };
                    }
                }

                _ => {
                    // Any other write invalidates the destination, so a stale constant cannot be
                    // attributed to a record it never reached.
                    if let Some(d) = gpr(insn.op0_register()) {
                        val[d as usize] = V::Unknown;
                        scaled[d as usize] = false;
                        recid[d as usize] = None;
                    }
                    if let Some(x) = xmm(insn.op0_register()) {
                        xr[x as usize] = (V::Unknown, V::Unknown);
                    }
                }
            }
        }

        // Group the collected stores by the record they were written to, then keep the groups that
        // present the full binding shape.
        // BTreeMap, and it is not a style choice: the emit order below decides which row survives the
        // `(name, cpp_name)` dedup at the end, so a HashMap made that a hash-order coin flip in a
        // byte-reproducible artifact. Record ids are minted in ascending address order by the `Add` arm,
        // so ordering by id is the natural reading order and changes nothing outside a tie.
        let mut groups: BTreeMap<u32, HashMap<i64, u64>> = BTreeMap::new();
        for ((rec, d), v) in stores {
            groups.entry(rec).or_default().insert(d, v);
        }
        for g in groups.values() {
            // The initialiser writes the name pair at the record's own `+0`, so a group without both is
            // not a binding — a partial match on some other structure, or a record whose construction
            // the tracker only saw half of.
            let (Some(&n), Some(&c)) = (g.get(&F_NAME), g.get(&F_CPP)) else {
                continue;
            };
            let (Some(name), Some(cpp_name)) = (ident(img, n), ident(img, c)) else {
                continue;
            };
            let ret_raw = g.get(&F_RET).copied().unwrap_or(u64::MAX);
            if ret_raw > u16::MAX as u64 {
                continue;
            }
            let ret_raw = ret_raw as u16;
            // The implementation is recorded when present but NOT required. It is written at the top of
            // the initialiser's next loop iteration, so whether it lands in this record's group depends
            // on which register the compiler happened to reuse — a binding whose fields are otherwise
            // complete must not be dropped over a scheduling accident. (The shared marshalling thunk at
            // `+0x38` is read past for the same reason and no longer recorded: nothing consumed it, and
            // the record-layout table in this module's header is where that offset is documented.)
            //
            // An earlier revision did require both, on the reasoning that `libserver` holds another table
            // of similar stride (the network field serialisers) whose records carry no code pointer. That
            // was treating a symptom: the record-base anchor above rejects those structurally, because
            // they are not built by `idx*5 << 4 + [class+0x28]`. Requiring the pair on top of that cost
            // real bindings — `CBaseEntity::EmitSound` among them — for no additional safety.
            out.push(VScriptFunc {
                name,
                cpp_name,
                description: g.get(&F_DESC).and_then(|&v| text(img, v, MAX_DESC)),
                ret_raw,
                ret: ret_type_name(ret_raw),
                imp: g.get(&F_IMPL).and_then(|&v| decode_pmf(img, v)),
            });
        }
    }

    out.sort_by(|a, b| (&a.name, &a.cpp_name).cmp(&(&b.name, &b.cpp_name)));
    out.dedup_by(|a, b| a.name == b.name && a.cpp_name == b.cpp_name);
    out
}
