/**
 ******************************************************************************
 * Xenia : Xbox 360 Emulator Research Project                                 *
 ******************************************************************************
 * Copyright 2025 Ben Vanik. All rights reserved.                             *
 * Released under the BSD license - see LICENSE in the root for more details. *
 ******************************************************************************
 */

#include <X11/Xlib-xcb.h>
#include <gdk/gdkx.h>
#include <xcb/xcb.h>

#include "xenia/base/assert.h"
#include "xenia/base/logging.h"
#include "xenia/base/platform_linux.h"
#include "xenia/ui/surface_gnulinux.h"
#include "xenia/ui/virtual_key.h"
#include "xenia/ui/window_gtk.h"

namespace xe {
namespace ui {

std::unique_ptr<Window> Window::Create(WindowedAppContext& app_context,
                                       const std::string_view title,
                                       uint32_t desired_logical_width,
                                       uint32_t desired_logical_height) {
  return std::make_unique<GTKWindow>(app_context, title, desired_logical_width,
                                     desired_logical_height);
}

GTKWindow::GTKWindow(WindowedAppContext& app_context,
                     const std::string_view title,
                     uint32_t desired_logical_width,
                     uint32_t desired_logical_height)
    : Window(app_context, title, desired_logical_width,
             desired_logical_height) {}

GTKWindow::~GTKWindow() {
  EnterDestructor();
  if (window_) {
    // Set window_ to null to ignore events from now on since this ui::GTKWindow
    // is entering an indeterminate state.
    GtkWidget* window = window_;
    window_ = nullptr;
    // Destroying the top-level window also destroys its children.
    drawing_area_ = nullptr;
    box_ = nullptr;
    gtk_widget_destroy(window);
  }
}

bool GTKWindow::OpenImpl() {
  window_ = gtk_window_new(GTK_WINDOW_TOPLEVEL);

  gtk_window_set_title(GTK_WINDOW(window_), GetTitle().c_str());

  // Create the vertical box container for the main menu and the drawing area.
  box_ = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
  gtk_container_add(GTK_CONTAINER(window_), box_);

  // Add the main menu (even if fullscreen was requested, for the initial layout
  // calculation).
  const auto* main_menu = dynamic_cast<const GTKMenuItem*>(GetMainMenu());
  GtkWidget* main_menu_widget = main_menu ? main_menu->handle() : nullptr;
  if (main_menu_widget) {
    gtk_box_pack_start(GTK_BOX(box_), main_menu_widget, false, false, 0);
  }

  // Create the drawing area for creating the surface for, which will be the
  // client area of the window occupying all the window space not taken by the
  // main menu.
  drawing_area_ = gtk_drawing_area_new();
  gtk_box_pack_end(GTK_BOX(box_), drawing_area_, true, true, 0);
  // The desired size is the client (drawing) area size. Let GTK auto-size the
  // entire window around it (as well as the width of the menu actually if it
  // happens to be bigger - the desired size in the Window will be updated later
  // to reflect that).
  gtk_widget_set_size_request(drawing_area_, GetDesiredLogicalWidth(),
                              GetDesiredLogicalHeight());

  // Attach the event handlers.
  // Keyboard events are processed by the window, mouse events are processed
  // within, and by, the drawing (client) area.
  gtk_widget_set_events(window_, GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK |
                                     GDK_FOCUS_CHANGE_MASK);
  gtk_widget_set_events(drawing_area_,
                        GDK_POINTER_MOTION_MASK | GDK_BUTTON_MOTION_MASK |
                            GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
                            GDK_SCROLL_MASK);
  g_signal_connect(G_OBJECT(window_), "event",
                   G_CALLBACK(WindowEventHandlerThunk),
                   reinterpret_cast<gpointer>(this));
  g_signal_connect(G_OBJECT(drawing_area_), "event",
                   G_CALLBACK(DrawingAreaEventHandlerThunk),
                   reinterpret_cast<gpointer>(this));
  g_signal_connect(G_OBJECT(drawing_area_), "draw", G_CALLBACK(DrawHandler),
                   reinterpret_cast<gpointer>(this));

  // Finally show all the widgets in the window, including the main menu.
  gtk_widget_show_all(window_);

  // Remove the size request after finishing the initial layout because it makes
  // it impossible to make the window smaller.
  gtk_widget_set_size_request(drawing_area_, -1, -1);

  // After setting up the initial layout for non-fullscreen, enter fullscreen if
  // requested.
  if (IsFullscreen()) {
    if (main_menu_widget) {
      gtk_container_remove(GTK_CONTAINER(box_), main_menu_widget);
    }
    gtk_window_fullscreen(GTK_WINDOW(window_));
  }

  // Make sure the initial state after opening is reported to the common Window
  // class no matter how GTK sends the events.
  {
    WindowDestructionReceiver destruction_receiver(this);

    // TODO(Triang3l): Report the desired client area size.

    GtkAllocation drawing_area_allocation;
    gtk_widget_get_allocation(drawing_area_, &drawing_area_allocation);
    OnActualSizeUpdate(uint32_t(drawing_area_allocation.width),
                       uint32_t(drawing_area_allocation.height),
                       WindowResizeAction::kManual, destruction_receiver);
    if (destruction_receiver.IsWindowDestroyedOrClosed()) {
      return true;
    }

    if (gtk_window_has_toplevel_focus(GTK_WINDOW(window_))) {
      OnFocusUpdate(true, destruction_receiver);
      if (destruction_receiver.IsWindowDestroyedOrClosed()) {
        return true;
      }
    }
  }

  return true;
}

void GTKWindow::RequestCloseImpl() { gtk_window_close(GTK_WINDOW(window_)); }

void GTKWindow::ApplyNewFullscreen() {
  // Various functions here may trigger events that may result in the listeners
  // being invoked, and potentially cause the destruction of the window or
  // fullscreen being toggled from inside this function.
  WindowDestructionReceiver destruction_receiver(this);

  const auto* main_menu = dynamic_cast<const GTKMenuItem*>(GetMainMenu());
  GtkWidget* main_menu_widget = main_menu ? main_menu->handle() : nullptr;

  // Changing the menu and the fullscreen state may change the size of the
  // drawing area too, don't handle the resize multiple times (also potentially
  // with the listeners changing the desired fullscreen if called from the
  // handling of some event like GDK_CONFIGURE).
  BeginBatchedSizeUpdate();

  if (IsFullscreen()) {
    if (main_menu_widget) {
      gtk_container_remove(GTK_CONTAINER(box_), main_menu_widget);
      if (destruction_receiver.IsWindowDestroyedOrClosed()) {
        if (!destruction_receiver.IsWindowDestroyed()) {
          EndBatchedSizeUpdate(destruction_receiver);
        }
        return;
      }
    }
    gtk_window_fullscreen(GTK_WINDOW(window_));
    if (destruction_receiver.IsWindowDestroyedOrClosed()) {
      if (!destruction_receiver.IsWindowDestroyed()) {
        EndBatchedSizeUpdate(destruction_receiver);
      }
      return;
    }
  } else {
    gtk_window_unfullscreen(GTK_WINDOW(window_));
    if (destruction_receiver.IsWindowDestroyedOrClosed()) {
      if (!destruction_receiver.IsWindowDestroyed()) {
        EndBatchedSizeUpdate(destruction_receiver);
      }
      return;
    }
    if (main_menu_widget) {
      gtk_box_pack_start(GTK_BOX(box_), main_menu_widget, false, false, 0);
      if (destruction_receiver.IsWindowDestroyedOrClosed()) {
        if (!destruction_receiver.IsWindowDestroyed()) {
          EndBatchedSizeUpdate(destruction_receiver);
        }
        return;
      }
      // If the new menu is used for the first time, it will be in the hidden
      // state initially. The menu might have been changed while in fullscreen
      // without having shown it.
      gtk_widget_show_all(main_menu_widget);
      if (destruction_receiver.IsWindowDestroyedOrClosed()) {
        if (!destruction_receiver.IsWindowDestroyed()) {
          EndBatchedSizeUpdate(destruction_receiver);
        }
        return;
      }
    }
  }

  EndBatchedSizeUpdate(destruction_receiver);
  if (destruction_receiver.IsWindowDestroyedOrClosed()) {
    return;
  }
}

void GTKWindow::ApplyNewTitle() {
  gtk_window_set_title(GTK_WINDOW(window_), GetTitle().c_str());
}

void GTKWindow::ApplyNewMainMenu(MenuItem* old_main_menu) {
  if (IsFullscreen()) {
    // The menu will be set when exiting fullscreen.
    return;
  }
  // The fullscreen state may have been changed by some callback invoked, such
  // as the configure (resize) one, recheck it after making changes also.

  WindowDestructionReceiver destruction_receiver(this);

  // Changing the menu may change the size of the drawing area too, and here the
  // menu may be changed twice (to detach the old one and to attach the new),
  // don't handle the resize multiple times.
  BeginBatchedSizeUpdate();

  if (old_main_menu) {
    const GTKMenuItem& old_gtk_main_menu =
        *dynamic_cast<const GTKMenuItem*>(old_main_menu);
    gtk_container_remove(GTK_CONTAINER(box_), old_gtk_main_menu.handle());
    if (destruction_receiver.IsWindowDestroyedOrClosed() || IsFullscreen()) {
      if (!destruction_receiver.IsWindowDestroyed()) {
        EndBatchedSizeUpdate(destruction_receiver);
      }
      return;
    }
  }

  const auto* new_main_menu = dynamic_cast<const GTKMenuItem*>(GetMainMenu());
  if (!new_main_menu) {
    EndBatchedSizeUpdate(destruction_receiver);
    return;
  }
  GtkWidget* new_main_menu_widget = new_main_menu->handle();
  gtk_box_pack_start(GTK_BOX(box_), new_main_menu_widget, false, false, 0);
  if (destruction_receiver.IsWindowDestroyedOrClosed() || IsFullscreen()) {
    if (!destruction_receiver.IsWindowDestroyed()) {
      EndBatchedSizeUpdate(destruction_receiver);
    }
    return;
  }
  // If the new menu is used for the first time, it will be in the hidden state
  // initially.
  gtk_widget_show_all(new_main_menu_widget);
  if (destruction_receiver.IsWindowDestroyedOrClosed() || IsFullscreen()) {
    if (!destruction_receiver.IsWindowDestroyed()) {
      EndBatchedSizeUpdate(destruction_receiver);
    }
    return;
  }

  EndBatchedSizeUpdate(destruction_receiver);
  if (destruction_receiver.IsWindowDestroyedOrClosed()) {
    return;
  }
}

void GTKWindow::FocusImpl() { gtk_window_activate_focus(GTK_WINDOW(window_)); }

std::unique_ptr<Surface> GTKWindow::CreateSurfaceImpl(
    Surface::TypeFlags allowed_types) {
  GdkDisplay* display = gtk_widget_get_display(window_);
  GdkWindow* drawing_area_window = gtk_widget_get_window(drawing_area_);
  bool type_known = false;
  bool type_supported_by_display = false;
  if (allowed_types & Surface::kTypeFlag_XcbWindow) {
    type_known = true;
    if (GDK_IS_X11_DISPLAY(display)) {
      type_supported_by_display = true;
      return std::make_unique<XcbWindowSurface>(
          XGetXCBConnection(gdk_x11_display_get_xdisplay(display)),
          gdk_x11_window_get_xid(drawing_area_window));
    }
  }
  // TODO(Triang3l): Wayland surface.
  if (type_known && !type_supported_by_display) {
    XELOGE(
        "GTKWindow: The window system of the GTK window is not supported by "
        "Xenia");
  }
  return nullptr;
}

void GTKWindow::RequestPaintImpl() { gtk_widget_queue_draw(drawing_area_); }

void GTKWindow::HandleSizeUpdate(
    WindowDestructionReceiver& destruction_receiver) {
  if (!drawing_area_) {
    // Batched size update ended when the window has already been closed, for
    // instance.
    return;
  }

  // TODO(Triang3l): Report the desired client area size.

  GtkAllocation drawing_area_allocation;
  gtk_widget_get_allocation(drawing_area_, &drawing_area_allocation);
  OnActualSizeUpdate(uint32_t(drawing_area_allocation.width),
                     uint32_t(drawing_area_allocation.height),
                     WindowResizeAction::kManual, destruction_receiver);
  if (destruction_receiver.IsWindowDestroyedOrClosed()) {
    return;
  }
}

void GTKWindow::BeginBatchedSizeUpdate() {
  // It's okay if batched_size_update_contained_* are not false when beginning
  // a batched update, in case the new batched update was started by a window
  // listener called from within EndBatchedSizeUpdate.
  ++batched_size_update_depth_;
}

void GTKWindow::EndBatchedSizeUpdate(
    WindowDestructionReceiver& destruction_receiver) {
  assert_not_zero(batched_size_update_depth_);
  if (--batched_size_update_depth_) {
    return;
  }
  // Resetting batched_size_update_contained_* in closing, not opening, because
  // a listener may start a new batch, and finish it, and there won't be need to
  // handle the deferred messages twice.
  if (batched_size_update_contained_configure_) {
    batched_size_update_contained_configure_ = false;
    HandleSizeUpdate(destruction_receiver);
    if (destruction_receiver.IsWindowDestroyed()) {
      return;
    }
  }
  if (batched_size_update_contained_draw_) {
    batched_size_update_contained_draw_ = false;
    RequestPaint();
  }
}

VirtualKey GTKWindow::TranslateVirtualKey(guint keyval) {
  switch (keyval) {
    case GDK_KEY_a:
    case GDK_KEY_A:
      return VirtualKey::kA;
    case GDK_KEY_b:
    case GDK_KEY_B:
      return VirtualKey::kB;
    case GDK_KEY_c:
    case GDK_KEY_C:
      return VirtualKey::kC;
    case GDK_KEY_d:
    case GDK_KEY_D:
      return VirtualKey::kD;
    case GDK_KEY_e:
    case GDK_KEY_E:
      return VirtualKey::kE;
    case GDK_KEY_f:
    case GDK_KEY_F:
      return VirtualKey::kF;
    case GDK_KEY_g:
    case GDK_KEY_G:
      return VirtualKey::kG;
    case GDK_KEY_h:
    case GDK_KEY_H:
      return VirtualKey::kH;
    case GDK_KEY_i:
    case GDK_KEY_I:
      return VirtualKey::kI;
    case GDK_KEY_j:
    case GDK_KEY_J:
      return VirtualKey::kJ;
    case GDK_KEY_k:
    case GDK_KEY_K:
      return VirtualKey::kK;
    case GDK_KEY_l:
    case GDK_KEY_L:
      return VirtualKey::kL;
    case GDK_KEY_m:
    case GDK_KEY_M:
      return VirtualKey::kM;
    case GDK_KEY_n:
    case GDK_KEY_N:
      return VirtualKey::kN;
    case GDK_KEY_o:
    case GDK_KEY_O:
      return VirtualKey::kO;
    case GDK_KEY_p:
    case GDK_KEY_P:
      return VirtualKey::kP;
    case GDK_KEY_q:
    case GDK_KEY_Q:
      return VirtualKey::kQ;
    case GDK_KEY_r:
    case GDK_KEY_R:
      return VirtualKey::kR;
    case GDK_KEY_s:
    case GDK_KEY_S:
      return VirtualKey::kS;
    case GDK_KEY_t:
    case GDK_KEY_T:
      return VirtualKey::kT;
    case GDK_KEY_u:
    case GDK_KEY_U:
      return VirtualKey::kU;
    case GDK_KEY_v:
    case GDK_KEY_V:
      return VirtualKey::kV;
    case GDK_KEY_w:
    case GDK_KEY_W:
      return VirtualKey::kW;
    case GDK_KEY_x:
    case GDK_KEY_X:
      return VirtualKey::kX;
    case GDK_KEY_y:
    case GDK_KEY_Y:
      return VirtualKey::kY;
    case GDK_KEY_z:
    case GDK_KEY_Z:
      return VirtualKey::kZ;
    case GDK_KEY_0:
      return VirtualKey::k0;
    case GDK_KEY_1:
      return VirtualKey::k1;
    case GDK_KEY_2:
      return VirtualKey::k2;
    case GDK_KEY_3:
      return VirtualKey::k3;
    case GDK_KEY_4:
      return VirtualKey::k4;
    case GDK_KEY_5:
      return VirtualKey::k5;
    case GDK_KEY_6:
      return VirtualKey::k6;
    case GDK_KEY_7:
      return VirtualKey::k7;
    case GDK_KEY_8:
      return VirtualKey::k8;
    case GDK_KEY_9:
      return VirtualKey::k9;
    case GDK_KEY_semicolon:
      return VirtualKey::kOem1;
    case GDK_KEY_apostrophe:
      return VirtualKey::kOem7;
    case GDK_KEY_comma:
      return VirtualKey::kOemComma;
    case GDK_KEY_period:
      return VirtualKey::kOemPeriod;
    case GDK_KEY_Up:
      return VirtualKey::kUp;
    case GDK_KEY_Down:
      return VirtualKey::kDown;
    case GDK_KEY_Left:
      return VirtualKey::kLeft;
    case GDK_KEY_Right:
      return VirtualKey::kRight;
    case GDK_KEY_BackSpace:
      return VirtualKey::kBack;
    case GDK_KEY_Tab:
      return VirtualKey::kTab;
    case GDK_KEY_Return:
      return VirtualKey::kReturn;
    case GDK_KEY_Control_L:
      return VirtualKey::kLControl;
    case GDK_KEY_Control_R:
      return VirtualKey::kRControl;
    case GDK_KEY_Alt_L:
      return VirtualKey::kLMenu;
    case GDK_KEY_Alt_R:
      return VirtualKey::kRMenu;
    case GDK_KEY_Shift_L:
      return VirtualKey::kLShift;
    case GDK_KEY_Shift_R:
      return VirtualKey::kRShift;
    case GDK_KEY_space:
      return VirtualKey::kSpace;
    case GDK_KEY_Caps_Lock:
      return VirtualKey::kCapital;
    case GDK_KEY_Escape:
      return VirtualKey::kEscape;
    case GDK_KEY_F1:
      return VirtualKey::kF1;
    case GDK_KEY_F2:
      return VirtualKey::kF2;
    case GDK_KEY_F3:
      return VirtualKey::kF3;
    case GDK_KEY_F4:
      return VirtualKey::kF4;
    case GDK_KEY_F5:
      return VirtualKey::kF5;
    case GDK_KEY_F6:
      return VirtualKey::kF6;
    case GDK_KEY_F7:
      return VirtualKey::kF7;
    case GDK_KEY_F8:
      return VirtualKey::kF8;
    case GDK_KEY_F9:
      return VirtualKey::kF9;
    case GDK_KEY_F10:
      return VirtualKey::kF10;
    case GDK_KEY_F11:
      return VirtualKey::kF11;
    case GDK_KEY_F12:
      return VirtualKey::kF12;
    case GDK_KEY_KP_Multiply:
      return VirtualKey::kMultiply;
    case GDK_KEY_KP_Add:
      return VirtualKey::kAdd;
    case GDK_KEY_KP_Subtract:
      return VirtualKey::kSubtract;
    case GDK_KEY_KP_Divide:
      return VirtualKey::kDivide;
    case GDK_KEY_Pause:
    case GDK_KEY_Break:
      return VirtualKey::kPause;
    default:
      XELOGW("Unhandled key code: {}", keyval);
      return VirtualKey(keyval);
  }
}

bool GTKWindow::HandleMouse(GdkEvent* event,
                            WindowDestructionReceiver& destruction_receiver) {
  MouseEvent::Button button = MouseEvent::Button::kNone;
  int32_t x = 0;
  int32_t y = 0;
  int32_t scroll_x = 0;
  int32_t scroll_y = 0;
  switch (event->type) {
    case GDK_MOTION_NOTIFY: {
      auto motion_event = reinterpret_cast<const GdkEventMotion*>(event);
      x = motion_event->x;
      y = motion_event->y;
    } break;
    case GDK_BUTTON_PRESS:
    case GDK_BUTTON_RELEASE: {
      auto button_event = reinterpret_cast<const GdkEventButton*>(event);
      switch (button_event->button) {
        case 1:
          button = MouseEvent::Button::kLeft;
          break;
        case 2:
          button = MouseEvent::Button::kMiddle;
          break;
        case 3:
          button = MouseEvent::Button::kRight;
          break;
        case 4:
          button = MouseEvent::Button::kX1;
          break;
        case 5:
          button = MouseEvent::Button::kX2;
          break;
        default:
          // Still handle the movement.
          break;
      }
      x = button_event->x;
      y = button_event->y;
    } break;
    case GDK_SCROLL: {
      auto scroll_event = reinterpret_cast<const GdkEventScroll*>(event);
      x = scroll_event->x;
      y = scroll_event->y;
      scroll_x = scroll_event->delta_x * MouseEvent::kScrollPerDetent;
      // In GDK, positive is towards the bottom of the screen, not forward from
      // the user.
      scroll_y = -scroll_event->delta_y * MouseEvent::kScrollPerDetent;
    } break;
    default:
      return false;
  }

  MouseEvent e(this, button, x, y, scroll_x, scroll_y);
  switch (event->type) {
    case GDK_MOTION_NOTIFY:
      OnMouseMove(e, destruction_receiver);
      break;
    case GDK_BUTTON_PRESS:
      OnMouseDown(e, destruction_receiver);
      break;
    case GDK_BUTTON_RELEASE:
      OnMouseUp(e, destruction_receiver);
      break;
    case GDK_SCROLL:
      OnMouseWheel(e, destruction_receiver);
      break;
    default:
      break;
  }
  // Returning immediately anyway - no need to check
  // destruction_receiver.IsWindowDestroyed().
  return e.is_handled();
}

bool GTKWindow::HandleKeyboard(
    GdkEventKey* event, WindowDestructionReceiver& destruction_receiver) {
  unsigned int modifiers = event->state;
  bool shift_pressed = modifiers & GDK_SHIFT_MASK;
  bool ctrl_pressed = modifiers & GDK_CONTROL_MASK;
  bool alt_pressed = modifiers & GDK_META_MASK;
  bool super_pressed = modifiers & GDK_SUPER_MASK;

  // Translate GTK to VK
  VirtualKey vk = TranslateVirtualKey(event->keyval);
  uint32_t unicode_char = gdk_keyval_to_unicode(event->keyval);

  bool is_key_pressed = false;

  // Backspace has unicode value but is not printable therefore we want
  // OnKeyDown not OnKeyChar
  if (unicode_char > 0) {
    if (std::isprint(unicode_char)) {
      is_key_pressed = true;
      vk = static_cast<VirtualKey>(unicode_char);
    }
  }

  KeyEvent e(this, vk, 1, event->type == GDK_KEY_RELEASE, shift_pressed,
             ctrl_pressed, alt_pressed, super_pressed);
  switch (event->type) {
    case GDK_KEY_PRESS:
      if (is_key_pressed) {
        OnKeyChar(e, destruction_receiver);
      } else {
        OnKeyDown(e, destruction_receiver);

        if (destruction_receiver.IsWindowDestroyedOrClosed()) {
          return e.is_handled();
        }
      }
      break;
    case GDK_KEY_RELEASE:
      OnKeyUp(e, destruction_receiver);
      break;
    default:
      break;
  }
  // Returning immediately anyway - no need to check
  // destruction_receiver.IsWindowDestroyed().
  return e.is_handled();
}

gboolean GTKWindow::WindowEventHandler(GdkEvent* event) {
  switch (event->type) {
    case GDK_DELETE:
    // In case the widget was somehow forcibly destroyed without GDK_DELETE.
    case GDK_DESTROY: {
      WindowDestructionReceiver destruction_receiver(this);
      OnBeforeClose(destruction_receiver);
      if (destruction_receiver.IsWindowDestroyed()) {
        break;
      }
      // Set window_ to null to ignore events from now on since this
      // ui::GTKWindow is entering an indeterminate state - this should be done
      // at some point in closing anyway.
      GtkWidget* window = window_;
      window_ = nullptr;
      // Destroying the top-level window also destroys its children.
      drawing_area_ = nullptr;
      box_ = nullptr;
      if (event->type != GDK_DESTROY) {
        gtk_widget_destroy(window);
      }
      OnAfterClose();
    } break;

    case GDK_FOCUS_CHANGE: {
      auto focus_event = reinterpret_cast<const GdkEventFocus*>(event);
      WindowDestructionReceiver destruction_receiver(this);
      OnFocusUpdate(bool(focus_event->in), destruction_receiver);
      if (destruction_receiver.IsWindowDestroyedOrClosed()) {
        break;
      }
    } break;

    case GDK_KEY_PRESS:
    case GDK_KEY_RELEASE: {
      WindowDestructionReceiver destruction_receiver(this);
      HandleKeyboard(reinterpret_cast<GdkEventKey*>(event),
                     destruction_receiver);
      if (destruction_receiver.IsWindowDestroyedOrClosed()) {
        break;
      }
    } break;

    default:
      break;
  }

  // The window might have been destroyed by the handlers, don't interact with
  // *this in this function from now on.

  return GDK_EVENT_PROPAGATE;
}

gboolean GTKWindow::WindowEventHandlerThunk(GtkWidget* widget, GdkEvent* event,
                                            gpointer user_data) {
  auto* window = static_cast<GTKWindow*>(user_data);
  if (!window || widget != window->window_ ||
      reinterpret_cast<const GdkEventAny*>(event)->window !=
          gtk_widget_get_window(window->window_)) {
    return GDK_EVENT_PROPAGATE;
  }
  return window->WindowEventHandler(event);
}

gboolean GTKWindow::DrawingAreaEventHandler(GdkEvent* event) {
  switch (event->type) {
    case GDK_MOTION_NOTIFY:
    case GDK_BUTTON_PRESS:
    case GDK_BUTTON_RELEASE:
    case GDK_SCROLL: {
      WindowDestructionReceiver destruction_receiver(this);
      HandleMouse(event, destruction_receiver);
      if (destruction_receiver.IsWindowDestroyedOrClosed()) {
        break;
      }
    } break;

    case GDK_CONFIGURE: {
      if (batched_size_update_depth_) {
        batched_size_update_contained_configure_ = true;
      } else {
        WindowDestructionReceiver destruction_receiver(this);
        HandleSizeUpdate(destruction_receiver);
        if (destruction_receiver.IsWindowDestroyedOrClosed()) {
          break;
        }
      }
    } break;
    default:
      break;
  }

  // The window might have been destroyed by the handlers, don't interact with
  // *this in this function from now on.

  return GDK_EVENT_PROPAGATE;
}

gboolean GTKWindow::DrawingAreaEventHandlerThunk(GtkWidget* widget,
                                                 GdkEvent* event,
                                                 gpointer user_data) {
  auto* window = static_cast<GTKWindow*>(user_data);
  if (!window || widget != window->drawing_area_ ||
      reinterpret_cast<const GdkEventAny*>(event)->window !=
          gtk_widget_get_window(window->drawing_area_)) {
    return GDK_EVENT_PROPAGATE;
  }
  return window->DrawingAreaEventHandler(event);
}

gboolean GTKWindow::DrawHandler(GtkWidget* widget, cairo_t* cr,
                                gpointer user_data) {
  auto* window = static_cast<GTKWindow*>(user_data);
  if (!window || widget != window->drawing_area_) {
    return false;
  }
  if (window->batched_size_update_depth_) {
    window->batched_size_update_contained_draw_ = true;
  } else {
    window->OnPaint();
  }
  return true;
}

std::unique_ptr<ui::MenuItem> MenuItem::Create(Type type,
                                               const std::string& text,
                                               const std::string& hotkey,
                                               std::function<void()> callback) {
  return std::make_unique<GTKMenuItem>(type, text, hotkey, callback);
}

void GTKMenuItem::ActivateHandler(GtkWidget* menu_item, gpointer user_data) {
  static_cast<GTKMenuItem*>(user_data)->OnSelected();
  // The menu item might have been destroyed by its OnSelected, don't do
  // anything with it here from now on.
}

GTKMenuItem::GTKMenuItem(Type type, const std::string& text,
                         const std::string& hotkey,
                         std::function<void()> callback)
    : MenuItem(type, text, hotkey, std::move(callback)) {
  std::string label = text;
  // TODO(dougvj) Would we ever need to escape underscores?
  // Replace & with _ for gtk to see the memonic
  std::replace(label.begin(), label.end(), '&', '_');
  const auto* gtk_label = label.c_str();
  switch (type) {
    case MenuItem::Type::kNormal:
    default:
      menu_ = gtk_menu_bar_new();
      break;
    case MenuItem::Type::kPopup:
      menu_ = gtk_menu_item_new_with_mnemonic(gtk_label);
      break;
    case MenuItem::Type::kSeparator:
      menu_ = gtk_separator_menu_item_new();
      break;
    case MenuItem::Type::kString:
      if (!hotkey.empty()) {
        // Create a box to hold the label and the accelerator
        GtkWidget* box = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);

        // Create the main label with mnemonic
        GtkWidget* label_widget = gtk_label_new_with_mnemonic(gtk_label);
        gtk_label_set_xalign(GTK_LABEL(label_widget), 0.0);
        gtk_box_pack_start(GTK_BOX(box), label_widget, FALSE, FALSE, 0);

        // Create the accelerator label (right-aligned)
        GtkWidget* accel_label = gtk_label_new(hotkey.c_str());
        gtk_widget_set_margin_start(accel_label, 20);
        gtk_box_pack_end(GTK_BOX(box), accel_label, FALSE, FALSE, 0);

        // Create menu item and add the box
        menu_ = gtk_menu_item_new();
        gtk_container_add(GTK_CONTAINER(menu_), box);
        gtk_widget_show_all(box);
      } else {
        menu_ = gtk_menu_item_new_with_mnemonic(gtk_label);
      }
      break;
  }
  if (menu_) {
    // Own the object because it may be detached from and re-attached to a
    // Window.
    g_object_ref_sink(menu_);
    if (GTK_IS_MENU_ITEM(menu_)) {
      g_signal_connect(menu_, "activate", G_CALLBACK(ActivateHandler), this);
    }
  }
}

GTKMenuItem::~GTKMenuItem() {
  if (menu_) {
    g_object_unref(menu_);
  }
}

void GTKMenuItem::OnChildAdded(MenuItem* generic_child_item) {
  auto child_item = dynamic_cast<GTKMenuItem*>(generic_child_item);
  GtkWidget* submenu = nullptr;
  switch (child_item->type()) {
    case Type::kNormal:
      // Nothing special.
      break;
    case Type::kPopup:
      if (GTK_IS_MENU_ITEM(menu_)) {
        submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_));
        // Get sub menu and if it doesn't exist create it
        if (submenu == nullptr) {
          submenu = gtk_menu_new();
          gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_), submenu);
        }
        gtk_menu_shell_append(GTK_MENU_SHELL(submenu), child_item->handle());
      } else {
        gtk_menu_shell_append(GTK_MENU_SHELL(menu_), child_item->handle());
      }
      break;
    case Type::kSeparator:
    case Type::kString:
      assert(GTK_IS_MENU_ITEM(menu_));
      // Get sub menu and if it doesn't exist create it
      submenu = gtk_menu_item_get_submenu(GTK_MENU_ITEM(menu_));
      if (submenu == nullptr) {
        submenu = gtk_menu_new();
        gtk_menu_item_set_submenu(GTK_MENU_ITEM(menu_), submenu);
      }
      gtk_menu_shell_append(GTK_MENU_SHELL(submenu), child_item->handle());
      break;
  }
}

void GTKMenuItem::OnChildRemoved(MenuItem* generic_child_item) {}

}  // namespace ui

}  // namespace xe
