#include "shell/bar/widgets/tray_widget.h"

#include "config/config_service.h"
#include "core/log.h"
#include "core/ui_phase.h"
#include "dbus/tray/tray_service.h"
#include "render/core/image_file_loader.h"
#include "render/core/image_source_log.h"
#include "render/core/texture_manager.h"
#include "render/scene/input_area.h"
#include "render/scene/node.h"
#include "render/text/glyph_registry.h"
#include "shell/panel/panel_manager.h"
#include "shell/tray/tray_identifier.h"
#include "system/desktop_entry.h"
#include "ui/app_icon_colorization.h"
#include "ui/builders.h"
#include "ui/palette.h"
#include "ui/style.h"
#include "util/string_utils.h"

#include <algorithm>
#include <cmath>
#include <filesystem>
#include <linux/input-event-codes.h>
#include <memory>
#include <optional>
#include <string>

namespace {

  namespace fs = std::filesystem;

  constexpr Logger kLog("tray");

  using tray::identifierVariants;

  void addIconAlias(std::unordered_map<std::string, std::string>& index, std::string_view key, std::string_view icon) {
    if (key.empty() || icon.empty()) {
      return;
    }

    for (const auto& variant : identifierVariants(key)) {
      index.try_emplace(variant, std::string(icon));
    }
  }

  std::string execBasename(std::string_view exec) {
    if (exec.empty()) {
      return {};
    }

    std::string token;
    bool inSingle = false;
    bool inDouble = false;
    for (char c : exec) {
      if (c == '\'' && !inDouble) {
        inSingle = !inSingle;
        continue;
      }
      if (c == '"' && !inSingle) {
        inDouble = !inDouble;
        continue;
      }
      if (c == ' ' && !inSingle && !inDouble) {
        break;
      }
      token.push_back(c);
    }

    if (token.empty()) {
      return {};
    }
    if (const auto slash = token.find_last_of('/'); slash != std::string::npos && slash + 1 < token.size()) {
      token = token.substr(slash + 1);
    }
    return token;
  }

  bool isSymbolicIconName(std::string_view name) {
    return name.contains("symbolic") || name.ends_with("-panel") || name.ends_with("_panel");
  }

  bool isSymbolicIconPath(std::string_view path) { return path.contains("symbolic") || path.contains("/status/"); }

  bool isSvgPath(std::string_view path) { return path.ends_with(".svg") || path.ends_with(".SVG"); }

  std::pair<std::int32_t, std::int32_t> trayPointerCoords(const InputArea& area, const InputArea::PointerData& data) {
    float absX = 0.0F;
    float absY = 0.0F;
    Node::absolutePosition(&area, absX, absY);
    return {
        static_cast<std::int32_t>(std::lround(absX + data.localX)),
        static_cast<std::int32_t>(std::lround(absY + data.localY)),
    };
  }

  std::optional<LoadedImageFile>
  loadSymbolicTrayIcon(const std::string& path, int targetSize, const Color& symbolicColor) {
    auto loaded = loadImageFile(path, targetSize);
    if (!loaded) {
      kLog.debug(
          "tray widget symbolic icon decode failed path={} error={}", ImageSourceLog::describe(path), loaded.error()
      );
      return std::nullopt;
    }

    auto toByte = [](float channel) -> std::uint8_t {
      const float clamped = std::clamp(channel, 0.0F, 1.0F);
      return static_cast<std::uint8_t>(std::lround(clamped * 255.0F));
    };
    const std::uint8_t rr = toByte(symbolicColor.r);
    const std::uint8_t gg = toByte(symbolicColor.g);
    const std::uint8_t bb = toByte(symbolicColor.b);

    // Symbolic assets come in two broad forms:
    // 1) Proper alpha-mask icons (transparent background), where source alpha
    //    already defines the shape and luminance should be ignored.
    // 2) Opaque monochrome bitmaps/SVG rasterizations, where luminance polarity
    //    (light-on-dark vs dark-on-light) must be converted into alpha.
    std::size_t transparentPixels = 0;
    float lumSum = 0.0F;
    float alphaWeight = 0.0F;
    const std::size_t pixelCount = loaded->rgba.size() / 4U;
    for (std::size_t i = 0; i + 3 < loaded->rgba.size(); i += 4) {
      const std::uint8_t a = loaded->rgba[i + 3];
      if (a <= 8) {
        ++transparentPixels;
        continue;
      }
      const float lum = (static_cast<float>(loaded->rgba[i]) * 0.299F
                         + static_cast<float>(loaded->rgba[i + 1]) * 0.587F
                         + static_cast<float>(loaded->rgba[i + 2]) * 0.114F)
          / 255.0F;
      const float w = static_cast<float>(a) / 255.0F;
      lumSum += lum * w;
      alphaWeight += w;
    }

    const float transparentRatio =
        pixelCount == 0 ? 0.0F : static_cast<float>(transparentPixels) / static_cast<float>(pixelCount);
    const bool useSourceAlphaMask = transparentRatio > 0.10F;
    const float avgLum = alphaWeight > 0.0F ? lumSum / alphaWeight : 0.0F;
    const bool invertLumaMask = avgLum > 0.5F;

    for (std::size_t i = 0; i + 3 < loaded->rgba.size(); i += 4) {
      const std::uint8_t a = loaded->rgba[i + 3];
      if (a == 0) {
        continue;
      }
      const float lum =
          (loaded->rgba[i] * 0.299F + loaded->rgba[i + 1] * 0.587F + loaded->rgba[i + 2] * 0.114F) / 255.0F;
      const float mask = useSourceAlphaMask ? 1.0F : (invertLumaMask ? (1.0F - lum) : lum);
      loaded->rgba[i + 0] = rr;
      loaded->rgba[i + 1] = gg;
      loaded->rgba[i + 2] = bb;
      loaded->rgba[i + 3] = static_cast<std::uint8_t>(std::lround(a * std::clamp(mask, 0.0F, 1.0F)));
    }

    return std::move(*loaded);
  }

