// SPDX-FileCopyrightText: 2019-2026 Connor McLaughlin <stenzek@gmail.com>
// SPDX-License-Identifier: CC-BY-NC-ND-4.0

#include "qtwindowinfo.h"
#include "qtutils.h"

#include "core/core.h"
#include "core/host.h"

#include "util/gpu_device.h"

#include "common/error.h"
#include "common/log.h"

#include <QtGui/QGuiApplication>
#include <QtGui/QScreen>
#include <QtWidgets/QWidget>

#if defined(_WIN32)

#include "common/windows_headers.h"
#include <dwmapi.h>

#elif defined(__APPLE__)

#include "common/cocoa_tools.h"
#include <CoreFoundation/CFString.h>
#include <IOKit/pwr_mgt/IOPMLib.h>

#elif defined(__linux__)

#include <QtDBus/QDBusConnection>
#include <QtDBus/QDBusInterface>
#include <QtDBus/QDBusMessage>
#include <QtDBus/QDBusReply>
#include <qpa/qplatformnativeinterface.h>

#else

#include <qpa/qplatformnativeinterface.h>

#endif

LOG_CHANNEL(Host);

using namespace Qt::StringLiterals;

namespace {

struct WindowInfoLocals
{
#if defined(__APPLE__)
  IOPMAssertionID screensaver_inhibit_assertion;
#elif defined(__linux__)
  // Prevent screensaver inhibits when running on platforms that don't have it (e.g. gamescope).
  std::optional<QDBusInterface> screensaver_inhibit_interface;
  u32 screensaver_inhibit_cookie = 0;
  bool disable_screensaver_inhibit = false;
#endif

  bool screensaver_inhibited = false;
};

} // namespace

static WindowInfoLocals s_window_info_locals;

WindowInfoType QtUtils::GetWindowInfoType()
{
  // Windows and Apple are easy here since there's no display connection.
#if defined(_WIN32)
  return WindowInfoType::Win32;
#elif defined(__APPLE__)
  return WindowInfoType::MacOS;
#else
  const QString platform_name = QGuiApplication::platformName();
  if (platform_name == "xcb"_L1)
  {
    // This is only used for determining the automatic Vulkan renderer, therefore XCB/XLib doesn't matter here.
    // See the comment below for information about this bullshit.
    return WindowInfoType::XCB;
  }
  else if (platform_name == "wayland"_L1)
  {
    return WindowInfoType::Wayland;
  }
  else
  {
    return WindowInfoType::Surfaceless;
  }
#endif
}

std::optional<WindowInfo> QtUtils::GetWindowInfoForWidget(QWidget* widget, RenderAPI render_api, Error* error)
{
  WindowInfo wi = {};

  // Windows and Apple are easy here since there's no display connection.
#if defined(_WIN32)
  wi.type = WindowInfoType::Win32;
  wi.window_handle = reinterpret_cast<void*>(widget->winId());
#elif defined(__APPLE__)
  wi.type = WindowInfoType::MacOS;
  wi.window_handle = reinterpret_cast<void*>(widget->winId());
#else
  QPlatformNativeInterface* pni = QGuiApplication::platformNativeInterface();
  const QString platform_name = QGuiApplication::platformName();
  if (platform_name == QStringLiteral("xcb"))
  {
    // This is fucking ridiculous. NVIDIA+XWayland doesn't support Xlib, and NVIDIA+Xorg doesn't support XCB.
    // Use Xlib if we're not running under Wayland, or we're not requesting OpenGL. Vulkan+XCB seems fine.
    const char* xdg_session_type = std::getenv("XDG_SESSION_TYPE");
    const bool is_running_on_xwayland = (xdg_session_type && std::strstr(xdg_session_type, "wayland"));
    if (is_running_on_xwayland || render_api == RenderAPI::Vulkan)
    {
      wi.type = WindowInfoType::XCB;
      wi.display_connection = pni->nativeResourceForWindow("connection", widget->windowHandle());
    }
    else
    {
      wi.type = WindowInfoType::Xlib;
      wi.display_connection = pni->nativeResourceForWindow("display", widget->windowHandle());
    }
    wi.window_handle = reinterpret_cast<void*>(widget->winId());
  }
  else if (platform_name == QStringLiteral("wayland"))
  {
    wi.type = WindowInfoType::Wayland;
    wi.display_connection = pni->nativeResourceForWindow("display", widget->windowHandle());
    wi.window_handle = pni->nativeResourceForWindow("surface", widget->windowHandle());
  }
  else
  {
    Error::SetStringFmt(error, "Unknown PNI platform {}", platform_name.toStdString());
    return std::nullopt;
  }
#endif

  UpdateSurfaceSize(widget, render_api, &wi);
  UpdateSurfaceRefreshRate(widget, &wi);

  INFO_LOG("Window size: {}x{} (Qt {}x{}), scale: {}, refresh rate {} hz", wi.surface_width, wi.surface_height,
           widget->width(), widget->height(), wi.surface_scale, wi.surface_refresh_rate);

  return wi;
}

