#include "RA_Interface.h"

#include <winhttp.h>
#include <cassert>
#include <stdexcept>
#include <string>

#ifndef CCONV
#define CCONV __cdecl
#endif

// Initialization
static const char*  (CCONV* _RA_IntegrationVersion)() = nullptr;
static const char*  (CCONV* _RA_HostName)() = nullptr;
static const char*  (CCONV* _RA_HostUrl)() = nullptr;
static int          (CCONV* _RA_InitI)(HWND hMainWnd, int nConsoleID, const char* sClientVer) = nullptr;
static int          (CCONV* _RA_InitOffline)(HWND hMainWnd, int nConsoleID, const char* sClientVer) = nullptr;
static int          (CCONV* _RA_InitClient)(HWND hMainWnd, const char* sClientName, const char* sClientVer) = nullptr;
static int          (CCONV* _RA_InitClientOffline)(HWND hMainWnd, const char* sClientName, const char* sClientVer) = nullptr;
static void         (CCONV* _RA_InstallSharedFunctions)(int(*)(), void(*)(), void(*)(), void(*)(), void(*)(char*), void(*)(), void(*)(const char*)) = nullptr;
static void         (CCONV* _RA_SetForceRepaint)(int bEnable) = nullptr;
static HMENU        (CCONV* _RA_CreatePopupMenu)() = nullptr;
static int          (CCONV* _RA_GetPopupMenuItems)(RA_MenuItem*) = nullptr;
static void         (CCONV* _RA_InvokeDialog)(LPARAM nID) = nullptr;
static void         (CCONV* _RA_SetUserAgentDetail)(const char* sDetail);
static void         (CCONV* _RA_AttemptLogin)(int bBlocking) = nullptr;
static int          (CCONV* _RA_SetConsoleID)(unsigned int nConsoleID) = nullptr;
static void         (CCONV* _RA_ClearMemoryBanks)() = nullptr;
static void         (CCONV* _RA_InstallMemoryBank)(int nBankID, RA_ReadMemoryFunc* pReader, RA_WriteMemoryFunc* pWriter, int nBankSize) = nullptr;
static void         (CCONV* _RA_InstallMemoryBankBlockReader)(int nBankID, RA_ReadMemoryBlockFunc* pReader) = nullptr;
static int          (CCONV* _RA_Shutdown)() = nullptr;
// Overlay
static int          (CCONV* _RA_IsOverlayFullyVisible)() = nullptr;
static void         (CCONV* _RA_SetPaused)(int bIsPaused) = nullptr;
static void         (CCONV* _RA_NavigateOverlay)(ControllerInput* pInput) = nullptr;
static void         (CCONV* _RA_UpdateHWnd)(HWND hMainHWND);
// Game Management
static unsigned int (CCONV* _RA_IdentifyRom)(const BYTE* pROM, unsigned int nROMSize) = nullptr;
static unsigned int (CCONV* _RA_IdentifyHash)(const char* sHash) = nullptr;
static void         (CCONV* _RA_ActivateGame)(unsigned int nGameId) = nullptr;
static int          (CCONV* _RA_OnLoadNewRom)(const BYTE* pROM, unsigned int nROMSize) = nullptr;
static int          (CCONV* _RA_ConfirmLoadNewRom)(int bQuitting) = nullptr;
// Runtime Functionality
static void         (CCONV* _RA_DoAchievementsFrame)() = nullptr;
static void         (CCONV* _RA_SuspendRepaint)() = nullptr;
static void         (CCONV* _RA_ResumeRepaint)() = nullptr;
static void         (CCONV* _RA_UpdateAppTitle)(const char* pMessage) = nullptr;
static const char*  (CCONV* _RA_UserName)() = nullptr;
static int          (CCONV* _RA_HardcoreModeIsActive)(void) = nullptr;
static int          (CCONV* _RA_WarnDisableHardcore)(const char* sActivity) = nullptr;
static void         (CCONV* _RA_OnReset)() = nullptr;
static void         (CCONV* _RA_OnSaveState)(const char* sFilename) = nullptr;
static void         (CCONV* _RA_OnLoadState)(const char* sFilename) = nullptr;
static int          (CCONV* _RA_CaptureState)(char* pBuffer, int nBufferSize) = nullptr;
static void         (CCONV* _RA_RestoreState)(const char* pBuffer) = nullptr;

static HINSTANCE g_hRADLL = nullptr;

void RA_AttemptLogin(int bBlocking)
{
    if (_RA_AttemptLogin != nullptr)
        _RA_AttemptLogin(bBlocking);
}

const char* RA_UserName(void)
{
    if (_RA_UserName != nullptr)
        return _RA_UserName();

    return "";
}

void RA_NavigateOverlay(ControllerInput* pInput)
{
    if (_RA_NavigateOverlay != nullptr)
        _RA_NavigateOverlay(pInput);
}

void RA_UpdateRenderOverlay(HDC hDC, ControllerInput* pInput, float fDeltaTime, RECT* prcSize, bool Full_Screen, bool Paused)
{
    if (_RA_NavigateOverlay != nullptr)
        _RA_NavigateOverlay(pInput);
}

int RA_IsOverlayFullyVisible(void)
{
    if (_RA_IsOverlayFullyVisible != nullptr)
        return _RA_IsOverlayFullyVisible();

    return 0;
}

void RA_UpdateHWnd(HWND hMainWnd)
{
    if (_RA_UpdateHWnd != nullptr)
        _RA_UpdateHWnd(hMainWnd);
}

