#pragma once

#include "wayland/surface.h"

#include <cstdint>
#include <functional>
#include <memory>

struct wl_output;
struct xdg_popup;
struct xdg_surface;
struct zwlr_layer_surface_v1;

struct PopupSurfaceConfig {
  std::int32_t anchorX = 0;
  std::int32_t anchorY = 0;
  std::int32_t anchorWidth = 1;
  std::int32_t anchorHeight = 1;
  std::uint32_t width = 1;
  std::uint32_t height = 1;
  std::uint32_t anchor = 0;
  std::uint32_t gravity = 0;
  std::uint32_t constraintAdjustment = 0;
  std::int32_t offsetX = 0;
  std::int32_t offsetY = 0;
  std::uint32_t serial = 0;
  bool grab = true;
  bool reactive = false;
};

class PopupSurface : public Surface {
public:
  explicit PopupSurface(WaylandConnection& connection);
  ~PopupSurface() override;

  using Surface::initialize;
  bool initialize() override { return false; }
  bool initialize(zwlr_layer_surface_v1* parentLayerSurface, wl_output* output, PopupSurfaceConfig config);
  bool initializeAsChild(xdg_surface* parentXdgSurface, wl_output* output, PopupSurfaceConfig config);
  // When commit is false, the geometry change (size, viewport destination, reposition) is left
  // pending on the wl_surface so the caller can publish it atomically with the next rendered
  // buffer. Committing here with the previous buffer still attached makes the compositor scale
  // that stale buffer to the new geometry (visible as a stretched popup on NVIDIA).
  bool resize(std::uint32_t width, std::uint32_t height, bool commit = true);
  bool repositionAnchor(const PopupSurfaceConfig& anchorConfig, bool commit = true);

  void setDismissedCallback(std::function<void()> callback);

  [[nodiscard]] xdg_surface* xdgSurface() const noexcept { return m_xdgSurface; }
  [[nodiscard]] std::int32_t configuredX() const noexcept { return m_configuredX; }
  [[nodiscard]] std::int32_t configuredY() const noexcept { return m_configuredY; }

  static void handleXdgSurfaceConfigure(void* data, xdg_surface* surface, std::uint32_t serial);
  static void handlePopupConfigure(
      void* data, xdg_popup* popup, std::int32_t x, std::int32_t y, std::int32_t width, std::int32_t height
  );
  static void handlePopupDone(void* data, xdg_popup* popup);
  static void handlePopupRepositioned(void* data, xdg_popup* popup, std::uint32_t token);

private:
  void destroyRoleObjects();
  // Decide whether to take xdg_popup_grab vs. enroll in the active focus_grab
  // host. Sets m_enrolledInGrabHost when we registered with the host.
  void wireGrab();
  void unenrollFromGrabHost();

  PopupSurfaceConfig m_config;
  xdg_surface* m_xdgSurface = nullptr;
  xdg_popup* m_popup = nullptr;
  std::function<void()> m_dismissedCallback;
  std::uint32_t m_pendingWidth = 0;
  std::uint32_t m_pendingHeight = 0;
  std::uint32_t m_repositionToken = 0;
  std::int32_t m_configuredX = 0;
  std::int32_t m_configuredY = 0;
  bool m_enrolledInGrabHost = false;
  // Set false by the destructor. The init roundtrip re-enters event dispatch and can
  // destroy this popup mid-init; a captured copy of this token lets init detect that
  // and avoid touching freed `this`.
  std::shared_ptr<bool> m_alive = std::make_shared<bool>(true);
};
