#include <Python.h>
/* Python.h must be included before any system headers,
    to ensure visibility macros are properly set. */
#include <stdexcept>

#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
// Windows 10, for latest HiDPI API support.
#define WINVER 0x0A00
#if defined(_WIN32_WINNT)
#if _WIN32_WINNT < WINVER
#undef _WIN32_WINNT
#define _WIN32_WINNT WINVER
#endif
#else
#define _WIN32_WINNT WINVER
#endif
#endif
#include <pybind11/pybind11.h>
#ifdef __linux__
#include <dlfcn.h>
#endif
#ifdef _WIN32
#include <Objbase.h>
#include <Shobjidl.h>
#include <Windows.h>
#define UNUSED_ON_NON_WINDOWS(x) x
#else
#define UNUSED_ON_NON_WINDOWS Py_UNUSED
#endif

namespace py = pybind11;
using namespace pybind11::literals;

static bool
mpl_xdisplay_is_valid(void)
{
#ifdef __linux__
    void* libX11;
    // The getenv check is redundant but helps performance as it is much faster
    // than dlopen().
    if (getenv("DISPLAY")
        && (libX11 = dlopen("libX11.so.6", RTLD_LAZY))) {
        struct Display* display = nullptr;
        auto XOpenDisplay = (struct Display* (*)(char const*))
            dlsym(libX11, "XOpenDisplay");
        auto XCloseDisplay = (int (*)(struct Display*))
            dlsym(libX11, "XCloseDisplay");
        if (XOpenDisplay && XCloseDisplay
                && (display = XOpenDisplay(nullptr))) {
            XCloseDisplay(display);
        }
        if (dlclose(libX11)) {
            throw std::runtime_error(dlerror());
        }
        if (display) {
            return true;
        }
    }
    return false;
#else
    return true;
#endif
}

static bool
mpl_display_is_valid(void)
{
#ifdef __linux__
    if (mpl_xdisplay_is_valid()) {
        return true;
    }
    void* libwayland_client;
    if (getenv("WAYLAND_DISPLAY")
        && (libwayland_client = dlopen("libwayland-client.so.0", RTLD_LAZY))) {
        struct wl_display* display = nullptr;
        auto wl_display_connect = (struct wl_display* (*)(char const*))
            dlsym(libwayland_client, "wl_display_connect");
        auto wl_display_disconnect = (void (*)(struct wl_display*))
            dlsym(libwayland_client, "wl_display_disconnect");
        if (wl_display_connect && wl_display_disconnect
                && (display = wl_display_connect(nullptr))) {
            wl_display_disconnect(display);
        }
        if (dlclose(libwayland_client)) {
            throw std::runtime_error(dlerror());
        }
        if (display) {
            return true;
        }
    }
    return false;
#else
    return true;
#endif
}

static py::object
mpl_GetCurrentProcessExplicitAppUserModelID(void)
{
#ifdef _WIN32
    wchar_t* appid = NULL;
    HRESULT hr = GetCurrentProcessExplicitAppUserModelID(&appid);
    if (FAILED(hr)) {
        PyErr_SetFromWindowsErr(hr);
        throw py::error_already_set();
    }
    auto py_appid = py::cast(appid);
    CoTaskMemFree(appid);
    return py_appid;
#else
    return py::none();
#endif
}

static void
mpl_SetCurrentProcessExplicitAppUserModelID(const wchar_t* UNUSED_ON_NON_WINDOWS(appid))
{
#ifdef _WIN32
    HRESULT hr = SetCurrentProcessExplicitAppUserModelID(appid);
    if (FAILED(hr)) {
        PyErr_SetFromWindowsErr(hr);
        throw py::error_already_set();
    }
#endif
}

static py::object
mpl_GetForegroundWindow(void)
{
#ifdef _WIN32
  if (HWND hwnd = GetForegroundWindow()) {
    return py::capsule(hwnd, "HWND");
  } else {
    return py::none();
  }
#else
  return py::none();
#endif
}