unsigned int RA_IdentifyRom(BYTE* pROMData, unsigned int nROMSize)
{
    if (_RA_IdentifyRom != nullptr)
        return _RA_IdentifyRom(pROMData, nROMSize);

    return 0;
}

unsigned int RA_IdentifyHash(const char* sHash)
{
    if (_RA_IdentifyHash!= nullptr)
        return _RA_IdentifyHash(sHash);

    return 0;
}

void RA_ActivateGame(unsigned int nGameId)
{
    if (_RA_ActivateGame != nullptr)
        _RA_ActivateGame(nGameId);
}

void RA_OnLoadNewRom(BYTE* pROMData, unsigned int nROMSize)
{
    if (_RA_OnLoadNewRom != nullptr)
        _RA_OnLoadNewRom(pROMData, nROMSize);
}

void RA_ClearMemoryBanks(void)
{
    if (_RA_ClearMemoryBanks != nullptr)
        _RA_ClearMemoryBanks();
}

void RA_InstallMemoryBank(int nBankID, RA_ReadMemoryFunc pReader, RA_WriteMemoryFunc pWriter, int nBankSize)
{
    if (_RA_InstallMemoryBank != nullptr)
        _RA_InstallMemoryBank(nBankID, pReader, pWriter, nBankSize);
}

void RA_InstallMemoryBankBlockReader(int nBankID, RA_ReadMemoryBlockFunc pReader)
{
    if (_RA_InstallMemoryBankBlockReader != nullptr)
        _RA_InstallMemoryBankBlockReader(nBankID, pReader);
}

HMENU RA_CreatePopupMenu(void)
{
    return (_RA_CreatePopupMenu != nullptr) ? _RA_CreatePopupMenu() : nullptr;
}

int RA_GetPopupMenuItems(RA_MenuItem *pItems)
{
    return (_RA_GetPopupMenuItems != nullptr) ? _RA_GetPopupMenuItems(pItems) : 0;
}

void RA_UpdateAppTitle(const char* sCustomMsg)
{
    if (_RA_UpdateAppTitle != nullptr)
        _RA_UpdateAppTitle(sCustomMsg);
}

void RA_HandleHTTPResults(void)
{
}

int RA_ConfirmLoadNewRom(int bIsQuitting)
{
    return _RA_ConfirmLoadNewRom ? _RA_ConfirmLoadNewRom(bIsQuitting) : 1;
}

void RA_InvokeDialog(LPARAM nID)
{
    if (_RA_InvokeDialog != nullptr)
        _RA_InvokeDialog(nID);
}

void RA_SetPaused(bool bIsPaused)
{
    if (_RA_SetPaused != nullptr)
        _RA_SetPaused(bIsPaused);
}

void RA_OnLoadState(const char* sFilename)
{
    if (_RA_OnLoadState != nullptr)
        _RA_OnLoadState(sFilename);
}

void RA_OnSaveState(const char* sFilename)
{
    if (_RA_OnSaveState != nullptr)
        _RA_OnSaveState(sFilename);
}

int RA_CaptureState(char* pBuffer, int nBufferSize)
{
    if (_RA_CaptureState != nullptr)
        return _RA_CaptureState(pBuffer, nBufferSize);

    return 0;
}

void RA_RestoreState(const char* pBuffer)
{
    if (_RA_RestoreState != nullptr)
        _RA_RestoreState(pBuffer);
}

void RA_OnReset(void)
{
    if (_RA_OnReset != nullptr)
        _RA_OnReset();
}

void RA_DoAchievementsFrame(void)
{
    if (_RA_DoAchievementsFrame != nullptr)
        _RA_DoAchievementsFrame();
}

void RA_SetForceRepaint(int bEnable)
{
    if (_RA_SetForceRepaint != nullptr)
        _RA_SetForceRepaint(bEnable);
}

void RA_SuspendRepaint(void)
{
  if (_RA_SuspendRepaint != nullptr)
    _RA_SuspendRepaint();
}

void RA_ResumeRepaint(void)
{
  if (_RA_ResumeRepaint != nullptr)
    _RA_ResumeRepaint();
}

void RA_SetConsoleID(unsigned int nConsoleID)
{
    if (_RA_SetConsoleID != nullptr)
        _RA_SetConsoleID(nConsoleID);
}

int RA_HardcoreModeIsActive(void)
{
    return (_RA_HardcoreModeIsActive != nullptr) ? _RA_HardcoreModeIsActive() : 0;
}

int RA_WarnDisableHardcore(const char* sActivity)
{
    // If Hardcore mode not active, allow the activity.
    if (!RA_HardcoreModeIsActive())
        return 1;

    // DLL function will display a yes/no dialog. If the user chooses yes, the DLL will disable hardcore mode, and the activity can proceed.
    if (_RA_WarnDisableHardcore != nullptr)
        return _RA_WarnDisableHardcore(sActivity);

    // We cannot disable hardcore mode, so just warn the user and prevent the activity.
    std::string sMessage;
    sMessage = "You cannot " + std::string(sActivity) + " while Hardcore mode is active.";
    MessageBoxA(nullptr, sMessage.c_str(), "Warning", MB_OK | MB_ICONWARNING);
    return 0;
}

void RA_DisableHardcore()
{
    // passing nullptr to _RA_WarnDisableHardcore will just disable hardcore mode without prompting.
    if (_RA_WarnDisableHardcore != nullptr)
        _RA_WarnDisableHardcore(nullptr);
}

