#if defined(_WIN32)
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#else
#include <dlfcn.h>
#endif
#include <stdarg.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include <strings.h>

#include "coduo_engine_abi.h"
#include "coduo_engine_native_varargs.h"
#include "coduo_engine_structs.h"
#include "vm_private.h"
#include "../core_memory/core_memory_private.h"
#include "../core_runtime/core_runtime_private.h"
#include "../filesystem/fs_private.h"

coduo_vm_t *VM_Create(const char * module,
                      coduo_vm_system_call_fn systemCall);
void VM_Free(coduo_vm_t *vm);
void Sys_UnloadDll(coduo_native_dll_host_handle_t dllHandle);
void Sys_UnableToLoadDLLError(void);
coduo_native_dll_host_handle_t
Sys_LoadDll(const char * name,
            char * fqpath,
            coduo_vm_main_host_entry_out_t entryPoint,
            coduo_game_syscall_fn syscall);

static coduo_dll_entry_fn
/* NOT_FROM_ORIGINAL_SOURCE:
 * POSIX dlsym uses void * even for function symbols;
 * keep the non-ISO conversion local so GCC -pedantic does not reject the build.
 */
coduomp_vm_load_dll_entry_symbol(void *handle, const char *symbolName)
{
#if defined(_WIN32)
    FARPROC symbol = GetProcAddress((HMODULE)handle, symbolName);
#else
    void *symbol = dlsym(handle, symbolName);
#endif
    coduo_dll_entry_fn function = NULL;

    _Static_assert(sizeof(function) == sizeof(symbol),
                   "dlsym function pointer size mismatch");
    memcpy(&function, &symbol, sizeof(function));
    return function;
}

static coduo_vm_main_fn
/* NOT_FROM_ORIGINAL_SOURCE:
 * POSIX dlsym uses void * even for function symbols;
 * keep the non-ISO conversion local so GCC -pedantic does not reject the build.
 */
coduomp_vm_load_dll_main_symbol(void *handle, const char *symbolName)
{
#if defined(_WIN32)
    FARPROC symbol = GetProcAddress((HMODULE)handle, symbolName);
#else
    void *symbol = dlsym(handle, symbolName);
#endif
    coduo_vm_main_fn function = NULL;

    _Static_assert(sizeof(function) == sizeof(symbol),
                   "dlsym function pointer size mismatch");
    memcpy(&function, &symbol, sizeof(function));
    return function;
}

void VM_Init(void)
{
    Com_Memset(vmTable, 0, sizeof(vmTable));
}

#if UINTPTR_MAX > UINT32_MAX
static coduo_native_vararg_layout_t
/* NOT_FROM_ORIGINAL_SOURCE:
 * The original i386 VM boundary reads contiguous
 * cdecl stack words. Native 64-bit ABIs distribute promoted varargs across
 * registers and the stack, so these adapters use the audited argument count
 * and promoted host type for each command before constructing the same
 * intptr_t vectors. The i386 path below remains the original word-copy form.
 */
coduomp_vm_dll_syscall_argument_layout(intptr_t command)
{
    switch ((coduo_syscall_id_t)command) {
#define CODUO_VM_SYSCALL_LAYOUT_CASE(enum_name, argument_types) \
    case enum_name:                                              \
        return (coduo_native_vararg_layout_t){                   \
            argument_types, sizeof(argument_types) - 1};
        CODUO_ENGINE_NATIVE_SYSCALL_LAYOUTS(CODUO_VM_SYSCALL_LAYOUT_CASE)
#undef CODUO_VM_SYSCALL_LAYOUT_CASE
    default:
        return (coduo_native_vararg_layout_t){"", 0};
    }
}