void QtUtils::UpdateSurfaceSize(QWidget* widget, RenderAPI render_api, WindowInfo* wi)
{
  // Why this nonsense? Qt's device independent sizes are integer, and fractional scaling is lossy.
  // We can't get back the "real" size of the window. So we have to platform natively query the actual client size.
#if defined(_WIN32)
  if (RECT rc; GetClientRect(static_cast<HWND>(wi->window_handle), &rc))
  {
    const qreal device_pixel_ratio = widget->devicePixelRatio();
    wi->surface_width = static_cast<u16>(rc.right - rc.left);
    wi->surface_height = static_cast<u16>(rc.bottom - rc.top);
    wi->surface_scale = static_cast<float>(device_pixel_ratio);
    return;
  }
#elif defined(__APPLE__)
  if (std::optional<std::pair<int, int>> size =
        CocoaTools::GetViewSizeInPixels(reinterpret_cast<void*>(widget->winId())))
  {
    wi->surface_width = static_cast<u16>(size->first);
    wi->surface_height = static_cast<u16>(size->second);
    wi->surface_scale = static_cast<float>(widget->devicePixelRatio());

    // Only use "real" fractional window scale for Metal renderer.
    // Vulkan returns suboptimal constantly, triggering swap chain recreations.
    if (render_api == RenderAPI::Metal && Core::GetBaseBoolSettingValue("Main", "UseFractionalWindowScale", false))
    {
      if (const std::optional<double> real_device_pixel_ratio = CocoaTools::GetViewRealScalingFactor(wi->window_handle))
      {
        const QSize scaled_size =
          ApplyDevicePixelRatioToSize(widget->size(), static_cast<qreal>(real_device_pixel_ratio.value()));
        wi->surface_width = static_cast<u16>(scaled_size.width());
        wi->surface_height = static_cast<u16>(scaled_size.height());
        wi->surface_scale = static_cast<float>(real_device_pixel_ratio.value());
      }
    }

    return;
  }
#endif

  // On Linux, fuck you, enjoy round trip to the X server, and on Wayland you can't query it in the first place...
  // I ain't dealing with this crap OS. Enjoy your mismatched sizes and shit experience.
  const qreal device_pixel_ratio = widget->devicePixelRatio();
  const QSize scaled_size = ApplyDevicePixelRatioToSize(widget->size(), device_pixel_ratio);
  wi->surface_width = static_cast<u16>(scaled_size.width());
  wi->surface_height = static_cast<u16>(scaled_size.height());
  wi->surface_scale = static_cast<float>(device_pixel_ratio);
}

void QtUtils::UpdateSurfaceRefreshRate(QWidget* widget, WindowInfo* wi)
{
  const QScreen* widget_screen = widget->screen();
  if (!widget_screen)
    widget_screen = QGuiApplication::primaryScreen();
  wi->surface_refresh_rate = widget_screen ? std::max(static_cast<float>(widget_screen->refreshRate()), 0.0f) : 0.0f;
}

#ifdef __linux__

static void FormatQDBusReplyError(Error* error, const char* prefix, const QDBusError& qerror)
{
  Error::SetStringFmt(error, "{}{}: {}", prefix, qerror.name().toStdString(), qerror.message().toStdString());
}

#endif