static size_t DownloadToFile(char* pData, size_t nDataSize, void* pUserData)
{
    FILE* file = (FILE*)pUserData;
    return fwrite(pData, 1, nDataSize, file);
}

typedef struct DownloadBuffer
{
    char* pBuffer;
    size_t nBufferSize;
    size_t nOffset;
} DownloadBuffer;

static size_t DownloadToBuffer(char* pData, size_t nDataSize, void* pUserData)
{
    DownloadBuffer* pBuffer = (DownloadBuffer*)pUserData;
    const size_t nRemaining = pBuffer->nBufferSize - pBuffer->nOffset;
    if (nDataSize > nRemaining)
        nDataSize = nRemaining;

    if (nDataSize > 0)
    {
        memcpy(pBuffer->pBuffer + pBuffer->nOffset, pData, nDataSize);
        pBuffer->nOffset += nDataSize;
    }

    return nDataSize;
}

typedef size_t (DownloadFunc)(char* pData, size_t nDataSize, void* pUserData);

static BOOL DoBlockingHttpCall(const char* sHostUrl, const char* sRequestedPage, const char* sPostData,
  DownloadFunc fnDownload, void* pDownloadUserData, DWORD* pBytesRead, DWORD* pStatusCode)
{
    BOOL bResults = FALSE, bSuccess = FALSE;
    HINTERNET hSession = nullptr, hConnect = nullptr, hRequest = nullptr;
    size_t nHostnameLen;

    WCHAR wBuffer[1024];
    size_t nTemp;
    DWORD nBytesAvailable = 0;
    DWORD nBytesToRead = 0;
    DWORD nBytesFetched = 0;
    (*pBytesRead) = 0;

    INTERNET_PORT nPort = INTERNET_DEFAULT_HTTP_PORT;
    const char* sHostName = sHostUrl;
    if (_strnicmp(sHostName, "http://", 7) == 0)
    {
        sHostName += 7;
    }
    else if (_strnicmp(sHostName, "https://", 8) == 0)
    {
        sHostName += 8;
        nPort = INTERNET_DEFAULT_HTTPS_PORT;
    }

    const char* sPort = strchr(sHostName, ':');
    if (sPort)
    {
        nHostnameLen = sPort - sHostName;
        nPort = atoi(sPort + 1);
    }
    else
    {
        nHostnameLen = strlen(sHostName);
    }

    // Use WinHttpOpen to obtain a session handle.
    hSession = WinHttpOpen(L"RetroAchievements Client Bootstrap",
        WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
        WINHTTP_NO_PROXY_NAME,
        WINHTTP_NO_PROXY_BYPASS, 0);

    // Specify an HTTP server.
    if (hSession == nullptr)
    {
        *pStatusCode = GetLastError();
    }
    else
    {
#if defined(_MSC_VER) && _MSC_VER >= 1400
        mbstowcs_s(&nTemp, wBuffer, sizeof(wBuffer) / sizeof(wBuffer[0]), sHostName, nHostnameLen);
#else
        nTemp = mbstowcs(wBuffer, sHostName, nHostnameLen);
#endif

        if (nTemp > 0)
        {
            hConnect = WinHttpConnect(hSession, wBuffer, nPort, 0);
        }

        // Create an HTTP Request handle.
        if (hConnect == nullptr)
        {
            *pStatusCode = GetLastError();
        }
        else
        {
#if defined(_MSC_VER) && _MSC_VER >= 1400
            mbstowcs_s(&nTemp, wBuffer, sizeof(wBuffer) / sizeof(wBuffer[0]), sRequestedPage, strlen(sRequestedPage) + 1);
#else
            nTemp = mbstowcs(wBuffer, sRequestedPage, strlen(sRequestedPage) + 1);
#endif

            hRequest = WinHttpOpenRequest(hConnect,
                sPostData ? L"POST" : L"GET",
                wBuffer,
                nullptr,
                WINHTTP_NO_REFERER,
                WINHTTP_DEFAULT_ACCEPT_TYPES,
                (nPort == INTERNET_DEFAULT_HTTPS_PORT) ? WINHTTP_FLAG_SECURE : 0);

            // Send a Request.
            if (hRequest == nullptr)
            {
                *pStatusCode = GetLastError();
            }
            else
            {
                if (sPostData)
                {
                    const size_t nPostDataLength = strlen(sPostData);
                    bResults = WinHttpSendRequest(hRequest,
                        L"Content-Type: application/x-www-form-urlencoded",
                        0, (LPVOID)sPostData, (DWORD)nPostDataLength, (DWORD)nPostDataLength, 0);
                }
                else
                {
                    bResults = WinHttpSendRequest(hRequest,
                        L"Content-Type: application/x-www-form-urlencoded",
                        0, WINHTTP_NO_REQUEST_DATA, 0, 0, 0);
                }

                if (!bResults || !WinHttpReceiveResponse(hRequest, nullptr))
                {
                    *pStatusCode = GetLastError();
                }
                else
                {
                    char buffer[16384];
                    DWORD dwSize = sizeof(DWORD);
                    WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, pStatusCode, &dwSize, WINHTTP_NO_HEADER_INDEX);

                    bSuccess = TRUE;
                    do
                    {
                        nBytesAvailable = 0;
                        WinHttpQueryDataAvailable(hRequest, &nBytesAvailable);
                        if (nBytesAvailable == 0)
                            break;

                        do
                        {
                            if (nBytesAvailable > sizeof(buffer))
                                nBytesToRead = sizeof(buffer);
                            else
                                nBytesToRead = nBytesAvailable;

                            nBytesFetched = 0;
                            if (WinHttpReadData(hRequest, buffer, nBytesToRead, &nBytesFetched))
                            {
                                size_t nBytesWritten = fnDownload(buffer, nBytesFetched, pDownloadUserData);
                                if (nBytesWritten < nBytesFetched)
                                {
                                    if (*pStatusCode == 200)
                                        *pStatusCode = 998;

                                    bSuccess = FALSE;
                                    break;
                                }

                                (*pBytesRead) += (DWORD)nBytesWritten;
                                nBytesAvailable -= nBytesFetched;
                            }
                            else
                            {
                                if (*pStatusCode == 200)
                                    *pStatusCode = GetLastError();

                                bSuccess = FALSE;
                                break;
                            }
                        } while (nBytesAvailable > 0);
                    } while (TRUE);
                }

                WinHttpCloseHandle(hRequest);
            }

            WinHttpCloseHandle(hConnect);
        }

        WinHttpCloseHandle(hSession);
    }

    return bSuccess;
}

