#pragma once

#include "compositors/compositor_platform.h"
#include "core/timer_manager.h"
#include "shell/bar/widget.h"
#include "system/desktop_entry.h"
#include "system/icon_resolver.h"
#include "ui/palette.h"
#include "ui/signal.h"

#include <cstdint>
#include <limits>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>

class ContextMenuPopup;
class ConfigService;
class Box;
class Flex;
class InputArea;
class Label;
class TaskbarWidgetTestAccess;
struct wl_output;
struct zwlr_foreign_toplevel_handle_v1;

enum class WorkspaceLabelPlacement {
  Corner,
  Centered,
  Inside,
};

enum class WorkspaceGroupContent {
  Icons,
  Count,
  Dots,
};

struct TaskbarWidgetOptions {
  bool groupByWorkspace = false;
  bool showAllOutputs = false;
  bool onlyActiveWorkspace = false;
  bool showWorkspaceLabel = true;
  WorkspaceLabelPlacement workspaceLabelPlacement = WorkspaceLabelPlacement::Corner;
  WorkspaceGroupContent workspaceGroupContent = WorkspaceGroupContent::Icons;
  bool hideEmptyWorkspaces = false;
  bool workspaceGroupCapsule = true;
  bool focusedOutputOnly = false;
  bool minimal = false;
  bool groupSingleIconPerApp = false;
  bool showActiveIndicator = true;
  ColorSpec activeIndicatorColor = colorSpecFromRole(ColorRole::Primary);
  float activeOpacity = 1.0F;
  float inactiveOpacity = 1.0F;
  float iconScale = 1.0F;
  int itemSpacing = 4;
  std::vector<std::string> pinned;
  float pinnedOpacity = 0.5F;
  ColorSpec focusedColor = colorSpecFromRole(ColorRole::Primary);
  ColorSpec occupiedColor = colorSpecFromRole(ColorRole::Secondary);
  ColorSpec emptyColor = colorSpecFromRole(ColorRole::Secondary);
  ColorSpec urgentColor = colorSpecFromRole(ColorRole::Error);
  bool showWindowTitle = false;
  int windowTitleMaxWidth = 100;
  int taskbarMaxWidth = 8192;
};

struct TaskbarWidgetContext {
  std::string barPosition;
  std::string barName;
  // Config key for [widget.<name>] overrides.
  std::string widgetName;
};

class TaskbarWidget : public Widget {
public:
  TaskbarWidget(
      CompositorPlatform& platform, ConfigService& config, wl_output* output, TaskbarWidgetOptions options,
      TaskbarWidgetContext context
  );
  ~TaskbarWidget() override;

  void create() override;
  [[nodiscard]] bool onPointerEvent(const PointerEvent& event) override;
  [[nodiscard]] bool wantsBarHoverHighlight() const noexcept override { return false; }
  // Steps through whatever this taskbar shows: workspace groups when grouping, else tasks.
  void cycleAdjacent(int direction);

private:
  friend class TaskbarWidgetTestAccess;

  struct TaskModel {
    std::uintptr_t handleKey = 0;
    std::uint64_t order = 0;
    std::string appId;
    std::string idLower;
    std::string startupWmClassLower;
    std::string nameLower;
    std::string appIdLower;
    std::string title;
    std::string iconPath;
    std::string workspaceKey;
    std::string workspaceWindowId;
    // Authoritative compositor window identity for focus/close actions.
    // Unlike workspaceWindowId, never rewritten by workspace-placement
    // reconciliation. Empty on compositors without exact identity.
    std::string exactWindowId;
    // Desktop entry id used for pin persistence / launch (empty for unmatched windows).
    std::string desktopEntryId;
    std::uint64_t workspaceOrder = std::numeric_limits<std::uint64_t>::max();
    std::size_t instanceCount = 1;
    bool active = false;
    // Pinned flat-strip slot (empty when not running). Ignored while group_by_workspace is on.
    bool pinned = false;
    bool running = true;
    zwlr_foreign_toplevel_handle_v1* firstHandle = nullptr;
  };

  struct WorkspaceModel {
    Workspace workspace;
    std::string key;
    std::string label;
    wl_output* hostOutput = nullptr;
  };

  struct PendingWorkspaceTransition {
    std::string targetWorkspaceKey;
    std::uint8_t votes = 0;
  };

  struct ModelComparison {
    bool layoutEqual = false;
    // Meaningful only when layoutEqual is true and the existing task tiles will be retained.
    bool titlesChanged = false;
    bool activesChanged = false;
  };

  struct TaskRef {
    std::size_t index = 0;
    std::uint64_t generation = 0;
  };

