//! Generic function-pointer array detection (M8 + M11).
//!
//! M3 already detects "vtable" candidates — runs of ≥3 contiguous function
//! pointers in `.rdata` / `.data` (with COL/RTTI walk on top). This module
//! widens the net:
//!
//! - **Dispatch tables** (M8): runs of ≥2 function pointers in `.rdata` /
//!   `.data` that are NOT already classified as vtables. Captures switch
//!   jump tables, callback registries, command tables, gameplay state
//!   machines, etc.
//! - **Static initialiser tables** (M11): function-pointer arrays in
//!   `.rdata` whose entries all have classic constructor-like prologues
//!   (small frame; either leaf or calling well-known runtime helpers).
//!   The MSVC convention names the bracketing symbols `__xc_a` /
//!   `__xc_z` (C++ ctors) and `__xi_a` / `__xi_z` (C runtime), but the
//!   names are stripped from Sylpheed; we classify by structure.
//!
//! All findings are written to a single `function_pointer_arrays` table
//! with a `kind` column — `"vtable"`, `"dispatch_table"`, or `"static_init"`.
//! Vtable rows are duplicated from M3's `vtables` table for join
//! convenience (so a single query covers all classification kinds).
//!
//! ### What this module does NOT do
//!
//! - No alias-based classification — `static_init` is heuristic and may
//!   include any function-pointer array near the binary's `__xc_*` region.
//! - Does not parse the bracket symbols' actual addresses — we'd need
//!   debug symbols, which Sylpheed doesn't ship.
//! - Two-element runs in `.data` are common false positives (struct fields
//!   that happen to alias function entries); we only emit `dispatch_table`
//!   rows for `.rdata`.

use std::collections::BTreeSet;

use xenia_xex::pe::PeSection;

use crate::vtables::Vtable;

/// One detected function-pointer array.
#[derive(Debug, Clone)]
pub struct FuncPtrArray {
    pub address: u32,
    pub length: u32,
    pub kind: &'static str, // "vtable" | "dispatch_table" | "static_init"
    /// Array entries (function VAs).
    pub entries: Vec<u32>,
}

/// Run the pass. `vtables` is the M3 result — those addresses are skipped
/// in the dispatch-table scan to avoid duplication. `function_starts` is
/// the M1 corrected function-start set (used to validate that each array
/// entry actually points at a known function).
#[tracing::instrument(skip_all, fields(image_base = format_args!("{:#010x}", image_base)))]
pub fn analyze(
    pe: &[u8],
    image_base: u32,
    sections: &[PeSection],
    function_starts: &BTreeSet<u32>,
    vtables: &[Vtable],
) -> Vec<FuncPtrArray> {
    let started = std::time::Instant::now();
    let vtable_addrs: BTreeSet<u32> = vtables.iter().map(|v| v.address).collect();
    let mut out: Vec<FuncPtrArray> = Vec::new();

    // Re-emit vtables in this table for unified-query convenience.
    for v in vtables {
        out.push(FuncPtrArray {
            address: v.address,
            length: v.length,
            kind: "vtable",
            entries: v.methods.clone(),
        });
    }

    // Scan only .rdata for dispatch tables — .data has too many false
    // positives from struct fields aliasing function VAs.
    for section in sections {
        if section.name != ".rdata" { continue; }
        let raw_start = section.virtual_address as usize;
        let raw_end = (section.virtual_address + section.virtual_size) as usize;
        if raw_end > pe.len() { continue; }
        let bytes = &pe[raw_start..raw_end.min(pe.len())];
        let va_base = image_base + section.virtual_address;

        let mut i = 0usize;
        while i + 8 <= bytes.len() {
            if !i.is_multiple_of(4) { i += 1; continue; }
            let mut entries: Vec<u32> = Vec::new();
            let mut j = i;
            while j + 4 <= bytes.len() {
                let val = u32::from_be_bytes([bytes[j], bytes[j + 1], bytes[j + 2], bytes[j + 3]]);
                if function_starts.contains(&val) {
                    entries.push(val);
                    j += 4;
                } else {
                    break;
                }
            }
            if entries.len() >= 2 {
                let address = va_base + (i as u32);
                if !vtable_addrs.contains(&address) {
                    let kind = classify_run(image_base, &entries, pe);
                    out.push(FuncPtrArray {
                        address,
                        length: entries.len() as u32,
                        kind,
                        entries,
                    });
                }
                i += j - i;
            } else {
                i += 4;
            }
        }
    }

    let elapsed_ms = started.elapsed().as_millis() as f64;
    let n_vt = out.iter().filter(|a| a.kind == "vtable").count();
    let n_dt = out.iter().filter(|a| a.kind == "dispatch_table").count();
    let n_si = out.iter().filter(|a| a.kind == "static_init").count();
    metrics::histogram!("analysis.phase_ms", "phase" => "funcptr_arrays").record(elapsed_ms);
    tracing::info!(
        total = out.len(), vtable = n_vt, dispatch_table = n_dt, static_init = n_si,
        elapsed_ms,
        "function-pointer array scan complete",
    );
    out
}

