//! Locating primitives: where a library file lives ON DISK, and where the function entries live
//! INSIDE an image.
//!
//! On disk: `find_file` resolves a lib by name under a build tree (nearest-depth-wins, so a Metamod
//! shim can't shadow the real engine lib) and `load_lib` turns a build dir *or* a bare `.so` into a
//! loaded [`CodeImage`]. Both are leaf primitives (they touch only `elf` + the filesystem), so the
//! low-level readers — `schema` especially — depend on THIS module rather than up on the engine.
//!
//! In an image: [`candidate_entries`] enumerates plausible function starts without symbols — relocation
//! values that point into code (vtable slots + function pointers — covers virtual functions) unioned
//! with the targets of direct near `call`s found by a linear sweep. `xref` unions this with `.eh_frame`
//! starts to index the whole binary.

use crate::elf::CodeImage;
use anyhow::{Context, Result};
use iced_x86::{Decoder, DecoderOptions, FlowControl, OpKind};
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};

/// Every plausible function entry address in `img`: relocation values that point into code, plus
/// the targets of direct near `call`s found by a linear sweep. Sorted, de-duplicated.
pub fn candidate_entries(img: &CodeImage) -> Vec<u64> {
    let mut set: BTreeSet<u64> = img.code_pointer_targets().into_iter().collect();
    for (va, code) in img.exec_blocks() {
        let mut dec = Decoder::with_ip(64, code, va, DecoderOptions::NONE);
        while dec.can_decode() {
            let insn = dec.decode(); // iced advances one byte on invalid, so the sweep self-resyncs
            if insn.flow_control() == FlowControl::Call
                && matches!(
                    insn.op0_kind(),
                    OpKind::NearBranch16 | OpKind::NearBranch32 | OpKind::NearBranch64
                )
            {
                let t = insn.near_branch_target();
                if img.is_code(t) {
                    set.insert(t);
                }
            }
        }
    }
    set.into_iter().collect()
}

/// Shallowest file named `name` under `dir` (bounded depth), ties broken by sorted path.
///
/// NEAREST-DEPTH-WINS, not first-`read_dir`-hit: a game install legitimately holds several files of the same
/// basename, and the shallowest is the real one. A CS2 tree has the engine's own
/// `csgo/bin/linuxsteamrt64/libserver.so` at depth 3 and Metamod's ~300 KB loader shim of the SAME name at
/// `csgo/addons/metamod/bin/linuxsteamrt64/libserver.so` (depth 5, plus any `bin.*.bak` siblings). Depth-first
/// order made which one you derive from a property of directory-entry order — deriving against the shim would
/// yield garbage — and plain sorting is WORSE, since `addons` sorts before `bin`. Sorting is only the tie-break
/// among equally-shallow candidates, so the result never depends on filesystem enumeration order.
pub(crate) fn find_file(dir: &Path, name: &str, depth: usize) -> Option<PathBuf> {
    let mut level = vec![dir.to_path_buf()];
    for _ in 0..depth {
        let (mut hits, mut next) = (Vec::new(), Vec::new());
        for d in &level {
            let Ok(rd) = std::fs::read_dir(d) else {
                continue;
            };
            for e in rd.flatten() {
                let p = e.path();
                if p.is_dir() {
                    next.push(p);
                } else if p.file_name().and_then(|s| s.to_str()) == Some(name) {
                    hits.push(p);
                }
            }
        }
        if !hits.is_empty() {
            hits.sort();
            return hits.into_iter().next();
        }
        if next.is_empty() {
            return None;
        }
        next.sort();
        level = next;
    }
    None
}

/// Locate `lib` under `dir` (depth 8) and load it as a `CodeImage` — the `find_file` + load pattern the
/// command entry points share.
pub(crate) fn load_lib(path: &Path, lib: &str) -> Result<CodeImage> {
    // Accept a build DIR (find `lib` within, depth 8) or a direct `.so` FILE (load as-is), so callers can
    // pass `path/to/build_dir` or `path/to/libserver.so` interchangeably (e.g. `classify-change --prev`).
    let file = if path.is_file() {
        path.to_path_buf()
    } else {
        find_file(path, lib, 8)
            .with_context(|| format!("{lib} not found under {}", path.display()))?
    };
    CodeImage::load(&file)
}