  struct TaskTile {
    std::size_t taskIndex = 0;
    InputArea* area = nullptr;
    // Flat mode with visible titles; null otherwise.
    Label* titleLabel = nullptr;
    // Present when m_showActiveIndicator; visibility tracks task.active.
    Box* activeIndicator = nullptr;
  };

  // Gesture state for drag-to-reorder of pinned tiles in the flat strip. Holds no obligation to
  // restore the scene: that lives in m_dragFloatTile / m_dragSpacer, so resetting a gesture can
  // never leave a tile stranded outside the layout flow.
  struct DragState {
    bool active = false;
    bool armed = false; // hold fired; goes active on the next motion
    std::size_t sourceIndex = 0;
    std::size_t targetIndex = 0;
    // m_taskGeneration when the drag began; a rebuild invalidates the indices above.
    std::uint64_t generation = 0;
    // Pointer position along the strip's layout axis: x when horizontal, y when vertical.
    float startMain = 0.0F;
    float currentMain = 0.0F;
    // The held tile; also the "this tile owns the gesture" marker for motion and cancel.
    InputArea* area = nullptr;
    // Resting position of the held tile, captured before it left the layout flow.
    float restMain = 0.0F;
    float restCross = 0.0F;
    std::size_t pinnedCount = 0; // pin count when the drag began; bounds the travel range
    Timer holdTimer;
  };

  void doLayout(Renderer& renderer, float containerWidth, float containerHeight) override;
  void doUpdate(Renderer& renderer) override;

  void rebuild(Renderer& renderer);
  void clearChildren(Flex* flex) const;
  void buildTaskButtons(Renderer& renderer);
  void updateModels();
  void syncWorkspaceGroupingCapability();
  [[nodiscard]] static std::string toLower(std::string value);
  [[nodiscard]] static std::string workspaceLabel(const Workspace& workspace, std::size_t index);
  [[nodiscard]] static ModelComparison compareModels(
      bool groupByWorkspace, const std::vector<TaskModel>& previousTasks,
      const std::vector<WorkspaceModel>& previousWorkspaces, const std::vector<TaskModel>& nextTasks,
      const std::vector<WorkspaceModel>& nextWorkspaces
  );
  void buildDesktopIconIndex();
  [[nodiscard]] std::string resolveIconPath(const std::string& appId, const std::string& iconNameOrPath);
  void openTaskContextMenu(const TaskModel& task, InputArea& area);
  void activateAdjacentWorkspace(int direction);
  void activateAdjacentTask(int direction);
  [[nodiscard]] bool activeWorkspaceIndex(std::size_t& index) const;
  [[nodiscard]] const std::vector<WorkspaceModel>& navigationWorkspaces() const noexcept;
  [[nodiscard]] wl_output* toplevelOutputFilter() const noexcept;
  [[nodiscard]] bool useMultiOutputWorkspaceKeys() const noexcept;
  [[nodiscard]] std::string workspaceKeyPrefixForOutput(wl_output* out) const;
  [[nodiscard]] wl_output* workspaceHostOutput(const WorkspaceModel& model) const noexcept;
  [[nodiscard]] ColorSpec workspaceFillColor(const Workspace& workspace) const;
  [[nodiscard]] ColorSpec workspaceTextColor(const Workspace& workspace) const;
  [[nodiscard]] bool isFocusedOutput() const;
  [[nodiscard]] static ColorSpec readableColorForFill(const ColorSpec& fill);
  [[nodiscard]] static ColorRole onRoleForFill(ColorRole fill);
  [[nodiscard]] static bool taskInWorkspaceGroup(const TaskModel& task, const WorkspaceModel& ws);
  [[nodiscard]] static const TaskModel*
  resolveTask(const std::vector<TaskModel>& tasks, TaskRef ref, std::uint64_t currentGeneration);
  [[nodiscard]] static std::string_view workspaceBindingWindowId(const TaskModel& task);
  void activateTaskModel(const TaskModel& task);
  void closeTaskModel(const TaskModel& task);
  void applyPinnedMerge(std::vector<TaskModel>& tasks);
  void activateOrLaunchPinned(const TaskModel& task);
  void launchDesktopEntry(const TaskModel& task);
  [[nodiscard]] const std::vector<std::string>& pinnedConfigIds() const noexcept;
  [[nodiscard]] bool reorderEnabled() const;
  [[nodiscard]] float pointerMainOnStrip(const InputArea& area, float localX, float localY) const;
  [[nodiscard]] std::size_t computeDragTargetIndex() const;
  [[nodiscard]] bool commitDragReorder();
  void beginDrag();
  void updateDragTarget();
  void moveDragTile();
  void endDrag(bool commit);
  void applyDragLayout();
  void requestDragLayout();
  [[nodiscard]] static bool taskMatchesDesktopEntry(const TaskModel& task, const DesktopEntry& entry);
  void setEntryPinned(const DesktopEntry& entry, bool pinned);
  [[nodiscard]] std::optional<DesktopEntry> desktopEntryForTask(const TaskModel& task) const;

