#include <RED4ext/Api/ApiVersion.hpp>
#include <RED4ext/Api/v1/EMainReason.hpp>
#include <RED4ext/Api/v1/PluginHandle.hpp>
#include <RED4ext/Api/v1/PluginInfo.hpp>
#include <RED4ext/Api/v1/Runtime.hpp>
#include <RED4ext/Api/v1/Version.hpp>
#include <RED4ext/Common.hpp>

#include <windows.h>

#include <algorithm>
#include <array>
#include <chrono>
#include <cstring>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <initializer_list>
#include <mutex>
#include <sstream>
#include <string_view>

namespace RED4ext::v1
{
struct Logger
{
    void (*Trace)(PluginHandle aHandle, const char* aMessage);
    void (*TraceF)(PluginHandle aHandle, const char* aFormat, ...);
    void (*TraceW)(PluginHandle aHandle, const wchar_t* aMessage);
    void (*TraceWF)(PluginHandle aHandle, const wchar_t* aFormat, ...);
    void (*Debug)(PluginHandle aHandle, const char* aMessage);
    void (*DebugF)(PluginHandle aHandle, const char* aFormat, ...);
    void (*DebugW)(PluginHandle aHandle, const wchar_t* aMessage);
    void (*DebugWF)(PluginHandle aHandle, const wchar_t* aFormat, ...);
    void (*Info)(PluginHandle aHandle, const char* aMessage);
    void (*InfoF)(PluginHandle aHandle, const char* aFormat, ...);
    void (*InfoW)(PluginHandle aHandle, const wchar_t* aMessage);
    void (*InfoWF)(PluginHandle aHandle, const wchar_t* aFormat, ...);
    void (*Warn)(PluginHandle aHandle, const char* aMessage);
    void (*WarnF)(PluginHandle aHandle, const char* aFormat, ...);
    void (*WarnW)(PluginHandle aHandle, const wchar_t* aMessage);
    void (*WarnWF)(PluginHandle aHandle, const wchar_t* aFormat, ...);
    void (*Error)(PluginHandle aHandle, const char* aMessage);
    void (*ErrorF)(PluginHandle aHandle, const char* aFormat, ...);
    void (*ErrorW)(PluginHandle aHandle, const wchar_t* aMessage);
    void (*ErrorWF)(PluginHandle aHandle, const wchar_t* aFormat, ...);
    void (*Critical)(PluginHandle aHandle, const char* aMessage);
    void (*CriticalF)(PluginHandle aHandle, const char* aFormat, ...);
    void (*CriticalW)(PluginHandle aHandle, const wchar_t* aMessage);
    void (*CriticalWF)(PluginHandle aHandle, const wchar_t* aFormat, ...);
};

struct GameState
{
    bool (*OnEnter)(void* aApp);
    bool (*OnUpdate)(void* aApp);
    bool (*OnExit)(void* aApp);
};

struct GameStates
{
    bool (*Add)(PluginHandle aHandle, uint32_t aType, GameState* aState);
};

struct Hooking
{
    bool (*Attach)(PluginHandle aHandle, void* aTarget, void* aDetour, void** aOriginal);
    bool (*Detach)(PluginHandle aHandle, void* aTarget);
};

struct Scripts
{
    bool (*Add)(PluginHandle aHandle, const wchar_t* aPath);
    bool (*RegisterNeverRefType)(const char* aType);
    bool (*RegisterMixedRefType)(const char* aType);
};

struct Sdk;

struct Sdk
{
    SemVer* runtime;
    Logger* logger;
    Hooking* hooking;
    GameStates* gameStates;
    Scripts* scripts;
};
}