  bool isUniqueBusName(std::string_view value) { return !value.empty() && value.front() == ':'; }

} // namespace

TrayWidget::TrayWidget(ConfigService& config, TrayService* tray, Options options)
    : m_config(config), m_tray(tray), m_hiddenItems(std::move(options.hiddenItems)),
      m_pinnedItems(std::move(options.pinnedItems)), m_hidePassive(options.hidePassive),
      m_drawerMode(options.drawerMode), m_itemActivated(std::move(options.itemActivated)),
      m_barPosition(std::move(options.barPosition)), m_panelGridMode(options.panelGridMode),
      m_panelGridColumns(std::clamp<std::size_t>(options.panelGridColumns, 1U, 5U)),
      m_inlineEntryGap(std::max(0.0F, options.inlineEntryGap)), m_matchAdjacentSpacing(options.matchAdjacentSpacing),
      m_customItemSize(options.customItemSize) {
  auto normalizeTokens = [](std::vector<std::string>& tokens) {
    std::vector<std::string> normalized;
    normalized.reserve(tokens.size());
    for (const auto& token : tokens) {
      for (const auto& variant : identifierVariants(token)) {
        if (!std::ranges::contains(normalized, variant)) {
          normalized.push_back(variant);
        }
      }
    }
    tokens = std::move(normalized);
  };
  normalizeTokens(m_hiddenItems);
  normalizeTokens(m_pinnedItems);
  buildDesktopIconIndex();
}

std::string TrayWidget::resolveFromTrayThemePath(std::string_view themePath, std::string_view iconName) {
  if (themePath.empty() || iconName.empty()) {
    return {};
  }

  const std::string themePathKey(themePath);
  auto [cacheIt, inserted] = m_trayThemePathIcons.try_emplace(themePathKey);
  if (inserted) {
    std::error_code ec;
    if (!fs::is_directory(themePathKey, ec)) {
      return {};
    }

    auto& iconIndex = cacheIt->second;
    for (fs::recursive_directory_iterator it(themePathKey, fs::directory_options::skip_permission_denied, ec), end;
         !ec && it != end; it.increment(ec)) {
      if (ec || !it->is_regular_file()) {
        continue;
      }

      const fs::path path = it->path();
      const auto extension = StringUtils::toLower(path.extension().string());
      if (extension != ".svg" && extension != ".png") {
        continue;
      }

      const std::string stem = path.stem().string();
      for (const auto& variant : identifierVariants(stem)) {
        iconIndex.try_emplace(variant, path.string());
      }
    }
  }

  const auto& iconIndex = cacheIt->second;
  for (const auto& variant : identifierVariants(iconName)) {
    if (const auto it = iconIndex.find(variant); it != iconIndex.end()) {
      return it->second;
    }
  }

  return {};
}

float TrayWidget::resolvedInlineEntryGap() const {
  if (!m_matchAdjacentSpacing) {
    return m_inlineEntryGap;
  }
  const auto& cap = barCapsuleSpec();
  const float pad = cap.enabled ? cap.padding * m_contentScale : 0.0F;
  return m_inlineEntryGap + 2.0F * pad;
}

std::optional<ColorSpec> TrayWidget::currentAppIconColorizeTint() const {
  return effectiveShellAppIconColorizationTint(m_config.config().shell);
}

void TrayWidget::refreshAppIconColorization(Renderer& renderer) {
  (void)renderer;
  const auto tint = currentAppIconColorizeTint();
  for (Image* image : m_colorizedAppIcons) {
    if (image != nullptr) {
      image->setAppIconColorization(tint);
    }
  }
}

void TrayWidget::create() {
  m_paletteConn = paletteChanged().connect([this]() {
    m_appIconColorizeDirty = true;
    requestUpdate();
  });
  m_appIconColorizeConn = shellAppIconColorizationChanged().connect([this]() {
    m_rebuildPending = true;
    requestUpdate();
  });

  auto container = ui::flex(
      m_panelGridMode ? FlexDirection::Vertical : FlexDirection::Horizontal,
      {
          .out = &m_container,
          .align = m_panelGridMode ? FlexAlign::Start : FlexAlign::Center,
          .justify = m_panelGridMode ? std::optional<FlexJustify>{} : std::optional<FlexJustify>{FlexJustify::Start},
          .gap = resolvedInlineEntryGap(),
      }
  );

  setRoot(std::move(container));
}