/* NOT_FROM_ORIGINAL_SOURCE: native-host game-command vararg layout adapter. */
static coduo_native_vararg_layout_t
coduo_engine_vm_game_command_argument_layout(int32_t command)
{
    switch ((coduo_game_command_t)command) {
#define CODUO_VM_GAME_LAYOUT_CASE(command_enum, argument_types) \
    case command_enum:                                        \
        return (coduo_native_vararg_layout_t){                 \
            argument_types, sizeof(argument_types) - 1};
        CODUO_ENGINE_NATIVE_GAME_COMMAND_LAYOUTS(CODUO_VM_GAME_LAYOUT_CASE)
#undef CODUO_VM_GAME_LAYOUT_CASE
    default:
        return (coduo_native_vararg_layout_t){"", 0};
    }
}

/* NOT_FROM_ORIGINAL_SOURCE: reads one promoted native-host vararg slot. */
static intptr_t
coduo_engine_vm_read_native_argument(va_list *ap,
                                     coduo_native_vararg_type_t type)
{
    switch (type) {
    case CODUO_NATIVE_VARARG_POINTER:
        return (intptr_t)va_arg(*ap, void *);

    case CODUO_NATIVE_VARARG_UNSIGNED_INT:
        return (intptr_t)va_arg(*ap, unsigned int);

    case CODUO_NATIVE_VARARG_SIZE:
        return (intptr_t)va_arg(*ap, size_t);

    case CODUO_NATIVE_VARARG_SIGNED_INT:
    default:
        return (intptr_t)va_arg(*ap, int);
    }
}
#endif

intptr_t VM_DllSyscall(intptr_t arg, ...)
{
#if UINTPTR_MAX == UINT32_MAX
    return currentVM->systemCall(&arg);
#else
    enum {
        CODUO_VM_NATIVE_SYSCALL_MAX_ARGUMENTS = 12
    };
    intptr_t args[CODUO_VM_NATIVE_SYSCALL_MAX_ARGUMENTS + 1];
    coduo_native_vararg_layout_t layout =
        coduomp_vm_dll_syscall_argument_layout(arg);
    size_t argumentCount = layout.argumentCount;

    if (argumentCount > CODUO_VM_NATIVE_SYSCALL_MAX_ARGUMENTS) {
        argumentCount = CODUO_VM_NATIVE_SYSCALL_MAX_ARGUMENTS;
    }

    args[0] = arg;

    va_list ap;
    va_start(ap, arg);
    for (size_t index = 1; index <= argumentCount; index++) {
        args[index] = coduo_engine_vm_read_native_argument(
            &ap, CODUO_NATIVE_VARARG_TYPE_AT(layout, index - 1));
    }
    va_end(ap);

    return currentVM->systemCall(args);
#endif
}

coduo_vm_t *VM_Restart(coduo_vm_t *vm)
{
    coduo_vm_system_call_fn systemCall = vm->systemCall;
    char module[CODUO_MAX_QPATH];

    Q_strncpyz(module, vm->name, sizeof(module));
    VM_Free(vm);
    return VM_Create(module, systemCall);
}

coduo_vm_t *VM_Create(const char * module,
                      coduo_vm_system_call_fn systemCall)
{
    if (module == NULL || module[0] == '\0' || systemCall == NULL) {
        Com_Error(0, "\x15" "VM_Create: bad parms");
    }

    Hunk_MemoryRemaining();

    for (int slot = 0; slot < CODUO_VM_COUNT; slot++) {
        if (Q_stricmp(vmTable[slot].name, module) == 0) {
            return &vmTable[slot];
        }
    }

    int slot;
    for (slot = 0; slot < CODUO_VM_COUNT && vmTable[slot].name[0] != '\0';
         slot++) {
    }

    if (slot == CODUO_VM_COUNT) {
        Com_Error(0, "\x15" "VM_Create: no free vm_t");
    }

    coduo_vm_t *vm = &vmTable[slot];

    Q_strncpyz(vm->name, module, sizeof(vm->name));
    vm->systemCall = systemCall;
    vm->dllHandle =
        Sys_LoadDll(module, vm->fqpath, &vm->entryPoint, VM_DllSyscall);

    if (vm->dllHandle == NULL) {
        Sys_UnableToLoadDLLError();
        return NULL;
    }

    return vm;
}