bool Host::SetScreensaverInhibit(bool inhibit, Error* error)
{
  if (s_window_info_locals.screensaver_inhibited == inhibit)
    return true;

#if defined(_WIN32)

  if (SetThreadExecutionState(ES_CONTINUOUS | (inhibit ? (ES_DISPLAY_REQUIRED | ES_SYSTEM_REQUIRED) : 0)) == NULL)
  {
    Error::SetWin32(error, "SetThreadExecutionState() failed: ", GetLastError());
    return false;
  }

  s_window_info_locals.screensaver_inhibited = inhibit;
  return true;

#elif defined(__APPLE__)

  if (inhibit)
  {
    const CFStringRef reason = CFSTR("DuckStation VM is running.");
    const IOReturn ret =
      IOPMAssertionCreateWithName(kIOPMAssertionTypePreventUserIdleDisplaySleep, kIOPMAssertionLevelOn, reason,
                                  &s_window_info_locals.screensaver_inhibit_assertion);
    if (ret != kIOReturnSuccess)
    {
      Error::SetStringFmt(error, "IOPMAssertionCreateWithName() failed: {}", static_cast<s32>(ret));
      return false;
    }

    s_window_info_locals.screensaver_inhibited = true;
    return true;
  }
  else
  {
    const IOReturn ret = IOPMAssertionRelease(s_window_info_locals.screensaver_inhibit_assertion);
    if (ret != kIOReturnSuccess)
    {
      Error::SetStringFmt(error, "IOPMAssertionRelease() failed: {}", static_cast<s32>(ret));
      return false;
    }

    s_window_info_locals.screensaver_inhibit_assertion = kIOPMNullAssertionID;
    s_window_info_locals.screensaver_inhibited = false;
    return true;
  }

#elif defined(__linux__)

  if (s_window_info_locals.disable_screensaver_inhibit)
  {
    // pretend it succeeded so the caller doesn't throw an error
    s_window_info_locals.screensaver_inhibited = inhibit;
    return true;
  }

  if (!s_window_info_locals.screensaver_inhibit_interface.has_value())
  {
    const QDBusConnection connection = QDBusConnection::sessionBus();
    if (!connection.isConnected())
    {
      Error::SetStringView(error, "Failed to connect to the D-Bus session bus");
      return false;
    }

    s_window_info_locals.screensaver_inhibit_interface.emplace(
      "org.freedesktop.ScreenSaver", "/org/freedesktop/ScreenSaver", "org.freedesktop.ScreenSaver", connection);
    if (!s_window_info_locals.screensaver_inhibit_interface->isValid())
    {
      s_window_info_locals.screensaver_inhibit_interface.reset();
      Error::SetStringView(error, "org.freedesktop.ScreenSaver interface is invalid");
      return false;
    }
  }

  if (inhibit)
  {
    const QDBusReply<quint32> msg = s_window_info_locals.screensaver_inhibit_interface->call(
      "Inhibit", u"DuckStation"_s, u"DuckStation VM is running."_s);
    if (!msg.isValid())
    {
      FormatQDBusReplyError(error, "Inhibit message call failed: ", msg.error());
      return false;
    }

    s_window_info_locals.screensaver_inhibit_cookie = msg.value();
    s_window_info_locals.screensaver_inhibited = true;
    return true;
  }
  else
  {
    const QDBusReply<void> msg = s_window_info_locals.screensaver_inhibit_interface->call(
      "UnInhibit", s_window_info_locals.screensaver_inhibit_cookie);
    if (!msg.isValid())
    {
      FormatQDBusReplyError(error, "UnInhibit message call failed: ", msg.error());
      return false;
    }

    s_window_info_locals.screensaver_inhibit_cookie = 0;
    s_window_info_locals.screensaver_inhibited = false;
    return true;
  }

#else

  Error::SetStringView(error, "Not implemented.");
  return false;

#endif
}

#ifdef _WIN32

bool QtUtils::SetWindowRoundedCornerState(QWidget* widget, bool enabled)
{
  const HWND window_handle = reinterpret_cast<HWND>(widget->winId());
  const DWM_WINDOW_CORNER_PREFERENCE value = enabled ? DWMWCP_DEFAULT : DWMWCP_DONOTROUND;
  const HRESULT hr = DwmSetWindowAttribute(window_handle, DWMWA_WINDOW_CORNER_PREFERENCE, &value, sizeof(value));
  if (FAILED(hr))
  {
    ERROR_LOG("DwmSetWindowAttribute(DWMWA_WINDOW_CORNER_PREFERENCE) failed: ",
              Error::CreateHResult(hr).GetDescription());
  }

  return SUCCEEDED(hr);
}

#endif // _WIN32

void QtHost::DisableScreensaverInhibit()
{
#ifdef __linux__
  s_window_info_locals.disable_screensaver_inhibit = true;
#endif
}
