/* SPDX-License-Identifier: GPL-2.0-or-later */

/** \file
 * \ingroup GHOST
 */

#include "GHOST_WindowWayland.h"
#include "GHOST_SystemWayland.h"
#include "GHOST_WaylandUtils.h"
#include "GHOST_WindowManager.h"
#include "GHOST_utildefines.h"

#include "GHOST_Event.h"

#include "GHOST_ContextEGL.h"
#include "GHOST_ContextNone.h"

#include <wayland-client-protocol.h>

#ifdef WITH_GHOST_WAYLAND_DYNLOAD
#  include <wayland_dynload_egl.h>
#endif
#include <wayland-egl.h>

#include <algorithm> /* For `std::find`. */

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
#  ifdef WITH_GHOST_WAYLAND_DYNLOAD
#    include <wayland_dynload_libdecor.h>
#  endif
#  include <libdecor.h>
#endif

/* Generated by `wayland-scanner`. */
#include <xdg-decoration-unstable-v1-client-protocol.h>
#include <xdg-shell-client-protocol.h>

#include <atomic>

/* Logging, use `ghost.wl.*` prefix. */
#include "CLG_log.h"

static constexpr size_t base_dpi = 96;

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
/* Access `use_libdecor` in #GHOST_SystemWayland. */
#  define use_libdecor GHOST_SystemWayland::use_libdecor_runtime()
#endif

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
struct WGL_LibDecor_Window {
  struct libdecor_frame *frame = nullptr;
  bool configured = false;
};

static void gwl_libdecor_window_destroy(WGL_LibDecor_Window *decor)
{
  libdecor_frame_unref(decor->frame);
  delete decor;
}
#endif /* WITH_GHOST_WAYLAND_LIBDECOR */

struct WGL_XDG_Decor_Window {
  struct xdg_surface *surface = nullptr;
  struct zxdg_toplevel_decoration_v1 *toplevel_decor = nullptr;
  struct xdg_toplevel *toplevel = nullptr;
  enum zxdg_toplevel_decoration_v1_mode mode = (enum zxdg_toplevel_decoration_v1_mode)0;
};

static void gwl_xdg_decor_window_destroy(WGL_XDG_Decor_Window *decor)
{
  if (decor->toplevel_decor) {
    zxdg_toplevel_decoration_v1_destroy(decor->toplevel_decor);
  }
  xdg_toplevel_destroy(decor->toplevel);
  xdg_surface_destroy(decor->surface);
  delete decor;
}

/* -------------------------------------------------------------------- */
/** \name Internal #GWL_Window
 * \{ */

struct GWL_WindowFrame {
  int32_t size[2] = {0, 0};
  bool is_maximised = false;
  bool is_fullscreen = false;
  bool is_active = false;
};

struct GWL_Window {
  GHOST_WindowWayland *ghost_window = nullptr;
  GHOST_SystemWayland *ghost_system = nullptr;
  struct wl_surface *wl_surface = nullptr;
  /**
   * Outputs on which the window is currently shown on.
   *
   * This is an ordered set (whoever adds to this is responsible for keeping members unique).
   * In practice this is rarely manipulated and is limited by the number of physical displays.
   */
  std::vector<GWL_Output *> outputs;

  /** The scale value written to #wl_surface_set_buffer_scale. */
  int scale = 0;
  /**
   * The fractional scale used to calculate the DPI.
   * (always set, even when scaling is rounded to whole units).
   */
  wl_fixed_t scale_fractional = 0;

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
  WGL_LibDecor_Window *libdecor = nullptr;
#endif
  WGL_XDG_Decor_Window *xdg_decor = nullptr;

  /**
   * The current value of frame, copied from `frame_pending` when applying updates.
   * This avoids the need for locking when reading from `frame`.
   */
  GWL_WindowFrame frame;
  GWL_WindowFrame frame_pending;

#ifdef USE_EVENT_BACKGROUND_THREAD
  /**
   * Needed so calls such as #GHOST_Window::setClientSize
   * doesn't conflict with WAYLAND callbacks that may run in a thread.
   */
  std::mutex frame_pending_mutex;
#endif

  wl_egl_window *egl_window = nullptr;

  std::string title;

  bool is_dialog = false;

#ifdef USE_EVENT_BACKGROUND_THREAD
  /**
   * These pending actions can't be performed when WAYLAND handlers are running from a thread.
   * Postpone their execution until the main thread can handle them.
   */
  std::atomic<bool> pending_actions[3];
#endif /* USE_EVENT_BACKGROUND_THREAD */
};

static void gwl_window_title_set(GWL_Window *win, const char *title)
{
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
  if (use_libdecor) {
    WGL_LibDecor_Window &decor = *win->libdecor;
    libdecor_frame_set_title(decor.frame, title);
  }
  else
#endif
  {
    WGL_XDG_Decor_Window &decor = *win->xdg_decor;
    xdg_toplevel_set_title(decor.toplevel, title);
  }

  win->title = title;
}

static GHOST_TWindowState gwl_window_state_get(const GWL_Window *win)
{
  if (win->frame.is_fullscreen) {
    return GHOST_kWindowStateFullScreen;
  }
  if (win->frame.is_maximised) {
    return GHOST_kWindowStateMaximized;
  }
  return GHOST_kWindowStateNormal;
}

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
/**
 * \note Keep in sync with #gwl_window_state_set_for_xdg.
 */