void TrayWidget::doLayout(Renderer& renderer, float containerWidth, float containerHeight) {
  if (m_container == nullptr) {
    return;
  }
  if (m_drawerMode && m_drawerChevron != nullptr && m_drawerTrigger != nullptr) {
    const bool panelOpen = PanelManager::instance().isOpenPanel("tray-drawer");
    const std::string glyphName = drawerChevronGlyph(panelOpen);
    if (m_drawerChevronGlyph != glyphName) {
      m_drawerChevronGlyph = glyphName;
      m_drawerChevron->setGlyph(glyphName);
      m_drawerChevron->setGlyphSize(m_drawerTrigger->width());
      m_drawerChevron->measure(renderer);
      m_drawerChevron->setPosition(
          std::round((m_drawerTrigger->width() - m_drawerChevron->width()) * 0.5F),
          std::round((m_drawerTrigger->height() - m_drawerChevron->height()) * 0.5F)
      );
      requestRedraw();
    }
  }
  if (m_panelGridMode) {
    syncState(renderer);
    if (m_rebuildPending) {
      rebuild(renderer);
      m_rebuildPending = false;
    }
    m_container->setGap(resolvedInlineEntryGap());
    m_container->layout(renderer);
    layoutHoverOverlays();
    return;
  }
  const bool vertical = containerHeight > containerWidth;
  if (vertical != m_isVertical) {
    m_isVertical = vertical;
    m_container->setDirection(m_isVertical ? FlexDirection::Vertical : FlexDirection::Horizontal);
  }
  syncState(renderer);
  if (containerHeight > 0.0F && std::abs(containerHeight - m_contentHeight) > 0.5F) {
    m_contentHeight = containerHeight;
    m_rebuildPending = true;
  }
  if (m_rebuildPending) {
    rebuild(renderer);
    m_rebuildPending = false;
  }
  if (m_appIconColorizeDirty) {
    refreshAppIconColorization(renderer);
    m_appIconColorizeDirty = false;
  }

  m_container->setGap(resolvedInlineEntryGap());
  m_container->layout(renderer);
  layoutHoverOverlays();
}

void TrayWidget::doUpdate(Renderer& renderer) {
  syncState(renderer);
  if (m_rebuildPending) {
    rebuild(renderer);
    m_rebuildPending = false;
  }
  if (m_appIconColorizeDirty) {
    refreshAppIconColorization(renderer);
    m_appIconColorizeDirty = false;
  }
}

void TrayWidget::syncState(Renderer& renderer) {
  (void)renderer;
  const auto desktopVersion = desktopEntriesVersion();
  const bool desktopEntriesChanged = desktopVersion != m_desktopEntriesVersion;
  if (desktopEntriesChanged) {
    buildDesktopIconIndex();
    m_preferredIconPaths.clear();
  }

  const auto next_items = (m_tray != nullptr) ? m_tray->items() : std::vector<TrayItemInfo>{};
  if (!desktopEntriesChanged && next_items == m_items) {
    return;
  }

  std::unordered_map<std::string, const TrayItemInfo*> previousById;
  previousById.reserve(m_items.size());
  for (const auto& oldItem : m_items) {
    previousById[oldItem.id] = &oldItem;
  }

  std::unordered_map<std::string, bool> stillPresent;
  stillPresent.reserve(next_items.size());
  for (const auto& item : next_items) {
    stillPresent[item.id] = true;

    auto hashVec = [](const std::vector<std::uint8_t>& vec) -> std::size_t {
      if (vec.empty()) {
        return 0;
      }
      return std::hash<std::string_view>{}(std::string_view(reinterpret_cast<const char*>(vec.data()), vec.size()));
    };
    const std::size_t currentHash = hashVec(item.iconArgb32) ^ (hashVec(item.attentionArgb32) << 1);

    if (!m_initialPixmaps.contains(item.id)) {
      m_initialPixmaps[item.id] = currentHash;
    } else if (m_initialPixmaps[item.id] != currentHash) {
      m_preferPixmap[item.id] = true;
    }

    const auto prevIt = previousById.find(item.id);
    if (prevIt == previousById.end() || prevIt->second == nullptr) {
      continue;
    }
    const TrayItemInfo& prev = *prevIt->second;

    // Resolved icon path cache is keyed by item id. Invalidate when icon-relevant
    // metadata changes, or we can keep showing stale paths after icon switches.
    if (prev.iconName != item.iconName
        || prev.overlayIconName != item.overlayIconName
        || prev.attentionIconName != item.attentionIconName
        || prev.iconThemePath != item.iconThemePath
        || prev.needsAttention != item.needsAttention
        || prev.status != item.status
        || prev.overlayWidth != item.overlayWidth
        || prev.overlayHeight != item.overlayHeight
        || prev.overlayArgb32 != item.overlayArgb32
        || prev.iconWidth != item.iconWidth
        || prev.iconHeight != item.iconHeight
        || prev.iconArgb32 != item.iconArgb32
        || prev.attentionWidth != item.attentionWidth
        || prev.attentionHeight != item.attentionHeight
        || prev.attentionArgb32 != item.attentionArgb32) {
      m_preferredIconPaths.erase(item.id);
    }
  }
  for (auto it = m_preferredIconPaths.begin(); it != m_preferredIconPaths.end();) {
    if (!stillPresent.contains(it->first)) {
      it = m_preferredIconPaths.erase(it);
    } else {
      ++it;
    }
  }
  for (auto it = m_initialPixmaps.begin(); it != m_initialPixmaps.end();) {
    if (!stillPresent.contains(it->first)) {
      it = m_initialPixmaps.erase(it);
    } else {
      ++it;
    }
  }
  for (auto it = m_preferPixmap.begin(); it != m_preferPixmap.end();) {
    if (!stillPresent.contains(it->first)) {
      it = m_preferPixmap.erase(it);
    } else {
      ++it;
    }
  }

  m_items = next_items;
  m_rebuildPending = true;
  if (root() != nullptr) {
    root()->markLayoutDirty();
  }
  requestRedraw();
}

