// GetLocaleInfoEx
#define _WIN32_WINNT 0x0600

#include "lang.h"

#define WIN32_NO_STATUS
#include <windows.h>
#undef WIN32_NO_STATUS

#include <winternl.h>
#include <ntstatus.h>

#include "avs/game.h"
#include "games/iidx/iidx.h"
#include "games/gitadora/gitadora.h"
#include "games/sdvx/sdvx.h"
#include "util/detour.h"
#include "util/logging.h"
#include "util/utils.h"

// ANSI/OEM Japanese; Japanese (Shift-JIS)
constexpr UINT CODEPAGE_SHIFT_JIS = 932;

static decltype(GetACP) *GetACP_orig = nullptr;
static decltype(GetOEMCP) *GetOEMCP_orig = nullptr;
static decltype(MultiByteToWideChar) *MultiByteToWideChar_orig = nullptr;
static decltype(GetLocaleInfoEx) *GetLocaleInfoEx_orig = nullptr;

#ifdef SPICE64
static decltype(GetSystemDefaultLCID) *GetSystemDefaultLCID_orig = nullptr;
static decltype(IsDBCSLeadByte) *IsDBCSLeadByte_orig = nullptr;
static decltype(WideCharToMultiByte) *WideCharToMultiByte_orig = nullptr;
static decltype(GetLocaleInfoA) *GetLocaleInfoA_orig = nullptr;
static decltype(GetThreadLocale) *GetThreadLocale_orig = nullptr;
#endif

static NTSTATUS NTAPI RtlMultiByteToUnicodeN_hook(
        PWCH UnicodeString,
        ULONG MaxBytesInUnicodeString,
        PULONG BytesInUnicodeString,
        const CHAR *MultiByteString,
        ULONG BytesInMultiByteString)
{
    // try to convert
    auto wc_num = MultiByteToWideChar(
            CODEPAGE_SHIFT_JIS,
            0,
            MultiByteString,
            static_cast<int>(BytesInMultiByteString),
            UnicodeString,
            static_cast<int>(MaxBytesInUnicodeString)
    );

    // error handling
    if (!wc_num) {
        auto error = GetLastError();

        switch (error) {
            case ERROR_INSUFFICIENT_BUFFER:
                return STATUS_BUFFER_TOO_SMALL;

            case ERROR_INVALID_PARAMETER:
            case ERROR_INVALID_FLAGS:
                return STATUS_INVALID_PARAMETER;

            case ERROR_NO_UNICODE_TRANSLATION:
                return STATUS_UNMAPPABLE_CHARACTER;

            default:
                return STATUS_UNSUCCESSFUL;
        }
    }

    // set byte count
    if (BytesInUnicodeString) {
        *BytesInUnicodeString = 2 * static_cast<UINT>(wc_num);
    }

    // return success
    return STATUS_SUCCESS;
}

static UINT WINAPI GetACP_hook() {
    return CODEPAGE_SHIFT_JIS;
}

static UINT WINAPI GetOEMCP_hook() {
    return CODEPAGE_SHIFT_JIS;
}

#ifdef SPICE64

static LCID WINAPI GetSystemDefaultLCID_hook() {
    // ja-JP per https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c
    // this is passed to LCMapStringA for kana conversions (e.g., for subscreen song search)
    return 0x411;
}

static LCID WINAPI GetThreadLocale_hook() {
    return 0x411;
}

#endif

static int WINAPI MultiByteToWideChar_hook(
        UINT CodePage,
        DWORD dwFlags,
        LPCCH lpMultiByteStr,
        int cbMultiByte,
        LPWSTR lpWideCharStr,
        int cchWideChar)
{
    switch (CodePage) {
        case CP_ACP:
        case CP_THREAD_ACP:

            // this fixes pop'n music's mojibake issue with the system locale not set to Japanese
            SetThreadLocale(MAKELANGID(LANG_JAPANESE, SUBLANG_JAPANESE_JAPAN));

            CodePage = CODEPAGE_SHIFT_JIS;

            break;

        default:
            break;
    }

    return MultiByteToWideChar_orig(
            CodePage,
            dwFlags,
            lpMultiByteStr,
            cbMultiByte,
            lpWideCharStr,
            cchWideChar);
}