static bool gwl_window_state_set_for_libdecor(struct libdecor_frame *frame,
                                              const GHOST_TWindowState state,
                                              const GHOST_TWindowState state_current)
{
  switch (state) {
    case GHOST_kWindowStateNormal:
      /* Unset states. */
      switch (state_current) {
        case GHOST_kWindowStateMaximized: {
          libdecor_frame_unset_maximized(frame);
          break;
        }
        case GHOST_kWindowStateFullScreen: {
          libdecor_frame_unset_fullscreen(frame);
          break;
        }
        default: {
          break;
        }
      }
      break;
    case GHOST_kWindowStateMaximized: {
      libdecor_frame_set_maximized(frame);
      break;
    }
    case GHOST_kWindowStateMinimized: {
      libdecor_frame_set_minimized(frame);
      break;
    }
    case GHOST_kWindowStateFullScreen: {
      libdecor_frame_set_fullscreen(frame, nullptr);
      break;
    }
    case GHOST_kWindowStateEmbedded: {
      return false;
    }
  }
  return true;
}

#endif /* WITH_GHOST_WAYLAND_LIBDECOR */

/**
 * \note Keep in sync with #gwl_window_state_set_for_libdecor.
 */
static bool gwl_window_state_set_for_xdg(struct xdg_toplevel *toplevel,
                                         const GHOST_TWindowState state,
                                         const GHOST_TWindowState state_current)
{
  switch (state) {
    case GHOST_kWindowStateNormal:
      /* Unset states. */
      switch (state_current) {
        case GHOST_kWindowStateMaximized: {
          xdg_toplevel_unset_maximized(toplevel);
          break;
        }
        case GHOST_kWindowStateFullScreen: {
          xdg_toplevel_unset_fullscreen(toplevel);
          break;
        }
        default: {
          break;
        }
      }
      break;
    case GHOST_kWindowStateMaximized: {
      xdg_toplevel_set_maximized(toplevel);
      break;
    }
    case GHOST_kWindowStateMinimized: {
      xdg_toplevel_set_minimized(toplevel);
      break;
    }
    case GHOST_kWindowStateFullScreen: {
      xdg_toplevel_set_fullscreen(toplevel, nullptr);
      break;
    }
    case GHOST_kWindowStateEmbedded: {
      return false;
    }
  }
  return true;
}