static BOOL IsNetworkError(DWORD nStatusCode)
{
    switch (nStatusCode)
    {
        case 12002: // timeout
        case 12007: // dns lookup failed
        case 12017: // handle closed before request completed
        case 12019: // handle not initialized
        case 12028: // data not available at this time
        case 12029: // handshake failed
        case 12030: // connection aborted
        case 12031: // connection reset
        case 12032: // explicit request to retry
        case 12152: // response could not be parsed, corrupt?
        case 12163: // lost connection during request
            return TRUE;

        default:
            return FALSE;
    }
}

static BOOL DoBlockingHttpCallWithRetry(const char* sHostUrl, const char* sRequestedPage, const char* sPostData,
  char pBufferOut[], unsigned int nBufferOutSize, DWORD* pBytesRead, DWORD* pStatusCode)
{
    int nRetries = 4;
    do
    {
        DownloadBuffer downloadBuffer;
        memset(&downloadBuffer, 0, sizeof(downloadBuffer));
        downloadBuffer.pBuffer = pBufferOut;
        downloadBuffer.nBufferSize = nBufferOutSize;

        if (DoBlockingHttpCall(sHostUrl, sRequestedPage, sPostData, DownloadToBuffer, &downloadBuffer, pBytesRead, pStatusCode) != FALSE)
            return TRUE;

        if (!IsNetworkError(*pStatusCode))
            return FALSE;

        --nRetries;
    } while (nRetries);

    return FALSE;
}

static BOOL DoBlockingHttpCallWithRetry(const char* sHostUrl, const char* sRequestedPage, const char* sPostData,
  FILE* pFile, DWORD* pBytesRead, DWORD* pStatusCode)
{
  int nRetries = 4;
  do
  {
      fseek(pFile, 0, SEEK_SET);
      if (DoBlockingHttpCall(sHostUrl, sRequestedPage, sPostData, DownloadToFile, pFile, pBytesRead, pStatusCode) != FALSE)
        return TRUE;

      if (!IsNetworkError(*pStatusCode))
        return FALSE;

      --nRetries;
  } while (nRetries);

  return FALSE;
}

#ifndef RA_UTEST
static std::wstring GetIntegrationPath()
{
    wchar_t sBuffer[2048];
    DWORD iIndex = GetModuleFileNameW(0, sBuffer, 2048);
    while (iIndex > 0 && sBuffer[iIndex - 1] != '\\' && sBuffer[iIndex - 1] != '/')
        --iIndex;

#if defined(_MSC_VER) && _MSC_VER >= 1400
    wcscpy_s(&sBuffer[iIndex], sizeof(sBuffer)/sizeof(sBuffer[0]) - iIndex, L"RA_Integration.dll");
#else
    wcscpy(&sBuffer[iIndex], L"RA_Integration.dll");
#endif

    return std::wstring(sBuffer);
}
#endif

#if 0

