// Turn on libc extensions so that we can use dladdr() on Unix-like systems
#if defined(__has_include) && !(defined(_GNU_SOURCE) || defined(_DARWIN_C_SOURCE))
#if __has_include(<features.h>)
// We're using a glibc-compatible library
#if !defined(_GNU_SOURCE)
#define _GNU_SOURCE
#endif
#elif __has_include(<Availability.h>)
// We're on Apple/Darwin
#define _DARWIN_C_SOURCE
#endif
#else // No __has_include
#if __GNUC__ < 5 && !defined(_GNU_SOURCE)
// Best guess on older GCC is that we are using glibc
#define _GNU_SOURCE
#endif
#endif

#include "../mongocrypt-dll-private.h"

#ifndef _WIN32

#include <assert.h>
#include <stdio.h>
#include <string.h>

#include <dlfcn.h>

mcr_dll mcr_dll_open(const char *filepath) {
    void *handle = dlopen(filepath, RTLD_LAZY | RTLD_LOCAL);
    if (handle == NULL) {
        // Failed to open. Return NULL and copy the error message
        return (mcr_dll){
            ._native_handle = NULL,
            .error_string = mstr_copy_cstr(dlerror()),
        };
    } else {
        // Okay
        return (mcr_dll){
            ._native_handle = handle,
            .error_string = MSTR_NULL,
        };
    }
}

void mcr_dll_close_handle(mcr_dll); // -Wmissing-prototypes: not for external use despite external linkage.

void mcr_dll_close_handle(mcr_dll dll) {
    if (dll._native_handle) {
        dlclose(dll._native_handle);
    }
}

void *mcr_dll_sym(mcr_dll dll, const char *sym) {
    return dlsym(dll._native_handle, sym);
}

#endif

#ifdef __APPLE__

#include <mach-o/dyld.h>
#include <mach-o/nlist.h>

mcr_dll_path_result mcr_dll_path(mcr_dll dll) {
    // Clear the three low bits of the module handle:
    uintptr_t needle = ((uintptr_t)dll._native_handle & ~UINT64_C(0x3));
    // Iterate each loaded dyld image
    /// NOTE: Not thread safe. Is there a thread-safe way to do this?
    for (uint32_t idx = 0; idx < _dyld_image_count(); ++idx) {
        // Get the filepath:
        /// NOTE: Between here and `dlopen`, `dyld_name` could be invalidated by
        /// a concurrent call to `dlclose()`. Is there a better way?
        const char *dyld_name = _dyld_get_image_name(idx);
        // Try and open it. This will return an equivalent pointer to the original
        // handle to the loaded image since they are deduplicated and reference
        // counted.
        void *try_handle = dlopen(dyld_name, RTLD_LAZY);
        if (!dyld_name) {
            // Ouch: `idx` was invalidated before we called _dyld_get_image_name.
            // This will have caused `dlopen()` to return the default handle:
            assert(try_handle == RTLD_DEFAULT);
            continue;
        }
        // Copy the string before closing, to shrink the chance of `dyld_name`
        // being used-after-freed.
        mstr ret_name = mstr_copy_cstr(dyld_name);
        // Mask off the mode bits:
        uintptr_t cur = (uintptr_t)try_handle & ~UINT64_C(0x3);
        // Close our reference to the image. We only care about the handle value.
        dlclose(try_handle);
        if (needle == cur) {
            // We've found the handle
            return (mcr_dll_path_result){.path = ret_name};
        }
        // Not this name.
        mstr_free(ret_name);
    }
    return (mcr_dll_path_result){.error_string = mstr_copy_cstr("Handle not found in loaded modules")};
}

bool mcr_dll_path_supported(void) {
    return true;
}

#elif defined(__linux__) || defined(__FreeBSD__)

#include <link.h>

mcr_dll_path_result mcr_dll_path(mcr_dll dll) {
    struct link_map *map = NULL;
    int rc = dlinfo(dll._native_handle, RTLD_DI_LINKMAP, &map);
    if (rc == 0) {
        assert(NULL != map);
        return (mcr_dll_path_result){.path = mstr_copy_cstr(map->l_name)};
    } else {
        return (mcr_dll_path_result){.error_string = mstr_copy_cstr(dlerror())};
    }
}

bool mcr_dll_path_supported(void) {
    return true;
}

#elif defined(_WIN32)

// Handled in os_win/os_dll.c

#else

mcr_dll_path_result mcr_dll_path(mcr_dll dll) {
    return (mcr_dll_path_result){.error_string =
                                     mstr_copy_cstr("Don't know how to do mcr_dll_path() on this platform")};
}

bool mcr_dll_path_supported(void) {
    return false;
}

#endif