static bool gwl_window_state_set(GWL_Window *win, const GHOST_TWindowState state)
{
  const GHOST_TWindowState state_current = gwl_window_state_get(win);
  bool result;
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
  if (use_libdecor) {
    result = gwl_window_state_set_for_libdecor(win->libdecor->frame, state, state_current);
  }
  else
#endif
  {
    result = gwl_window_state_set_for_xdg(win->xdg_decor->toplevel, state, state_current);
  }
  return result;
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Internal #GWL_Window Pending Actions
 * \{ */

static void gwl_window_frame_pending_size_set(GWL_Window *win)
{
  if (win->frame_pending.size[0] == 0 || win->frame_pending.size[1] == 0) {
    return;
  }

  win->frame.size[0] = win->frame_pending.size[0];
  win->frame.size[1] = win->frame_pending.size[1];

  wl_egl_window_resize(win->egl_window, UNPACK2(win->frame.size), 0, 0);
  win->ghost_window->notify_size();

  win->frame_pending.size[0] = 0;
  win->frame_pending.size[1] = 0;
}

static void gwl_window_frame_update_from_pending(GWL_Window *win);

#ifdef USE_EVENT_BACKGROUND_THREAD

enum eGWL_PendingWindowActions {
  PENDING_FRAME_CONFIGURE = 0,
  PENDING_EGL_RESIZE,
#  ifdef GHOST_OPENGL_ALPHA
  PENDING_OPAQUE_SET,
#  endif
  PENDING_SWAP_BUFFERS,
  PENDING_SCALE_UPDATE,
};
#  define PENDING_NUM (PENDING_SWAP_BUFFERS + 1)

static void gwl_window_pending_actions_tag(GWL_Window *win, enum eGWL_PendingWindowActions type)
{
  win->pending_actions[int(type)].store(true);
  win->ghost_system->has_pending_actions_for_window.store(true);
}

static void gwl_window_pending_actions_handle(GWL_Window *win)
{
  if (win->pending_actions[PENDING_FRAME_CONFIGURE].exchange(false)) {
    gwl_window_frame_update_from_pending(win);
  }
  if (win->pending_actions[PENDING_EGL_RESIZE].exchange(false)) {
    wl_egl_window_resize(win->egl_window, UNPACK2(win->frame.size), 0, 0);
  }
#  ifdef GHOST_OPENGL_ALPHA
  if (win->pending_actions[PENDING_OPAQUE_SET].exchange(false)) {
    win->ghost_window->setOpaque();
  }
#  endif
  if (win->pending_actions[PENDING_SCALE_UPDATE].exchange(false)) {
    win->ghost_window->outputs_changed_update_scale();
  }
  if (win->pending_actions[PENDING_SWAP_BUFFERS].exchange(false)) {
    win->ghost_window->swapBuffers();
  }
}

#endif /* USE_EVENT_BACKGROUND_THREAD */

/**
 * Update the window's #GWL_WindowFrame
 */
static void gwl_window_frame_update_from_pending_lockfree(GWL_Window *win)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  GHOST_ASSERT(win->ghost_system->main_thread_id == std::this_thread::get_id(),
               "Only from main thread!");

#endif

  bool do_redraw = false;

  if (win->frame_pending.size[0] != 0 && win->frame_pending.size[1] != 0) {
    if ((win->frame.size[0] != win->frame_pending.size[0]) ||
        (win->frame.size[1] != win->frame_pending.size[1])) {
      gwl_window_frame_pending_size_set(win);
    }
  }

  bool is_active_ghost = (win->ghost_window ==
                          win->ghost_system->getWindowManager()->getActiveWindow());

  if (win->frame_pending.is_active) {
    win->ghost_window->activate();
  }
  else {
    win->ghost_window->deactivate();
  }

  if (is_active_ghost != win->frame_pending.is_active) {
    do_redraw = true;
  }

  win->frame_pending.size[0] = win->frame.size[0];
  win->frame_pending.size[1] = win->frame.size[1];

  win->frame = win->frame_pending;

  /* Signal not to apply the scale unless it's configured. */
  win->frame_pending.size[0] = 0;
  win->frame_pending.size[1] = 0;

  if (do_redraw) {
#ifdef USE_EVENT_BACKGROUND_THREAD
    /* Could swap buffers, use pending to a redundant call in some cases. */
    gwl_window_pending_actions_tag(win, PENDING_SWAP_BUFFERS);
#else
    win->ghost_window->swapBuffers();
#endif
  }
}

static void gwl_window_frame_update_from_pending(GWL_Window *win)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_frame_guard{win->frame_pending_mutex};
#endif
  gwl_window_frame_update_from_pending_lockfree(win);
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Internal Utilities
 * \{ */

/**
 * Return -1 if `output_a` has a scale smaller than `output_b`, 0 when there equal, otherwise 1.
 */
static int output_scale_cmp(const GWL_Output *output_a, const GWL_Output *output_b)
{
  if (output_a->scale < output_b->scale) {
    return -1;
  }
  if (output_a->scale > output_b->scale) {
    return 1;
  }
  if (output_a->has_scale_fractional || output_b->has_scale_fractional) {
    const wl_fixed_t scale_fractional_a = output_a->has_scale_fractional ?
                                              output_a->scale_fractional :
                                              wl_fixed_from_int(output_a->scale);
    const wl_fixed_t scale_fractional_b = output_b->has_scale_fractional ?
                                              output_b->scale_fractional :
                                              wl_fixed_from_int(output_b->scale);
    if (scale_fractional_a < scale_fractional_b) {
      return -1;
    }
    if (scale_fractional_a > scale_fractional_b) {
      return 1;
    }
  }
  return 0;
}

static int outputs_max_scale_or_default(const std::vector<GWL_Output *> &outputs,
                                        const int32_t scale_default,
                                        wl_fixed_t *r_scale_fractional)
{
  const GWL_Output *output_max = nullptr;
  for (const GWL_Output *reg_output : outputs) {
    if (!output_max || (output_scale_cmp(output_max, reg_output) == -1)) {
      output_max = reg_output;
    }
  }

  if (output_max) {
    if (r_scale_fractional) {
      *r_scale_fractional = output_max->has_scale_fractional ?
                                output_max->scale_fractional :
                                wl_fixed_from_int(output_max->scale);
    }
    return output_max->scale;
  }

  if (r_scale_fractional) {
    *r_scale_fractional = wl_fixed_from_int(scale_default);
  }
  return scale_default;
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Listener (XDG Top Level), #xdg_toplevel_listener
 * \{ */

static CLG_LogRef LOG_WL_XDG_TOPLEVEL = {"ghost.wl.handle.xdg_toplevel"};
#define LOG (&LOG_WL_XDG_TOPLEVEL)

static void xdg_toplevel_handle_configure(void *data,
                                          xdg_toplevel * /*xdg_toplevel*/,
                                          const int32_t width,
                                          const int32_t height,
                                          wl_array *states)
{
  /* TODO: log `states`, not urgent. */
  CLOG_INFO(LOG, 2, "configure (size=[%d, %d])", width, height);

  GWL_Window *win = static_cast<GWL_Window *>(data);

#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_frame_guard{win->frame_pending_mutex};
#endif

  win->frame_pending.size[0] = win->scale * width;
  win->frame_pending.size[1] = win->scale * height;

  win->frame_pending.is_maximised = false;
  win->frame_pending.is_fullscreen = false;
  win->frame_pending.is_active = false;

  enum xdg_toplevel_state *state;
  WL_ARRAY_FOR_EACH (state, states) {
    switch (*state) {
      case XDG_TOPLEVEL_STATE_MAXIMIZED:
        win->frame_pending.is_maximised = true;
        break;
      case XDG_TOPLEVEL_STATE_FULLSCREEN:
        win->frame_pending.is_fullscreen = true;
        break;
      case XDG_TOPLEVEL_STATE_ACTIVATED:
        win->frame_pending.is_active = true;
        break;
      default:
        break;
    }
  }
}

static void xdg_toplevel_handle_close(void *data, xdg_toplevel * /*xdg_toplevel*/)
{
  CLOG_INFO(LOG, 2, "close");

  GWL_Window *win = static_cast<GWL_Window *>(data);

  win->ghost_window->close();
}

static const xdg_toplevel_listener xdg_toplevel_listener = {
    xdg_toplevel_handle_configure,
    xdg_toplevel_handle_close,
};

#undef LOG

/** \} */

/* -------------------------------------------------------------------- */
/** \name Listener (LibDecor Frame), #libdecor_frame_interface
 * \{ */

#ifdef WITH_GHOST_WAYLAND_LIBDECOR

static CLG_LogRef LOG_WL_LIBDECOR_FRAME = {"ghost.wl.handle.libdecor_frame"};
#  define LOG (&LOG_WL_LIBDECOR_FRAME)

static void frame_handle_configure(struct libdecor_frame *frame,
                                   struct libdecor_configuration *configuration,
                                   void *data)
{
  CLOG_INFO(LOG, 2, "configure");

#  ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_frame_guard{static_cast<GWL_Window *>(data)->frame_pending_mutex};
#  endif

  GWL_WindowFrame *frame_pending = &static_cast<GWL_Window *>(data)->frame_pending;

  /* Set the size. */
  int size_next[2];
  {
    const int scale = static_cast<GWL_Window *>(data)->scale;
    if (!libdecor_configuration_get_content_size(
            configuration, frame, &size_next[0], &size_next[1])) {
      GWL_Window *win = static_cast<GWL_Window *>(data);
      size_next[0] = win->frame.size[0] / scale;
      size_next[1] = win->frame.size[1] / scale;
    }

    frame_pending->size[0] = scale * size_next[0];
    frame_pending->size[1] = scale * size_next[1];
  }

  /* Set the state. */
  {
    enum libdecor_window_state window_state;
    if (!libdecor_configuration_get_window_state(configuration, &window_state)) {
      window_state = LIBDECOR_WINDOW_STATE_NONE;
    }

    frame_pending->is_maximised = window_state & LIBDECOR_WINDOW_STATE_MAXIMIZED;
    frame_pending->is_fullscreen = window_state & LIBDECOR_WINDOW_STATE_FULLSCREEN;
    frame_pending->is_active = window_state & LIBDECOR_WINDOW_STATE_ACTIVE;
  }

  /* Commit the changes. */
  {
    GWL_Window *win = static_cast<GWL_Window *>(data);
    struct libdecor_state *state = libdecor_state_new(UNPACK2(size_next));
    libdecor_frame_commit(frame, state, configuration);
    libdecor_state_free(state);

    win->libdecor->configured = true;
  }

  /* Apply the changes. */
  {
    GWL_Window *win = static_cast<GWL_Window *>(data);
#  ifdef USE_EVENT_BACKGROUND_THREAD
    GHOST_SystemWayland *system = win->ghost_system;
    const bool is_main_thread = system->main_thread_id == std::this_thread::get_id();
    if (!is_main_thread) {
      gwl_window_pending_actions_tag(win, PENDING_FRAME_CONFIGURE);
    }
    else
#  endif
    {
      gwl_window_frame_update_from_pending_lockfree(win);
    }
  }
}

static void frame_handle_close(struct libdecor_frame * /*frame*/, void *data)
{
  CLOG_INFO(LOG, 2, "close");

  GWL_Window *win = static_cast<GWL_Window *>(data);

  win->ghost_window->close();
}

static void frame_handle_commit(struct libdecor_frame * /*frame*/, void *data)
{
  CLOG_INFO(LOG, 2, "commit");

  GWL_Window *win = static_cast<GWL_Window *>(data);

#  ifdef USE_EVENT_BACKGROUND_THREAD
  gwl_window_pending_actions_tag(win, PENDING_SWAP_BUFFERS);
#  else
  win->ghost_window->swapBuffers();
#  endif
}

static struct libdecor_frame_interface libdecor_frame_iface = {
    frame_handle_configure,
    frame_handle_close,
    frame_handle_commit,
};

#  undef LOG

#endif /* WITH_GHOST_WAYLAND_LIBDECOR. */

/** \} */

/* -------------------------------------------------------------------- */
/** \name Listener (XDG Decoration Listener), #zxdg_toplevel_decoration_v1_listener
 * \{ */

static CLG_LogRef LOG_WL_XDG_TOPLEVEL_DECORATION = {"ghost.wl.handle.xdg_toplevel_decoration"};
#define LOG (&LOG_WL_XDG_TOPLEVEL_DECORATION)

static void xdg_toplevel_decoration_handle_configure(
    void *data,
    struct zxdg_toplevel_decoration_v1 * /*zxdg_toplevel_decoration_v1*/,
    const uint32_t mode)
{
  CLOG_INFO(LOG, 2, "configure (mode=%u)", mode);

  GWL_Window *win = static_cast<GWL_Window *>(data);

  win->xdg_decor->mode = (zxdg_toplevel_decoration_v1_mode)mode;
}

static const zxdg_toplevel_decoration_v1_listener xdg_toplevel_decoration_v1_listener = {
    xdg_toplevel_decoration_handle_configure,
};

#undef LOG

/** \} */

/* -------------------------------------------------------------------- */
/** \name Listener (XDG Surface Handle Configure), #xdg_surface_listener
 * \{ */

static CLG_LogRef LOG_WL_XDG_SURFACE = {"ghost.wl.handle.xdg_surface"};
#define LOG (&LOG_WL_XDG_SURFACE)

static void xdg_surface_handle_configure(void *data,
                                         xdg_surface *xdg_surface,
                                         const uint32_t serial)
{
  GWL_Window *win = static_cast<GWL_Window *>(data);

  if (win->xdg_decor->surface != xdg_surface) {
    CLOG_INFO(LOG, 2, "configure (skipped)");
    return;
  }
  CLOG_INFO(LOG, 2, "configure");

#ifdef USE_EVENT_BACKGROUND_THREAD
  GHOST_SystemWayland *system = win->ghost_system;
  const bool is_main_thread = system->main_thread_id == std::this_thread::get_id();
  if (!is_main_thread) {
    /* NOTE(@campbellbarton): this only gets one redraw,
     * I could not find a case where this causes problems. */
    gwl_window_pending_actions_tag(win, PENDING_FRAME_CONFIGURE);
  }
  else
#endif
  {
    gwl_window_frame_update_from_pending(win);
  }

  xdg_surface_ack_configure(xdg_surface, serial);
}

static const xdg_surface_listener xdg_surface_listener = {
    xdg_surface_handle_configure,
};

#undef LOG

/** \} */

/* -------------------------------------------------------------------- */
/** \name Listener (Surface), #wl_surface_listener
 * \{ */

static CLG_LogRef LOG_WL_SURFACE = {"ghost.wl.handle.surface"};
#define LOG (&LOG_WL_SURFACE)

static void surface_handle_enter(void *data,
                                 struct wl_surface * /*wl_surface*/,
                                 struct wl_output *wl_output)
{
  if (!ghost_wl_output_own(wl_output)) {
    CLOG_INFO(LOG, 2, "enter (skipped)");
    return;
  }
  CLOG_INFO(LOG, 2, "enter");

  GWL_Output *reg_output = ghost_wl_output_user_data(wl_output);
  GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(data);
  if (win->outputs_enter(reg_output)) {
    win->outputs_changed_update_scale();
  }
}

static void surface_handle_leave(void *data,
                                 struct wl_surface * /*wl_surface*/,
                                 struct wl_output *wl_output)
{
  if (!ghost_wl_output_own(wl_output)) {
    CLOG_INFO(LOG, 2, "leave (skipped)");
    return;
  }
  CLOG_INFO(LOG, 2, "leave");

  GWL_Output *reg_output = ghost_wl_output_user_data(wl_output);
  GHOST_WindowWayland *win = static_cast<GHOST_WindowWayland *>(data);
  if (win->outputs_leave(reg_output)) {
    win->outputs_changed_update_scale();
  }
}

static const struct wl_surface_listener wl_surface_listener = {
    surface_handle_enter,
    surface_handle_leave,
};

#undef LOG

/** \} */

/* -------------------------------------------------------------------- */
/** \name GHOST Implementation
 *
 * WAYLAND specific implementation of the #GHOST_Window interface.
 * \{ */

GHOST_TSuccess GHOST_WindowWayland::hasCursorShape(GHOST_TStandardCursor cursorShape)
{
  return system_->cursor_shape_check(cursorShape);
}

GHOST_WindowWayland::GHOST_WindowWayland(GHOST_SystemWayland *system,
                                         const char *title,
                                         const int32_t /*left*/,
                                         const int32_t /*top*/,
                                         const uint32_t width,
                                         const uint32_t height,
                                         const GHOST_TWindowState state,
                                         const GHOST_IWindow *parentWindow,
                                         const GHOST_TDrawingContextType type,
                                         const bool is_dialog,
                                         const bool stereoVisual,
                                         const bool exclusive)
    : GHOST_Window(width, height, state, stereoVisual, exclusive),
      system_(system),
      window_(new GWL_Window)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system->server_mutex};
#endif

  window_->ghost_window = this;
  window_->ghost_system = system;

  window_->frame.size[0] = int32_t(width);
  window_->frame.size[1] = int32_t(height);

  window_->is_dialog = is_dialog;

  /* NOTE(@campbellbarton): The scale set here to avoid flickering on startup.
   * When all monitors use the same scale (which is quite common) there aren't any problems.
   *
   * When monitors have different scales there may still be a visible window resize on startup.
   * Ideally it would be possible to know the scale this window will use however that's only
   * known once #surface_enter callback runs (which isn't guaranteed to run at all).
   *
   * Using the maximum scale is best as it results in the window first being smaller,
   * avoiding a large window flashing before it's made smaller. */
  window_->scale = outputs_max_scale_or_default(system_->outputs(), 1, &window_->scale_fractional);

  /* Window surfaces. */
  window_->wl_surface = wl_compositor_create_surface(system_->wl_compositor());
  ghost_wl_surface_tag(window_->wl_surface);

  wl_surface_set_buffer_scale(window_->wl_surface, window_->scale);

  wl_surface_add_listener(window_->wl_surface, &wl_surface_listener, window_);

  window_->egl_window = wl_egl_window_create(
      window_->wl_surface, int(window_->frame.size[0]), int(window_->frame.size[1]));

  /* NOTE: The limit is in points (not pixels) so Hi-DPI will limit to larger number of pixels.
   * This has the advantage that the size limit is the same when moving the window between monitors
   * with different scales set. If it was important to limit in pixels it could be re-calculated
   * when the `window_->scale` changed. */
  const int32_t size_min[2] = {320, 240};

  /* This value is expected to match the base name of the `.desktop` file. see T101805.
   *
   * NOTE: the XDG desktop-entry-spec defines that this should follow the "reverse DNS" convention.
   * For e.g. `org.blender.Blender` - however the `.desktop` file distributed with Blender is
   * simply called `blender.desktop`, so the it's important to follow that name.
   * Other distributions such as SNAP & FLATPAK may need to change this value T101779.
   * Currently there isn't a way to configure this, we may want to support that. */
  const char *xdg_app_id = (
#ifdef WITH_GHOST_WAYLAND_APP_ID
      STRINGIFY(WITH_GHOST_WAYLAND_APP_ID)
#else
      "blender"
#endif
  );

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
  if (use_libdecor) {
    window_->libdecor = new WGL_LibDecor_Window;
    WGL_LibDecor_Window &decor = *window_->libdecor;

    /* create window decorations */
    decor.frame = libdecor_decorate(
        system_->libdecor_context(), window_->wl_surface, &libdecor_frame_iface, window_);
    libdecor_frame_map(window_->libdecor->frame);

    libdecor_frame_set_min_content_size(decor.frame, UNPACK2(size_min));
    libdecor_frame_set_app_id(decor.frame, xdg_app_id);

    if (parentWindow) {
      WGL_LibDecor_Window &decor_parent =
          *dynamic_cast<const GHOST_WindowWayland *>(parentWindow)->window_->libdecor;
      libdecor_frame_set_parent(decor.frame, decor_parent.frame);
    }
  }
  else
#endif
  {
    window_->xdg_decor = new WGL_XDG_Decor_Window;
    WGL_XDG_Decor_Window &decor = *window_->xdg_decor;
    decor.surface = xdg_wm_base_get_xdg_surface(system_->xdg_decor_shell(), window_->wl_surface);
    decor.toplevel = xdg_surface_get_toplevel(decor.surface);

    xdg_toplevel_set_min_size(decor.toplevel, UNPACK2(size_min));
    xdg_toplevel_set_app_id(decor.toplevel, xdg_app_id);

    if (system_->xdg_decor_manager()) {
      decor.toplevel_decor = zxdg_decoration_manager_v1_get_toplevel_decoration(
          system_->xdg_decor_manager(), decor.toplevel);
      zxdg_toplevel_decoration_v1_add_listener(
          decor.toplevel_decor, &xdg_toplevel_decoration_v1_listener, window_);
      zxdg_toplevel_decoration_v1_set_mode(decor.toplevel_decor,
                                           ZXDG_TOPLEVEL_DECORATION_V1_MODE_SERVER_SIDE);
    }

    xdg_surface_add_listener(decor.surface, &xdg_surface_listener, window_);
    xdg_toplevel_add_listener(decor.toplevel, &xdg_toplevel_listener, window_);

    if (parentWindow && is_dialog) {
      WGL_XDG_Decor_Window &decor_parent =
          *dynamic_cast<const GHOST_WindowWayland *>(parentWindow)->window_->xdg_decor;
      xdg_toplevel_set_parent(decor.toplevel, decor_parent.toplevel);
    }
  }

  gwl_window_title_set(window_, title);

  wl_surface_set_user_data(window_->wl_surface, this);

  /* Call top-level callbacks. */
  wl_surface_commit(window_->wl_surface);
  wl_display_roundtrip(system_->wl_display());

#ifdef GHOST_OPENGL_ALPHA
  setOpaque();
#endif

  /* Causes a glitch with `libdecor` for some reason. */
#ifdef WITH_GHOST_WAYLAND_LIBDECOR
  if (use_libdecor) {
    /* Additional round-trip is needed to ensure `xdg_toplevel` is set. */
    wl_display_roundtrip(system_->wl_display());

    /* NOTE: LIBDECOR requires the window to be created & configured before the state can be set.
     * Workaround this by using the underlying `xdg_toplevel` */
    WGL_LibDecor_Window &decor = *window_->libdecor;
    struct xdg_toplevel *toplevel = libdecor_frame_get_xdg_toplevel(decor.frame);
    gwl_window_state_set_for_xdg(toplevel, state, GHOST_kWindowStateNormal);
  }
  else
#endif
  {
    gwl_window_state_set(window_, state);
  }

  /* EGL context. */
  if (setDrawingContextType(type) == GHOST_kFailure) {
    GHOST_PRINT("Failed to create EGL context" << std::endl);
  }

  /* Set swap interval to 0 to prevent blocking. */
  setSwapInterval(0);
}

#ifdef USE_EVENT_BACKGROUND_THREAD
GHOST_TSuccess GHOST_WindowWayland::swapBuffers()
{
  GHOST_ASSERT(system_->main_thread_id == std::this_thread::get_id(), "Only from main thread!");
  return GHOST_Window::swapBuffers();
}
#endif /* USE_EVENT_BACKGROUND_THREAD */

GHOST_TSuccess GHOST_WindowWayland::setWindowCursorGrab(GHOST_TGrabCursorMode mode)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif

  GHOST_Rect bounds_buf;
  GHOST_Rect *bounds = nullptr;
  if (m_cursorGrab == GHOST_kGrabWrap) {
    if (getCursorGrabBounds(bounds_buf) == GHOST_kFailure) {
      getClientBounds(bounds_buf);
    }
    bounds = &bounds_buf;
  }
  if (system_->window_cursor_grab_set(mode,
                                      m_cursorGrab,
                                      m_cursorGrabInitPos,
                                      bounds,
                                      m_cursorGrabAxis,
                                      window_->wl_surface,
                                      window_->scale)) {
    return GHOST_kSuccess;
  }
  return GHOST_kFailure;
}

GHOST_TSuccess GHOST_WindowWayland::setWindowCursorShape(GHOST_TStandardCursor shape)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
  const GHOST_TSuccess ok = system_->cursor_shape_set(shape);
  m_cursorShape = (ok == GHOST_kSuccess) ? shape : GHOST_kStandardCursorDefault;
  return ok;
}

