// SPDX-License-Identifier: GPL-3.0-or-later
// MoonWell compatibility module for WarcraftXL.
//
// The stock 3.3.5a executable stays untouched on disk. These narrowly scoped
// changes are verified and applied to the process image during WarcraftXL's
// boot phase, before GlueXML is loaded.

#include "core/Logger.hpp"
#include "core/Mem.hpp"
#include "runtime/LuaBindings.hpp"
#include "runtime/ModuleInstall.hpp"

#include <windows.h>

#include <array>
#include <cstddef>
#include <cstdint>
#include <cstring>

namespace moonwell
{
    namespace
    {
        constexpr uint32_t kTraitorFlag = 0x40000000u;
        bool g_loginCharacterIsTraitor = false;

        int __cdecl SetLoginCharacterFlags(void* state)
        {
            uint32_t flags = 0;
            if (state && wxl::runtime::lua::IsNumber(state, 1))
                flags = static_cast<uint32_t>(wxl::runtime::lua::ToNumber(state, 1));

            g_loginCharacterIsTraitor = (flags & kTraitorFlag) != 0;
            wxl::runtime::lua::PushBoolean(state, g_loginCharacterIsTraitor ? 1 : 0);
            return 1;
        }

        int __cdecl IsTraitor(void* state)
        {
            wxl::runtime::lua::PushBoolean(state, g_loginCharacterIsTraitor ? 1 : 0);
            return 1;
        }

        struct Patch
        {
            const char* name;
            uintptr_t address;
            const uint8_t* expected;
            const uint8_t* replacement;
            size_t size;
        };

        template <size_t N>
        bool Apply(const char* name, uintptr_t address,
                   const std::array<uint8_t, N>& expected,
                   const std::array<uint8_t, N>& replacement)
        {
            const auto* current = reinterpret_cast<const uint8_t*>(address);
            if (std::memcmp(current, replacement.data(), N) == 0)
                return true;
            if (std::memcmp(current, expected.data(), N) != 0)
            {
                WLOG_ERROR("moonwell: '%s' byte mismatch at %p; stock build 12340 required",
                           name, reinterpret_cast<void*>(address));
                return false;
            }
            if (!wxl::core::mem::Patch(reinterpret_cast<void*>(address), replacement.data(), N))
            {
                WLOG_ERROR("moonwell: '%s' patch failed at %p", name,
                           reinterpret_cast<void*>(address));
                return false;
            }
            WLOG_INFO("moonwell: applied '%s' at %p", name, reinterpret_cast<void*>(address));
            return true;
        }

        bool InstallGlueUnlock()
        {
            // Same stock-client checks formerly changed in the distributed Wow.exe.
            // They allow custom GlueXML while keeping WarcraftXL's callback validator intact.
            bool ok = true;
            ok &= Apply("glue archive override", 0x005F4DBF,
                        std::array<uint8_t, 1>{0x74}, std::array<uint8_t, 1>{0xEB});
            ok &= Apply("glue callback gate", 0x00816625,
                        std::array<uint8_t, 1>{0x75}, std::array<uint8_t, 1>{0xEB});
            ok &= Apply("glue callback result A", 0x0081663F,
                        std::array<uint8_t, 1>{0x01}, std::array<uint8_t, 1>{0x03});
            ok &= Apply("glue callback result B", 0x00816695,
                        std::array<uint8_t, 1>{0x01}, std::array<uint8_t, 1>{0x03});
            ok &= Apply("glue callback compare", 0x00816746,
                        std::array<uint8_t, 1>{0x7F}, std::array<uint8_t, 1>{0xEB});
            ok &= Apply("glue callback return", 0x0081675F,
                        std::array<uint8_t, 7>{0x83,0xC0,0x03,0x5E,0x8B,0xE5,0x5D},
                        std::array<uint8_t, 7>{0xB8,0x03,0x00,0x00,0x00,0xEB,0xED});
            return ok;
        }

        bool InstallCharacterModePacket()
        {
            // CreateCharacter(name, modeId): capture Lua argument 2 and place it in
            // the final CMSG_CHAR_CREATE byte that is zero in the stock client.
            constexpr std::array<uint8_t, 64> expectedCreate = {
                0x55,0x8B,0xEC,0x56,0x8B,0x75,0x08,0x6A,0x01,0x56,0xE8,0xF1,0xD2,0x36,0x00,
                0x83,0xC4,0x08,0x85,0xC0,0x74,0x0D,0x6A,0x00,0x6A,0x01,0x56,0xE8,0x60,0xD4,
                0x36,0x00,0x83,0xC4,0x0C,0x6A,0x00,0x6A,0x01,0x56,0xE8,0x53,0xD4,0x36,0x00,
                0x50,0xE8,0xED,0xF6,0xFF,0xFF,0x83,0xC4,0x10,0x33,0xC0,0x5E,0x5D,0xC3,
                0xCC,0xCC,0xCC,0xCC,0xCC
            };
            constexpr std::array<uint8_t, 64> replacementCreate = {
                0x55,0x8B,0xEC,0x56,0x8B,0x75,0x08,0x6A,0x02,0x56,0xE8,0xC1,0xD3,0x36,0x00,
                0xDB,0x1C,0x24,0x58,0x83,0xC4,0x04,0xA2,0x21,0x42,0xAC,0x00,0x6A,0x00,0x6A,
                0x01,0x56,0xE8,0x5B,0xD4,0x36,0x00,0x50,0xE8,0xF5,0xF6,0xFF,0xFF,0x83,0xC4,
                0x10,0x33,0xC0,0x5E,0x5D,0xC3,0x88,0x55,0xF0,0x8A,0x15,0x21,0x42,0xAC,
                0x00,0x88,0x55,0xF4,0xC3
            };
            constexpr std::array<uint8_t, 7> expectedPacket =
                {0x88,0x55,0xF0,0xC6,0x45,0xF4,0x00};
            constexpr std::array<uint8_t, 7> replacementPacket =
                {0xE8,0x5F,0x08,0x00,0x00,0x90,0x90};

            return Apply("CreateCharacter mode argument", 0x004E0C60,
                         expectedCreate, replacementCreate)
                && Apply("CMSG_CHAR_CREATE mode byte", 0x004E042F,
                         expectedPacket, replacementPacket);
        }

