#pragma once
#include <cstdint>
#include <qflags.h>
#include <qfuture.h>
#include <qobject.h>
#include <qpromise.h>
#include <qstringview.h>
#include <QGuiApplication>
#include <QScreen>
#include <QWindow>
#include <ranges>
#include <vector>

/**
 * Abstract class from which all window manager implementations should inherit from.
 * Window managers vary in capability and as such, many of the methods in here are optional.
 * Only basic window manager interactions such as listing windows, focusing... are mandatory for all
 * implementations.
 */
class AbstractWindowManager : public QObject {
  Q_OBJECT

signals:
  /**
   * An event invalidating the current list of windows has occured and views
   * dealing with them should request an update. This typically occurs when a new window is created, closed,
   * or killed.
   */
  void windowsChanged() const;
  void focusChanged() const;

public:
  enum class Capability {
    Fullscreen = 1,
    Minimize = 1 << 1,
    ToggleFloating = 1 << 2,
    ToggleOverview = 1 << 3,
    SetSticky = 1 << 4
  };

  /**
   * Window geometry in Qt logical coordinates, composable with `Screen::bounds`.
   * The X11 backend currently reports raw native pixels instead.
   */
  struct WindowBounds {
    int32_t x = 0;
    int32_t y = 0;
    int32_t width = 0;
    int32_t height = 0;
  };

  struct Screen {
    QString name;

    /**
     * Screen geometry in Qt logical coordinates. Positions are meaningful across screens, but sizes are
     * affected by display scaling: use `physicalResolution` to get the real pixel size of the screen.
     */
    QRect bounds;

    /**
     * The real pixel size of the screen, unaffected by any kind of scaling.
     */
    QSize physicalResolution;

    QString manufacturer;
    QString model;
    std::optional<QString> serial;

    /**
     * Whether this screen currently displays the launcher window. Always false when the window
     * is not shown.
     */
    bool active = false;
  };

  /**
   * A Window from the current window manager.
   */
  class AbstractWindow {
  public:
    virtual ~AbstractWindow() = default;

    virtual QString id() const = 0;
    virtual QString title() const = 0;
    virtual QString wmClass() const = 0;

    /**
     * The pid of the process that owns that window, if such information is available.
     */
    virtual std::optional<int> pid() const { return std::nullopt; }

    virtual std::optional<QString> workspace() const { return std::nullopt; }
    virtual std::optional<WindowBounds> bounds() const { return std::nullopt; }
    virtual bool fullScreen() const { return false; }

    virtual bool canClose() const { return true; }
    virtual bool canFullScreen() const { return true; }
    virtual bool sticky() const { return false; }
  };

  class AbstractWorkspace {
  public:
    virtual ~AbstractWorkspace() = default;

    virtual QString id() const = 0;
    virtual QString name() const { return id(); }

    /**
     * The monitor this workspace belongs to. Workspaces that span all monitors (Windows virtual
     * desktops, X11 desktops) return nullopt.
     */
    virtual std::optional<QString> monitor() const { return std::nullopt; }
    virtual bool hasFullScreen() const { return false; }
  };

  using WindowPtr = std::shared_ptr<AbstractWindow>;
  using WindowList = std::vector<WindowPtr>;
  using WorkspacePtr = std::shared_ptr<AbstractWorkspace>;
  using WorkspaceList = std::vector<WorkspacePtr>;

public:
  ~AbstractWindowManager() override = default;

  /**
   * Unique identifier for this window manager.
   */
  virtual QString id() const = 0;

  /**
   * Window manager name to display in debug context. Unlike `id()` this does not need to return
   * a unique string. Defaults to `id()` if not reimplemented.
   */
  virtual QString displayName() const { return id(); }

  virtual WindowList listWindowsSync() const { return {}; };

  /**
   * List available screens, marking the one displaying `activeWindow` as active, if any.
   */
  virtual std::vector<Screen> listScreensSync(QWindow *activeWindow = nullptr) const {
    const QScreen *activeScreen =
        activeWindow && activeWindow->isVisible() ? activeWindow->screen() : nullptr;
    auto tr = [&](const QScreen *qtScreen) -> Screen {
      Screen sc{.name = qtScreen->name(),
                .bounds = qtScreen->geometry(),
                .physicalResolution = qtScreen->size() * qtScreen->devicePixelRatio(),
                .manufacturer = qtScreen->manufacturer(),
                .model = qtScreen->model()};
      if (auto serial = qtScreen->serialNumber(); !serial.isEmpty()) { sc.serial = serial; }
      sc.active = qtScreen == activeScreen;
      return sc;
    };
    return QGuiApplication::screens() | std::views::transform(tr) | std::ranges::to<std::vector>();
  }