static void FetchIntegrationFromWeb(char* sLatestVersionUrl, DWORD* pStatusCode)
{
    DWORD nBytesRead = 0;
    const wchar_t* sDownloadFilename = L"RA_Integration.download";
    const wchar_t* sFilename = L"RA_Integration.dll";
    const wchar_t* sOldFilename = L"RA_Integration.old";

#if defined(_MSC_VER) && _MSC_VER >= 1400
    FILE* pf;
    errno_t nErr = _wfopen_s(&pf, sDownloadFilename, L"wb");
#else
    FILE* pf = _wfopen(sDownloadFilename, L"wb");
#endif

    if (!pf)
    {
#if defined(_MSC_VER) && _MSC_VER >= 1400
        wchar_t sErrBuffer[2048];
        _wcserror_s(sErrBuffer, sizeof(sErrBuffer) / sizeof(sErrBuffer[0]), nErr);

        std::wstring sErrMsg = std::wstring(L"Unable to write ") + sOldFilename + L"\n" + sErrBuffer;
#else
        std::wstring sErrMsg = std::wstring(L"Unable to write ") + sOldFilename + L"\n" + _wcserror(errno);
#endif

        MessageBoxW(nullptr, sErrMsg.c_str(), L"Error", MB_OK | MB_ICONERROR);
        return;
    }

    char* pSplit = sLatestVersionUrl + 8; /* skip over protocol */
    while (*pSplit != '/')
    {
        if (!*pSplit)
        {
            *pStatusCode = 997;
            return;
        }
        ++pSplit;
    }
    *pSplit++ = '\0';

    if (DoBlockingHttpCallWithRetry(sLatestVersionUrl, pSplit, nullptr, pf, &nBytesRead, pStatusCode))
    {
        fclose(pf);

        /* wait up to one second for the DLL to actually be released - sometimes it's not immediate */
        for (int i = 0; i < 10; i++)
        {
            if (GetModuleHandleW(sFilename) == nullptr)
                break;

            Sleep(100);
        }

        // delete the last old dll if it's still present
        DeleteFileW(sOldFilename);

        // if there's a dll present, rename it
        if (GetFileAttributesW(sFilename) != INVALID_FILE_ATTRIBUTES &&
            !MoveFileW(sFilename, sOldFilename))
        {
            MessageBoxW(nullptr, L"Could not rename old dll", L"Error", MB_OK | MB_ICONERROR);
        }
        // rename the download to be the dll
        else if (!MoveFileW(sDownloadFilename, sFilename))
        {
            MessageBoxW(nullptr, L"Could not rename new dll", L"Error", MB_OK | MB_ICONERROR);
        }

        // delete the old dll
        DeleteFileW(sOldFilename);
    }
    else
    {
        fclose(pf);
    }
}

#endif

//Returns the last Win32 error, in string format. Returns an empty string if there is no error.
static std::string GetLastErrorAsString()
{
    //Get the error message, if any.
    DWORD errorMessageID = ::GetLastError();
    if (errorMessageID == 0)
        return "No error message has been recorded";

    LPSTR messageBuffer = nullptr;
    size_t size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        nullptr, errorMessageID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), (LPSTR)&messageBuffer, 0, nullptr);

    std::string message(messageBuffer, size);

    //Free the buffer.
    LocalFree(messageBuffer);

    return message;
}

