// Copyright (C) 2026 CoelhoFZ
// SPDX-License-Identifier: GPL-3.0-only
// This file is part of unlocker-CoelhoFZ (GPLv3, see LICENSE).

// dllmain.cpp - winmm.dll proxy.
//
// The UWP/MSIX game imports winmm legitimately; a fake winmm.dll in the
// app directory wins the DLL search (classic DLL hijack). The proxy:
//   1. Loads the REAL winmm from System32 (without it audio breaks).
//   2. Chains ALL winmm calls to the real one (stubs generated by
//      gen_winmm_stubs.py - 180 exports with the real winmm ordinals).
//   3. Loads the GDK shim (dev/release) or runs the 2-stage packer
//      (harden): stub -> encrypted BLOB1 mapper -> encrypted BLOB2 payload.
//      The final winmm.dll contains no readable PE and no payload key.
//   4. Only in the TARGET process (Minecraft Bedrock): any other exe in the
//      folder gets just the transparent proxy and nothing else
//      (common/target_process.h).
//
// TODO(RE): the original does the anti-tamper check (constants 0x2b992ddfa232/
// 233, cmove + decoy not rax) and hosts the CLR (.NET UnlockCoelhoFZ.Main:Init
// via mscoree.dll/reflection) - out of scope for this initial sketch.

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <shellapi.h>

#include <cstdio>
#include <cstring>
#include <cwchar>

#include "winmm_stubs.h"
#include "enc_strings.h"
#include "donate_strings.h"
#include "stub_loader.h"
#include "target_process.h"
#include "apihash.h"