coduo_native_dll_host_handle_t VM_Find(char *nameAndPathOut)
{
    for (int slot = 0; slot < CODUO_VM_COUNT; slot++) {
        if (strcasecmp(nameAndPathOut, vmTable[slot].name) == 0) {
            strncpy(nameAndPathOut, vmTable[slot].fqpath,
                    CODUO_NATIVE_DLL_LOCAL_PATH_BUFFER_SIZE - 1);
            nameAndPathOut[CODUO_NATIVE_DLL_LOCAL_PATH_BUFFER_SIZE - 1] = '\0';
            return vmTable[slot].dllHandle;
        }
    }

    return NULL;
}

void VM_Free(coduo_vm_t *vm)
{
    Sys_UnloadDll(vm->dllHandle);
    Com_Memset(vm, 0, sizeof(*vm));
    currentVM = NULL;
}

void VM_Clear(void)
{
    for (int slot = 0; slot < CODUO_VM_COUNT; slot++) {
        if (vmTable[slot].dllHandle != NULL) {
            Sys_UnloadDll(vmTable[slot].dllHandle);
        }

        Com_Memset(&vmTable[slot], 0, sizeof(vmTable[slot]));
    }

    currentVM = NULL;
}

intptr_t VM_Call(coduo_vm_t *vm,
                               int32_t command, ...)
{
    coduo_vm_t *previousVM = currentVM;
    intptr_t args[12];

    currentVM = vm;

    va_list ap;
    va_start(ap, command);
#if UINTPTR_MAX > UINT32_MAX
    for (size_t index = 0; index < sizeof(args) / sizeof(args[0]); index++) {
        args[index] = 0;
    }

    coduo_native_vararg_layout_t layout =
        coduo_engine_vm_game_command_argument_layout(command);
    size_t argumentCount = layout.argumentCount;
    if (argumentCount > sizeof(args) / sizeof(args[0])) {
        argumentCount = sizeof(args) / sizeof(args[0]);
    }

    for (size_t index = 0; index < argumentCount; index++) {
        args[index] = coduo_engine_vm_read_native_argument(
            &ap, CODUO_NATIVE_VARARG_TYPE_AT(layout, index));
    }
#else
    for (size_t index = 0; index < sizeof(args) / sizeof(args[0]); index++) {
        args[index] = va_arg(ap, intptr_t);
    }
#endif
    va_end(ap);

    intptr_t result =
        vm->entryPoint(command, args[0], args[1], args[2], args[3], args[4],
                       args[5], args[6], args[7], args[8], args[9], args[10],
                       args[11]);

    currentVM = previousVM;
    return result;
}

void Sys_UnloadDll(coduo_native_dll_host_handle_t dllHandle)
{
    if (dllHandle == NULL) {
        Com_Printf("Sys_UnloadDll(NULL)\n");
        return;
    }

#if defined(_WIN32)
    if (FreeLibrary((HMODULE)dllHandle) == 0) {
        Com_Printf("Sys_UnloadGame failed on FreeLibrary: %lu!\n",
                   (unsigned long)GetLastError());
    }
#else
    dlclose(dllHandle);
    const char *error = dlerror();

    if (error != NULL) {
        Com_Printf("Sys_UnloadGame failed on dlclose: \"%s\"!\n", error);
    }
#endif
}

void Sys_UnableToLoadDLLError(void)
{
    Com_Error(0, "Unable to load shared library\n");
}