static const char* CCONV _RA_InstallIntegration()
{
    SetErrorMode(0);

    std::wstring sIntegrationPath = GetIntegrationPath();

    DWORD dwAttrib = GetFileAttributesW(sIntegrationPath.c_str());
    if (dwAttrib == INVALID_FILE_ATTRIBUTES)
        return "0.0";

    g_hRADLL = LoadLibraryW(sIntegrationPath.c_str());
    if (g_hRADLL == nullptr)
    {
        char buffer[1024];
        sprintf_s(buffer, 1024, "Could not load RA_Integration.dll: %d\n%s\n", ::GetLastError(), GetLastErrorAsString().c_str());
        MessageBoxA(nullptr, buffer, "Warning", MB_OK | MB_ICONWARNING);

        return "0.0";
    }

    //	Install function pointers one by one

    _RA_IntegrationVersion = (const char* (CCONV*)())                                GetProcAddress(g_hRADLL, "_RA_IntegrationVersion");
    _RA_HostName = (const char* (CCONV*)())                                          GetProcAddress(g_hRADLL, "_RA_HostName");
    _RA_HostUrl = (const char* (CCONV*)())                                           GetProcAddress(g_hRADLL, "_RA_HostUrl");
    _RA_InitI = (int(CCONV*)(HWND, int, const char*))                                GetProcAddress(g_hRADLL, "_RA_InitI");
    _RA_InitOffline = (int(CCONV*)(HWND, int, const char*))                          GetProcAddress(g_hRADLL, "_RA_InitOffline");
    _RA_InitClient = (int(CCONV*)(HWND, const char*, const char*))                   GetProcAddress(g_hRADLL, "_RA_InitClient");
    _RA_InitClientOffline = (int(CCONV*)(HWND, const char*, const char*))            GetProcAddress(g_hRADLL, "_RA_InitClientOffline");
    _RA_InstallSharedFunctions = (void(CCONV*)(int(*)(), void(*)(), void(*)(), void(*)(), void(*)(char*), void(*)(), void(*)(const char*))) GetProcAddress(g_hRADLL, "_RA_InstallSharedFunctionsExt");
    _RA_SetForceRepaint = (void(CCONV*)(int))                                        GetProcAddress(g_hRADLL, "_RA_SetForceRepaint");
    _RA_CreatePopupMenu = (HMENU(CCONV*)(void))                                      GetProcAddress(g_hRADLL, "_RA_CreatePopupMenu");
    _RA_GetPopupMenuItems = (int(CCONV*)(RA_MenuItem*))                              GetProcAddress(g_hRADLL, "_RA_GetPopupMenuItems");
    _RA_InvokeDialog = (void(CCONV*)(LPARAM))                                        GetProcAddress(g_hRADLL, "_RA_InvokeDialog");
    _RA_SetUserAgentDetail = (void(CCONV*)(const char*))                             GetProcAddress(g_hRADLL, "_RA_SetUserAgentDetail");
    _RA_AttemptLogin = (void(CCONV*)(int))                                           GetProcAddress(g_hRADLL, "_RA_AttemptLogin");
    _RA_SetConsoleID = (int(CCONV*)(unsigned int))                                   GetProcAddress(g_hRADLL, "_RA_SetConsoleID");
    _RA_ClearMemoryBanks = (void(CCONV*)())                                          GetProcAddress(g_hRADLL, "_RA_ClearMemoryBanks");
    _RA_InstallMemoryBank = (void(CCONV*)(int, RA_ReadMemoryFunc*, RA_WriteMemoryFunc*, int)) GetProcAddress(g_hRADLL, "_RA_InstallMemoryBank");
    _RA_InstallMemoryBankBlockReader = (void(CCONV*)(int, RA_ReadMemoryBlockFunc*))  GetProcAddress(g_hRADLL, "_RA_InstallMemoryBankBlockReader");
    _RA_Shutdown = (int(CCONV*)())                                                   GetProcAddress(g_hRADLL, "_RA_Shutdown");
    _RA_IsOverlayFullyVisible = (int(CCONV*)())                                      GetProcAddress(g_hRADLL, "_RA_IsOverlayFullyVisible");
    _RA_SetPaused = (void(CCONV*)(int))                                              GetProcAddress(g_hRADLL, "_RA_SetPaused");
    _RA_NavigateOverlay = (void(CCONV*)(ControllerInput*))                           GetProcAddress(g_hRADLL, "_RA_NavigateOverlay");
    _RA_UpdateHWnd = (void(CCONV*)(HWND))                                            GetProcAddress(g_hRADLL, "_RA_UpdateHWnd");
    _RA_IdentifyRom = (unsigned int(CCONV*)(const BYTE*, unsigned int))              GetProcAddress(g_hRADLL, "_RA_IdentifyRom");
    _RA_IdentifyHash = (unsigned int(CCONV*)(const char*))                           GetProcAddress(g_hRADLL, "_RA_IdentifyHash");
    _RA_ActivateGame = (void(CCONV*)(unsigned int))                                  GetProcAddress(g_hRADLL, "_RA_ActivateGame");
    _RA_OnLoadNewRom = (int(CCONV*)(const BYTE*, unsigned int))                      GetProcAddress(g_hRADLL, "_RA_OnLoadNewRom");
    _RA_ConfirmLoadNewRom = (int(CCONV*)(int))                                       GetProcAddress(g_hRADLL, "_RA_ConfirmLoadNewRom");
    _RA_DoAchievementsFrame = (void(CCONV*)())                                       GetProcAddress(g_hRADLL, "_RA_DoAchievementsFrame");
    _RA_SuspendRepaint = (void(CCONV*)())                                            GetProcAddress(g_hRADLL, "_RA_SuspendRepaint");
    _RA_ResumeRepaint = (void(CCONV*)())                                             GetProcAddress(g_hRADLL, "_RA_ResumeRepaint");
    _RA_UpdateAppTitle = (void(CCONV*)(const char*))                                 GetProcAddress(g_hRADLL, "_RA_UpdateAppTitle");
    _RA_UserName = (const char* (CCONV*)())                                          GetProcAddress(g_hRADLL, "_RA_UserName");
    _RA_HardcoreModeIsActive = (int(CCONV*)())                                       GetProcAddress(g_hRADLL, "_RA_HardcoreModeIsActive");
    _RA_WarnDisableHardcore = (int(CCONV*)(const char*))                             GetProcAddress(g_hRADLL, "_RA_WarnDisableHardcore");
    _RA_OnReset = (void(CCONV*)())                                                   GetProcAddress(g_hRADLL, "_RA_OnReset");
    _RA_OnSaveState = (void(CCONV*)(const char*))                                    GetProcAddress(g_hRADLL, "_RA_OnSaveState");
    _RA_OnLoadState = (void(CCONV*)(const char*))                                    GetProcAddress(g_hRADLL, "_RA_OnLoadState");
    _RA_CaptureState = (int(CCONV*)(char*, int))                                     GetProcAddress(g_hRADLL, "_RA_CaptureState");
    _RA_RestoreState = (void(CCONV*)(const char*))                                   GetProcAddress(g_hRADLL, "_RA_RestoreState");

    return _RA_IntegrationVersion ? _RA_IntegrationVersion() : "0.0";
}

static void GetJsonField(const char* sJson, const char* sField, char *pBuffer, size_t nBufferSize)
{
    const size_t nFieldSize = strlen(sField);
    const char* pValue;

    *pBuffer = 0;
    do
    {
        const char* pScan = strstr(sJson, sField);
        if (!pScan)
            return;

        if (pScan[-1] != '"' || pScan[nFieldSize] != '"')
        {
            sJson = pScan + 1;
            continue;
        }

        pScan += nFieldSize + 1;
        while (*pScan == ':' || isspace(*pScan))
            ++pScan;
        if (*pScan != '"')
            return;

        pValue = ++pScan;
        while (*pScan != '"')
        {
            if (!*pScan)
                return;

            ++pScan;
        }

        while (pValue < pScan && nBufferSize > 1)
        {
            if (*pValue == '\\')
                ++pValue;

            *pBuffer++ = *pValue++;
            nBufferSize--;
        }

        *pBuffer = '\0';
        return;

    } while (1);
}

static unsigned long long ParseVersion(const char* sVersion)
{
    char* pPart;

    unsigned long long major = strtoul(sVersion, &pPart, 10);
    if (*pPart == '.')
        ++pPart;

    unsigned long long minor = strtoul(pPart, &pPart, 10);
    if (*pPart == '.')
        ++pPart;

    unsigned long long patch = strtoul(pPart, &pPart, 10);
    if (*pPart == '.')
        ++pPart;

    unsigned long long revision = strtoul(pPart, &pPart, 10);

    // 64-bit max signed value is 9223 37203 68547 75807
    unsigned long long version = (major * 100000) + minor;
    version = (version * 100000) + patch;
    version = (version * 100000) + revision;
    return version;
}