void TrayWidget::rebuild(Renderer& renderer) {
  uiAssertNotRendering("TrayWidget::rebuild");
  if (m_container == nullptr) {
    return;
  }

  for (auto* image : m_loadedImages) {
    if (image != nullptr) {
      image->clear(renderer);
    }
  }
  m_loadedImages.clear();
  m_colorizedAppIcons.clear();

  clearHoverOverlays();

  while (!m_container->children().empty()) {
    m_container->removeChild(m_container->children().back().get());
  }

  auto attachHover = [this](InputArea& area, float size) {
    if (!barCapsuleSpec().hoverHighlight) {
      return;
    }

    Box* hoverBoxPtr = nullptr;
    ColorSpec hoverFill = widgetForegroundOr(colorSpecFromRole(ColorRole::OnSurface));
    hoverFill.alpha = 0.0F;
    const float padding = barCapsuleSpec().padding * m_contentScale;
    const float boxSize = size + padding * 2.0F;

    auto hoverBox = ui::box({
        .out = &hoverBoxPtr,
        .fill = hoverFill,
        .radius = resolvedBarCapsuleRadius(boxSize, boxSize),
        .width = boxSize,
        .height = boxSize,
        .configure = [](Box& box) {
          box.setZIndex(-1);
          box.setHitTestVisible(false);
        },
    });

    if (m_hoverOverlayParent != nullptr) {
      m_hoverOverlayParent->addChild(std::move(hoverBox));
      m_hoverOverlays.push_back({.area = &area, .box = hoverBoxPtr, .padding = padding});
    } else {
      hoverBoxPtr->setPosition(-padding, -padding);
      area.addChild(std::move(hoverBox));
    }

    auto progress = std::make_shared<float>(0.0F);
    area.setOnEnter([this, hoverBoxPtr, progress](const InputArea::PointerData&) {
      if (m_animations == nullptr)
        return;
      m_animations->cancelForOwner(hoverBoxPtr);
      const ColorSpec fill = widgetForegroundOr(colorSpecFromRole(ColorRole::OnSurface));
      m_animations->animate(
          *progress, 1.0F, Style::animFast, Easing::EaseOutCubic,
          [this, hoverBoxPtr, fill, progress](float p) {
            *progress = p;
            hoverBoxPtr->setVisible(p > 0.001F);
            ColorSpec c = fill;
            c.alpha = 0.1F * p;
            hoverBoxPtr->setFill(c);
            requestRedraw();
          },
          {}, hoverBoxPtr
      );
      requestFrameTick();
    });

    area.setOnLeave([this, hoverBoxPtr, progress]() {
      if (m_animations == nullptr)
        return;
      m_animations->cancelForOwner(hoverBoxPtr);
      const ColorSpec fill = widgetForegroundOr(colorSpecFromRole(ColorRole::OnSurface));
      m_animations->animate(
          *progress, 0.0F, Style::animFast, Easing::EaseOutCubic,
          [this, hoverBoxPtr, fill, progress](float p) {
            *progress = p;
            hoverBoxPtr->setVisible(p > 0.001F);
            ColorSpec c = fill;
            c.alpha = 0.1F * p;
            hoverBoxPtr->setFill(c);
            requestRedraw();
          },
          {}, hoverBoxPtr
      );
      requestFrameTick();
    });
  };

  if (m_drawerMode) {
    m_drawerTrigger = nullptr;
    m_drawerChevron = nullptr;
    m_drawerChevronGlyph.clear();
    bool hasDrawerItems = false;
    for (const auto& item : m_items) {
      if ((m_hidePassive && tray::isPassiveStatus(item)) || isHiddenItem(item) || isPinnedItem(item)) {
        continue;
      }
      hasDrawerItems = true;
      break;
    }
    if (hasDrawerItems) {
      const float itemSize = m_customItemSize.value_or(Style::baseGlyphSize) * m_contentScale;
      auto triggerArea = ui::inputArea({});
      auto* triggerPtr = triggerArea.get();
      m_drawerTrigger = triggerPtr;
      triggerArea->setSize(itemSize, itemSize);
      triggerArea->setOnClick([this, triggerPtr](const InputArea::PointerData& data) {
        if (data.button == BTN_LEFT) {
          float ax = 0.0F;
          float ay = 0.0F;
          Node::absolutePosition(triggerPtr, ax, ay);
          // Open below / away from the bar edge relative to the tray button center.
          const float centerX = ax + triggerPtr->width() * 0.5F;
          const float centerY = ay + triggerPtr->height() * 0.5F;
          float anchorX = centerX;
          float anchorY = centerY;
          if (m_barPosition == "top") {
            anchorY += triggerPtr->height() * 0.5F + Style::spaceXs * m_contentScale;
          } else if (m_barPosition == "bottom") {
            anchorY -= triggerPtr->height() * 0.5F + Style::spaceXs * m_contentScale;
          } else if (m_barPosition == "left") {
            anchorX += triggerPtr->width() * 0.5F + Style::spaceXs * m_contentScale;
          } else if (m_barPosition == "right") {
            anchorX -= triggerPtr->width() * 0.5F + Style::spaceXs * m_contentScale;
          }
          requestPanelToggle("tray-drawer", {}, anchorX, anchorY);
        }
      });
      const bool panelOpen = PanelManager::instance().isOpenPanel("tray-drawer");
      m_drawerChevronGlyph = drawerChevronGlyph(panelOpen);
      auto glyph = ui::glyph({
          .glyph = m_drawerChevronGlyph,
          .glyphSize = itemSize,
          .color = widgetIconColorOr(colorSpecFromRole(ColorRole::OnSurface)),
      });
      glyph->measure(renderer);
      glyph->setPosition(
          std::round((itemSize - glyph->width()) * 0.5F), std::round((itemSize - glyph->height()) * 0.5F)
      );
      m_drawerChevron = glyph.get();
      triggerArea->addChild(std::move(glyph));
      attachHover(*triggerArea, itemSize);
      m_container->addChild(std::move(triggerArea));
    }
  }

  Flex* gridRow = nullptr;
  std::size_t gridCol = 0;
  for (const auto& item : m_items) {
    if ((m_hidePassive && tray::isPassiveStatus(item)) || isHiddenItem(item)) {
      continue;
    }
    if (m_drawerMode && !isPinnedItem(item)) {
      continue;
    }
    if (m_panelGridMode && isPinnedItem(item)) {
      continue;
    }
    const std::string iconPath = resolveIconPath(item);
    const float itemSize = m_customItemSize.value_or(Style::baseGlyphSize) * m_contentScale;
    const float iconSize = itemSize;
    const int iconRequestSize = std::max(32, static_cast<int>(std::round(iconSize * 2.0F)));

    std::unique_ptr<Node> iconNode;
    float iconW = iconSize;
    float iconH = iconSize;

    if (!iconPath.empty()) {
      auto image = ui::image({
          .fit = ImageFit::Contain,
          .width = iconSize,
          .height = iconSize,
      });
      const bool symbolicPath = isSymbolicIconPath(iconPath);
      const auto appIconTint = currentAppIconColorizeTint();
      if (!symbolicPath && appIconTint.has_value()) {
        image->setAppIconColorization(appIconTint);
      }
      bool loadedFromFile = false;
      const Color symbolicColor = resolveColorSpec(widgetIconColorOr(colorSpecFromRole(ColorRole::OnSurface)));
      if (symbolicPath && isSvgPath(iconPath)) {
        if (auto symbolic = loadSymbolicTrayIcon(iconPath, iconRequestSize, symbolicColor)) {
          loadedFromFile = image->setSourceRaw(
              renderer, symbolic->rgba.data(), symbolic->rgba.size(), symbolic->width, symbolic->height, 0,
              PixmapFormat::RGBA, true
          );
        }
      }
      if (!loadedFromFile) {
        loadedFromFile = image->setSourceFile(renderer, iconPath, iconRequestSize, true);
      }

      if (loadedFromFile) {
        if (symbolicPath && !isSvgPath(iconPath)) {
          image->setTint(symbolicColor);
        }
        iconW = iconSize;
        iconH = iconSize;
        m_loadedImages.push_back(image.get());
        if (appIconTint.has_value() && !symbolicPath) {
          m_colorizedAppIcons.push_back(image.get());
        }
        iconNode = std::move(image);
      } else {
        kLog.debug("tray widget icon id={} source=file path={} failed-to-load", item.id, iconPath);
      }
    }

    if (iconNode == nullptr) {
      const auto& pixmap =
          item.needsAttention && !item.attentionArgb32.empty() ? item.attentionArgb32 : item.iconArgb32;
      const std::int32_t pixmapW =
          item.needsAttention && !item.attentionArgb32.empty() ? item.attentionWidth : item.iconWidth;
      const std::int32_t pixmapH =
          item.needsAttention && !item.attentionArgb32.empty() ? item.attentionHeight : item.iconHeight;

      if (!pixmap.empty() && pixmapW > 0 && pixmapH > 0) {
        auto image = ui::image({
            .fit = ImageFit::Contain,
            .width = iconSize,
            .height = iconSize,
        });
        const auto pixmapTint = currentAppIconColorizeTint();
        if (pixmapTint.has_value()) {
          image->setAppIconColorization(pixmapTint);
        }
        if (image->setSourceRaw(
                renderer, pixmap.data(), pixmap.size(), pixmapW, pixmapH, 0, PixmapFormat::ARGB, true
            )) {
          iconW = iconSize;
          iconH = iconSize;
          m_loadedImages.push_back(image.get());
          if (pixmapTint.has_value()) {
            m_colorizedAppIcons.push_back(image.get());
          }
          iconNode = std::move(image);
        } else {
          kLog.debug("tray widget icon id={} source=pixmap size={}x{} failed-to-load", item.id, pixmapW, pixmapH);
        }
      }
    }

    std::unique_ptr<Node> overlayNode;
    float overlayW = iconSize;
    float overlayH = iconSize;
    if (!item.needsAttention) {
      auto resolveOverlayPath = [this, &item, iconRequestSize](const std::string& overlayName) -> std::string {
        if (overlayName.empty()) {
          return {};
        }
        if (const auto themed = resolveFromTrayThemePath(item.iconThemePath, overlayName); !themed.empty()) {
          return themed;
        }
        if (const auto direct = m_iconResolver.resolve(overlayName, iconRequestSize); !direct.empty()) {
          return direct;
        }
        if (const auto it = m_appIcons.find(overlayName); it != m_appIcons.end()) {
          return m_iconResolver.resolve(it->second, iconRequestSize);
        }
        const std::string lower = StringUtils::toLower(overlayName);
        if (const auto it = m_appIcons.find(lower); it != m_appIcons.end()) {
          return m_iconResolver.resolve(it->second, iconRequestSize);
        }
        return {};
      };

      const std::string overlayPath = resolveOverlayPath(item.overlayIconName);
      if (!overlayPath.empty()) {
        auto overlayImage = ui::image({
            .fit = ImageFit::Contain,
            .width = iconSize,
            .height = iconSize,
        });
        if (overlayImage->setSourceFile(renderer, overlayPath, iconRequestSize, true)) {
          overlayW = iconSize;
          overlayH = iconSize;
          m_loadedImages.push_back(overlayImage.get());
          overlayNode = std::move(overlayImage);
        }
      }

      if (overlayNode == nullptr && !item.overlayArgb32.empty() && item.overlayWidth > 0 && item.overlayHeight > 0) {
        auto overlayImage = ui::image({
            .fit = ImageFit::Contain,
            .width = iconSize,
            .height = iconSize,
        });
        if (overlayImage->setSourceRaw(
                renderer, item.overlayArgb32.data(), item.overlayArgb32.size(), item.overlayWidth, item.overlayHeight,
                0, PixmapFormat::ARGB, true
            )) {
          overlayW = iconSize;
          overlayH = iconSize;
          m_loadedImages.push_back(overlayImage.get());
          overlayNode = std::move(overlayImage);
        }
      }
    }

    if (iconNode == nullptr) {
      const std::string fallback = iconForItem(item);
      auto glyph = ui::glyph({
          .glyph = fallback,
          .glyphSize = iconSize,
          .color = item.needsAttention ? colorSpecFromRole(ColorRole::Error)
                                       : widgetIconColorOr(colorSpecFromRole(ColorRole::OnSurface)),
      });
      glyph->measure(renderer);
      iconW = glyph->width();
      iconH = glyph->height();
      iconNode = std::move(glyph);
    }

    if (overlayNode != nullptr && iconNode != nullptr) {
      auto stack = ui::node({});
      stack->setSize(itemSize, itemSize);
      iconNode->setPosition(std::round((itemSize - iconW) * 0.5F), std::round((itemSize - iconH) * 0.5F));
      overlayNode->setPosition(std::round((itemSize - overlayW) * 0.5F), std::round((itemSize - overlayH) * 0.5F));
      stack->addChild(std::move(iconNode));
      stack->addChild(std::move(overlayNode));
      iconNode = std::move(stack);
      iconW = itemSize;
      iconH = itemSize;
    }

    // Wrap icon in InputArea for click handling
    auto area = ui::inputArea({});
    area->setSize(itemSize, itemSize);
    iconNode->setPosition(std::round((itemSize - iconW) * 0.5F), std::round((itemSize - iconH) * 0.5F));
    auto itemId = item.id;
    area->setAcceptedButtons(InputArea::buttonMask({BTN_LEFT, BTN_RIGHT}));
    InputArea* areaPtr = area.get();
    area->setOnClick([this, itemId, areaPtr](const InputArea::PointerData& data) {
      if (m_tray == nullptr) {
        return;
      }
      const auto [x, y] = trayPointerCoords(*areaPtr, data);
      if (data.button == BTN_LEFT) {
        (void)m_tray->activateItem(itemId, x, y);
        if (m_itemActivated) {
          m_itemActivated();
        }
      } else if (data.button == BTN_RIGHT) {
        if (m_tray->itemUsesDBusMenu(itemId)) {
          m_tray->requestMenuToggle(itemId, m_contentScale);
        } else {
          (void)m_tray->openContextMenu(itemId, x, y);
        }
      }
    });
    area->addChild(std::move(iconNode));

    if (const std::string tooltipText = tray::formatTrayItemTooltip(item); !tooltipText.empty()) {
      area->setTooltip(tooltipText);
    }

    attachHover(*area, itemSize);

    if (m_panelGridMode) {
      if (gridRow == nullptr || gridCol >= m_panelGridColumns) {
        gridRow = static_cast<Flex*>(m_container->addChild(
            ui::row({
                .align = FlexAlign::Center,
                .gap = resolvedInlineEntryGap(),
            })
        ));
        gridCol = 0;
      }
      gridRow->addChild(std::move(area));
      ++gridCol;
    } else {
      m_container->addChild(std::move(area));
    }
  }

  m_container->setVisible(!m_container->children().empty());
}