coduo_native_dll_host_handle_t
Sys_LoadDll(const char * name,
            char * fqpath,
            coduo_vm_main_host_entry_out_t entryPoint,
            coduo_game_syscall_fn syscall)
{
#if !defined(_WIN32)
    const char *error = NULL;
#endif
    coduo_native_dll_local_name_buffer_t dllName;
    coduo_native_dll_local_path_buffer_t dllPath;

    fqpath[0] = '\0';
    snprintf(dllName, sizeof(dllName), CODUO_NATIVE_DLL_FILENAME_FORMAT, name);

    Sys_Cwd();
    const char *homepath = Cvar_VariableString("fs_homepath");
    const char *basepath = Cvar_VariableString("fs_basepath");
    const char *game = Cvar_VariableString("fs_game");

    FS_BuildOSPath(homepath, game, dllName, dllPath);
    Com_Printf("Sys_LoadDll(%s)... ", dllPath);

#if defined(_WIN32)
    coduo_native_dll_host_handle_t handle =
        (coduo_native_dll_host_handle_t)LoadLibraryA(dllPath);
    Sys_CheckCrashOrRerun();
#else
    coduo_native_dll_host_handle_t handle =
        dlopen(dllPath, CODUO_NATIVE_DLL_DLOPEN_MODE_NOW);
#endif

    if (handle == NULL) {
        Com_Printf("failed\n");

        FS_BuildOSPath(basepath, game, dllName, dllPath);
        Com_Printf("Sys_LoadDll(%s)... ", dllPath);

#if defined(_WIN32)
        handle = (coduo_native_dll_host_handle_t)LoadLibraryA(dllPath);
        Sys_CheckCrashOrRerun();
#else
        handle = dlopen(dllPath, CODUO_NATIVE_DLL_DLOPEN_MODE_NOW);
#endif
        if (handle == NULL) {
#if defined(_WIN32)
            Com_Printf("\nSys_LoadDll(%s) failed with Windows error %lu\n",
                       dllPath, (unsigned long)GetLastError());
#else
            error = dlerror();
            Com_Printf("\nSys_LoadDll(%s) failed:\n\"%s\"\n", dllPath, error);
#endif
        } else {
            Com_Printf("ok\n");
        }

        if (handle == NULL) {
#if defined(_WIN32)
            Com_Printf("Sys_LoadDll(%s) failed LoadLibrary() completely!\n",
                       name);
#else
            Com_Printf("Sys_LoadDll(%s) failed dlopen() completely!\n", name);
#endif
            return NULL;
        }
    } else {
        Com_Printf("ok\n");
    }

    Q_strncpyz(fqpath, dllPath, CODUO_NATIVE_DLL_VM_SLOT_PATH_COPY_SIZE);

    coduo_dll_entry_fn dllEntry =
        coduomp_vm_load_dll_entry_symbol(handle,
                                         CODUO_NATIVE_DLL_ENTRY_SYMBOL);
    *entryPoint =
        coduomp_vm_load_dll_main_symbol(handle,
                                        CODUO_NATIVE_DLL_MAIN_SYMBOL);

    /* ORIGINAL_BINARY_BUG: stock snapshots only the second dlsym result's
     * loader error.  When dllEntry alone is absent, the successful vmMain
     * lookup clears its diagnostic.  See original-bugs/
     * engine-dllentry-missing-diagnostic-lost.md. */
    if (*entryPoint == NULL || dllEntry == NULL) {
#if defined(_WIN32)
        Com_Printf("Sys_LoadDll(%s) failed GetProcAddress(vmMain/dllEntry): "
                   "Windows error %lu!\n",
                   name, (unsigned long)GetLastError());

        if (FreeLibrary((HMODULE)handle) == 0) {
            Com_Printf("Sys_LoadDll(%s) failed FreeLibrary: Windows error "
                       "%lu\n",
                       name, (unsigned long)GetLastError());
        }
#else
        error = dlerror();
        Com_Printf("Sys_LoadDll(%s) failed dlsym(vmMain):\n\"%s\" !\n", name,
                   error);

        dlclose(handle);
        error = dlerror();
        if (error != NULL) {
            Com_Printf("Sys_LoadDll(%s) failed dlcose:\n\"%s\"\n", name,
                       error);
        }
#endif

        return NULL;
    }

    Com_Printf("Sys_LoadDll(%s) found **vmMain** at  %p  \n", name,
               *entryPoint);
    dllEntry(syscall);
    Com_Printf("Sys_LoadDll(%s) succeeded!\n", name);

    return handle;
}