static void RA_InitCommon(HWND hMainHWND, int nEmulatorID, const char* sClientName, const char* sClientVersion)
{
    char sVerInstalled[32];
#if defined(_MSC_VER) && _MSC_VER >= 1400
    strcpy_s(sVerInstalled, sizeof(sVerInstalled), _RA_InstallIntegration());
#else
    strcpy(sVerInstalled, _RA_InstallIntegration());
#endif

    char sHostUrl[256] = "";
    if (_RA_HostUrl != nullptr)
    {
#if defined(_MSC_VER) && _MSC_VER >= 1400
        strcpy_s(sHostUrl, sizeof(sHostUrl), _RA_HostUrl());
#else
        strcpy(sHostUrl, _RA_HostUrl());
#endif
    }
    else if (_RA_HostName != nullptr)
    {
      sprintf_s(sHostUrl, "http://%s", _RA_HostName());
    }

    if (!sHostUrl[0])
    {
#if defined(_MSC_VER) && _MSC_VER >= 1400
        strcpy_s(sHostUrl, sizeof(sHostUrl), "http://retroachievements.org");
#else
        strcpy(sHostUrl, "http://retroachievements.org");
#endif
    }
    else if (_RA_InitOffline != nullptr && strcmp(sHostUrl, "http://OFFLINE") == 0)
    {
        if (sClientName == nullptr)
            _RA_InitOffline(hMainHWND, nEmulatorID, sClientVersion);
        else
            _RA_InitClientOffline(hMainHWND, sClientName, sClientVersion);
        return;
    }

    DWORD nBytesRead = 0;
    DWORD nStatusCode = 0;
    char buffer[1024];
    ZeroMemory(buffer, 1024);

    if (DoBlockingHttpCallWithRetry(sHostUrl, "dorequest.php", "r=latestintegration", buffer, sizeof(buffer), &nBytesRead, &nStatusCode) == FALSE)
    {
        if (_RA_InitOffline != nullptr)
        {
            sprintf_s(buffer, sizeof(buffer), "Cannot access %s (status code %u)\nWorking offline.", sHostUrl, nStatusCode);
            MessageBoxA(hMainHWND, buffer, "Warning", MB_OK | MB_ICONWARNING);

            _RA_InitOffline(hMainHWND, nEmulatorID, sClientVersion);
        }
        else
        {
            sprintf_s(buffer, sizeof(buffer), "Cannot access %s (status code %u)\nPlease try again later.", sHostUrl, nStatusCode);
            MessageBoxA(hMainHWND, buffer, "Warning", MB_OK | MB_ICONWARNING);

            RA_Shutdown();
        }
        return;
    }

    /* remove trailing zeros from client version */
    char* ptr = sVerInstalled + strlen(sVerInstalled);
    while (ptr[-1] == '0' && ptr[-2] == '.' && (ptr - 2) > sVerInstalled)
        ptr -= 2;
    *ptr = '\0';
    if (strchr(sVerInstalled, '.') == NULL)
    {
        *ptr++ = '.';
        *ptr++ = '0';
        *ptr = '\0';
    }

    char sLatestVersionUrl[256];
    char sVersionBuffer[32];
    GetJsonField(buffer, "MinimumVersion", sVersionBuffer, sizeof(sVersionBuffer));
    const unsigned long long nMinimumDLLVer = ParseVersion(sVersionBuffer);

    GetJsonField(buffer, "LatestVersion", sVersionBuffer, sizeof(sVersionBuffer));
    const unsigned long long nLatestDLLVer = ParseVersion(sVersionBuffer);

#if defined(_M_X64) || defined(__amd64__)
    GetJsonField(buffer, "LatestVersionUrlX64", sLatestVersionUrl, sizeof(sLatestVersionUrl));
#else
    GetJsonField(buffer, "LatestVersionUrl", sLatestVersionUrl, sizeof(sLatestVersionUrl));
#endif

    if (nLatestDLLVer == 0 || !sLatestVersionUrl[0])
    {
        /* NOTE: repurposing sLatestVersionUrl for the error message */
        GetJsonField(buffer, "Error", sLatestVersionUrl, sizeof(sLatestVersionUrl));
        if (sLatestVersionUrl[0])
            sprintf_s(buffer, sizeof(buffer), "Failed to fetch latest integration version.\n\n%s", sLatestVersionUrl);
        else
            sprintf_s(buffer, sizeof(buffer), "The latest integration check did not return a valid response.");

        MessageBoxA(hMainHWND, buffer, "Error", MB_OK | MB_ICONERROR);
        RA_Shutdown();
        return;
    }

    int nMBReply = 0;
    unsigned long long nVerInstalled = ParseVersion(sVerInstalled);
    if (nVerInstalled < nMinimumDLLVer)
    {
        RA_Shutdown(); // Unhook the DLL so we can replace it.

        if (nVerInstalled == 0)
        {
            sprintf_s(buffer, sizeof(buffer), "Install RetroAchievements toolset?\n\n"
                "In order to earn achievements you must download the toolset library.");
        }
        else
        {
            sprintf_s(buffer, sizeof(buffer), "Upgrade RetroAchievements toolset?\n\n"
                "A required upgrade to the toolset is available. If you don't upgrade, you won't be able to earn achievements.\n\n"
                "Latest Version: %s\nInstalled Version: %s", sVersionBuffer, sVerInstalled);
        }

        nMBReply = MessageBoxA(hMainHWND, buffer, "Warning", MB_YESNO | MB_ICONWARNING);
    }
    else if (nVerInstalled < nLatestDLLVer)
    {
        sprintf_s(buffer, sizeof(buffer), "Upgrade RetroAchievements toolset?\n\n"
            "An optional upgrade to the toolset is available.\n\n"
            "Latest Version: %s\nInstalled Version: %s", sVersionBuffer, sVerInstalled);

        nMBReply = MessageBoxA(hMainHWND, buffer, "Warning", MB_YESNO | MB_ICONWARNING);

        if (nMBReply == IDYES)
            RA_Shutdown(); // Unhook the DLL so we can replace it.
    }

    if (nMBReply == IDYES)
    {
        //FetchIntegrationFromWeb(sLatestVersionUrl, &nStatusCode);
        nStatusCode = 0;

        if (nStatusCode == 200)
            nVerInstalled = ParseVersion(_RA_InstallIntegration());

        if (nVerInstalled < nLatestDLLVer)
        {
            sprintf_s(buffer, sizeof(buffer), "Failed to update toolset (status code %u).", nStatusCode);
            MessageBoxA(hMainHWND, buffer, "Error", MB_OK | MB_ICONERROR);
        }
    }

    if (nVerInstalled < nMinimumDLLVer)
    {
        RA_Shutdown();

        sprintf_s(buffer, sizeof(buffer), "%s toolset is required to earn achievements.", nVerInstalled == 0 ? "The" : "A newer");
        MessageBoxA(hMainHWND, buffer, "Warning", MB_OK | MB_ICONWARNING);
    }
    else if (sClientName == nullptr)
    {
        if (!_RA_InitI(hMainHWND, nEmulatorID, sClientVersion))
            RA_Shutdown();
    }
    else
    {
        if (!_RA_InitClient(hMainHWND, sClientName, sClientVersion))
            RA_Shutdown();
    }
}