        bool InstallCharacterFlags()
        {
            // GetCharacterInfo(index) gains return value 11: the character flags
            // from CharacterInfo+0x170. The glue code uses bit 0x40000000.
            constexpr uintptr_t patchAddress = 0x004E332D;
            constexpr uintptr_t returnAddress = 0x004E3336;
            constexpr uintptr_t luaPushNumber = 0x0084E2A0;
            constexpr std::array<uint8_t, 5> expected = {0x83,0xC4,0x2C,0x5E,0xB8};

            if (*reinterpret_cast<const uint8_t*>(patchAddress) == 0xE9)
                return true;
            if (std::memcmp(reinterpret_cast<const void*>(patchAddress), expected.data(), expected.size()) != 0)
            {
                WLOG_ERROR("moonwell: GetCharacterInfo byte mismatch at %p",
                           reinterpret_cast<void*>(patchAddress));
                return false;
            }

            auto* cave = static_cast<uint8_t*>(VirtualAlloc(nullptr, 30, MEM_COMMIT | MEM_RESERVE,
                                                             PAGE_EXECUTE_READWRITE));
            if (!cave)
            {
                WLOG_ERROR("moonwell: cannot allocate GetCharacterInfo thunk (win32=%lu)", GetLastError());
                return false;
            }

            const uint8_t prefix[] = {
                0x83,0xEC,0x08,             // sub esp, 8
                0xDB,0x86,0x70,0x01,0x00,0x00, // fild dword ptr [esi+170h]
                0xDD,0x1C,0x24,             // fstp qword ptr [esp]
                0x57,                       // push edi (lua_State*)
                0xE8,0,0,0,0,              // call lua_pushnumber
                0x83,0xC4,0x38,             // cleanup own args + stolen add esp,2Ch
                0x5E,                       // stolen pop esi
                0x6A,0x0B,0x58,             // eax = 11
                0xE9,0,0,0,0               // jump to stock epilogue
            };
            static_assert(sizeof(prefix) == 30);
            std::memcpy(cave, prefix, sizeof(prefix));

            const auto callRel = static_cast<int32_t>(luaPushNumber -
                (reinterpret_cast<uintptr_t>(cave) + 18));
            const auto backRel = static_cast<int32_t>(returnAddress -
                (reinterpret_cast<uintptr_t>(cave) + 30));
            std::memcpy(cave + 14, &callRel, sizeof(callRel));
            std::memcpy(cave + 26, &backRel, sizeof(backRel));
            FlushInstructionCache(GetCurrentProcess(), cave, 30);

            std::array<uint8_t, 5> jump{0xE9,0,0,0,0};
            const auto caveRel = static_cast<int32_t>(reinterpret_cast<uintptr_t>(cave) -
                (patchAddress + jump.size()));
            std::memcpy(jump.data() + 1, &caveRel, sizeof(caveRel));
            if (!wxl::core::mem::Patch(reinterpret_cast<void*>(patchAddress), jump.data(), jump.size()))
            {
                VirtualFree(cave, 0, MEM_RELEASE);
                WLOG_ERROR("moonwell: GetCharacterInfo jump patch failed");
                return false;
            }
            WLOG_INFO("moonwell: GetCharacterInfo now returns charFlags as value 11");
            return true;
        }

        void InstallBoot()
        {
            const bool glue = InstallGlueUnlock();
            const bool create = InstallCharacterModePacket();
            const bool flags = InstallCharacterFlags();
            if (!glue || !create || !flags)
                WLOG_ERROR("moonwell: compatibility module incomplete; see mismatches above");
            else
                WLOG_INFO("moonwell: compatibility module ready (stock Wow.exe remains untouched)");
            wxl::core::log::Flush();
        }

        DWORD WINAPI FlushRuntimeLog(LPVOID)
        {
            // Let the remainder of RunAll() and the core-ready line reach the
            // shared buffered logger, then make startup diagnostics durable.
            Sleep(1000);
            wxl::core::log::Flush();
            return 0;
        }

        void InstallRuntimeLogFlush()
        {
            HANDLE thread = CreateThread(nullptr, 0, &FlushRuntimeLog, nullptr, 0, nullptr);
            if (thread) CloseHandle(thread);
        }

        struct Registration
        {
            Registration()
            {
                wxl::runtime::lua::RegisterFunction(
                    "MoonWellSetLoginCharacterFlags", &SetLoginCharacterFlags);
                wxl::runtime::lua::RegisterFunction("MoonWellIsTraitor", &IsTraitor);
                wxl::runtime::modules::RegisterBoot("moonwell", &InstallBoot);
                wxl::runtime::modules::Register("moonwell-log-flush", &InstallRuntimeLogFlush);
            }
        } g_registration;
    }
}