bool GHOST_WindowWayland::getCursorGrabUseSoftwareDisplay()
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
  return system_->cursor_grab_use_software_display_get(m_cursorGrab);
}

GHOST_TSuccess GHOST_WindowWayland::setWindowCustomCursorShape(
    uint8_t *bitmap, uint8_t *mask, int sizex, int sizey, int hotX, int hotY, bool canInvertColor)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
  return system_->cursor_shape_custom_set(bitmap, mask, sizex, sizey, hotX, hotY, canInvertColor);
}

GHOST_TSuccess GHOST_WindowWayland::getCursorBitmap(GHOST_CursorBitmapRef *bitmap)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
  return system_->cursor_bitmap_get(bitmap);
}

void GHOST_WindowWayland::setTitle(const char *title)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
  gwl_window_title_set(window_, title);
}

std::string GHOST_WindowWayland::getTitle() const
{
  /* No need to lock `server_mutex` (WAYLAND never changes this). */
  return window_->title.empty() ? "untitled" : window_->title;
}

void GHOST_WindowWayland::getWindowBounds(GHOST_Rect &bounds) const
{
  getClientBounds(bounds);
}

void GHOST_WindowWayland::getClientBounds(GHOST_Rect &bounds) const
{
  /* No need to lock `server_mutex` (WAYLAND never changes this in a thread). */
  bounds.set(0, 0, UNPACK2(window_->frame.size));
}