static int WINAPI GetLocaleInfoEx_hook (
        LPCWSTR lpLocaleName,
        LCTYPE LCType,
        LPWSTR lpLCData,
        int cchData)
{

#if 0
    if (lpLocaleName == LOCALE_NAME_INVARIANT) {
        log_misc("hooks::lang", "GetLocaleInfoEx_hook hit (LOCALE_NAME_INVARIANT), {}, {}", LCType, cchData);
    } else if (lpLocaleName == LOCALE_NAME_SYSTEM_DEFAULT) {
        log_misc("hooks::lang", "GetLocaleInfoEx_hook hit (LOCALE_NAME_SYSTEM_DEFAULT), {}, {}", LCType, cchData);
    } else if (lpLocaleName == LOCALE_NAME_USER_DEFAULT) {
        log_misc("hooks::lang", "GetLocaleInfoEx_hook hit (LOCALE_NAME_USER_DEFAULT), {}, {}", LCType, cchData);
    } else {
        log_misc("hooks::lang", "GetLocaleInfoEx_hook hit ({}), {}, {}", ws2s(lpLocaleName), LCType, cchData);
    }
#endif

    if (lpLocaleName == LOCALE_NAME_USER_DEFAULT &&
        LCType == LOCALE_SISO639LANGNAME &&
        lpLCData != NULL &&
        cchData >= 3) {
        log_misc("hooks::lang",
                 "GetLocaleInfoEx_hook hit (LOCALE_NAME_USER_DEFAULT / LOCALE_SISO639LANGNAME), return `ja`");
        wcscpy(lpLCData, L"ja");
        return 3;
    }

    if (lpLocaleName == LOCALE_NAME_USER_DEFAULT &&
        LCType == LOCALE_SISO3166CTRYNAME &&
        lpLCData != NULL &&
        cchData >= 3) {
        log_misc("hooks::lang",
                 "GetLocaleInfoEx_hook hit (LOCALE_NAME_USER_DEFAULT / LOCALE_SISO3166CTRYNAME), return `JP`");
        wcscpy(lpLCData, L"JP");
        return 3;
    }

    return GetLocaleInfoEx_orig(lpLocaleName, LCType, lpLCData, cchData);
}

#ifdef SPICE64

static BOOL WINAPI IsDBCSLeadByte_hook (
    BYTE TestChar
    )
{
    return IsDBCSLeadByteEx(CODEPAGE_SHIFT_JIS, TestChar);
}

static
int
WINAPI
WideCharToMultiByte_hook(
    UINT CodePage,
    DWORD dwFlags,
    LPCWCH lpWideCharStr,
    int cchWideChar,
    LPSTR lpMultiByteStr,
    int cbMultiByte,
    LPCCH lpDefaultChar,
    LPBOOL lpUsedDefaultChar
    )
{
    switch (CodePage) {
        case CP_ACP:
        case CP_THREAD_ACP:
            SetThreadLocale(MAKELANGID(LANG_JAPANESE, SUBLANG_JAPANESE_JAPAN));
            CodePage = CODEPAGE_SHIFT_JIS;
            break;

        default:
            break;
    }
    return WideCharToMultiByte_orig(
        CodePage,
        dwFlags,
        lpWideCharStr,
        cchWideChar,
        lpMultiByteStr,
        cbMultiByte,
        lpDefaultChar,
        lpUsedDefaultChar);
}