static void
mpl_SetForegroundWindow(py::capsule UNUSED_ON_NON_WINDOWS(handle_p))
{
#ifdef _WIN32
    if (strcmp(handle_p.name(), "HWND") != 0) {
        throw std::runtime_error("Handle must be a value returned from Win32_GetForegroundWindow");
    }
    HWND handle = static_cast<HWND>(handle_p.get_pointer());
    if (!SetForegroundWindow(handle)) {
        throw std::runtime_error("Error setting window");
    }
#endif
}

static void
mpl_SetProcessDpiAwareness_max(void)
{
#ifdef _WIN32
#ifdef _DPI_AWARENESS_CONTEXTS_
    // These functions and options were added in later Windows 10 updates, so
    // must be loaded dynamically.
    HMODULE user32 = LoadLibrary("user32.dll");
    auto IsValidDpiAwarenessContext = (BOOL (WINAPI *)(DPI_AWARENESS_CONTEXT))
        GetProcAddress(user32, "IsValidDpiAwarenessContext");
    auto SetProcessDpiAwarenessContext = (BOOL (WINAPI *)(DPI_AWARENESS_CONTEXT))
        GetProcAddress(user32, "SetProcessDpiAwarenessContext");
    DPI_AWARENESS_CONTEXT ctxs[3] = {
        DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2,  // Win10 Creators Update
        DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE,     // Win10
        DPI_AWARENESS_CONTEXT_SYSTEM_AWARE};         // Win10
    if (IsValidDpiAwarenessContext && SetProcessDpiAwarenessContext) {
        for (size_t i = 0; i < sizeof(ctxs) / sizeof(DPI_AWARENESS_CONTEXT); ++i) {
            if (IsValidDpiAwarenessContext(ctxs[i])) {
                SetProcessDpiAwarenessContext(ctxs[i]);
                break;
            }
        }
    } else {
        // Added in Windows Vista.
        SetProcessDPIAware();
    }
    FreeLibrary(user32);
#else
    // Added in Windows Vista.
    SetProcessDPIAware();
#endif
#endif
}

PYBIND11_MODULE(_c_internal_utils, m, py::mod_gil_not_used())
{
    m.def(
        "display_is_valid", &mpl_display_is_valid,
        R"""(        --
        Check whether the current X11 or Wayland display is valid.

        On Linux, returns True if either $DISPLAY is set and XOpenDisplay(NULL)
        succeeds, or $WAYLAND_DISPLAY is set and wl_display_connect(NULL)
        succeeds.

        On other platforms, always returns True.)""");
    m.def(
        "xdisplay_is_valid", &mpl_xdisplay_is_valid,
        R"""(        --
        Check whether the current X11 display is valid.

        On Linux, returns True if either $DISPLAY is set and XOpenDisplay(NULL)
        succeeds. Use this function if you need to specifically check for X11
        only (e.g., for Tkinter).

        On other platforms, always returns True.)""");
    m.def(
        "Win32_GetCurrentProcessExplicitAppUserModelID",
        &mpl_GetCurrentProcessExplicitAppUserModelID,
        R"""(        --
        Wrapper for Windows's GetCurrentProcessExplicitAppUserModelID.

        On non-Windows platforms, always returns None.)""");
    m.def(
        "Win32_SetCurrentProcessExplicitAppUserModelID",
        &mpl_SetCurrentProcessExplicitAppUserModelID,
        "appid"_a, py::pos_only(),
        R"""(        --
        Wrapper for Windows's SetCurrentProcessExplicitAppUserModelID.

        On non-Windows platforms, does nothing.)""");
    m.def(
        "Win32_GetForegroundWindow", &mpl_GetForegroundWindow,
        R"""(        --
        Wrapper for Windows' GetForegroundWindow.

        On non-Windows platforms, always returns None.)""");
    m.def(
        "Win32_SetForegroundWindow", &mpl_SetForegroundWindow,
        "hwnd"_a,
        R"""(        --
        Wrapper for Windows' SetForegroundWindow.

        On non-Windows platforms, does nothing.)""");
    m.def(
        "Win32_SetProcessDpiAwareness_max", &mpl_SetProcessDpiAwareness_max,
        R"""(        --
        Set Windows' process DPI awareness to best option available.

        On non-Windows platforms, does nothing.)""");
}