GHOST_TSuccess GHOST_WindowWayland::setClientWidth(const uint32_t width)
{
  return setClientSize(width, uint32_t(window_->frame.size[1]));
}

GHOST_TSuccess GHOST_WindowWayland::setClientHeight(const uint32_t height)
{
  return setClientSize(uint32_t(window_->frame.size[0]), height);
}

GHOST_TSuccess GHOST_WindowWayland::setClientSize(const uint32_t width, const uint32_t height)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
  std::lock_guard lock_frame_guard{window_->frame_pending_mutex};
#endif

  window_->frame_pending.size[0] = width;
  window_->frame_pending.size[1] = height;

  gwl_window_frame_pending_size_set(window_);

  return GHOST_kSuccess;
}

void GHOST_WindowWayland::screenToClient(int32_t inX,
                                         int32_t inY,
                                         int32_t &outX,
                                         int32_t &outY) const
{
  outX = inX;
  outY = inY;
}

void GHOST_WindowWayland::clientToScreen(int32_t inX,
                                         int32_t inY,
                                         int32_t &outX,
                                         int32_t &outY) const
{
  outX = inX;
  outY = inY;
}

GHOST_WindowWayland::~GHOST_WindowWayland()
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif

  releaseNativeHandles();

  wl_egl_window_destroy(window_->egl_window);

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
  if (use_libdecor) {
    gwl_libdecor_window_destroy(window_->libdecor);
  }
  else