namespace
{
constexpr uint32_t kGameStateRunning = 2;
constexpr uintptr_t kGpsSystemTickRva = 0x0E6B62C;
constexpr uintptr_t kJournalTrackEntryImplRva = 0x05944FC;
constexpr uintptr_t kJournalListenerArrayOffset = 0x210;
constexpr uintptr_t kJournalListenerCountOffset = 0x21C;
constexpr uintptr_t kMappinSystemResolverRva = 0x02BB840;
constexpr uintptr_t kFrameMappinPathWrapperRva = 0x27C4314;
constexpr uintptr_t kFrameMappinPathCoreRva = 0x27BC1EC;
constexpr uintptr_t kSetSelectedMappinWrapperRva = 0x27C4A38;
constexpr uintptr_t kSetSelectedMappinByIdWrapperRva = 0x27C4944;
constexpr uintptr_t kSetSelectedMappinByPositionWrapperRva = 0x27C49C4;
constexpr uintptr_t kSetSelectedMappinCoreRva = 0x27C1684;
constexpr uintptr_t kTrackCustomPositionMappinWrapperRva = 0x27C4AAC;
constexpr uintptr_t kTrackCustomPositionMappinCoreRva = 0x27C2318;
constexpr uintptr_t kUntrackCustomPositionMappinWrapperRva = 0x27C4BA0;
constexpr uintptr_t kTrackMappinWrapperRva = 0x27C4AB4;
constexpr uintptr_t kTrackMappinCoreRva = 0x27C11D0;
constexpr uintptr_t kSetMappinTrackingAlternativeWrapperRva = 0x1246794;
constexpr uintptr_t kMappinUpdateCustomPositionSlotOffset = 0x1F0;
constexpr uintptr_t kMappinSetTrackedSlotOffset = 0x220;
constexpr uintptr_t kMappinClearTrackedSlotOffset = 0x228;
constexpr uintptr_t kMappinFramePathSlotOffset = 0x280;
constexpr uintptr_t kMappinPositionQuerySlotOffset = 0x2E0;
constexpr uintptr_t kMappinCreateCustomPositionSlotOffset = 0x2F0;
constexpr uintptr_t kMappinReleaseQuerySlotOffset = 0x368;
constexpr uintptr_t kMappinActiveRouteSlotOffset = 0x3A8;
constexpr uintptr_t kJournalListenerObjectiveVt28Rva = 0x0EA89A8;
constexpr uintptr_t kJournalListenerObjectiveVt30Rva = 0x0EA8958;
constexpr uintptr_t kJournalListenerRouteVt28Rva = 0x0E63F80;
constexpr uintptr_t kJournalListenerRouteVt30Rva = 0x0E63E6C;
constexpr uintptr_t kJournalListenerRouteAltVt28Rva = 0x0E63F00;
constexpr uintptr_t kJournalListenerRouteAltVt30Rva = 0x08D136C;
constexpr uintptr_t kJournalListenerUiVt28Rva = 0x0431A34;
constexpr uintptr_t kJournalListenerUiVt30Rva = 0x055A4E4;
constexpr uintptr_t kJournalListenerTrackerVt28Rva = 0x14DE238;

HMODULE gModule = nullptr;
std::mutex gLogMutex;
RED4ext::v1::PluginHandle gHandle = nullptr;
const RED4ext::v1::Sdk* gSdk = nullptr;
uint32_t gUpdateLogCount = 0;
bool gScannedExecutable = false;
uint32_t gGpsTickLogCount = 0;
uint32_t gJournalTrackEntryLogCount = 0;
uint32_t gJournalTrackEntryListenerDumpCount = 0;
uint32_t gFrameMappinPathWrapperLogCount = 0;
uint32_t gFrameMappinPathCoreLogCount = 0;
uint32_t gSetSelectedMappinWrapperLogCount = 0;
uint32_t gSetSelectedMappinByIdWrapperLogCount = 0;
uint32_t gSetSelectedMappinByPositionWrapperLogCount = 0;
uint32_t gSetSelectedMappinCoreLogCount = 0;
uint32_t gTrackCustomPositionMappinWrapperLogCount = 0;
uint32_t gTrackCustomPositionMappinCoreLogCount = 0;
uint32_t gUntrackCustomPositionMappinWrapperLogCount = 0;
uint32_t gTrackMappinWrapperLogCount = 0;
uint32_t gTrackMappinCoreLogCount = 0;
uint32_t gSetMappinTrackingAlternativeLogCount = 0;
uint32_t gMappinSystemSlotLogCount = 0;
uint32_t gMappinUpdateCustomPositionLogCount = 0;
uint32_t gMappinSetTrackedLogCount = 0;
uint32_t gMappinClearTrackedLogCount = 0;
uint32_t gMappinFramePathLogCount = 0;
uint32_t gMappinPositionQueryLogCount = 0;
uint32_t gMappinCreateCustomPositionLogCount = 0;
uint32_t gMappinReleaseQueryLogCount = 0;
uint32_t gMappinActiveRouteLogCount = 0;
uint32_t gJournalListenerObjectiveVt28LogCount = 0;
uint32_t gJournalListenerObjectiveVt30LogCount = 0;
uint32_t gJournalListenerRouteVt28LogCount = 0;
uint32_t gJournalListenerRouteVt30LogCount = 0;
uint32_t gJournalListenerRouteAltVt28LogCount = 0;
uint32_t gJournalListenerRouteAltVt30LogCount = 0;
uint32_t gJournalListenerUiVt28LogCount = 0;
uint32_t gJournalListenerUiVt30LogCount = 0;
uint32_t gJournalListenerTrackerVt28LogCount = 0;
uint64_t gLogStartTick = 0;
uint64_t gLogFrequency = 0;
void* gMappinSetTrackedTarget = nullptr;
void* gMappinClearTrackedTarget = nullptr;
void* gMappinUpdateCustomPositionTarget = nullptr;
void* gMappinFramePathTarget = nullptr;
void* gMappinPositionQueryTarget = nullptr;
void* gMappinCreateCustomPositionTarget = nullptr;
void* gMappinReleaseQueryTarget = nullptr;
void* gMappinActiveRouteTarget = nullptr;
bool gMappinSlotHookAttachAttempted = false;

using ProbeFunc = void (*)(void* aThis, void* a2, void* a3, void* a4);
using JournalTrackEntryFunc = void (*)(void* aThis, void* aEntryHandle);
using ScriptNativeVoidFunc = void (*)(void* aThis, void* aStack, void* aResult);
using MappinSystemResolverFunc = void* (*)(void* aThis);
using FrameMappinPathCoreFunc = void (*)(void* aThis, uint32_t aPathKind, void* a3, void* aPathRect);
using SetSelectedMappinCoreFunc = void (*)(void* aThis, void* aMappinHandle);
using TrackCustomPositionMappinCoreFunc = void (*)(void* aThis);
using TrackMappinCoreFunc = void (*)(void* aThis, void* aMappinHandle);
using MappinUpdateCustomPositionFunc = void (*)(void* aSystem, void* aHandle, void* aPositionData);
using MappinSetTrackedFunc = void (*)(void* aSystem, void* aMappinId);
using MappinClearTrackedFunc = void (*)(void* aSystem);
using MappinFramePathFunc = void (*)(void* aSystem, uint32_t aPathKind, void* aResult);
using MappinPositionQueryFunc = void* (*)(void* aSystem, void* a2, void* a3, void* a4, void* a5);
using MappinCreateCustomPositionFunc = void* (*)(void* aSystem, void* aName, void* aParams, void* aPositionData);
using MappinReleaseQueryFunc = void (*)(void* aSystem, void* aHandle);
using MappinActiveRouteFunc = void* (*)(void* aSystem);
using JournalListenerCallbackFunc = void (*)(void* aListener, void* aEvent);
ProbeFunc gOriginalGpsSystemTick = nullptr;
JournalTrackEntryFunc gOriginalJournalTrackEntry = nullptr;
ScriptNativeVoidFunc gOriginalFrameMappinPathWrapper = nullptr;
FrameMappinPathCoreFunc gOriginalFrameMappinPathCore = nullptr;
ScriptNativeVoidFunc gOriginalSetSelectedMappinWrapper = nullptr;
ScriptNativeVoidFunc gOriginalSetSelectedMappinByIdWrapper = nullptr;
ScriptNativeVoidFunc gOriginalSetSelectedMappinByPositionWrapper = nullptr;
SetSelectedMappinCoreFunc gOriginalSetSelectedMappinCore = nullptr;
ScriptNativeVoidFunc gOriginalTrackCustomPositionMappinWrapper = nullptr;
TrackCustomPositionMappinCoreFunc gOriginalTrackCustomPositionMappinCore = nullptr;
ScriptNativeVoidFunc gOriginalUntrackCustomPositionMappinWrapper = nullptr;
ScriptNativeVoidFunc gOriginalTrackMappinWrapper = nullptr;
TrackMappinCoreFunc gOriginalTrackMappinCore = nullptr;
ScriptNativeVoidFunc gOriginalSetMappinTrackingAlternativeWrapper = nullptr;
MappinUpdateCustomPositionFunc gOriginalMappinUpdateCustomPosition = nullptr;
MappinSetTrackedFunc gOriginalMappinSetTracked = nullptr;
MappinClearTrackedFunc gOriginalMappinClearTracked = nullptr;
MappinFramePathFunc gOriginalMappinFramePath = nullptr;
MappinPositionQueryFunc gOriginalMappinPositionQuery = nullptr;
MappinCreateCustomPositionFunc gOriginalMappinCreateCustomPosition = nullptr;
MappinReleaseQueryFunc gOriginalMappinReleaseQuery = nullptr;
MappinActiveRouteFunc gOriginalMappinActiveRoute = nullptr;
JournalListenerCallbackFunc gOriginalJournalListenerObjectiveVt28 = nullptr;
JournalListenerCallbackFunc gOriginalJournalListenerObjectiveVt30 = nullptr;
JournalListenerCallbackFunc gOriginalJournalListenerRouteVt28 = nullptr;
JournalListenerCallbackFunc gOriginalJournalListenerRouteVt30 = nullptr;
JournalListenerCallbackFunc gOriginalJournalListenerRouteAltVt28 = nullptr;
JournalListenerCallbackFunc gOriginalJournalListenerRouteAltVt30 = nullptr;
JournalListenerCallbackFunc gOriginalJournalListenerUiVt28 = nullptr;
JournalListenerCallbackFunc gOriginalJournalListenerUiVt30 = nullptr;
JournalListenerCallbackFunc gOriginalJournalListenerTrackerVt28 = nullptr;

std::filesystem::path GetLogPath()
{
    wchar_t modulePath[MAX_PATH]{};
    if (gModule && GetModuleFileNameW(gModule, modulePath, MAX_PATH) > 0)
    {
        auto path = std::filesystem::path(modulePath);
        return path.parent_path() / L"EdgeWeightGPS.log";
    }

    return std::filesystem::path(L"red4ext") / L"plugins" / L"EdgeWeightGPS" / L"EdgeWeightGPS.log";
}

void Log(std::string_view aMessage)
{
    std::lock_guard lock(gLogMutex);

    auto now = std::chrono::system_clock::now();
    auto time = std::chrono::system_clock::to_time_t(now);
    std::tm localTime{};
    localtime_s(&localTime, &time);

    std::ofstream out(GetLogPath(), std::ios::app);
    if (!out)
    {
        return;
    }

    uint64_t elapsedMs = 0;
    if (gLogStartTick && gLogFrequency)
    {
        LARGE_INTEGER counter{};
        QueryPerformanceCounter(&counter);
        elapsedMs = static_cast<uint64_t>((counter.QuadPart - gLogStartTick) * 1000 / gLogFrequency);
    }

    out << std::put_time(&localTime, "%Y-%m-%d %H:%M:%S") << "." << std::setw(3) << std::setfill('0')
        << elapsedMs % 1000 << std::setfill(' ') << " +" << elapsedMs << "ms " << aMessage << "\n";
}

void LogRed4ext(std::string_view aMessage)
{
    Log(aMessage);

    if (gSdk && gSdk->logger && gSdk->logger->Info && gHandle)
    {
        std::string text(aMessage);
        gSdk->logger->Info(gHandle, text.c_str());
    }
}

uintptr_t GetImageBase()
{
    return reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
}

uintptr_t GetImageEnd()
{
    const auto imageBase = GetImageBase();
    if (!imageBase)
    {
        return 0;
    }

    const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(imageBase);
    if (dos->e_magic != IMAGE_DOS_SIGNATURE)
    {
        return 0;
    }

    const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(imageBase + dos->e_lfanew);
    if (nt->Signature != IMAGE_NT_SIGNATURE)
    {
        return 0;
    }

    return imageBase + nt->OptionalHeader.SizeOfImage;
}

bool IsInImage(const void* aPointer)
{
    const auto imageBase = GetImageBase();
    const auto imageEnd = GetImageEnd();
    const auto address = reinterpret_cast<uintptr_t>(aPointer);
    return imageBase && imageEnd && address >= imageBase && address < imageEnd;
}

void AppendPointerWithRva(std::ostringstream& aLine, const char* aLabel, const void* aPointer)
{
    aLine << " " << aLabel << "=" << aPointer;

    if (IsInImage(aPointer))
    {
        const auto imageBase = GetImageBase();
        const auto address = reinterpret_cast<uintptr_t>(aPointer);
        aLine << " " << aLabel << "_rva=0x" << std::hex << (address - imageBase) << std::dec;
    }
}

void AppendReturnRva(std::ostringstream& aLine, const void* aReturnAddress)
{
    if (IsInImage(aReturnAddress))
    {
        const auto imageBase = GetImageBase();
        const auto address = reinterpret_cast<uintptr_t>(aReturnAddress);
        aLine << " ret_rva=0x" << std::hex << (address - imageBase) << std::dec;
    }
}

void LogHookCall(const char* aName, uint32_t aCount, void* aThis, void* a2, void* a3, void* a4)
{
    std::ostringstream line;
    line << "hook " << aName << " call=" << aCount << " this=" << aThis << " a2=" << a2 << " a3=" << a3
         << " a4=" << a4;
    AppendReturnRva(line, __builtin_return_address(0));
    LogRed4ext(line.str());
}

void LogScriptNativeCall(const char* aName, uint32_t aCount, void* aThis, void* aStack, void* aResult)
{
    std::ostringstream line;
    line << "hook " << aName << " call=" << aCount << " this=" << aThis << " stack=" << aStack
         << " result=" << aResult;
    LogRed4ext(line.str());
}

void LogFrameMappinPathCoreCall(uint32_t aCount, void* aThis, uint32_t aPathKind, void* a3, void* aPathRect)
{
    std::ostringstream line;
    line << "hook FrameMappinPathCore call=" << aCount << " this=" << aThis << " pathKind=" << aPathKind
         << " a3=" << a3 << " pathRect=" << aPathRect;
    AppendReturnRva(line, __builtin_return_address(0));
    LogRed4ext(line.str());
}

void LogSetSelectedMappinCoreCall(uint32_t aCount, void* aThis, void* aMappinHandle)
{
    std::ostringstream line;
    line << "hook SetSelectedMappinCore call=" << aCount << " this=" << aThis
         << " mappinHandle=" << aMappinHandle;
    AppendReturnRva(line, __builtin_return_address(0));
    LogRed4ext(line.str());
}

void LogTrackCustomPositionMappinCoreCall(uint32_t aCount, void* aThis)
{
    std::ostringstream line;
    line << "hook TrackCustomPositionMappinCore call=" << aCount << " this=" << aThis;
    AppendReturnRva(line, __builtin_return_address(0));
    LogRed4ext(line.str());
}

void LogTrackMappinCoreCall(uint32_t aCount, void* aThis, void* aMappinHandle)
{
    std::ostringstream line;
    line << "hook TrackMappinCore call=" << aCount << " this=" << aThis << " mappinHandle=" << aMappinHandle;
    LogRed4ext(line.str());
}

void LogMappinSystemSlots(uint32_t aCount, void* aSystem, void* aVtable)
{
    auto* vtable = reinterpret_cast<uint8_t*>(aVtable);
    auto readSlot = [vtable](uintptr_t aOffset) -> void* {
        if (!vtable)
        {
            return nullptr;
        }

        return *reinterpret_cast<void**>(vtable + aOffset);
    };

    std::ostringstream line;
    line << "mappin-system slots call=" << aCount;
    AppendPointerWithRva(line, "system", aSystem);
    AppendPointerWithRva(line, "vtable", aVtable);
    AppendPointerWithRva(line, "slot1f0", readSlot(kMappinUpdateCustomPositionSlotOffset));
    AppendPointerWithRva(line, "slot220", readSlot(kMappinSetTrackedSlotOffset));
    AppendPointerWithRva(line, "slot228", readSlot(kMappinClearTrackedSlotOffset));
    AppendPointerWithRva(line, "slot250", readSlot(0x250));
    AppendPointerWithRva(line, "slot280", readSlot(kMappinFramePathSlotOffset));
    AppendPointerWithRva(line, "slot2e0", readSlot(kMappinPositionQuerySlotOffset));
    AppendPointerWithRva(line, "slot2f0", readSlot(kMappinCreateCustomPositionSlotOffset));
    AppendPointerWithRva(line, "slot368", readSlot(kMappinReleaseQuerySlotOffset));
    AppendPointerWithRva(line, "slot3a8", readSlot(kMappinActiveRouteSlotOffset));
    LogRed4ext(line.str());
}

void InspectAndHookMappinSystem(void* aThis);

const char* ReasonToString(RED4ext::v1::EMainReason aReason)
{
    switch (aReason)
    {
    case RED4ext::v1::EMainReason::Load:
        return "Load";
    case RED4ext::v1::EMainReason::Unload:
        return "Unload";
    }

    return "Unknown";
}

bool IsReadablePage(DWORD aProtect)
{
    if ((aProtect & PAGE_GUARD) != 0 || (aProtect & PAGE_NOACCESS) != 0)
    {
        return false;
    }

    const auto baseProtect = aProtect & 0xFF;
    return baseProtect == PAGE_READONLY || baseProtect == PAGE_READWRITE || baseProtect == PAGE_WRITECOPY ||
           baseProtect == PAGE_EXECUTE_READ || baseProtect == PAGE_EXECUTE_READWRITE ||
           baseProtect == PAGE_EXECUTE_WRITECOPY;
}

bool IsReadableMemory(const void* aPointer, size_t aSize)
{
    if (!aPointer || aSize == 0)
    {
        return false;
    }

    const auto begin = reinterpret_cast<uintptr_t>(aPointer);
    const auto end = begin + aSize;
    if (end < begin)
    {
        return false;
    }

    auto cursor = begin;
    while (cursor < end)
    {
        MEMORY_BASIC_INFORMATION mbi{};
        if (!VirtualQuery(reinterpret_cast<const void*>(cursor), &mbi, sizeof(mbi)))
        {
            return false;
        }

        if (mbi.State != MEM_COMMIT || !IsReadablePage(mbi.Protect))
        {
            return false;
        }

        const auto regionEnd = reinterpret_cast<uintptr_t>(mbi.BaseAddress) + mbi.RegionSize;
        if (regionEnd <= cursor)
        {
            return false;
        }

        cursor = regionEnd;
    }

    return true;
}

template<typename T>
bool TryReadMemory(const void* aPointer, T& aValue)
{
    if (!IsReadableMemory(aPointer, sizeof(T)))
    {
        return false;
    }

    aValue = *reinterpret_cast<const T*>(aPointer);
    return true;
}

void* ReadPointerField(void* aObject, uintptr_t aOffset)
{
    void* value = nullptr;
    if (!aObject)
    {
        return nullptr;
    }

    TryReadMemory(reinterpret_cast<const void*>(reinterpret_cast<uintptr_t>(aObject) + aOffset), value);
    return value;
}

void AppendObjectVtableSlots(std::ostringstream& aLine, void* aObject, std::initializer_list<uintptr_t> aSlots)
{
    auto* vtable = ReadPointerField(aObject, 0);
    AppendPointerWithRva(aLine, "vtable", vtable);

    for (const auto slot : aSlots)
    {
        auto* target = vtable ? ReadPointerField(vtable, slot) : nullptr;
        std::ostringstream label;
        label << "slot" << std::hex << slot;
        AppendPointerWithRva(aLine, label.str().c_str(), target);
    }
}

void LogJournalListenerContext(const char* aName, uint32_t aCount, void* aListener, void* aEvent)
{
    std::ostringstream line;
    line << "hook " << aName << " call=" << aCount;
    AppendPointerWithRva(line, "listener", aListener);
    AppendPointerWithRva(line, "event", aEvent);
    AppendReturnRva(line, __builtin_return_address(0));

    auto* field08 = ReadPointerField(aListener, 0x08);
    auto* field18 = ReadPointerField(aListener, 0x18);
    auto* field20 = ReadPointerField(aListener, 0x20);
    auto* field70 = ReadPointerField(aListener, 0x70);
    auto* field78 = ReadPointerField(aListener, 0x78);
    AppendPointerWithRva(line, "field08", field08);
    AppendPointerWithRva(line, "field18", field18);
    AppendPointerWithRva(line, "field20", field20);
    AppendPointerWithRva(line, "field70", field70);
    AppendPointerWithRva(line, "field78", field78);
    LogRed4ext(line.str());

    if (field70)
    {
        std::ostringstream owner;
        owner << "journal-listener-context " << aName << " call=" << aCount;
        AppendPointerWithRva(owner, "field70", field70);
        AppendObjectVtableSlots(owner, field70, {0x1C8, 0x218, 0x230, 0x238, 0x240, 0x248, 0x340, 0x348, 0x358});
        LogRed4ext(owner.str());
    }

    if (field78 && field78 != field70)
    {
        std::ostringstream service;
        service << "journal-listener-context " << aName << " call=" << aCount;
        AppendPointerWithRva(service, "field78", field78);
        AppendObjectVtableSlots(service, field78,
                                {0x1C8, 0x218, 0x220, 0x230, 0x238, 0x240, 0x248, 0x340, 0x348, 0x358});
        LogRed4ext(service.str());
    }
}

void ScanRegionForString(const uint8_t* aRegionBegin, size_t aRegionSize, uintptr_t aImageBase, const char* aNeedle,
                         uint32_t& aMatches)
{
    constexpr uint32_t kMaxMatchesPerNeedle = 8;

    const auto needleSize = std::strlen(aNeedle);
    if (needleSize == 0 || aRegionSize < needleSize || aMatches >= kMaxMatchesPerNeedle)
    {
        return;
    }

    const auto* cursor = aRegionBegin;
    const auto* regionEnd = aRegionBegin + aRegionSize;

    while (cursor + needleSize <= regionEnd && aMatches < kMaxMatchesPerNeedle)
    {
        if (std::memcmp(cursor, aNeedle, needleSize) == 0)
        {
            const auto rva = reinterpret_cast<uintptr_t>(cursor) - aImageBase;
            std::ostringstream line;
            line << "exe-string match needle=\"" << aNeedle << "\" rva=0x" << std::hex << rva << std::dec;
            LogRed4ext(line.str());
            ++aMatches;
            cursor += needleSize;
            continue;
        }

        ++cursor;
    }
}

void ScanExecutableStrings()
{
    if (gScannedExecutable)
    {
        return;
    }
    gScannedExecutable = true;

    const auto imageBase = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
    if (!imageBase)
    {
        LogRed4ext("exe-string scan skipped: main module unavailable");
        return;
    }

    const auto* dos = reinterpret_cast<const IMAGE_DOS_HEADER*>(imageBase);
    if (dos->e_magic != IMAGE_DOS_SIGNATURE)
    {
        LogRed4ext("exe-string scan skipped: bad DOS header");
        return;
    }

    const auto* nt = reinterpret_cast<const IMAGE_NT_HEADERS*>(imageBase + dos->e_lfanew);
    if (nt->Signature != IMAGE_NT_SIGNATURE)
    {
        LogRed4ext("exe-string scan skipped: bad NT header");
        return;
    }

    const auto imageSize = static_cast<size_t>(nt->OptionalHeader.SizeOfImage);
    std::ostringstream header;
    header << "exe-string scan begin base=0x" << std::hex << imageBase << " size=0x" << imageSize << std::dec;
    LogRed4ext(header.str());

    constexpr std::array<const char*, 21> kNeedles{
        "GPSSystem/Tick",
        "GPSSystem",
        "GPSSettings",
        "RunGPSQuery",
        "UpdateGPSQuery",
        "gameuiGPSGameController",
        "OnSetHasGPSPortal",
        "IsGPSPortal",
        "m_cookedGpsData",
        "gameCookedGpsMappinData",
        "GetNavigationFunctionalTests",
        "TrafficSystem_Pathfinding",
        "TrafficSystem_PathMap",
        "Pathfinding Algorithm Failed",
        "No Path Found in Traffic",
        "PathFindingFailed",
        "worldTrafficLanePlayerGPSInfo",
        "worldTrafficPersistentResource",
        "worldTrafficConnectivityOutLane",
    };

    for (const auto* needle : kNeedles)
    {
        uint32_t matches = 0;
        uintptr_t cursor = imageBase;
        const uintptr_t end = imageBase + imageSize;

        while (cursor < end)
        {
            MEMORY_BASIC_INFORMATION mbi{};
            if (!VirtualQuery(reinterpret_cast<const void*>(cursor), &mbi, sizeof(mbi)))
            {
                break;
            }

            const auto regionBegin = reinterpret_cast<uintptr_t>(mbi.BaseAddress);
            const auto regionEnd = regionBegin + mbi.RegionSize;
            if (mbi.State == MEM_COMMIT && mbi.Type == MEM_IMAGE && IsReadablePage(mbi.Protect))
            {
                const auto scanBegin = std::max(regionBegin, imageBase);
                const auto scanEnd = std::min(regionEnd, end);
                if (scanBegin < scanEnd)
                {
                    ScanRegionForString(reinterpret_cast<const uint8_t*>(scanBegin), scanEnd - scanBegin, imageBase,
                                        needle, matches);
                }
            }

            if (regionEnd <= cursor)
            {
                break;
            }
            cursor = regionEnd;
        }

        if (matches == 0)
        {
            std::ostringstream line;
            line << "exe-string no match needle=\"" << needle << "\"";
            LogRed4ext(line.str());
        }
    }

    LogRed4ext("exe-string scan end");
}

void DetourGpsSystemTick(void* aThis, void* a2, void* a3, void* a4)
{
    if (gGpsTickLogCount < 8)
    {
        LogHookCall("GPSSystem/Tick", gGpsTickLogCount, aThis, a2, a3, a4);
        ++gGpsTickLogCount;
    }

    if (gOriginalGpsSystemTick)
    {
        gOriginalGpsSystemTick(aThis, a2, a3, a4);
    }
}

void LogJournalListeners(uint32_t aCallCount, void* aThis)
{
    constexpr uint32_t kMaxListenerDumps = 16;
    constexpr uint32_t kMaxListenersPerDump = 24;
    constexpr uint32_t kSuspiciousListenerCount = 512;

    if (gJournalTrackEntryListenerDumpCount >= kMaxListenerDumps || !aThis)
    {
        return;
    }

    uintptr_t listenerBase = 0;
    uint32_t listenerCount = 0;
    const auto objectAddress = reinterpret_cast<uintptr_t>(aThis);
    const auto hasBase = TryReadMemory(reinterpret_cast<const void*>(objectAddress + kJournalListenerArrayOffset),
                                       listenerBase);
    const auto hasCount = TryReadMemory(reinterpret_cast<const void*>(objectAddress + kJournalListenerCountOffset),
                                        listenerCount);

    std::ostringstream header;
    header << "journal-listeners dump=" << gJournalTrackEntryListenerDumpCount << " call=" << aCallCount
           << " hasBase=" << hasBase << " hasCount=" << hasCount << " count=" << listenerCount;
    AppendPointerWithRva(header, "base", reinterpret_cast<const void*>(listenerBase));
    LogRed4ext(header.str());

    ++gJournalTrackEntryListenerDumpCount;

    if (!hasBase || !hasCount || !listenerBase || listenerCount > kSuspiciousListenerCount)
    {
        return;
    }

    const auto cappedCount = std::min(listenerCount, kMaxListenersPerDump);
    for (uint32_t index = 0; index < cappedCount; ++index)
    {
        void* listener = nullptr;
        void* vtable = nullptr;
        void* callback28 = nullptr;
        void* callback30 = nullptr;
        void* callback50 = nullptr;

        const auto listenerAddress = reinterpret_cast<const void*>(listenerBase + index * sizeof(void*));
        const auto hasListener = TryReadMemory(listenerAddress, listener);
        const auto hasVtable = hasListener && TryReadMemory(listener, vtable);
        if (hasVtable && vtable)
        {
            const auto vtableAddress = reinterpret_cast<uintptr_t>(vtable);
            TryReadMemory(reinterpret_cast<const void*>(vtableAddress + 0x28), callback28);
            TryReadMemory(reinterpret_cast<const void*>(vtableAddress + 0x30), callback30);
            TryReadMemory(reinterpret_cast<const void*>(vtableAddress + 0x50), callback50);
        }

        std::ostringstream line;
        line << "journal-listener call=" << aCallCount << " index=" << index << " hasListener=" << hasListener
             << " hasVtable=" << hasVtable;
        AppendPointerWithRva(line, "listener", listener);
        AppendPointerWithRva(line, "vtable", vtable);
        AppendPointerWithRva(line, "vt28", callback28);
        AppendPointerWithRva(line, "vt30", callback30);
        AppendPointerWithRva(line, "vt50", callback50);
        LogRed4ext(line.str());
    }
}

void DetourJournalTrackEntry(void* aThis, void* aEntryHandle)
{
    constexpr uint32_t kMaxCalls = 96;

    const auto callCount = gJournalTrackEntryLogCount;
    if (callCount < kMaxCalls)
    {
        std::ostringstream line;
        line << "hook JournalManager::TrackEntry impl call=" << callCount;
        AppendPointerWithRva(line, "this", aThis);
        AppendPointerWithRva(line, "entryHandle", aEntryHandle);
        AppendReturnRva(line, __builtin_return_address(0));
        LogRed4ext(line.str());

        LogJournalListeners(callCount, aThis);
        ++gJournalTrackEntryLogCount;
    }

    if (gOriginalJournalTrackEntry)
    {
        gOriginalJournalTrackEntry(aThis, aEntryHandle);
    }
}

void DispatchJournalListenerCallback(const char* aName, uint32_t& aCounter, JournalListenerCallbackFunc aOriginal,
                                     void* aListener, void* aEvent)
{
    constexpr uint32_t kMaxCalls = 64;

    if (aCounter < kMaxCalls)
    {
        LogJournalListenerContext(aName, aCounter, aListener, aEvent);
        ++aCounter;
    }

    if (aOriginal)
    {
        aOriginal(aListener, aEvent);
    }
}

void DetourJournalListenerObjectiveVt28(void* aListener, void* aEvent)
{
    DispatchJournalListenerCallback("JournalListener::Objective vt28", gJournalListenerObjectiveVt28LogCount,
                                    gOriginalJournalListenerObjectiveVt28, aListener, aEvent);
}

void DetourJournalListenerObjectiveVt30(void* aListener, void* aEvent)
{
    DispatchJournalListenerCallback("JournalListener::Objective vt30", gJournalListenerObjectiveVt30LogCount,
                                    gOriginalJournalListenerObjectiveVt30, aListener, aEvent);
}

void DetourJournalListenerRouteVt28(void* aListener, void* aEvent)
{
    DispatchJournalListenerCallback("JournalListener::Route vt28", gJournalListenerRouteVt28LogCount,
                                    gOriginalJournalListenerRouteVt28, aListener, aEvent);
}

void DetourJournalListenerRouteVt30(void* aListener, void* aEvent)
{
    DispatchJournalListenerCallback("JournalListener::Route vt30", gJournalListenerRouteVt30LogCount,
                                    gOriginalJournalListenerRouteVt30, aListener, aEvent);
}

void DetourJournalListenerRouteAltVt28(void* aListener, void* aEvent)
{
    DispatchJournalListenerCallback("JournalListener::RouteAlt vt28", gJournalListenerRouteAltVt28LogCount,
                                    gOriginalJournalListenerRouteAltVt28, aListener, aEvent);
}

void DetourJournalListenerRouteAltVt30(void* aListener, void* aEvent)
{
    DispatchJournalListenerCallback("JournalListener::RouteAlt vt30", gJournalListenerRouteAltVt30LogCount,
                                    gOriginalJournalListenerRouteAltVt30, aListener, aEvent);
}

void DetourJournalListenerUiVt28(void* aListener, void* aEvent)
{
    DispatchJournalListenerCallback("JournalListener::UI vt28", gJournalListenerUiVt28LogCount,
                                    gOriginalJournalListenerUiVt28, aListener, aEvent);
}

void DetourJournalListenerUiVt30(void* aListener, void* aEvent)
{
    DispatchJournalListenerCallback("JournalListener::UI vt30", gJournalListenerUiVt30LogCount,
                                    gOriginalJournalListenerUiVt30, aListener, aEvent);
}

void DetourJournalListenerTrackerVt28(void* aListener, void* aEvent)
{
    DispatchJournalListenerCallback("JournalListener::Tracker vt28", gJournalListenerTrackerVt28LogCount,
                                    gOriginalJournalListenerTrackerVt28, aListener, aEvent);
}

void DetourFrameMappinPathWrapper(void* aThis, void* aStack, void* aResult)
{
    if (gFrameMappinPathWrapperLogCount < 64)
    {
        LogScriptNativeCall("FrameMappinPathWrapper", gFrameMappinPathWrapperLogCount, aThis, aStack, aResult);
        ++gFrameMappinPathWrapperLogCount;
    }

    InspectAndHookMappinSystem(aThis);

    if (gOriginalFrameMappinPathWrapper)
    {
        gOriginalFrameMappinPathWrapper(aThis, aStack, aResult);
    }
}

void DetourFrameMappinPathCore(void* aThis, uint32_t aPathKind, void* a3, void* aPathRect)
{
    if (gFrameMappinPathCoreLogCount < 64)
    {
        LogFrameMappinPathCoreCall(gFrameMappinPathCoreLogCount, aThis, aPathKind, a3, aPathRect);
        ++gFrameMappinPathCoreLogCount;
    }

    InspectAndHookMappinSystem(aThis);

    if (gOriginalFrameMappinPathCore)
    {
        gOriginalFrameMappinPathCore(aThis, aPathKind, a3, aPathRect);
    }
}

void DetourSetSelectedMappinWrapper(void* aThis, void* aStack, void* aResult)
{
    if (gSetSelectedMappinWrapperLogCount < 64)
    {
        LogScriptNativeCall("SetSelectedMappinWrapper", gSetSelectedMappinWrapperLogCount, aThis, aStack, aResult);
        ++gSetSelectedMappinWrapperLogCount;
    }

    InspectAndHookMappinSystem(aThis);

    if (gOriginalSetSelectedMappinWrapper)
    {
        gOriginalSetSelectedMappinWrapper(aThis, aStack, aResult);
    }
}

void DetourSetSelectedMappinByIdWrapper(void* aThis, void* aStack, void* aResult)
{
    if (gSetSelectedMappinByIdWrapperLogCount < 64)
    {
        LogScriptNativeCall("SetSelectedMappinByIdWrapper", gSetSelectedMappinByIdWrapperLogCount, aThis, aStack,
                            aResult);
        ++gSetSelectedMappinByIdWrapperLogCount;
    }

    InspectAndHookMappinSystem(aThis);

    if (gOriginalSetSelectedMappinByIdWrapper)
    {
        gOriginalSetSelectedMappinByIdWrapper(aThis, aStack, aResult);
    }
}

void DetourSetSelectedMappinByPositionWrapper(void* aThis, void* aStack, void* aResult)
{
    if (gSetSelectedMappinByPositionWrapperLogCount < 64)
    {
        LogScriptNativeCall("SetSelectedMappinByPositionWrapper", gSetSelectedMappinByPositionWrapperLogCount, aThis,
                            aStack, aResult);
        ++gSetSelectedMappinByPositionWrapperLogCount;
    }

    InspectAndHookMappinSystem(aThis);

    if (gOriginalSetSelectedMappinByPositionWrapper)
    {
        gOriginalSetSelectedMappinByPositionWrapper(aThis, aStack, aResult);
    }
}

void DetourSetSelectedMappinCore(void* aThis, void* aMappinHandle)
{
    if (gSetSelectedMappinCoreLogCount < 64)
    {
        LogSetSelectedMappinCoreCall(gSetSelectedMappinCoreLogCount, aThis, aMappinHandle);
        ++gSetSelectedMappinCoreLogCount;
    }

    InspectAndHookMappinSystem(aThis);

    if (gOriginalSetSelectedMappinCore)
    {
        gOriginalSetSelectedMappinCore(aThis, aMappinHandle);
    }
}

void DetourTrackCustomPositionMappinWrapper(void* aThis, void* aStack, void* aResult)
{
    if (gTrackCustomPositionMappinWrapperLogCount < 64)
    {
        LogScriptNativeCall("TrackCustomPositionMappinWrapper", gTrackCustomPositionMappinWrapperLogCount, aThis,
                            aStack, aResult);
        ++gTrackCustomPositionMappinWrapperLogCount;
    }

    InspectAndHookMappinSystem(aThis);

    if (gOriginalTrackCustomPositionMappinWrapper)
    {
        gOriginalTrackCustomPositionMappinWrapper(aThis, aStack, aResult);
    }
}

void DetourTrackCustomPositionMappinCore(void* aThis)
{
    if (gTrackCustomPositionMappinCoreLogCount < 64)
    {
        LogTrackCustomPositionMappinCoreCall(gTrackCustomPositionMappinCoreLogCount, aThis);
        ++gTrackCustomPositionMappinCoreLogCount;
    }

    InspectAndHookMappinSystem(aThis);

    if (gOriginalTrackCustomPositionMappinCore)
    {
        gOriginalTrackCustomPositionMappinCore(aThis);
    }
}

void DetourUntrackCustomPositionMappinWrapper(void* aThis, void* aStack, void* aResult)
{
    if (gUntrackCustomPositionMappinWrapperLogCount < 64)
    {
        LogScriptNativeCall("UntrackCustomPositionMappinWrapper", gUntrackCustomPositionMappinWrapperLogCount, aThis,
                            aStack, aResult);
        ++gUntrackCustomPositionMappinWrapperLogCount;
    }

    InspectAndHookMappinSystem(aThis);

    if (gOriginalUntrackCustomPositionMappinWrapper)
    {
        gOriginalUntrackCustomPositionMappinWrapper(aThis, aStack, aResult);
    }
}

void DetourTrackMappinWrapper(void* aThis, void* aStack, void* aResult)
{
    if (gTrackMappinWrapperLogCount < 64)
    {
        LogScriptNativeCall("TrackMappinWrapper", gTrackMappinWrapperLogCount, aThis, aStack, aResult);
        ++gTrackMappinWrapperLogCount;
    }

    if (gOriginalTrackMappinWrapper)
    {
        gOriginalTrackMappinWrapper(aThis, aStack, aResult);
    }
}

void DetourTrackMappinCore(void* aThis, void* aMappinHandle)
{
    if (gTrackMappinCoreLogCount < 64)
    {
        LogTrackMappinCoreCall(gTrackMappinCoreLogCount, aThis, aMappinHandle);
        ++gTrackMappinCoreLogCount;
    }

    InspectAndHookMappinSystem(aThis);

    if (gOriginalTrackMappinCore)
    {
        gOriginalTrackMappinCore(aThis, aMappinHandle);
    }
}

void DetourSetMappinTrackingAlternativeWrapper(void* aThis, void* aStack, void* aResult)
{
    if (gSetMappinTrackingAlternativeLogCount < 64)
    {
        LogScriptNativeCall("SetMappinTrackingAlternativeWrapper", gSetMappinTrackingAlternativeLogCount, aThis,
                            aStack, aResult);
        ++gSetMappinTrackingAlternativeLogCount;
    }

    if (gOriginalSetMappinTrackingAlternativeWrapper)
    {
        gOriginalSetMappinTrackingAlternativeWrapper(aThis, aStack, aResult);
    }
}

void DetourMappinUpdateCustomPosition(void* aSystem, void* aHandle, void* aPositionData)
{
    if (gMappinUpdateCustomPositionLogCount < 32)
    {
        std::ostringstream line;
        line << "hook MappinSystem::UpdateCustomPosition slot1f0 call=" << gMappinUpdateCustomPositionLogCount;
        AppendPointerWithRva(line, "system", aSystem);
        AppendPointerWithRva(line, "handle", aHandle);
        AppendPointerWithRva(line, "positionData", aPositionData);
        AppendReturnRva(line, __builtin_return_address(0));
        LogRed4ext(line.str());
        ++gMappinUpdateCustomPositionLogCount;
    }

    if (gOriginalMappinUpdateCustomPosition)
    {
        gOriginalMappinUpdateCustomPosition(aSystem, aHandle, aPositionData);
    }
}

void DetourMappinSetTracked(void* aSystem, void* aMappinId)
{
    if (gMappinSetTrackedLogCount < 64)
    {
        std::ostringstream line;
        line << "hook MappinSystem::SetTracked slot220 call=" << gMappinSetTrackedLogCount;
        AppendPointerWithRva(line, "system", aSystem);
        AppendPointerWithRva(line, "mappinId", aMappinId);
        AppendReturnRva(line, __builtin_return_address(0));
        LogRed4ext(line.str());
        ++gMappinSetTrackedLogCount;
    }

    if (gOriginalMappinSetTracked)
    {
        gOriginalMappinSetTracked(aSystem, aMappinId);
    }
}

void DetourMappinClearTracked(void* aSystem)
{
    if (gMappinClearTrackedLogCount < 64)
    {
        std::ostringstream line;
        line << "hook MappinSystem::ClearTracked slot228 call=" << gMappinClearTrackedLogCount;
        AppendPointerWithRva(line, "system", aSystem);
        AppendReturnRva(line, __builtin_return_address(0));
        LogRed4ext(line.str());
        ++gMappinClearTrackedLogCount;
    }

    if (gOriginalMappinClearTracked)
    {
        gOriginalMappinClearTracked(aSystem);
    }
}

void DetourMappinFramePath(void* aSystem, uint32_t aPathKind, void* aResult)
{
    if (gMappinFramePathLogCount < 32)
    {
        std::ostringstream line;
        line << "hook MappinSystem::FramePath slot280 call=" << gMappinFramePathLogCount
             << " pathKind=" << aPathKind;
        AppendPointerWithRva(line, "system", aSystem);
        AppendPointerWithRva(line, "result", aResult);
        AppendReturnRva(line, __builtin_return_address(0));
        LogRed4ext(line.str());
        ++gMappinFramePathLogCount;
    }

    if (gOriginalMappinFramePath)
    {
        gOriginalMappinFramePath(aSystem, aPathKind, aResult);
    }
}

void* DetourMappinPositionQuery(void* aSystem, void* a2, void* a3, void* a4, void* a5)
{
    const auto shouldLog = gMappinPositionQueryLogCount < 32;
    if (shouldLog)
    {
        std::ostringstream line;
        line << "hook MappinSystem::PositionQuery slot2e0 call=" << gMappinPositionQueryLogCount;
        AppendPointerWithRva(line, "system", aSystem);
        AppendPointerWithRva(line, "a2", a2);
        AppendPointerWithRva(line, "a3", a3);
        AppendPointerWithRva(line, "a4", a4);
        AppendPointerWithRva(line, "a5", a5);
        AppendReturnRva(line, __builtin_return_address(0));
        LogRed4ext(line.str());
        ++gMappinPositionQueryLogCount;
    }

    void* result = nullptr;
    if (gOriginalMappinPositionQuery)
    {
        result = gOriginalMappinPositionQuery(aSystem, a2, a3, a4, a5);
    }

    if (shouldLog)
    {
        std::ostringstream line;
        line << "hook MappinSystem::PositionQuery slot2e0 result";
        AppendPointerWithRva(line, "value", result);
        LogRed4ext(line.str());
    }

    return result;
}

void* DetourMappinCreateCustomPosition(void* aSystem, void* aName, void* aParams, void* aPositionData)
{
    const auto shouldLog = gMappinCreateCustomPositionLogCount < 32;
    if (shouldLog)
    {
        std::ostringstream line;
        line << "hook MappinSystem::CreateCustomPosition slot2f0 call=" << gMappinCreateCustomPositionLogCount;
        AppendPointerWithRva(line, "system", aSystem);
        AppendPointerWithRva(line, "name", aName);
        AppendPointerWithRva(line, "params", aParams);
        AppendPointerWithRva(line, "positionData", aPositionData);
        AppendReturnRva(line, __builtin_return_address(0));
        LogRed4ext(line.str());
        ++gMappinCreateCustomPositionLogCount;
    }

    void* result = nullptr;
    if (gOriginalMappinCreateCustomPosition)
    {
        result = gOriginalMappinCreateCustomPosition(aSystem, aName, aParams, aPositionData);
    }

    if (shouldLog)
    {
        std::ostringstream line;
        line << "hook MappinSystem::CreateCustomPosition slot2f0 result";
        AppendPointerWithRva(line, "value", result);
        LogRed4ext(line.str());
    }

    return result;
}

void DetourMappinReleaseQuery(void* aSystem, void* aHandle)
{
    if (gMappinReleaseQueryLogCount < 32)
    {
        std::ostringstream line;
        line << "hook MappinSystem::ReleaseQuery slot368 call=" << gMappinReleaseQueryLogCount;
        AppendPointerWithRva(line, "system", aSystem);
        AppendPointerWithRva(line, "handle", aHandle);
        AppendReturnRva(line, __builtin_return_address(0));
        LogRed4ext(line.str());
        ++gMappinReleaseQueryLogCount;
    }

    if (gOriginalMappinReleaseQuery)
    {
        gOriginalMappinReleaseQuery(aSystem, aHandle);
    }
}

void* DetourMappinActiveRoute(void* aSystem)
{
    const auto shouldLog = gMappinActiveRouteLogCount < 32;
    if (shouldLog)
    {
        std::ostringstream line;
        line << "hook MappinSystem::ActiveRoute slot3a8 call=" << gMappinActiveRouteLogCount;
        AppendPointerWithRva(line, "system", aSystem);
        AppendReturnRva(line, __builtin_return_address(0));
        LogRed4ext(line.str());
        ++gMappinActiveRouteLogCount;
    }

    void* result = nullptr;
    if (gOriginalMappinActiveRoute)
    {
        result = gOriginalMappinActiveRoute(aSystem);
    }

    if (shouldLog)
    {
        std::ostringstream line;
        line << "hook MappinSystem::ActiveRoute slot3a8 result";
        AppendPointerWithRva(line, "value", result);
        LogRed4ext(line.str());
    }

    return result;
}

void AttachProbeHook(const char* aName, uintptr_t aRva, void* aDetour, void** aOriginal)
{
    if (!gSdk || !gSdk->hooking || !gSdk->hooking->Attach)
    {
        LogRed4ext("hook attach skipped: RED4ext hooking API unavailable");
        return;
    }

    auto* target = reinterpret_cast<void*>(GetImageBase() + aRva);
    const auto attached = gSdk->hooking->Attach(gHandle, target, aDetour, aOriginal);

    std::ostringstream line;
    line << "hook attach " << aName << " target=" << target << " rva=0x" << std::hex << aRva << std::dec
         << " result=" << (attached ? "ok" : "failed") << " original=" << (aOriginal ? *aOriginal : nullptr);
    LogRed4ext(line.str());
}

void AttachAbsoluteHook(const char* aName, void* aTarget, void* aDetour, void** aOriginal)
{
    if (!gSdk || !gSdk->hooking || !gSdk->hooking->Attach)
    {
        LogRed4ext("absolute hook attach skipped: RED4ext hooking API unavailable");
        return;
    }

    if (!aTarget)
    {
        std::ostringstream line;
        line << "absolute hook attach skipped " << aName << ": target=null";
        LogRed4ext(line.str());
        return;
    }

    const auto attached = gSdk->hooking->Attach(gHandle, aTarget, aDetour, aOriginal);

    std::ostringstream line;
    line << "absolute hook attach " << aName;
    AppendPointerWithRva(line, "target", aTarget);
    line << " result=" << (attached ? "ok" : "failed");
    AppendPointerWithRva(line, "original", aOriginal ? *aOriginal : nullptr);
    LogRed4ext(line.str());
}

void DetachProbeHook(const char* aName, uintptr_t aRva)
{
    if (!gSdk || !gSdk->hooking || !gSdk->hooking->Detach)
    {
        return;
    }

    auto* target = reinterpret_cast<void*>(GetImageBase() + aRva);
    const auto detached = gSdk->hooking->Detach(gHandle, target);

    std::ostringstream line;
    line << "hook detach " << aName << " target=" << target << " result=" << (detached ? "ok" : "failed");
    LogRed4ext(line.str());
}

void DetachAbsoluteHook(const char* aName, void* aTarget)
{
    if (!gSdk || !gSdk->hooking || !gSdk->hooking->Detach || !aTarget)
    {
        return;
    }

    const auto detached = gSdk->hooking->Detach(gHandle, aTarget);

    std::ostringstream line;
    line << "absolute hook detach " << aName;
    AppendPointerWithRva(line, "target", aTarget);
    line << " result=" << (detached ? "ok" : "failed");
    LogRed4ext(line.str());
}

void InspectAndHookMappinSystem(void* aThis)
{
    auto* resolver = reinterpret_cast<MappinSystemResolverFunc>(GetImageBase() + kMappinSystemResolverRva);
    void* system = resolver ? resolver(aThis) : nullptr;
    auto* vtable = system ? *reinterpret_cast<void***>(system) : nullptr;

    if (gMappinSystemSlotLogCount < 16)
    {
        LogMappinSystemSlots(gMappinSystemSlotLogCount, system, vtable);
        ++gMappinSystemSlotLogCount;
    }

    if (gMappinSlotHookAttachAttempted || !vtable)
    {
        return;
    }

    gMappinSlotHookAttachAttempted = true;
    gMappinUpdateCustomPositionTarget =
        *reinterpret_cast<void**>(reinterpret_cast<uintptr_t>(vtable) + kMappinUpdateCustomPositionSlotOffset);
    gMappinSetTrackedTarget =
        *reinterpret_cast<void**>(reinterpret_cast<uintptr_t>(vtable) + kMappinSetTrackedSlotOffset);
    gMappinClearTrackedTarget =
        *reinterpret_cast<void**>(reinterpret_cast<uintptr_t>(vtable) + kMappinClearTrackedSlotOffset);
    gMappinFramePathTarget =
        *reinterpret_cast<void**>(reinterpret_cast<uintptr_t>(vtable) + kMappinFramePathSlotOffset);
    gMappinPositionQueryTarget =
        *reinterpret_cast<void**>(reinterpret_cast<uintptr_t>(vtable) + kMappinPositionQuerySlotOffset);
    gMappinCreateCustomPositionTarget =
        *reinterpret_cast<void**>(reinterpret_cast<uintptr_t>(vtable) + kMappinCreateCustomPositionSlotOffset);
    gMappinReleaseQueryTarget =
        *reinterpret_cast<void**>(reinterpret_cast<uintptr_t>(vtable) + kMappinReleaseQuerySlotOffset);
    gMappinActiveRouteTarget =
        *reinterpret_cast<void**>(reinterpret_cast<uintptr_t>(vtable) + kMappinActiveRouteSlotOffset);

    AttachAbsoluteHook("MappinSystem::UpdateCustomPosition slot1f0", gMappinUpdateCustomPositionTarget,
                       reinterpret_cast<void*>(&DetourMappinUpdateCustomPosition),
                       reinterpret_cast<void**>(&gOriginalMappinUpdateCustomPosition));
    AttachAbsoluteHook("MappinSystem::SetTracked slot220", gMappinSetTrackedTarget,
                       reinterpret_cast<void*>(&DetourMappinSetTracked),
                       reinterpret_cast<void**>(&gOriginalMappinSetTracked));
    AttachAbsoluteHook("MappinSystem::ClearTracked slot228", gMappinClearTrackedTarget,
                       reinterpret_cast<void*>(&DetourMappinClearTracked),
                       reinterpret_cast<void**>(&gOriginalMappinClearTracked));
    AttachAbsoluteHook("MappinSystem::FramePath slot280", gMappinFramePathTarget,
                       reinterpret_cast<void*>(&DetourMappinFramePath),
                       reinterpret_cast<void**>(&gOriginalMappinFramePath));
    AttachAbsoluteHook("MappinSystem::PositionQuery slot2e0", gMappinPositionQueryTarget,
                       reinterpret_cast<void*>(&DetourMappinPositionQuery),
                       reinterpret_cast<void**>(&gOriginalMappinPositionQuery));
    AttachAbsoluteHook("MappinSystem::CreateCustomPosition slot2f0", gMappinCreateCustomPositionTarget,
                       reinterpret_cast<void*>(&DetourMappinCreateCustomPosition),
                       reinterpret_cast<void**>(&gOriginalMappinCreateCustomPosition));
    AttachAbsoluteHook("MappinSystem::ReleaseQuery slot368", gMappinReleaseQueryTarget,
                       reinterpret_cast<void*>(&DetourMappinReleaseQuery),
                       reinterpret_cast<void**>(&gOriginalMappinReleaseQuery));
    AttachAbsoluteHook("MappinSystem::ActiveRoute slot3a8", gMappinActiveRouteTarget,
                       reinterpret_cast<void*>(&DetourMappinActiveRoute),
                       reinterpret_cast<void**>(&gOriginalMappinActiveRoute));
}

bool OnRunningEnter(void* aApp)
{
    std::ostringstream line;
    line << "GameState Running OnEnter app=" << aApp;
    LogRed4ext(line.str());
    return true;
}

bool OnRunningUpdate(void* aApp)
{
    if (gUpdateLogCount < 5)
    {
        std::ostringstream line;
        line << "GameState Running OnUpdate app=" << aApp << " count=" << gUpdateLogCount;
        LogRed4ext(line.str());
        ++gUpdateLogCount;
    }

    return false;
}

bool OnRunningExit(void* aApp)
{
    std::ostringstream line;
    line << "GameState Running OnExit app=" << aApp;
    LogRed4ext(line.str());
    return true;
}

RED4ext::v1::GameState gRunningState{
    .OnEnter = OnRunningEnter,
    .OnUpdate = OnRunningUpdate,
    .OnExit = OnRunningExit,
};
}