void RA_Init(HWND hMainHWND, int nEmulatorID, const char* sClientVersion)
{
    RA_InitCommon(hMainHWND, nEmulatorID, nullptr, sClientVersion);
}

void RA_InitClient(HWND hMainHWND, const char* sClientName, const char* sClientVersion)
{
    RA_InitCommon(hMainHWND, -1, sClientName, sClientVersion);
}

void RA_SetUserAgentDetail(const char* sDetail)
{
    if (_RA_SetUserAgentDetail != nullptr)
        _RA_SetUserAgentDetail(sDetail);
}

void RA_InstallSharedFunctions(int(*)(void), void(*fpCauseUnpause)(void), void(*fpCausePause)(void), void(*fpRebuildMenu)(void), void(*fpEstimateTitle)(char*), void(*fpResetEmulation)(void), void(*fpLoadROM)(const char*))
{
    if (_RA_InstallSharedFunctions != nullptr)
        _RA_InstallSharedFunctions(nullptr, fpCauseUnpause, fpCausePause, fpRebuildMenu, fpEstimateTitle, fpResetEmulation, fpLoadROM);
}

void RA_Shutdown()
{
    //	Call shutdown on toolchain
    if (_RA_Shutdown != nullptr)
    {
      _RA_Shutdown();
    }

    //	Clear func ptrs
    _RA_IntegrationVersion = nullptr;
    _RA_HostName = nullptr;
    _RA_HostUrl = nullptr;
    _RA_InitI = nullptr;
    _RA_InitOffline = nullptr;
    _RA_InitClient = nullptr;
    _RA_InitClientOffline = nullptr;
    _RA_InstallSharedFunctions = nullptr;
    _RA_SetForceRepaint = nullptr;
    _RA_CreatePopupMenu = nullptr;
    _RA_GetPopupMenuItems = nullptr;
    _RA_InvokeDialog = nullptr;
    _RA_SetUserAgentDetail = nullptr;
    _RA_AttemptLogin = nullptr;
    _RA_SetConsoleID = nullptr;
    _RA_ClearMemoryBanks = nullptr;
    _RA_InstallMemoryBank = nullptr;
    _RA_InstallMemoryBankBlockReader = nullptr;
    _RA_Shutdown = nullptr;
    _RA_IsOverlayFullyVisible = nullptr;
    _RA_SetPaused = nullptr;
    _RA_NavigateOverlay = nullptr;
    _RA_UpdateHWnd = nullptr;
    _RA_IdentifyRom = nullptr;
    _RA_IdentifyHash = nullptr;
    _RA_ActivateGame = nullptr;
    _RA_OnLoadNewRom = nullptr;
    _RA_ConfirmLoadNewRom = nullptr;
    _RA_DoAchievementsFrame = nullptr;
    _RA_SuspendRepaint = nullptr;
    _RA_ResumeRepaint = nullptr;
    _RA_UpdateAppTitle = nullptr;
    _RA_UserName = nullptr;
    _RA_HardcoreModeIsActive = nullptr;
    _RA_WarnDisableHardcore = nullptr;
    _RA_OnReset = nullptr;
    _RA_OnSaveState = nullptr;
    _RA_OnLoadState = nullptr;
    _RA_CaptureState = nullptr;
    _RA_RestoreState = nullptr;

    /* unload the DLL */
    if (g_hRADLL)
    {
        FreeLibrary(g_hRADLL);
        g_hRADLL = nullptr;
    }
}