namespace {

// Encrypted shim name (the plaintext "unlock-CoelhoFZ.dll" does not exist in
// the binary). Persistent buffer: LoadLibraryA copies the path internally.
const char* ShimName() {
    static char buf[32];
    if (buf[0] == '\0')
        std::strcpy(buf, enc::dec(enc::kShimName, enc::kShimNameLen, 0x5A));
    return buf;
}

HMODULE g_realWinmm = nullptr;
HMODULE g_shim = nullptr;

bool LoadRealWinmm() {
    // Loads the legit winmm by the absolute System32 path (avoids
    // falling back to the proxy itself through the DLL search).
    wchar_t sys_dir[MAX_PATH];
    if (GetSystemDirectoryW(sys_dir, MAX_PATH) == 0) return false;
    wchar_t real_path[MAX_PATH];
    std::wcscpy(real_path, sys_dir);
    std::wcscat(real_path, L"\\winmm.dll");
    g_realWinmm = LoadLibraryW(real_path);
    return g_realWinmm != nullptr;
}

bool LoadShim(HINSTANCE hinst) {
    // GDK shim next to the proxy.
    char dir[MAX_PATH];
    if (!GetModuleFileNameA(hinst, dir, MAX_PATH)) return false;
    char* slash = std::strrchr(dir, '\\');
    if (slash == nullptr) return false;
    std::strcpy(slash + 1, ShimName());
    g_shim = LoadLibraryA(dir);
    return g_shim != nullptr;
}

// ============================================================================
// Donation popup (once, only on the 1st run of the unlock). The strings live
// encrypted in donate_strings.h (generated by tools/gen_donate.py) - the
// plaintext does not exist in the binary. Runs on a separate thread: DllMain
// must not hold the loader lock with MessageBox/ShellExecute/Reg.
// ============================================================================

// Decrypts each string (UTF-16) once and keeps it in a static buffer.
// -fno-threadsafe-statics: no guard - safe because only the donation thread uses
// it.
// 8 supported languages (same set as menu.ps1: pt/en/es/fr/zh/hi/ar/ru).
// Order == order of the arrays in donate_strings.h (gen_donate.py).
enum Lang { L_PT = 0, L_EN, L_ES, L_FR, L_ZH, L_HI, L_AR, L_RU };

// Language by the primary LANGID (robust across sub-languages: pt-BR/pt-PT,
// etc.). The Yes/No buttons of the MessageBox are localized automatically by
// Windows - we do not need to translate them.
Lang DetectLang() {
    switch (PRIMARYLANGID(apihash::GetUserDefaultUILanguage()())) {
        case LANG_PORTUGUESE: return L_PT;  // 0x16
        case LANG_SPANISH:    return L_ES;  // 0x0a
        case LANG_FRENCH:     return L_FR;  // 0x0c
        case LANG_CHINESE:    return L_ZH;  // 0x04
        case LANG_HINDI:      return L_HI;  // 0x39
        case LANG_ARABIC:     return L_AR;  // 0x01
        case LANG_RUSSIAN:    return L_RU;  // 0x19
        case LANG_ENGLISH:    return L_EN;  // 0x09
        default:              return L_EN;
    }
}

struct LangText {
    const uint16_t* msg;
    size_t msgLen;
    const uint16_t* title;
    size_t titleLen;
};

const LangText& LangStrings(Lang l) {
    static const LangText T[8] = {
        {donate::kMsg_pt, donate::kMsg_ptLen, donate::kTitle_pt, donate::kTitle_ptLen},
        {donate::kMsg_en, donate::kMsg_enLen, donate::kTitle_en, donate::kTitle_enLen},
        {donate::kMsg_es, donate::kMsg_esLen, donate::kTitle_es, donate::kTitle_esLen},
        {donate::kMsg_fr, donate::kMsg_frLen, donate::kTitle_fr, donate::kTitle_frLen},
        {donate::kMsg_zh, donate::kMsg_zhLen, donate::kTitle_zh, donate::kTitle_zhLen},
        {donate::kMsg_hi, donate::kMsg_hiLen, donate::kTitle_hi, donate::kTitle_hiLen},
        {donate::kMsg_ar, donate::kMsg_arLen, donate::kTitle_ar, donate::kTitle_arLen},
        {donate::kMsg_ru, donate::kMsg_ruLen, donate::kTitle_ru, donate::kTitle_ruLen},
    };
    return T[l];
}

const wchar_t* DMsg() {
    static wchar_t buf[512];
    const LangText& t = LangStrings(DetectLang());
    donate::wdec(t.msg, t.msgLen, 0x5A, buf);
    return buf;
}
const wchar_t* DTitle() {
    static wchar_t buf[64];
    const LangText& t = LangStrings(DetectLang());
    donate::wdec(t.title, t.titleLen, 0x5A, buf);
    return buf;
}
const wchar_t* DPixMsg() {
    static wchar_t buf[256];
    if (buf[0] == L'\0') donate::wdec(donate::kPixMsg, donate::kPixMsgLen, 0x5A, buf);
    return buf;
}
const wchar_t* DPixTitle() {
    static wchar_t buf[64];
    if (buf[0] == L'\0') donate::wdec(donate::kPixTitle, donate::kPixTitleLen, 0x5A, buf);
    return buf;
}
const wchar_t* DUrl() {
    static wchar_t buf[128];
    if (buf[0] == L'\0') donate::wdec(donate::kUrl, donate::kUrlLen, 0x5A, buf);
    return buf;
}
const wchar_t* DRegPath() {
    static wchar_t buf[128];
    if (buf[0] == L'\0') donate::wdec(donate::kRegPath, donate::kRegPathLen, 0x5A, buf);
    return buf;
}
const wchar_t* DRegValue() {
    static wchar_t buf[64];
    if (buf[0] == L'\0') donate::wdec(donate::kRegValue, donate::kRegValueLen, 0x5A, buf);
    return buf;
}

// Defense in depth: if ANY PEB resolve fails (apihash::* returns
// nullptr), calling nullptr(...) would crash the thread. The popup is optional -
// if resolution fails, it simply does NOT show (the unlock does not depend on
// it).
// Checks all APIs used in the donation flow BEFORE any call.
bool DonationApisReady() {
    return apihash::MessageBoxW() && apihash::RegOpenKeyExW() &&
           apihash::RegQueryValueExW() && apihash::RegCreateKeyExW() &&
           apihash::RegSetValueExW() && apihash::RegCloseKey() &&
           apihash::GetLocaleInfoW() && apihash::GetUserDefaultUILanguage() &&
           apihash::GetKeyboardLayout() && apihash::ShellExecuteW() &&
           apihash::Sleep();
}

// Combines OFFLINE signals (no IP/internet) that the user is from Brazil.
// Any one true -> treat as Brazil (shows PIX instead of the link).
// Covers the common case of someone who switches "Country or region" to USA
// but keeps a BR keyboard/language/timezone.
bool IsBrazil() {
    // 1) Country/region = BR (ISO code, no localization).
    wchar_t ctry[16] = {0};
    if (apihash::GetLocaleInfoW()(LOCALE_USER_DEFAULT, LOCALE_SISO3166CTRYNAME, ctry, 16) > 0 &&
        ctry[0] == L'B' && ctry[1] == L'R' && ctry[2] == L'\0')
        return true;

    // 2) Display language = Portuguese (Brazil) - LANGID 0x0416.
    if (apihash::GetUserDefaultUILanguage()() == 0x0416) return true;

    // 3) Active keyboard = pt-BR (ABNT/ABNT2) - low word = 0x0416.
    if (LOWORD(apihash::GetKeyboardLayout()(0)) == 0x0416) return true;

    // 4) Timezone = Brasilia/Cuiaba. Reads the TimeZoneKeyName registry
    // value (WITHOUT localization - the StandardName from GetTimeZoneInformation
    // is localized and would break the comparison). BR keys: "E. South America
    // Standard Time" (Brasilia) and "Central Brazilian Standard Time" (Cuiaba).
    HKEY tk = nullptr;
    if (apihash::RegOpenKeyExW()(HKEY_LOCAL_MACHINE,
                      L"SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation",
                      0, KEY_QUERY_VALUE, &tk) == ERROR_SUCCESS) {
        wchar_t tzk[128] = {0};
        DWORD tzksz = sizeof(tzk);
        bool br_tz =
            apihash::RegQueryValueExW()(tk, L"TimeZoneKeyName", nullptr, nullptr,
                             reinterpret_cast<BYTE*>(tzk), &tzksz) == ERROR_SUCCESS &&
            (wcsstr(tzk, L"South America Standard Time") != nullptr ||
             wcsstr(tzk, L"Brazilian Standard Time") != nullptr);
        apihash::RegCloseKey()(tk);
        if (br_tz) return true;
    }

    return false;
}

bool DonationAlreadyShown() {
    HKEY key;
    if (apihash::RegOpenKeyExW()(HKEY_CURRENT_USER, DRegPath(), 0, KEY_QUERY_VALUE, &key) !=
        ERROR_SUCCESS)
        return false;
    DWORD data = 0, size = sizeof(data), type = 0;
    LSTATUS st = apihash::RegQueryValueExW()(key, DRegValue(), nullptr, &type,
                                  reinterpret_cast<BYTE*>(&data), &size);
    apihash::RegCloseKey()(key);
    return st == ERROR_SUCCESS && data != 0;
}

void MarkDonationShown() {
    HKEY key;
    if (apihash::RegCreateKeyExW()(HKEY_CURRENT_USER, DRegPath(), 0, nullptr, 0,
                        KEY_SET_VALUE, nullptr, &key, nullptr) == ERROR_SUCCESS) {
        DWORD v = 1;
        apihash::RegSetValueExW()(key, DRegValue(), 0, REG_DWORD,
                       reinterpret_cast<const BYTE*>(&v), sizeof(v));
        apihash::RegCloseKey()(key);
    }
}

void MaybeShowDonationPopup() {
    if (!DonationApisReady()) return;  // resolve failed: no crash, no show.
    if (DonationAlreadyShown()) return;
    MarkDonationShown();  // writes BEFORE showing: shows at most 1x.

    Lang lang = DetectLang();
    UINT flags = MB_YESNO | MB_ICONINFORMATION | MB_SETFOREGROUND | MB_TOPMOST;
    if (lang == L_AR) flags |= MB_RIGHT | MB_RTLREADING;  // RTL for arabic

    int r = apihash::MessageBoxW()(nullptr, DMsg(), DTitle(), flags);
    if (r != IDYES) return;

    if (IsBrazil()) {
        // Brazil: only the PIX key (OK button), does not open the link.
        apihash::MessageBoxW()(nullptr, DPixMsg(), DPixTitle(),
                    MB_OK | MB_ICONINFORMATION | MB_SETFOREGROUND | MB_TOPMOST);
    } else {
        apihash::ShellExecuteW()(nullptr, L"open", DUrl(), nullptr, nullptr, SW_SHOWNORMAL);
    }
}

DWORD WINAPI DonationThread(LPVOID) {
    // Small wait for the game to finish loading before opening the popup.
    // If Sleep did not resolve (PEB walk failed), do not call nullptr: exit
    // straight away.
    if (!apihash::Sleep()) return 0;
    apihash::Sleep()(2000);
    MaybeShowDonationPopup();
    return 0;
}

}  // namespace