#endif
  {
    gwl_xdg_decor_window_destroy(window_->xdg_decor);
  }

  /* Clear any pointers to this window. This is needed because there are no guarantees
   * that flushing the display will the "leave" handlers before handling events. */
  system_->window_surface_unref(window_->wl_surface);

  wl_surface_destroy(window_->wl_surface);

  /* NOTE(@campbellbarton): Flushing will often run the appropriate handlers event
   * (#wl_surface_listener.leave in particular) to avoid attempted access to the freed surfaces.
   * This is not fool-proof though, hence the call to #window_surface_unref, see: T99078. */
  wl_display_flush(system_->wl_display());

  delete window_;
}

uint16_t GHOST_WindowWayland::getDPIHint()
{
  /* No need to lock `server_mutex`
   * (`outputs_changed_update_scale` never changes values in a non-main thread). */

  /* Using the physical DPI will cause wrong scaling of the UI
   * use a multiplier for the default DPI as a workaround. */
  return wl_fixed_to_int(window_->scale_fractional * base_dpi);
}

GHOST_TSuccess GHOST_WindowWayland::setWindowCursorVisibility(bool visible)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
  return system_->cursor_visibility_set(visible);
}

GHOST_TSuccess GHOST_WindowWayland::setState(GHOST_TWindowState state)
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
  return gwl_window_state_set(window_, state) ? GHOST_kSuccess : GHOST_kFailure;
}