  /**
   * Should return nullptr if there is no focused window. In particular, some wayland compositors may return
   * no focused window if focus was given to a layer shell surface, which is not a 'window' in wayland terms.
   *
   * If the window manager is unable to track the currently focused window, `supportsFocusTracking` must
   * return false.
   */
  virtual std::shared_ptr<AbstractWindow> getFocusedWindowSync() const { return nullptr; }

  /**
   * Whether the window manager is able to track what window is currently focused.
   * Note that a WM implementation can still implement `focusWindowSync` even if it
   * can't track the currently focused window.
   *
   * Most window managers should implement this, but there are a few exceptions, such as:
   * - KDE Plasma (WM implementation relies on an obscure dbus API)
   * - Gnome, if the vicinae gnome extension is not installed
   */
  virtual bool supportsFocusTracking() const { return false; }

  /**
   * Whether `getFocusedWindowSync` reliably reflects the launcher losing focus: while the launcher
   * holds focus it reports either nullptr or the launcher's own window, so consumers can poll for
   * the moment focus lands on the target app. Some wayland compositors (hyprland) keep reporting
   * the previously focused window while a layer shell surface has focus, making this impossible.
   */
  virtual bool supportsFocusHandoffDetection() const { return false; }

  virtual void focusWindowSync(const AbstractWindow &window) const {}

  virtual void focusWorkspaceSync(const AbstractWorkspace &workspace) const {}

  virtual QFlags<Capability> capabilities() const { return {}; }

  bool supports(Capability cap) const { return capabilities().testFlag(cap); }

  /**
   * Refresh the window list. No-op by default; poll-based implementations re-scan, event-driven ones that
   * stay current on their own can ignore it.
   */
  virtual void refresh() const {}

  /**
   * If this returns true, make sure to implement `workspaces` correctly and also
   * have every window return a correct workspace ID.
   */
  virtual bool hasWorkspaces() const { return false; }

  virtual WorkspaceList listWorkspaces() const { return {}; }

  /**
   * You can reimplement this if your window manager implementation has a more efficient way
   * of fetching windows for a specific workspace. In most cases, the performance impact is negligible.
   */
  virtual WindowList listWorkspaceWindows(const QString &workspaceId) {
    WindowList windows;

    for (const auto &win : listWindowsSync()) {
      if (win->workspace() != workspaceId) continue;
      windows.emplace_back(win);
    }

    return windows;
  }

  /**
   * The active workspace. If the window manager can be in a state where no workspace is active,
   * this should return null.
   */
  virtual WorkspacePtr getActiveWorkspace() const { return {}; }

  /**
   * Close a window. Returns true if successful, false otherwise.
   * This is a common operation that should be supported by all window managers.
   */
  virtual bool closeWindow(const AbstractWindow &window) const { return false; }

  virtual bool supportsSetSticky() const { return false; }
  virtual bool setSticky(const AbstractWindow &window, bool sticky) const { return false; }

  virtual bool setWindowBounds(const AbstractWindow &window, const WindowBounds &bounds) const {
    return false;
  }

  virtual bool supportsMoveToWorkspace() const { return false; }
  virtual bool moveToWorkspace(const AbstractWindow &window, const QString &workspaceId) const {
    return false;
  }

  /**
   * Switch the active workspace, without moving any window.
   */
  virtual bool supportsWorkspaceActivation() const { return false; }
  virtual bool activateWorkspace(const QString &workspaceId) const { return false; }

  virtual bool toggleFullscreen(const AbstractWindow &window) { return false; }

  /**
   * Toggle the target window from tiling to floating and vice-versa.
   * This mostly applies to tiling/scrolling WMs.
   */
  virtual bool toggleFloating(const AbstractWindow &window) { return false; }

  /**
   * Toggle the "overview" mode of the desktop, if supported.
   */
  virtual bool toggleOverview() { return false; }

  /**
   * To make sure the window manager IPC link is healthy.
   */
  virtual bool ping() const = 0;

  /**
   * Should determine whether this window manager can handle the current environment.
   * This is used to determine which window manager service to spawn at startup. Try to make this check as
   * precise as possible to make the best detection possible and avoid false positives.
   * For example, avoid just checking for wayland if you are dealing with a wayland compositor. Try to look
   * for a special environment variable or socket file that may be present.
   */
  virtual bool isActivatable() const = 0;

  /**
   * Called when the window manager is started, after it was deemed activatable for the current
   * environment.
   */
  virtual void start() = 0;

private:
};
