// ue4 reflection stuff
#include "expresso_recovered.h"

#include <cstring>

namespace expresso {

// verbatim from RVA 0x15A6A0:
//     lea  rax, [rcx - 0x10001]
//     mov  rdx, 0x7FFFFFFEFFFD
//     cmp  rax, rdx ; unsigned
//     ja   fail
//     mov  rax, 0xFFFF000000000000
//     test rax, rcx
//     jne  fail
bool IsCanonicalUserPointer(uint64_t addr) {
    if (static_cast<uint64_t>(addr - 0x10001ULL) > 0x7FFFFFFEFFFDULL) return false;
    if (addr & 0xFFFF000000000000ULL) return false;
    return true;
}

uint64_t SafeReadPtr(const IMemory& mem, uint64_t addr) {
    uint64_t v = 0;
    if (!IsCanonicalUserPointer(addr)) return 0;
    if (!mem.Read(addr, &v, sizeof(v))) return 0;
    return v;
}

bool SafeReadBool(const IMemory& mem, uint64_t addr) {
    uint8_t v = 0;
    if (!IsCanonicalUserPointer(addr)) return false;
    if (!mem.Read(addr, &v, sizeof(v))) return false;
    return v != 0;
}

int32_t SafeReadInt32(const IMemory& mem, uint64_t addr) {
    int32_t v = 0;
    if (!IsCanonicalUserPointer(addr)) return 0;
    if (!mem.Read(addr, &v, sizeof(v))) return 0;
    return v;
}

// movss xmm0, [rcx]; 0.0f on failure
float SafeReadFloat(const IMemory& mem, uint64_t addr) {
    float v = 0.0f;
    if (!IsCanonicalUserPointer(addr)) return 0.0f;
    if (!mem.Read(addr, &v, sizeof(v))) return 0.0f;
    return v;
}

bool SafeWriteInt32(IMemory& mem, uint64_t addr, int32_t v) {
    if (!IsCanonicalUserPointer(addr)) return false;
    return mem.Write(addr, &v, sizeof(v));
}

// literal `mov byte [rcx], 1`
bool SafeSetTrue(IMemory& mem, uint64_t addr) {
    const uint8_t one = 1;
    if (!IsCanonicalUserPointer(addr)) return false;
    return mem.Write(addr, &one, sizeof(one));
}

bool SafeWriteFloat(IMemory& mem, uint64_t addr, float v) {
    if (!IsCanonicalUserPointer(addr)) return false;
    return mem.Write(addr, &v, sizeof(v));
}

bool SafeWritePtr(IMemory& mem, uint64_t addr, uint64_t v) {
    if (!IsCanonicalUserPointer(addr)) return false;
    return mem.Write(addr, &v, sizeof(v));
}

// RVA 0x15AB00 FName index to string through the chunked pool
//
//   0x15AB18  lea rcx, [rdx + 0x314A7D0] ; &GNames
//   0x15AB25  call SafeReadPtr ; pool
//   0x15AB63  shr edi, 0x0E ; chunk index
//   0x15AB66  and esi, 0x3FFF ; index within chunk
//   0x15AB6C  shl edi, 3 ; * sizeof(void*)
//   0x15AB74  call SafeReadPtr ; chunk
//   0x15AB94  lea ecx, [rsi*8]
//   0x15ABA6  call SafeReadPtr ; FNameEntry*
//
// then decodes the entry into a 0xB0-byte stack buffer
bool GetNameString(const IMemory& mem, uint64_t game_base,
                   int32_t name_index, char* out, size_t out_size) {
    if (out == nullptr || out_size == 0) return false;
    out[0] = '\0';
    if (name_index <= 0 ||
        static_cast<uint32_t>(name_index) > limits::kMaxNameIndex) {
        return false;
    }

    const uint64_t pool = SafeReadPtr(mem, game_base + game_rva::kGNames);
    if (!IsCanonicalUserPointer(pool)) return false;

    const uint32_t idx        = static_cast<uint32_t>(name_index);
    const uint32_t chunk_idx  = idx >> fname::kChunkShift;
    const uint32_t entry_idx  = idx & fname::kChunkMask;

    const uint64_t chunk = SafeReadPtr(mem, pool + chunk_idx * 8u);
    if (!IsCanonicalUserPointer(chunk)) return false;

    const uint64_t entry = SafeReadPtr(mem, chunk + entry_idx * 8u);
    if (!IsCanonicalUserPointer(entry)) return false;

    // might look different idk, geometry not confirmed only layout
    const int32_t index_field = SafeReadInt32(mem, entry + 0x00);
    const bool    is_wide     = (index_field & 1) != 0;
    const uint64_t text       = entry + 0x10;

    size_t n = 0;
    if (is_wide) {
        for (; n + 1 < out_size; ++n) {
            uint16_t wc = 0;
            if (!mem.Read(text + n * 2, &wc, sizeof(wc))) break;
            if (wc == 0) break;
            out[n] = (wc < 0x80) ? static_cast<char>(wc) : '?';
        }
    } else {
        for (; n + 1 < out_size; ++n) {
            char c = 0;
            if (!mem.Read(text + n, &c, sizeof(c))) break;
            if (c == '\0') break;
            out[n] = c;
        }
    }
    out[n] = '\0';
    return n != 0;
}

// RVA 0x15C210, resolve a UFunction by name on an object's class chain
uint64_t FindFunctionByName(const IMemory& mem, uint64_t game_base,
                            uint64_t object, const char* name) {
    if (name == nullptr) return 0;
    if (!IsCanonicalUserPointer(object)) return 0;

    uint64_t cls = SafeReadPtr(mem, object + uobject::kClassPrivate);

    for (uint32_t depth = 0; depth < limits::kMaxClassDepth; ++depth) {
        if (!IsCanonicalUserPointer(cls)) return 0;

        uint64_t field = SafeReadPtr(mem, cls + uobject::kChildren);

        for (uint32_t i = 0; i < limits::kMaxFieldsPerCls; ++i) {
            if (!IsCanonicalUserPointer(field)) break;

            const int32_t name_index =
                SafeReadInt32(mem, field + uobject::kNameIndex);

            // 0x15C2DF: lea ecx,[rax-1] / cmp ecx,0x1FFFFE / ja skip
            if (static_cast<uint32_t>(name_index - 1) <= limits::kMaxNameIndex - 1) {
                char buf[0xB0];
                if (GetNameString(mem, game_base, name_index, buf, sizeof(buf)) &&
                    std::strcmp(buf, name) == 0) {
                    return field; // 0x15C387: mov rax, rdi
                }
            }
            field = SafeReadPtr(mem, field + uobject::kFieldNext);
        }
        cls = SafeReadPtr(mem, cls + uobject::kSuperStruct);
    }
    return 0;
}

// RVA 0x15C130 invocation primitive.
//
//   0x15C19E  lea  rcx, [rbx + 0x88] ; &fn->FunctionFlags
//   0x15C1A5  call SafeReadInt32 ; saved
//   0x15C1B3  or   edx, r12d ; saved | orFlags
//   0x15C1BD  call SafeWriteInt32
//   0x15C1CB  call r15 ; ProcessEvent(obj, fn, params)
//   0x15C1D8  call SafeWriteInt32 ; restore saved
bool CallUFunction(IMemory& mem, uint64_t object, uint64_t function,
                   void* params, uint32_t or_flags,
                   ProcessEventFn invoke, void* user) {
    if (invoke == nullptr) return false; // 0x15C15F
    if (!IsCanonicalUserPointer(object)) return false; // 0x15C168
    if (!IsCanonicalUserPointer(function)) return false; // 0x15C18D

    const uint64_t flags_addr = function + uobject::kFunctionFlags;
    const int32_t saved = SafeReadInt32(mem, flags_addr);

    SafeWriteInt32(mem, flags_addr,
                   static_cast<int32_t>(static_cast<uint32_t>(saved) | or_flags));

    invoke(object, function, params, user);

    SafeWriteInt32(mem, flags_addr, saved);
    return true;
}

// RVA 0x15AE60 cached local PC
uint64_t GetLocalPlayerController(const IMemory& mem, uint64_t game_base,
                                  LocalPlayerCache& cache, uint64_t now_ms) {
    const uint64_t world = SafeReadPtr(mem, game_base + game_rva::kGWorld);
    if (!IsCanonicalUserPointer(world)) return 0; // 0x15AEBD

    // 0x15AF00: reuse the cache only while world is unchanged and the entry
    // is younger than 2000 ms
    if (IsCanonicalUserPointer(cache.controller) &&
        cache.world == world &&
        now_ms - cache.timestamp < limits::kPcCacheMs) {
        if (IsCanonicalUserPointer(SafeReadPtr(mem, cache.controller)))
            return cache.controller;
    }

    const uint64_t game_instance =
        SafeReadPtr(mem, world + world::kOwningGameInstance);
    if (!IsCanonicalUserPointer(game_instance)) return 0;

    const uint64_t local_players =
        SafeReadPtr(mem, game_instance + world::kLocalPlayers);
    if (!IsCanonicalUserPointer(local_players)) return 0;

    // Fast path (0x15AF7F): treat local_players as the array data pointer and
    // take element 0 directly
    {
        const uint64_t player = SafeReadPtr(mem, local_players);
        if (IsCanonicalUserPointer(player)) {
            const uint64_t pc =
                SafeReadPtr(mem, player + world::kPlayerController);
            if (IsCanonicalUserPointer(pc) &&
                IsCanonicalUserPointer(SafeReadPtr(mem, pc))) {
                cache.controller = pc; // 0x15AFD8
                cache.world      = world; // 0x15AFDF
                cache.timestamp  = now_ms; // 0x15AFE6
                return pc;
            }
        }
    }

    // Slow path (0x15B004): treat local_players as &TArray{Data, Num} and walk
    const uint64_t data  = SafeReadPtr(mem, local_players);
    const int32_t  count = SafeReadInt32(mem, local_players + 8);
    if (!IsCanonicalUserPointer(data)) return 0;
    if (count <= 0 || static_cast<uint32_t>(count - 1) >=
                          limits::kMaxLocalPlayers) { // 0x15B03E
        return 0;
    }

    for (int32_t i = 0; i < count; ++i) {
        const uint64_t player = SafeReadPtr(mem, data + static_cast<uint64_t>(i) * 8);
        if (!IsCanonicalUserPointer(player)) continue;
        const uint64_t pc = SafeReadPtr(mem, player + world::kPlayerController);
        if (!IsCanonicalUserPointer(pc)) continue;
        if (!IsCanonicalUserPointer(SafeReadPtr(mem, pc))) continue;
        cache.controller = pc;
        cache.world      = world;
        cache.timestamp  = now_ms;
        return pc;
    }
    return 0;
}

// ClassPrivate -> NamePrivate -> string
//
//   lea rcx, [obj + 0x10] ; SafeReadPtr -> UClass*
//   lea rcx, [cls + 0x18] ; SafeReadInt32 -> FName::ComparisonIndex
//   GetNameString(&buf, gameBase, idx)
bool GetObjectClassName(const IMemory& mem, uint64_t game_base, uint64_t object,
                        char* out, size_t out_size) {
    if (out == nullptr || out_size == 0) return false;
    out[0] = '\0';
    if (!IsCanonicalUserPointer(object)) return false;

    const uint64_t cls = SafeReadPtr(mem, object + uobject::kClassPrivate);
    if (!IsCanonicalUserPointer(cls)) return false;

    const int32_t idx = SafeReadInt32(mem, cls + uobject::kNameIndex);
    if (idx == 0) return false; // 0x15B8AA
    if (static_cast<uint32_t>(idx) >= limits::kMaxClassNameIdx) // 0x15B8B2
        return false;

    return GetNameString(mem, game_base, idx, out, out_size);
}

} // namespace expresso