GHOST_TWindowState GHOST_WindowWayland::getState() const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif
  return gwl_window_state_get(window_);
}

GHOST_TSuccess GHOST_WindowWayland::invalidate()
{
  return GHOST_kSuccess;
}

GHOST_TSuccess GHOST_WindowWayland::setOrder(GHOST_TWindowOrder /*order*/)
{
  return GHOST_kSuccess;
}

GHOST_TSuccess GHOST_WindowWayland::beginFullScreen() const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
  if (use_libdecor) {
    libdecor_frame_set_fullscreen(window_->libdecor->frame, nullptr);
  }
  else
#endif
  {
    xdg_toplevel_set_fullscreen(window_->xdg_decor->toplevel, nullptr);
  }

  return GHOST_kSuccess;
}

GHOST_TSuccess GHOST_WindowWayland::endFullScreen() const
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  std::lock_guard lock_server_guard{*system_->server_mutex};
#endif

#ifdef WITH_GHOST_WAYLAND_LIBDECOR
  if (use_libdecor) {
    libdecor_frame_unset_fullscreen(window_->libdecor->frame);
  }
  else
#endif
  {
    xdg_toplevel_unset_fullscreen(window_->xdg_decor->toplevel);
  }
  return GHOST_kSuccess;
}

bool GHOST_WindowWayland::isDialog() const
{
  return window_->is_dialog;
}

#ifdef GHOST_OPENGL_ALPHA
void GHOST_WindowWayland::setOpaque() const
{
  struct wl_region *region;

  /* Make the window opaque. */
  region = wl_compositor_create_region(system_->wl_compositor());
  wl_region_add(region, 0, 0, UNPACK2(window_->size));
  wl_surface_set_opaque_region(window_->wl_surface, region);
  wl_region_destroy(region);
}
#endif

/**
 * \param type: The type of rendering context create.
 * \return Indication of success.
 */