BOOL WINAPI DllMain(HINSTANCE hinst, DWORD reason, LPVOID) {
    if (reason != DLL_PROCESS_ATTACH) return TRUE;
    DisableThreadLibraryCalls(hinst);
    // Pins the module in the process: in this install (Minecraft Launcher) the
    // game loads and RELEASES winmm. Without the pin, the popup thread and the
    // forward stubs execute on unmapped memory (WER "WINMM.dll_unloaded",
    // 0xc0000005 at the offset right after the DonationThread Sleep). A pinned
    // module cannot be unloaded by FreeLibrary - the memory stays valid until
    // the end of the process.
    {
        HMODULE self = nullptr;
        GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_PIN |
                               GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS,
                           reinterpret_cast<LPCWSTR>(&DllMain), &self);
    }
    // real winmm first (the forwarding stubs depend on it), then it
    // resolves the 180 exports and loads the shim.
    if (!LoadRealWinmm()) {
        return TRUE;  // no real winmm: inert proxy, game goes on without audio
    }
    if (!ResolveAllWinmmStubs(g_realWinmm)) {
        // Some GetProcAddress failed -> the matching trampoline would jump
        // to nullptr. Does not abort the load (the game may never call that
        // function).
#if !defined(HARDENED)
        OutputDebugStringA("[unlock-CoelhoFZ] winmm: some exports did not "
                           "resolve in the real winmm\n");
#endif
    }
    // TARGET PROCESS GATE: any exe that loads this winmm.dll gets the
    // transparent proxy above (audio/timers on the real winmm) and STOPS HERE.
    // The payload only runs in Minecraft Bedrock (see common/target_process.h):
    // in a third-party game/folder there is no target, so it does not load
    // xgameruntime, does not patch any export, does not popup and does not
    // expose the binary.
    // When unsure the verdict is false: a strange process goes on with forward
    // only.
    if (!targetproc::IsMinecraftTarget()) {
#if !defined(HARDENED)
        OutputDebugStringA("[unlock-CoelhoFZ] winmm: process is not Minecraft "
                           "Bedrock - payload not loaded (transparent proxy)\n");
#endif
        return TRUE;
    }

#if defined(HARDENED)
    // harden: the shim does NOT exist as a file. 2-stage packer:
    // 1) this stub (proxy/stub_loader.cpp) finds BLOB1 (mapper) in the overlay,
    //    decrypts it with the masked PER-BUILD key and maps it via miniloader;
    // 2) the mapper DllMain (proxy/mapper_dllmain.cpp) decrypts and maps
    //    BLOB2 (payload) - which installs the hook. No readable PE on disk.
    if (!StubLoadAndRunPayload(hinst)) return TRUE;
#else
    if (!LoadShim(hinst)) return TRUE;
#endif

    // Donation popup (1x): separate thread so as not to hold the loader lock.
    // Guards CreateThread/CloseHandle against nullptr (PEB resolve failed).
    if (auto create = apihash::CreateThread()) {
        if (HANDLE h = create(nullptr, 0, DonationThread, nullptr, 0, nullptr)) {
            if (auto close = apihash::CloseHandle()) close(h);
        }
    }
    return TRUE;
}