  CompositorPlatform& m_platform;
  ConfigService& m_configService;
  wl_output* m_output = nullptr;
  TaskbarWidgetOptions m_configOptions;
  bool m_groupByWorkspace = false;
  bool m_showAllOutputs = false;
  bool m_onlyActiveWorkspace = false;
  bool m_showWorkspaceLabel = true;
  WorkspaceLabelPlacement m_workspaceLabelPlacement = WorkspaceLabelPlacement::Corner;
  WorkspaceGroupContent m_workspaceGroupContent = WorkspaceGroupContent::Icons;
  bool m_hideEmptyWorkspaces = false;
  bool m_workspaceGroupCapsule = true;
  bool m_focusedOutputOnly = false;
  bool m_wasFocusedOutput = true;
  bool m_activeUsesFocusedColor = true;
  bool m_minimal = false;
  bool m_groupSingleIconPerApp = false;
  bool m_showActiveIndicator = true;
  ColorSpec m_activeIndicatorColor = colorSpecFromRole(ColorRole::Primary);
  float m_activeOpacity = 1.0F;
  float m_inactiveOpacity = 1.0F;
  float m_pinnedOpacity = 0.5F;
  ColorSpec m_focusedColor = colorSpecFromRole(ColorRole::Primary);
  ColorSpec m_occupiedColor = colorSpecFromRole(ColorRole::Secondary);
  ColorSpec m_emptyColor = colorSpecFromRole(ColorRole::Secondary);
  ColorSpec m_urgentColor = colorSpecFromRole(ColorRole::Error);
  bool m_showWindowTitle = false;
  float m_windowTitleMaxWidth = 100.0;
  float m_taskbarMaxWidth = 8192.0;
  std::string m_barPosition;
  std::string m_barName;
  std::string m_widgetName;
  DragState m_drag;
  // Drag visuals, owned by the Layout phase and outliving the gesture: the tile currently lifted
  // out of the flow, and the placeholder holding its drop gap open (the strip owns the node).
  InputArea* m_dragFloatTile = nullptr;
  Box* m_dragSpacer = nullptr;
  // Set by a committed drop: layout must hold the tile parked in its target gap until the deferred
  // pin-list write lands, otherwise it snaps back to the pre-write order for a frame.
  bool m_dragParked = false;
  bool m_suppressTileClick = false;
  // Guards deferred callbacks: writing the pin list reloads the bar, which destroys this widget.
  std::shared_ptr<void> m_aliveGuard = std::make_shared<int>(0);

  float m_tilePitchMain = 0.0F; // Main-axis distance between adjacent tiles; set while building the flat strip.
  bool m_rebuildPending = true;
  bool m_vertical = false;
  float m_containerWidth = 0.0F;
  float m_containerHeight = 0.0F;
  std::uint64_t m_textMetricsGeneration = 0;

  Flex* m_root = nullptr;
  Flex* m_taskStrip = nullptr;

  std::vector<TaskModel> m_tasks;
  // Retained controls resolve task indices only while this model generation matches.
  std::uint64_t m_taskGeneration = 0;
  // Non-owning; cleared before task-strip children are destroyed.
  std::vector<TaskTile> m_taskTiles;
  std::vector<WorkspaceModel> m_workspaces;
  // Full workspace list before "hide empty" filtering; used for scroll navigation.
  std::vector<WorkspaceModel> m_allWorkspaces;
  std::unordered_map<std::uintptr_t, PendingWorkspaceTransition> m_pendingWorkspaceTransitions;
  std::unordered_map<std::string, std::size_t> m_groupedAppCycleCursor;
  std::unordered_map<std::string, std::string> m_appIconsByLower;
  std::unique_ptr<ContextMenuPopup> m_contextMenuPopup;
  std::vector<zwlr_foreign_toplevel_handle_v1*> m_contextMenuHandles;
  zwlr_foreign_toplevel_handle_v1* m_contextMenuPrimaryHandle = nullptr;
  // KDE and Niri ext-foreign-toplevel tasks close through ToplevelInfo rather
  // than a wlr foreign-toplevel handle.
  std::vector<ToplevelInfo> m_contextMenuInfoWindows;
  ToplevelInfo m_contextMenuInfoPrimary;
  std::uint64_t m_desktopEntriesVersion = 0;
  IconResolver m_iconResolver;
  Signal<>::ScopedConnection m_appIconColorizeConn;
};