GHOST_Context *GHOST_WindowWayland::newDrawingContext(GHOST_TDrawingContextType type)
{
  GHOST_Context *context;
  switch (type) {
    case GHOST_kDrawingContextTypeNone:
      context = new GHOST_ContextNone(m_wantStereoVisual);
      break;
    case GHOST_kDrawingContextTypeOpenGL:
      for (int minor = 6; minor >= 0; --minor) {
        context = new GHOST_ContextEGL(system_,
                                       m_wantStereoVisual,
                                       EGLNativeWindowType(window_->egl_window),
                                       EGLNativeDisplayType(system_->wl_display()),
                                       EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
                                       4,
                                       minor,
                                       GHOST_OPENGL_EGL_CONTEXT_FLAGS,
                                       GHOST_OPENGL_EGL_RESET_NOTIFICATION_STRATEGY,
                                       EGL_OPENGL_API);

        if (context->initializeDrawingContext()) {
          return context;
        }
        delete context;
      }
      context = new GHOST_ContextEGL(system_,
                                     m_wantStereoVisual,
                                     EGLNativeWindowType(window_->egl_window),
                                     EGLNativeDisplayType(system_->wl_display()),
                                     EGL_CONTEXT_OPENGL_CORE_PROFILE_BIT,
                                     3,
                                     3,
                                     GHOST_OPENGL_EGL_CONTEXT_FLAGS,
                                     GHOST_OPENGL_EGL_RESET_NOTIFICATION_STRATEGY,
                                     EGL_OPENGL_API);
  }

  if (context->initializeDrawingContext()) {
    return context;
  }

  delete context;
  return nullptr;
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Public WAYLAND Direct Data Access
 *
 * Expose some members via methods.
 * \{ */

int GHOST_WindowWayland::scale() const
{
  return window_->scale;
}

wl_fixed_t GHOST_WindowWayland::scale_fractional() const
{
  return window_->scale_fractional;
}

wl_surface *GHOST_WindowWayland::wl_surface() const
{
  return window_->wl_surface;
}

const std::vector<GWL_Output *> &GHOST_WindowWayland::outputs()
{
  return window_->outputs;
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Public WAYLAND Window Level Functions
 *
 * High Level Windowing Utilities.
 * \{ */

GHOST_TSuccess GHOST_WindowWayland::close()
{
  return system_->pushEvent_maybe_pending(
      new GHOST_Event(system_->getMilliSeconds(), GHOST_kEventWindowClose, this));
}

GHOST_TSuccess GHOST_WindowWayland::activate()
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  const bool is_main_thread = system_->main_thread_id == std::this_thread::get_id();
  if (is_main_thread)
#endif
  {
    if (system_->getWindowManager()->setActiveWindow(this) == GHOST_kFailure) {
      return GHOST_kFailure;
    }
  }
  return system_->pushEvent_maybe_pending(
      new GHOST_Event(system_->getMilliSeconds(), GHOST_kEventWindowActivate, this));
}

GHOST_TSuccess GHOST_WindowWayland::deactivate()
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  /* Actual activation is handled when processing pending events. */
  const bool is_main_thread = system_->main_thread_id == std::this_thread::get_id();
  if (is_main_thread)
#endif
  {
    system_->getWindowManager()->setWindowInactive(this);
  }
  return system_->pushEvent_maybe_pending(
      new GHOST_Event(system_->getMilliSeconds(), GHOST_kEventWindowDeactivate, this));
}

GHOST_TSuccess GHOST_WindowWayland::notify_size()
{
#ifdef GHOST_OPENGL_ALPHA
#  ifdef USE_EVENT_BACKGROUND_THREAD
  /* Actual activation is handled when processing pending events. */
  const bool is_main_thread = system_->main_thread_id == std::this_thread::get_id();
  if (!is_main_thread) {
    gwl_window_pending_actions_tag(window_, PENDING_OPAQUE_SET);
  }
#  endif
  {
    setOpaque();
  }
#endif

  return system_->pushEvent_maybe_pending(
      new GHOST_Event(system_->getMilliSeconds(), GHOST_kEventWindowSize, this));
}

/** \} */

/* -------------------------------------------------------------------- */
/** \name Public WAYLAND Utility Functions
 *
 * Functionality only used for the WAYLAND implementation.
 * \{ */

/**
 * Return true when the windows scale or DPI changes.
 */
bool GHOST_WindowWayland::outputs_changed_update_scale()
{
#ifdef USE_EVENT_BACKGROUND_THREAD
  if (system_->main_thread_id != std::this_thread::get_id()) {
    gwl_window_pending_actions_tag(window_, PENDING_SCALE_UPDATE);
    return false;
  }
#endif

  wl_fixed_t scale_fractional_next = 0;
  const int scale_next = outputs_max_scale_or_default(outputs(), 0, &scale_fractional_next);
  if (UNLIKELY(scale_next == 0)) {
    return false;
  }

  const wl_fixed_t scale_fractional_curr = window_->scale_fractional;
  const int scale_curr = window_->scale;
  bool changed = false;

  if (scale_next != scale_curr) {
    window_->scale = scale_next;
    wl_surface_set_buffer_scale(window_->wl_surface, scale_next);

#ifdef USE_EVENT_BACKGROUND_THREAD
    std::lock_guard lock_frame_guard{window_->frame_pending_mutex};
#endif

    /* It's important to resize the window immediately, to avoid the window changing size
     * and flickering in a constant feedback loop (in some bases). */

    if ((window_->frame_pending.size[0] != 0) && (window_->frame_pending.size[1] != 0)) {
      /* Unlikely but possible there is a pending size change is set. */
      window_->frame.size[0] = window_->frame_pending.size[0];
      window_->frame.size[1] = window_->frame_pending.size[1];
    }

    /* Write to the pending values as these are what is applied. */
    window_->frame_pending.size[0] = (window_->frame.size[0] / scale_curr) * scale_next;
    window_->frame_pending.size[1] = (window_->frame.size[1] / scale_curr) * scale_next;

    gwl_window_frame_pending_size_set(window_);

    changed = true;
  }

  if (scale_fractional_next != scale_fractional_curr) {
    window_->scale_fractional = scale_fractional_next;
    changed = true;

    /* As this is a low-level function, we might want adding this event to be optional,
     * always add the event unless it causes issues. */
    GHOST_SystemWayland *system = window_->ghost_system;
    system->pushEvent(
        new GHOST_Event(system->getMilliSeconds(), GHOST_kEventWindowDPIHintChanged, this));
  }

  return changed;
}

bool GHOST_WindowWayland::outputs_enter(GWL_Output *output)
{
  std::vector<GWL_Output *> &outputs = window_->outputs;
  auto it = std::find(outputs.begin(), outputs.end(), output);
  if (it != outputs.end()) {
    return false;
  }
  outputs.push_back(output);
  return true;
}

bool GHOST_WindowWayland::outputs_leave(GWL_Output *output)
{
  std::vector<GWL_Output *> &outputs = window_->outputs;
  auto it = std::find(outputs.begin(), outputs.end(), output);
  if (it == outputs.end()) {
    return false;
  }
  outputs.erase(it);
  return true;
}

#ifdef USE_EVENT_BACKGROUND_THREAD

void GHOST_WindowWayland::pending_actions_handle()
{
  /* Caller must lock `server_mutex`, while individual actions could lock,
   * it's simpler to lock once when handling all window actions. */
  GWL_Window *win = window_;
  GHOST_ASSERT(win->ghost_system->main_thread_id == std::this_thread::get_id(),
               "Run from main thread!");

  gwl_window_pending_actions_handle(win);
}

#endif /* USE_EVENT_BACKGROUND_THREAD */

/** \} */