std::string TrayWidget::drawerChevronGlyph(bool panelOpen) const {
  if (m_barPosition == "bottom") {
    return panelOpen ? "chevron-down" : "chevron-up";
  }
  if (m_barPosition == "left") {
    return panelOpen ? "chevron-left" : "chevron-right";
  }
  if (m_barPosition == "right") {
    return panelOpen ? "chevron-right" : "chevron-left";
  }
  return panelOpen ? "chevron-up" : "chevron-down";
}

bool TrayWidget::isHiddenItem(const TrayItemInfo& item) const {
  if (m_hiddenItems.empty()) {
    return false;
  }

  std::vector<std::string> candidates;
  auto appendVariants = [&candidates](std::string_view text) {
    for (const auto& variant : identifierVariants(text)) {
      if (!std::ranges::contains(candidates, variant)) {
        candidates.push_back(variant);
      }
    }
  };

  appendVariants(item.id);
  appendVariants(item.busName);
  appendVariants(item.objectPath);
  appendVariants(item.itemName);
  appendVariants(item.processName);
  appendVariants(item.title);
  appendVariants(item.iconName);
  appendVariants(item.attentionIconName);

  for (const auto& needle : m_hiddenItems) {
    if (std::ranges::contains(candidates, needle)) {
      return true;
    }
  }

  return false;
}