BOOL APIENTRY DllMain(HMODULE aModule, DWORD aReason, LPVOID)
{
    if (aReason == DLL_PROCESS_ATTACH)
    {
        gModule = aModule;
        LARGE_INTEGER frequency{};
        LARGE_INTEGER counter{};
        QueryPerformanceFrequency(&frequency);
        QueryPerformanceCounter(&counter);
        gLogFrequency = static_cast<uint64_t>(frequency.QuadPart);
        gLogStartTick = static_cast<uint64_t>(counter.QuadPart);
    }
    return TRUE;
}

RED4EXT_C_EXPORT bool RED4EXT_CALL Main(RED4ext::v1::PluginHandle aHandle, RED4ext::v1::EMainReason aReason,
                                        const RED4ext::v1::Sdk* aSdk)
{
    gHandle = aHandle;
    gSdk = aSdk;

    std::ostringstream line;
    line << "EdgeWeightGPS Main reason=" << ReasonToString(aReason) << " handle=" << aHandle << " sdk=" << aSdk;
    if (aSdk && aSdk->runtime)
    {
        line << " runtime=" << static_cast<uint32_t>(aSdk->runtime->major) << "." << aSdk->runtime->minor << "."
             << aSdk->runtime->patch;
    }
    LogRed4ext(line.str());

    if (aReason == RED4ext::v1::EMainReason::Load && aSdk && aSdk->gameStates && aSdk->gameStates->Add)
    {
        const auto added = aSdk->gameStates->Add(aHandle, kGameStateRunning, &gRunningState);
        LogRed4ext(added ? "registered Running game-state callbacks" : "failed to register Running game-state callbacks");

        AttachProbeHook("GPSSystem/Tick", kGpsSystemTickRva, reinterpret_cast<void*>(&DetourGpsSystemTick),
                        reinterpret_cast<void**>(&gOriginalGpsSystemTick));
        AttachProbeHook("JournalManager::TrackEntry impl", kJournalTrackEntryImplRva,
                        reinterpret_cast<void*>(&DetourJournalTrackEntry),
                        reinterpret_cast<void**>(&gOriginalJournalTrackEntry));
        AttachProbeHook("JournalListener::Objective vt28", kJournalListenerObjectiveVt28Rva,
                        reinterpret_cast<void*>(&DetourJournalListenerObjectiveVt28),
                        reinterpret_cast<void**>(&gOriginalJournalListenerObjectiveVt28));
        AttachProbeHook("JournalListener::Objective vt30", kJournalListenerObjectiveVt30Rva,
                        reinterpret_cast<void*>(&DetourJournalListenerObjectiveVt30),
                        reinterpret_cast<void**>(&gOriginalJournalListenerObjectiveVt30));
        AttachProbeHook("JournalListener::Route vt28", kJournalListenerRouteVt28Rva,
                        reinterpret_cast<void*>(&DetourJournalListenerRouteVt28),
                        reinterpret_cast<void**>(&gOriginalJournalListenerRouteVt28));
        AttachProbeHook("JournalListener::Route vt30", kJournalListenerRouteVt30Rva,
                        reinterpret_cast<void*>(&DetourJournalListenerRouteVt30),
                        reinterpret_cast<void**>(&gOriginalJournalListenerRouteVt30));
        AttachProbeHook("JournalListener::RouteAlt vt28", kJournalListenerRouteAltVt28Rva,
                        reinterpret_cast<void*>(&DetourJournalListenerRouteAltVt28),
                        reinterpret_cast<void**>(&gOriginalJournalListenerRouteAltVt28));
        AttachProbeHook("JournalListener::RouteAlt vt30", kJournalListenerRouteAltVt30Rva,
                        reinterpret_cast<void*>(&DetourJournalListenerRouteAltVt30),
                        reinterpret_cast<void**>(&gOriginalJournalListenerRouteAltVt30));
        AttachProbeHook("JournalListener::UI vt28", kJournalListenerUiVt28Rva,
                        reinterpret_cast<void*>(&DetourJournalListenerUiVt28),
                        reinterpret_cast<void**>(&gOriginalJournalListenerUiVt28));
        AttachProbeHook("JournalListener::UI vt30", kJournalListenerUiVt30Rva,
                        reinterpret_cast<void*>(&DetourJournalListenerUiVt30),
                        reinterpret_cast<void**>(&gOriginalJournalListenerUiVt30));
        AttachProbeHook("JournalListener::Tracker vt28", kJournalListenerTrackerVt28Rva,
                        reinterpret_cast<void*>(&DetourJournalListenerTrackerVt28),
                        reinterpret_cast<void**>(&gOriginalJournalListenerTrackerVt28));
        AttachProbeHook("FrameMappinPathWrapper", kFrameMappinPathWrapperRva,
                        reinterpret_cast<void*>(&DetourFrameMappinPathWrapper),
                        reinterpret_cast<void**>(&gOriginalFrameMappinPathWrapper));
        AttachProbeHook("FrameMappinPathCore", kFrameMappinPathCoreRva,
                        reinterpret_cast<void*>(&DetourFrameMappinPathCore),
                        reinterpret_cast<void**>(&gOriginalFrameMappinPathCore));
        AttachProbeHook("SetSelectedMappinWrapper", kSetSelectedMappinWrapperRva,
                        reinterpret_cast<void*>(&DetourSetSelectedMappinWrapper),
                        reinterpret_cast<void**>(&gOriginalSetSelectedMappinWrapper));
        AttachProbeHook("SetSelectedMappinByIdWrapper", kSetSelectedMappinByIdWrapperRva,
                        reinterpret_cast<void*>(&DetourSetSelectedMappinByIdWrapper),
                        reinterpret_cast<void**>(&gOriginalSetSelectedMappinByIdWrapper));
        AttachProbeHook("SetSelectedMappinByPositionWrapper", kSetSelectedMappinByPositionWrapperRva,
                        reinterpret_cast<void*>(&DetourSetSelectedMappinByPositionWrapper),
                        reinterpret_cast<void**>(&gOriginalSetSelectedMappinByPositionWrapper));
        AttachProbeHook("SetSelectedMappinCore", kSetSelectedMappinCoreRva,
                        reinterpret_cast<void*>(&DetourSetSelectedMappinCore),
                        reinterpret_cast<void**>(&gOriginalSetSelectedMappinCore));
        AttachProbeHook("TrackCustomPositionMappinWrapper", kTrackCustomPositionMappinWrapperRva,
                        reinterpret_cast<void*>(&DetourTrackCustomPositionMappinWrapper),
                        reinterpret_cast<void**>(&gOriginalTrackCustomPositionMappinWrapper));
        AttachProbeHook("TrackCustomPositionMappinCore", kTrackCustomPositionMappinCoreRva,
                        reinterpret_cast<void*>(&DetourTrackCustomPositionMappinCore),
                        reinterpret_cast<void**>(&gOriginalTrackCustomPositionMappinCore));
        AttachProbeHook("UntrackCustomPositionMappinWrapper", kUntrackCustomPositionMappinWrapperRva,
                        reinterpret_cast<void*>(&DetourUntrackCustomPositionMappinWrapper),
                        reinterpret_cast<void**>(&gOriginalUntrackCustomPositionMappinWrapper));
        AttachProbeHook("TrackMappinWrapper", kTrackMappinWrapperRva,
                        reinterpret_cast<void*>(&DetourTrackMappinWrapper),
                        reinterpret_cast<void**>(&gOriginalTrackMappinWrapper));
        AttachProbeHook("TrackMappinCore", kTrackMappinCoreRva, reinterpret_cast<void*>(&DetourTrackMappinCore),
                        reinterpret_cast<void**>(&gOriginalTrackMappinCore));
        AttachProbeHook("SetMappinTrackingAlternativeWrapper", kSetMappinTrackingAlternativeWrapperRva,
                        reinterpret_cast<void*>(&DetourSetMappinTrackingAlternativeWrapper),
                        reinterpret_cast<void**>(&gOriginalSetMappinTrackingAlternativeWrapper));
    }

    if (aReason == RED4ext::v1::EMainReason::Unload)
    {
        DetachProbeHook("GPSSystem/Tick", kGpsSystemTickRva);
        DetachProbeHook("JournalManager::TrackEntry impl", kJournalTrackEntryImplRva);
        DetachProbeHook("JournalListener::Objective vt28", kJournalListenerObjectiveVt28Rva);
        DetachProbeHook("JournalListener::Objective vt30", kJournalListenerObjectiveVt30Rva);
        DetachProbeHook("JournalListener::Route vt28", kJournalListenerRouteVt28Rva);
        DetachProbeHook("JournalListener::Route vt30", kJournalListenerRouteVt30Rva);
        DetachProbeHook("JournalListener::RouteAlt vt28", kJournalListenerRouteAltVt28Rva);
        DetachProbeHook("JournalListener::RouteAlt vt30", kJournalListenerRouteAltVt30Rva);
        DetachProbeHook("JournalListener::UI vt28", kJournalListenerUiVt28Rva);
        DetachProbeHook("JournalListener::UI vt30", kJournalListenerUiVt30Rva);
        DetachProbeHook("JournalListener::Tracker vt28", kJournalListenerTrackerVt28Rva);
        DetachProbeHook("FrameMappinPathWrapper", kFrameMappinPathWrapperRva);
        DetachProbeHook("FrameMappinPathCore", kFrameMappinPathCoreRva);
        DetachProbeHook("SetSelectedMappinWrapper", kSetSelectedMappinWrapperRva);
        DetachProbeHook("SetSelectedMappinByIdWrapper", kSetSelectedMappinByIdWrapperRva);
        DetachProbeHook("SetSelectedMappinByPositionWrapper", kSetSelectedMappinByPositionWrapperRva);
        DetachProbeHook("SetSelectedMappinCore", kSetSelectedMappinCoreRva);
        DetachProbeHook("TrackCustomPositionMappinWrapper", kTrackCustomPositionMappinWrapperRva);
        DetachProbeHook("TrackCustomPositionMappinCore", kTrackCustomPositionMappinCoreRva);
        DetachProbeHook("UntrackCustomPositionMappinWrapper", kUntrackCustomPositionMappinWrapperRva);
        DetachProbeHook("TrackMappinWrapper", kTrackMappinWrapperRva);
        DetachProbeHook("TrackMappinCore", kTrackMappinCoreRva);
        DetachProbeHook("SetMappinTrackingAlternativeWrapper", kSetMappinTrackingAlternativeWrapperRva);
        DetachAbsoluteHook("MappinSystem::UpdateCustomPosition slot1f0", gMappinUpdateCustomPositionTarget);
        DetachAbsoluteHook("MappinSystem::SetTracked slot220", gMappinSetTrackedTarget);
        DetachAbsoluteHook("MappinSystem::ClearTracked slot228", gMappinClearTrackedTarget);
        DetachAbsoluteHook("MappinSystem::FramePath slot280", gMappinFramePathTarget);
        DetachAbsoluteHook("MappinSystem::PositionQuery slot2e0", gMappinPositionQueryTarget);
        DetachAbsoluteHook("MappinSystem::CreateCustomPosition slot2f0", gMappinCreateCustomPositionTarget);
        DetachAbsoluteHook("MappinSystem::ReleaseQuery slot368", gMappinReleaseQueryTarget);
        DetachAbsoluteHook("MappinSystem::ActiveRoute slot3a8", gMappinActiveRouteTarget);
    }

    return true;
}

RED4EXT_C_EXPORT void RED4EXT_CALL Query(RED4ext::v1::PluginInfo* aInfo)
{
    aInfo->name = L"EdgeWeightGPS";
    aInfo->author = L"salt+Codex";
    aInfo->version = RED4ext::v1::SemVer{0, 1, 0, {0, 0}};
    aInfo->runtime = RED4ext::v1::FileVer{3, 0, 80, 51928};
    aInfo->sdk = RED4ext::v1::SemVer{0, 5, 0, {0, 0}};
}

RED4EXT_C_EXPORT uint32_t RED4EXT_CALL Supports()
{
    return RED4EXT_API_VERSION_1_COMPAT_0;
}
