// lol id spoof
#include "expresso_recovered.h"

#include <cstring>

namespace expresso {

// The five APlayerState slots, in the order they are pushed at
// RVA 0x167B3C..0x167B5C
const uint32_t kSteamIdCandidateSlots[5] = {0x3C0, 0x3C8, 0x3D0, 0x3B8, 0x3B0};

// RVA 0x167ABF..0x167BE9 find the real SteamID64
uint64_t LocateOriginalSteamId(const IMemory& mem, uint64_t game_base,
                               LocalPlayerCache& cache, uint64_t now_ms) {
    const uint64_t pc = GetLocalPlayerController(mem, game_base, cache, now_ms);
    if (pc == 0) return 0;                                      // 0x167B10

    const uint64_t player_state = SafeReadPtr(mem, pc + world::kPlayerState);
    if (!IsCanonicalUserPointer(player_state)) return 0;        // 0x167B2D

    for (size_t i = 0; i < kSteamIdCandidateSlotCount; ++i) {
        const uint64_t slot = player_state + kSteamIdCandidateSlots[i];

        // the slot is a pointer to an object holding the ID
        const uint64_t pointee = SafeReadPtr(mem, slot); // 0x167B78
        if (pointee > 0x10000) { // 0x167B80
            for (uint32_t off = 0; off < kSteamIdProbeSpan; off += 8) { // 0x167BAD
                const uint64_t v = SafeReadPtr(mem, pointee + off); // 0x167B95
                if (steam::IsPlausibleId64(v)) return v; // 0x167BA1
            }
        }

        // the slot holds the ID inline
        const uint64_t direct = SafeReadPtr(mem, slot); // 0x167BB6
        if (steam::IsPlausibleId64(direct)) return direct; // 0x167BC1
    }
    return 0; // 0x167CB1
}

// RVA 0x166B21..0x166B48 which regions get scanned
namespace mem_flags {
constexpr uint32_t kMemCommit = 0x00001000;
constexpr uint32_t kMemImage = 0x01000000;
constexpr uint32_t kPageReadWrite = 0x00000004;
constexpr uint32_t kPageExecReadWrite= 0x00000040;
constexpr uint32_t kPageGuard = 0x00000100;
constexpr uint32_t kWritableMask = kPageReadWrite | kPageExecReadWrite;
} // namespace mem_flags

bool IsScannableRegion(const MemoryRegion& region) {
    if (region.state != mem_flags::kMemCommit) return false; // 0x166B21
    if (region.type == mem_flags::kMemImage) return false; // 0x166B2A
    if ((region.protect & mem_flags::kWritableMask) == 0)
        return false; // 0x166B36
    if (region.protect & mem_flags::kPageGuard) return false; // 0x166B3A
    if (region.size < 8) return false; // 0x166B44
    return true;
}

// RVA 0x166690 the matcher
size_t ScanRegionForQword(const IMemory& mem, uint64_t base, uint64_t size,
                          uint64_t needle, uint64_t* out, size_t out_capacity) {
    if (out == nullptr || out_capacity == 0 || size < 8) return 0;
    const size_t cap = (out_capacity < kMaxHitsPerChunk) ? out_capacity
                                                         : kMaxHitsPerChunk;
    size_t hits = 0;
    const uint64_t end = base + size - 7; // 0x1666A6
    for (uint64_t p = base; p < end && hits < cap; p += 8) {
        uint64_t v = 0;
        if (!mem.Read(p, &v, sizeof(v))) continue;
        if (v == needle) out[hits++] = p; // 0x1666BF
    }
    return hits;
}

// RVA 0x1665E0 one guarded write
bool ReplaceSteamIdAt(IMemory& mem, uint64_t addr, uint64_t new_id) {
    if (!IsCanonicalUserPointer(addr)) return false; // 0x166573

    uint64_t current = 0;
    if (!mem.Read(addr, &current, sizeof(current))) return false; // 0x166625
    if (!steam::IsPlausibleId64(current)) return false; // 0x166642

    return mem.Write(addr, &new_id, sizeof(new_id)); // 0x166659
}

// RVA 0x167260 apply across every located slot
size_t ReplaceSteamId(IMemory& mem, uint64_t* slots, size_t& slot_count,
                      uint64_t new_id) {
    if (slots == nullptr) return 0;

    size_t written = 0;
    for (size_t i = 0; i < slot_count; ++i) {
        if (ReplaceSteamIdAt(mem, slots[i], new_id)) { // 0x16734E
            slots[written++] = slots[i]; // 0x16736B compaction
        }
    }
    slot_count = written; // 0x16745B
    return written;
}

// RVA 0x167E70 the ProcessEvent blocklist
const char* const kBlockedIdentityFunctions[11] = {
    "CreateSteamIDFromString", // .rdata 0x59AA20, inserted at 0x167F07
    "GetUniqueNetIDFromPlayerState", // .rdata 0x59AA38 0x167F30
    "SteamID64", // .rdata 0x59AA58 0x167F58
    "GetLoginStatus", // 0x167F7F
    "GetUserAccount", // 0x167FA5
    "GetUserPrivilege", // 0x167FCB
    "bAntiCheatProtected", // 0x167FF1
    "GetUserID", // 0x168017
    "LoginUser", // 0x16803D
    "OnPlayerLoginChanged", // 0x168063
    "HasMatchmakingBan", // 0x16808C
};

// RVA 0x168183: movabs r9, 0xCBF29CE484222325
// RVA 0x168192: movabs r8, 0x100000001B3
// RVA 0x1681A0: movzx eax,[rcx+rbx]; xor r9,rax; imul r9,r8
uint64_t Fnv1a64(const char* s) {
    uint64_t h = 0xCBF29CE484222325ULL;
    if (s == nullptr) return h;
    for (const unsigned char* p = reinterpret_cast<const unsigned char*>(s);
         *p != 0; ++p) {
        h ^= static_cast<uint64_t>(*p);
        h *= 0x100000001B3ULL;
    }
    return h;
}

bool IsBlockedIdentityFunction(const char* name) {
    if (name == nullptr) return false;
    const uint64_t h = Fnv1a64(name);
    for (size_t i = 0; i < kBlockedIdentityFunctionCount; ++i) {
        if (h == Fnv1a64(kBlockedIdentityFunctions[i]) &&
            std::strcmp(name, kBlockedIdentityFunctions[i]) == 0) {
            return true;
        }
    }
    return false;
}

// RVA 0x167610 the event observer
IdentityEvent ClassifyIdentityEvent(const char* function_name) {
    if (function_name == nullptr) return IdentityEvent::kOther;
    if (std::strstr(function_name, "ClientReservationPending")) // 0x1676DB
        return IdentityEvent::kReservationPending;
    if (std::strstr(function_name, "OnRep_ReservationAccepted")) // 0x16772E
        return IdentityEvent::kReservationAccepted;
    if (std::strstr(function_name, "ClientTravelInternal")) // 0x1677C3
        return IdentityEvent::kClientTravelInternal;
    if (std::strstr(function_name, "ClientAckTraveling")) // 0x167885
        return IdentityEvent::kClientAckTraveling;
    return IdentityEvent::kOther;
}

void OnIdentityEvent(IdentitySpoofState& state, IdentityEvent event,
                     uint64_t now_ms) {
    if (!state.enabled) return; // 0x167633

    switch (event) {
    case IdentityEvent::kReservationPending:
        // The client has asked the session for a slot. Mark it in flight and
        // stop any scan, so nothing races the handshake (0x1676FB/0x167706)
        state.reservation_pending = true;
        state.scan_in_progress = false;
        break;

    case IdentityEvent::kReservationAccepted:
        // The session said yes. Arm the swap for 1700 ms from now, late
        // enough that the reservation is settled, early enough to be in place
        // before the server reads the identity again. (0x16774E..0x167797)
        if (!state.reservation_pending && // 0x167755
            state.candidate_count > 0 && // 0x167759
            !state.applied && // 0x167762
            !state.scan_in_progress && // 0x16776B
            !state.restore_armed) { // 0x167776
            state.apply_deadline_ms = now_ms + kApplyDelayMs; // 0x167787
            state.apply_armed = true; // 0x167797
        }
        break;

    case IdentityEvent::kClientTravelInternal: // 0x1677E7
    case IdentityEvent::kClientAckTraveling: // 0x1678A5
        // Travel is under way: the identity has served its purpose. Disarm any
        // pending apply and schedule the real ID to go back 800 ms from now
        state.apply_armed = false;
        if (!state.scan_in_progress && state.applied && !state.restore_armed) {
            state.scan_in_progress = false;
            state.restore_armed = true; // 0x167825
            state.restore_deadline_ms = now_ms + kRestoreDelayMs; // 0x167831
        }
        state.reservation_pending = false; // 0x167859
        break;

    case IdentityEvent::kOther:
        break;
    }
}

// RVA 0x167950 the per-frame tick, as a decision function
IdentityTickAction IdentitySpoofTick(IdentitySpoofState& state,
                                     uint64_t now_ms) {
    // deferred apply, armed by the observer (0x16798C)
    if (state.apply_armed && now_ms >= state.apply_deadline_ms) { // 0x1679A4
        state.apply_armed = false; // 0x1679AB
        if (!state.scan_in_progress && !state.restore_armed) { // 0x1679B1/BC
            state.applied = true; // 0x1679D7
            state.applied_at_ms = now_ms; // 0x1679EB
            return IdentityTickAction::kApplyFake;
        }
    }

    // the toggle went off: undo everything (0x167A13)
    if (!state.enabled) {
        if (!state.was_enabled) return IdentityTickAction::kNothing; // 0x167A18
        state.was_enabled = false; // 0x167A82
        if (state.applied && state.original_id != 0) { // 0x167A25/2E
            state.applied             = false;
            state.applied_at_ms       = 0;
            state.reservation_pending = false;
            state.scan_in_progress    = false;
            state.apply_armed         = false;
            return IdentityTickAction::kRestoreOriginal;
        }
        return IdentityTickAction::kNothing;
    }
    state.was_enabled = true; // 0x167A8E

    // no real ID yet: locate it, with a 119-tick retry back-off
    if (state.original_id == 0) { // 0x167A95
        if (state.retry_cooldown > 0) { // 0x167AA2
            --state.retry_cooldown; // 0x167CDE
            return IdentityTickAction::kNothing;
        }
        if (state.scan_complete) return IdentityTickAction::kNothing; // 0x167AB0
        return IdentityTickAction::kLocateOriginal;
    }

    // deferred restore, armed on travel (0x167D1B)
    if (state.restore_armed && now_ms >= state.restore_deadline_ms) { // 0x167D33
        state.restore_armed = false; // 0x167D3A
        if (state.applied) { // 0x167D40
            state.applied = false; // 0x167D5E
            state.applied_at_ms = 0;
            state.scan_in_progress = false;
            state.apply_armed = false;
            state.enabled = false; // 0x167D81
            return IdentityTickAction::kRestoreOriginal;
        }
    }

    // --- hard auto-revert 10 s after the swap (0x167DA0)
    if (state.applied && state.applied_at_ms != 0 && !state.restore_armed &&
        now_ms - state.applied_at_ms > kAutoRevertMs) { // 0x167DD4
        state.applied             = false; // 0x167DED
        state.applied_at_ms       = 0;
        state.reservation_pending = false;
        state.scan_in_progress    = false;
        state.apply_armed         = false;
        state.enabled             = false; // 0x167E16
        return IdentityTickAction::kRestoreOriginal;
    }

    return IdentityTickAction::kNothing;
}

} // namespace expresso