bool TrayWidget::isPinnedItem(const TrayItemInfo& item) const {
  if (m_pinnedItems.empty()) {
    return false;
  }

  for (const auto& needle : m_pinnedItems) {
    if (tray::tokenMatchesItem(needle, item)) {
      return true;
    }
  }

  return false;
}

void TrayWidget::buildDesktopIconIndex() {
  m_appIcons.clear();
  const auto& entries = desktopEntries();
  for (const auto& entry : entries) {
    if (entry.id.empty()) {
      continue;
    }

    if (!entry.icon.empty()) {
      addIconAlias(m_appIcons, entry.id, entry.icon);
      addIconAlias(m_appIcons, entry.name, entry.icon);
      addIconAlias(m_appIcons, entry.nameLower, entry.icon);
      addIconAlias(m_appIcons, entry.icon, entry.icon);
      addIconAlias(m_appIcons, execBasename(entry.exec), entry.icon);
      addIconAlias(m_appIcons, entry.startupWmClass, entry.icon);
    }
  }
  m_desktopEntriesVersion = desktopEntriesVersion();
}

std::string TrayWidget::resolveIconPath(const TrayItemInfo& item) {
  const bool hasNamedIcon = item.needsAttention ? !item.attentionIconName.empty() : !item.iconName.empty();
  if (const auto it = m_preferPixmap.find(item.id); it != m_preferPixmap.end() && it->second) {
    // Some indicators publish pixmaps late (after startup) that are monochrome
    // or stale. Keep using named-icon resolution when available.
    if (!hasNamedIcon) {
      return {};
    }
  }

  if (const auto it = m_preferredIconPaths.find(item.id); it != m_preferredIconPaths.end() && !it->second.empty()) {
    return it->second;
  }

  std::string preferred;
  if (item.needsAttention && !item.attentionIconName.empty()) {
    preferred = item.attentionIconName;
  } else if (item.needsAttention && !item.attentionArgb32.empty()) {
    preferred = "";
  } else {
    preferred = item.iconName;
  }
  if (const auto themed = resolveFromTrayThemePath(item.iconThemePath, preferred); !themed.empty()) {
    m_preferredIconPaths[item.id] = themed;
    return themed;
  }

  if (tray::looksGenericStatusItemName(preferred)) {
    preferred.clear();
  }

  // Match the on-screen request size used when the icon is loaded (see rebuild).
  const int iconTargetSize = std::max(
      32, static_cast<int>(std::round(m_customItemSize.value_or(Style::baseGlyphSize) * m_contentScale * 2.0F))
  );

  auto resolveMapped = [this, iconTargetSize](const std::string& name) -> std::string {
    if (name.empty()) {
      return {};
    }

    if (const auto it = m_appIcons.find(name); it != m_appIcons.end()) {
      if (const auto mapped = m_iconResolver.resolve(it->second, iconTargetSize); !mapped.empty()) {
        return mapped;
      }
    }

    const std::string lower = StringUtils::toLower(name);
    if (const auto it = m_appIcons.find(lower); it != m_appIcons.end()) {
      if (const auto mapped = m_iconResolver.resolve(it->second, iconTargetSize); !mapped.empty()) {
        return mapped;
      }
    }

    if (const auto dot = name.rfind('.'); dot != std::string::npos && dot + 1 < name.size()) {
      const auto tail = name.substr(dot + 1);
      if (const auto it = m_appIcons.find(tail); it != m_appIcons.end()) {
        if (const auto mapped = m_iconResolver.resolve(it->second, iconTargetSize); !mapped.empty()) {
          return mapped;
        }
      }
      const std::string tailLower = StringUtils::toLower(tail);
      if (const auto it = m_appIcons.find(tailLower); it != m_appIcons.end()) {
        if (const auto mapped = m_iconResolver.resolve(it->second, iconTargetSize); !mapped.empty()) {
          return mapped;
        }
      }
    }

    return {};
  };

  auto resolveDirect = [this, iconTargetSize](const std::string& name) -> std::string {
    if (name.empty()) {
      return {};
    }
    return m_iconResolver.resolve(name, iconTargetSize);
  };

  std::string symbolicFallback;

  const std::string stableBusName = isUniqueBusName(item.busName) ? std::string{} : item.busName;
  const std::string stableItemId = (!item.id.empty() && !isUniqueBusName(item.id))
      ? item.id
      : (isUniqueBusName(item.busName) ? item.objectPath : item.id);

  std::vector<std::pair<const char*, const std::string*>> candidates;
  candidates.reserve(12);
  candidates.emplace_back("preferred", &preferred);
  // When an explicit tray IconName is provided, treat it as authoritative.
  // Falling back to generic app-id/title mappings can hide stateful icon
  // changes (e.g. indicator on/off variants) behind a constant app icon.
  const bool hasTargetPixmap = item.needsAttention ? !item.attentionArgb32.empty() : !item.iconArgb32.empty();
  if (preferred.empty() && !hasTargetPixmap) {
    candidates.emplace_back("itemName", &item.itemName);
    candidates.emplace_back("processName", &item.processName);
    candidates.emplace_back("title", &item.title);
    candidates.emplace_back("objectPath", &item.objectPath);
    candidates.emplace_back("busName", &stableBusName);
    candidates.emplace_back("id", &stableItemId);
  }

  for (const auto& [_, candidate] : candidates) {
    for (const auto& variant : identifierVariants(*candidate)) {
      if (tray::looksGenericStatusItemName(variant)) {
        continue;
      }

      if (const auto mapped = resolveMapped(variant); !mapped.empty()) {
        if (!isSymbolicIconPath(mapped)) {
          m_preferredIconPaths[item.id] = mapped;
          return mapped;
        }
        if (symbolicFallback.empty()) {
          symbolicFallback = mapped;
        }
      }

      if (const auto direct = resolveDirect(variant); !direct.empty()) {
        if (!isSymbolicIconName(variant) && !isSymbolicIconPath(direct)) {
          m_preferredIconPaths[item.id] = direct;
          return direct;
        }
        if (symbolicFallback.empty()) {
          symbolicFallback = direct;
        }
      }
    }
  }

  return symbolicFallback;
}