/// Classify a non-vtable function-pointer array. Currently distinguishes
/// only "static_init" (all entries have constructor-like prologues — a
/// brief mfspr+stwu prologue with a small frame) from "dispatch_table"
/// (anything else).
fn classify_run(image_base: u32, entries: &[u32], pe: &[u8]) -> &'static str {
    // Heuristic: a static initialiser's prologue is small (frame ≤ 0x80,
    // typically ≤ 0x40). If every entry's first instruction is mfspr+LR
    // (opcode 31, xo 339, spr 8) followed by a small stwu, classify as
    // static_init.
    let mut all_ctor = true;
    let mut any_ctor = false;
    for &fn_va in entries {
        if !is_ctor_like(pe, image_base, fn_va) {
            all_ctor = false;
        } else {
            any_ctor = true;
        }
    }
    if all_ctor && any_ctor && entries.len() >= 3 {
        "static_init"
    } else {
        "dispatch_table"
    }
}

/// True if the function at `fn_va` looks like a tiny C++ static initialiser:
/// `mfspr r12, LR` immediately followed by `stwu r1, -N(r1)` with `N ≤ 0x80`.
fn is_ctor_like(pe: &[u8], image_base: u32, fn_va: u32) -> bool {
    let off = fn_va.wrapping_sub(image_base) as usize;
    if off + 8 > pe.len() { return false; }
    let i0 = u32::from_be_bytes([pe[off], pe[off + 1], pe[off + 2], pe[off + 3]]);
    let i1 = u32::from_be_bytes([pe[off + 4], pe[off + 5], pe[off + 6], pe[off + 7]]);
    // i0: mfspr rD, LR — opcode 31, xo 339, spr 8.
    let op0 = i0 >> 26;
    let xo0 = (i0 >> 1) & 0x3FF;
    let spr0 = (((i0 >> 11) & 0x1F) << 5) | ((i0 >> 16) & 0x1F);
    if !(op0 == 31 && xo0 == 339 && spr0 == 8) { return false; }
    // i1 must be stwu r1, -N(r1) with N ≤ 0x80, OR a `bl __savegprlr_*`
    // followed eventually by stwu (full prologue). Allow either.
    let op1 = i1 >> 26;
    if op1 == 37 {
        // stwu D-form: rS=1, rA=1
        let rs = (i1 >> 21) & 0x1F;
        let ra = (i1 >> 16) & 0x1F;
        let d = ((i1 & 0xFFFF) as i16) as i32;
        rs == 1 && ra == 1 && d <= 0 && (-d) <= 0x80
    } else if op1 == 18 {
        // bl __savegprlr_NN — accept; ctor with frame ≤ 0x80 is the
        // common case, but if the compiler emits a save-stub call we
        // can't easily verify the frame size without walking further.
        true
    } else {
        false
    }
}

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

    fn mk_section(name: &str, va: u32, size: u32) -> PeSection {
        PeSection {
            name: name.into(),
            virtual_address: va,
            virtual_size: size,
            raw_offset: va,
            raw_size: size,
            flags: 0x4000_0040,
        }
    }

    fn write_be_u32(buf: &mut [u8], at: usize, val: u32) {
        buf[at..at + 4].copy_from_slice(&val.to_be_bytes());
    }

    #[test]
    fn detects_dispatch_table_in_rdata() {
        let image_base = 0x82000000u32;
        let rdata_va = 0x1000u32;
        let mut pe = vec![0u8; 0x4000];

        // Two consecutive function pointers, no vtable shadowing them.
        let pcs = [image_base + 0x2000, image_base + 0x2010];
        for (i, p) in pcs.iter().enumerate() {
            write_be_u32(&mut pe, rdata_va as usize + i * 4, *p);
        }

        let sections = vec![mk_section(".rdata", rdata_va, 0x100)];
        let mut starts = BTreeSet::new();
        for &p in &pcs { starts.insert(p); }

        let arrs = analyze(&pe, image_base, &sections, &starts, &[]);
        assert_eq!(arrs.len(), 1);
        assert_eq!(arrs[0].kind, "dispatch_table");
        assert_eq!(arrs[0].length, 2);
    }

    #[test]
    fn vtable_overrides_dispatch_classification() {
        let image_base = 0x82000000u32;
        let rdata_va = 0x1000u32;
        let mut pe = vec![0u8; 0x4000];

        let pcs = [image_base + 0x2000, image_base + 0x2010, image_base + 0x2020];
        for (i, p) in pcs.iter().enumerate() {
            write_be_u32(&mut pe, rdata_va as usize + i * 4, *p);
        }
        let sections = vec![mk_section(".rdata", rdata_va, 0x100)];
        let mut starts = BTreeSet::new();
        for &p in &pcs { starts.insert(p); }

        let vt = Vtable {
            address: image_base + rdata_va,
            length: 3,
            col_address: None,
            class_name: "ANON_test".into(),
            rtti_present: false,
            base_classes_json: None,
            methods: pcs.to_vec(),
        };
        let arrs = analyze(&pe, image_base, &sections, &starts, &[vt]);
        // Vtable + (no dispatch-table dup): the M3 vtable is re-emitted, but
        // the scan also skips the same address from re-classification.
        assert_eq!(arrs.len(), 1);
        assert_eq!(arrs[0].kind, "vtable");
    }
}
