//! Generate a fresh, unique signature at a known address — the tool's product output.
//!
//! Walk instructions from the function entry, emit their bytes, but wildcard the
//! position-dependent ones (RIP-relative displacements and near-branch targets) so the
//! signature survives relocation. Stop as soon as the accumulated pattern matches exactly
//! once in the binary. Pure integer/byte work; the only float-free dependency is the decoder.

use crate::elf::CodeImage;
use crate::sig::Pattern;
use iced_x86::{ConstantOffsets, Decoder, DecoderOptions, Instruction, OpKind};

/// Call `emit(k, masked)` for each byte `k` of a just-decoded instruction, where `masked` is true for
/// the position-dependent bytes — a RIP-relative displacement or a near-branch target — that move on
/// relocation and so must be wildcarded (in a signature) or normalized away (in a cross-build digest).
/// The single source of truth for that masking, shared by `make_sig` and `normalized_digest` so the
/// signature the tool ships and the digest it compares builds with can never drift apart.
fn for_each_byte(
    instr: &Instruction,
    co: &ConstantOffsets,
    ilen: usize,
    mut emit: impl FnMut(usize, bool),
) {
    let rip_rel = instr.is_ip_rel_memory_operand();
    let branch = (0..instr.op_count()).any(|i| {
        matches!(
            instr.op_kind(i),
            OpKind::NearBranch16 | OpKind::NearBranch32 | OpKind::NearBranch64
        )
    });
    for k in 0..ilen {
        let in_disp = rip_rel
            && co.has_displacement()
            && k >= co.displacement_offset()
            && k < co.displacement_offset() + co.displacement_size();
        let in_imm = branch
            && co.has_immediate()
            && k >= co.immediate_offset()
            && k < co.immediate_offset() + co.immediate_size();
        emit(k, in_disp || in_imm);
    }
}

/// Build a unique signature for the function at `vaddr`, or `None` if it can't be made unique
/// within `max_bytes`.
pub fn make_sig(img: &CodeImage, vaddr: u64, max_bytes: usize) -> Option<String> {
    let code = img.code_at(vaddr)?;
    let mut decoder = Decoder::with_ip(64, code, vaddr, DecoderOptions::NONE);
    let mut instr = Instruction::default();
    let mut tokens: Vec<String> = Vec::new();
    let mut off = 0usize;

    while decoder.can_decode() && off < max_bytes {
        decoder.decode_out(&mut instr);
        let ilen = instr.len();
        if ilen == 0 || off + ilen > code.len() {
            break;
        }
        let co = decoder.get_constant_offsets(&instr);
        for_each_byte(&instr, &co, ilen, |k, masked| {
            tokens.push(if masked {
                "?".into()
            } else {
                format!("{:02X}", code[off + k])
            });
        });
        off += ilen;

        if let Ok(pat) = Pattern::parse(&tokens.join(" "))
            && img.find(&pat).len() == 1
        {
            return Some(tokens.join(" "));
        }
    }
    None
}

/// FNV-1a 64-bit digest of the function `[start, end)`, with the position-dependent bytes masked to a
/// constant (the same masking `make_sig` wildcards). The digest is therefore recompilation-shift-
/// invariant: a function whose body is unchanged but whose call/jump targets moved with the surrounding
/// layout digests IDENTICALLY across builds, while a genuine opcode/operand edit changes it. This is the
/// per-function identity `classify-change` compares two builds by. `None` if nothing decodes.
pub fn normalized_digest(img: &CodeImage, start: u64, end: u64) -> Option<u64> {
    let code = img.code_at(start)?;
    let span = (end.saturating_sub(start) as usize).min(code.len());
    digest_code(&code[..span], start)
}

/// The shared core of `normalized_digest`: FNV-1a over a raw code slice with the position-dependent
/// bytes masked. Split out from the `CodeImage` wrapper so it can be unit-tested on hand-assembled
/// bytes. `None` if nothing decodes.
fn digest_code(code: &[u8], ip: u64) -> Option<u64> {
    if code.is_empty() {
        return None;
    }
    let mut decoder = Decoder::with_ip(64, code, ip, DecoderOptions::NONE);
    let mut instr = Instruction::default();
    let mut h: u64 = 0xcbf29ce484222325; // FNV-1a offset basis
    let mut off = 0usize;
    let mut decoded = false;
    while decoder.can_decode() && off < code.len() {
        decoder.decode_out(&mut instr);
        let ilen = instr.len();
        if ilen == 0 || off + ilen > code.len() {
            break;
        }
        let co = decoder.get_constant_offsets(&instr);
        for_each_byte(&instr, &co, ilen, |k, masked| {
            // A masked byte hashes as a fixed 0 regardless of its build-specific value; kept in place
            // (not skipped) so instruction length still participates in the digest.
            let byte = if masked { 0 } else { code[off + k] };
            h = (h ^ byte as u64).wrapping_mul(0x100000001b3);
        });
        off += ilen;
        decoded = true;
    }
    decoded.then_some(h)
}

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

    #[test]
    fn masks_call_target_shift_invariant() {
        // Two builds of the same function whose only difference is where its `call rel32` lands (the
        // callee moved with the layout) must digest IDENTICALLY — the shift-invariance the whole
        // command rests on. `E8 xx xx xx xx` = call; the 4 immediate bytes differ, nothing else.
        let a = digest_code(&[0xE8, 0x11, 0x22, 0x33, 0x44, 0xC3], 0x1000); // call +0x44332211 ; ret
        let b = digest_code(&[0xE8, 0x55, 0x66, 0x77, 0x00, 0xC3], 0x1000); // call +0x00776655 ; ret
        assert_eq!(a, b);
        assert!(a.is_some());
    }

    #[test]
    fn masks_rip_relative_displacement() {
        // `lea rax, [rip+disp]` — the displacement moves every build; masking it makes the two equal.
        let a = digest_code(&[0x48, 0x8D, 0x05, 0x11, 0x22, 0x33, 0x44, 0xC3], 0x1000);
        let b = digest_code(&[0x48, 0x8D, 0x05, 0xAA, 0xBB, 0xCC, 0x00, 0xC3], 0x1000);
        assert_eq!(a, b);
    }

    #[test]
    fn distinguishes_opcode_change() {
        // A real body edit (add vs sub) must change the digest — the masking must NOT wash it out.
        let add = digest_code(&[0x48, 0x01, 0xD8, 0xC3], 0x1000); // add rax, rbx ; ret
        let sub = digest_code(&[0x48, 0x29, 0xD8, 0xC3], 0x1000); // sub rax, rbx ; ret
        assert_ne!(add, sub);
    }

    #[test]
    fn distinguishes_non_branch_immediate() {
        // A plain immediate (`mov eax, IMM`) is NOT masked — it's part of the function's identity, so a
        // changed constant is a real change, unlike a relocated branch/RIP displacement.
        let a = digest_code(&[0xB8, 0x01, 0x00, 0x00, 0x00, 0xC3], 0x1000); // mov eax, 1 ; ret
        let b = digest_code(&[0xB8, 0x02, 0x00, 0x00, 0x00, 0xC3], 0x1000); // mov eax, 2 ; ret
        assert_ne!(a, b);
    }

    #[test]
    fn empty_is_none() {
        assert_eq!(digest_code(&[], 0x1000), None);
    }
}