std::string TrayWidget::iconForItem(const TrayItemInfo& item) const {
  const std::string preferred =
      item.needsAttention && !item.attentionIconName.empty() ? item.attentionIconName : item.iconName;
  if (!preferred.empty() && GlyphRegistry::contains(preferred)) {
    return preferred;
  }
  if (item.needsAttention) {
    return "warning";
  }
  return "menu-2";
}
void TrayWidget::layoutHoverOverlays() {
  if (m_hoverOverlayParent == nullptr || m_hoverOverlays.empty()) {
    return;
  }
  float underlayX = 0.0F;
  float underlayY = 0.0F;
  Node::absolutePosition(m_hoverOverlayParent, underlayX, underlayY);

  for (auto& entry : m_hoverOverlays) {
    if (entry.area == nullptr || entry.box == nullptr) {
      continue;
    }
    float areaX = 0.0F;
    float areaY = 0.0F;
    Node::absolutePosition(entry.area, areaX, areaY);

    const float mainExtent = m_isVertical ? entry.area->height() : entry.area->width();

    const float hoverW = m_isVertical ? m_capsuleCross : mainExtent + 2.0F * entry.padding;
    const float hoverH = m_isVertical ? mainExtent + 2.0F * entry.padding : m_capsuleCross;

    entry.box->setSize(hoverW, hoverH);
    entry.box->setRadius(resolvedBarCapsuleRadius(hoverW, hoverH));

    float boxX = areaX - underlayX;
    float boxY = areaY - underlayY;

    if (m_isVertical) {
      boxX += (entry.area->width() - m_capsuleCross) * 0.5F;
      boxY -= entry.padding;
    } else {
      boxX -= entry.padding;
      boxY += (entry.area->height() - m_capsuleCross) * 0.5F;
    }

    entry.box->setPosition(boxX, boxY);
  }
}

// The hover overlay boxes live in `m_hoverOverlayParent` (an external node that outlives us or is
// wiped by `attachWidgetsToSections` before the next widget batch), and the entries' `area`
// pointers are inside our own scene subtree, which the bar destroys *before* clearing the widget
// vector. Touching either from the destructor is redundant at best and a use-after-free at worst.
TrayWidget::~TrayWidget() = default;

void TrayWidget::clearHoverOverlays() {
  if (m_hoverOverlayParent != nullptr) {
    for (auto& entry : m_hoverOverlays) {
      if (entry.area != nullptr) {
        entry.area->setOnEnter(nullptr);
        entry.area->setOnLeave(nullptr);
      }
      if (entry.box != nullptr) {
        m_hoverOverlayParent->removeChild(entry.box);
      }
    }
  }
  m_hoverOverlays.clear();
}