int
WINAPI
GetLocaleInfoA_hook(
    LCID Locale,
    LCTYPE LCType,
    LPSTR lpLCData,
    int cchData) {

    if (LCType == LOCALE_SISO639LANGNAME && lpLCData != NULL && cchData >= 3) {
        log_misc("hooks::lang", "GetLocaleInfoA_hook hit ({:#x}, LOCALE_SISO639LANGNAME), return `ja`", Locale);
        strcpy(lpLCData, "ja");
        return 3;
    }

    if (LCType == LOCALE_SISO3166CTRYNAME && lpLCData != NULL && cchData >= 3) {
        log_misc("hooks::lang",
                 "GetLocaleInfoA_hook hit ({:#x}, LOCALE_SISO3166CTRYNAME), return `JP`", Locale);
        strcpy(lpLCData, "JP");
        return 3;
    }
    
    log_misc("hooks::lang", "GetLocaleInfoA_hook hit, {:#x}, {:#x}", Locale, LCType);
    return GetLocaleInfoA_orig(Locale, LCType, lpLCData, cchData);
}

#endif

void hooks::lang::early_init() {
    log_info("hooks::lang", "early initialization");

    // hooking these two functions fixes the jubeat mojibake
    detour::trampoline_try("kernel32.dll", "GetACP", GetACP_hook, &GetACP_orig);
    detour::trampoline_try("kernel32.dll", "GetOEMCP", GetOEMCP_hook, &GetOEMCP_orig);

#ifdef SPICE64 // SDVX5+ specific code
    if (games::sdvx::is_valkyrie_model()) {
        log_info("hooks::lang", "hooking GetSystemDefaultLCID");
        detour::trampoline_try(
            "kernel32.dll",
            "GetSystemDefaultLCID",
            GetSystemDefaultLCID_hook,
            &GetSystemDefaultLCID_orig);
    }
#endif

    if (avs::game::is_model("XIF")) {
        log_info("hooks::lang", "hooking GetLocaleInfoEx");
        detour::trampoline_try(
            "kernel32.dll",
            "GetLocaleInfoEx",
            GetLocaleInfoEx_hook,
            &GetLocaleInfoEx_orig);
    }

#ifdef SPICE64

    if (avs::game::is_model("UJK")) {
        // CCJ build of Unity calls GetThreadLocale,
        // and then GetLocaleInfoA to figure out the locale; eventually this is
        // used to figure out the decimal separator, which is then used in parsing
        // Double values - which fails carding in if it isn't "."
        log_info("hooks::lang", "hooking GetThreadLocale");
        detour::trampoline_try(
            "kernel32.dll",
            "GetThreadLocale",
            GetThreadLocale_hook,
            &GetThreadLocale_orig);

        log_info("hooks::lang", "hooking GetLocaleInfoA");
        detour::trampoline_try(
            "kernel32.dll",
            "GetLocaleInfoA",
            GetLocaleInfoA_hook,
            &GetLocaleInfoA_orig);
    }

    // for TDJ subscreen search keyboard
    if (avs::game::is_model("LDJ") && games::iidx::TDJ_MODE) {
        log_info("hooks::lang", "hooking IsDBCSLeadByte");
        detour::trampoline_try(
            "kernel32.dll",
            "IsDBCSLeadByte",
            IsDBCSLeadByte_hook,
            &IsDBCSLeadByte_orig);
    }

    if (games::gitadora::is_arena_model()) {
        log_info("hooks::lang", "hooking WideCharToMultiByte");
        detour::trampoline_try(
            "kernel32.dll",
            "WideCharToMultiByte",
            WideCharToMultiByte_hook,
            &WideCharToMultiByte_orig);
    }
#endif

}

void hooks::lang::init() {
    log_info("hooks::lang", "initializing");

    detour::iat_try("RtlMultiByteToUnicodeN", RtlMultiByteToUnicodeN_hook, nullptr, "ntdll.dll");

    MultiByteToWideChar_orig = detour::iat_try(
            "MultiByteToWideChar",
            MultiByteToWideChar_hook,
            nullptr,
            "kernel32.dll");
}

bool hooks::lang::is_native_shiftjis() {
    return GetACP() == CODEPAGE_SHIFT_JIS;
}
