#include "ui/controls/button.h"

#include "core/input/keybind_matcher.h"
#include "render/animation/animation_manager.h"
#include "render/core/renderer.h"
#include "render/scene/input_area.h"
#include "ui/controls/glyph.h"
#include "ui/controls/label.h"
#include "ui/palette.h"
#include "ui/style.h"

#include <cmath>
#include <memory>

namespace {

  Button::ButtonStateColors makeState(ColorSpec bg, ColorSpec border, ColorSpec label) {
    return Button::ButtonStateColors{
        .bg = bg,
        .border = border,
        .label = label,
    };
  }

  Button::ButtonStateColors selectedState() {
    return makeState(
        colorSpecFromRole(ColorRole::Primary), colorSpecFromRole(ColorRole::Primary),
        colorSpecFromRole(ColorRole::OnPrimary)
    );
  }

  Button::ButtonPalette paletteForVariant(ButtonVariant variant) {
    constexpr float kDisabledAlpha = 0.55F;
    switch (variant) {
    case ButtonVariant::Default:
      return Button::ButtonPalette{
          .borderWidth = Style::borderWidth,
          .normal = makeState(
              colorSpecFromRole(ColorRole::SurfaceVariant), colorSpecFromRole(ColorRole::Outline),
              colorSpecFromRole(ColorRole::OnSurface)
          ),
          .hover =
              makeState(colorSpecFromRole(ColorRole::Hover), clearColorSpec(), colorSpecFromRole(ColorRole::OnHover)),
          .pressed = makeState(
              colorSpecFromRole(ColorRole::Primary), colorSpecFromRole(ColorRole::Primary),
              colorSpecFromRole(ColorRole::OnPrimary)
          ),
          .disabled = makeState(
              colorSpecFromRole(ColorRole::SurfaceVariant, kDisabledAlpha),
              colorSpecFromRole(ColorRole::Outline, Style::disabledOutlineAlpha),
              colorSpecFromRole(ColorRole::OnSurface, kDisabledAlpha)
          ),
          .selected = selectedState(),
      };
    case ButtonVariant::Primary:
      return Button::ButtonPalette{
          .borderWidth = 0.0F,
          .normal = makeState(
              colorSpecFromRole(ColorRole::Primary), clearColorSpec(), colorSpecFromRole(ColorRole::OnPrimary)
          ),
          .hover =
              makeState(colorSpecFromRole(ColorRole::Hover), clearColorSpec(), colorSpecFromRole(ColorRole::OnHover)),
          .pressed = makeState(
              colorSpecFromRole(ColorRole::Primary), clearColorSpec(), colorSpecFromRole(ColorRole::OnPrimary)
          ),
          .disabled = makeState(
              colorSpecFromRole(ColorRole::Primary, kDisabledAlpha), clearColorSpec(),
              colorSpecFromRole(ColorRole::OnPrimary)
          ),
          .selected = selectedState(),
      };
    case ButtonVariant::Secondary:
      return Button::ButtonPalette{
          .borderWidth = Style::borderWidth,
          .normal = makeState(
              colorSpecFromRole(ColorRole::Secondary), colorSpecFromRole(ColorRole::Outline),
              colorSpecFromRole(ColorRole::OnSecondary)
          ),
          .hover =
              makeState(colorSpecFromRole(ColorRole::Hover), clearColorSpec(), colorSpecFromRole(ColorRole::OnHover)),
          .pressed = makeState(
              colorSpecFromRole(ColorRole::Primary), colorSpecFromRole(ColorRole::Primary),
              colorSpecFromRole(ColorRole::OnPrimary)
          ),
          .disabled = makeState(
              colorSpecFromRole(ColorRole::Secondary, kDisabledAlpha),
              colorSpecFromRole(ColorRole::Outline, Style::disabledOutlineAlpha),
              colorSpecFromRole(ColorRole::OnSecondary)
          ),
          .selected = selectedState(),
      };
    case ButtonVariant::Destructive:
      return Button::ButtonPalette{
          .borderWidth = Style::borderWidth,
          .normal = makeState(
              colorSpecFromRole(ColorRole::Error), colorSpecFromRole(ColorRole::Outline),
              colorSpecFromRole(ColorRole::OnError)
          ),
          .hover =
              makeState(colorSpecFromRole(ColorRole::Hover), clearColorSpec(), colorSpecFromRole(ColorRole::OnHover)),
          .pressed = makeState(
              colorSpecFromRole(ColorRole::Error), colorSpecFromRole(ColorRole::Error),
              colorSpecFromRole(ColorRole::OnError)
          ),
          .disabled = makeState(
              colorSpecFromRole(ColorRole::Error, kDisabledAlpha),
              colorSpecFromRole(ColorRole::Outline, Style::disabledOutlineAlpha), colorSpecFromRole(ColorRole::OnError)
          ),
          .selected = selectedState(),
      };
    case ButtonVariant::Outline:
      return Button::ButtonPalette{
          .borderWidth = Style::borderWidth,
          .normal = makeState(
              colorSpecFromRole(ColorRole::Surface), colorSpecFromRole(ColorRole::Outline),
              colorSpecFromRole(ColorRole::OnSurface)
          ),
          .hover =
              makeState(colorSpecFromRole(ColorRole::Hover), clearColorSpec(), colorSpecFromRole(ColorRole::OnHover)),
          .pressed = makeState(
              colorSpecFromRole(ColorRole::Primary), colorSpecFromRole(ColorRole::Primary),
              colorSpecFromRole(ColorRole::OnPrimary)
          ),
          .disabled = makeState(
              colorSpecFromRole(ColorRole::Surface, kDisabledAlpha),
              colorSpecFromRole(ColorRole::Outline, Style::disabledOutlineAlpha),
              colorSpecFromRole(ColorRole::OnSurface, kDisabledAlpha)
          ),
          .selected = selectedState(),
      };
    case ButtonVariant::Ghost:
      return Button::ButtonPalette{
          .borderWidth = 0.0F,
          .normal = makeState(clearColorSpec(), clearColorSpec(), colorSpecFromRole(ColorRole::OnSurface)),
          .hover =
              makeState(colorSpecFromRole(ColorRole::Hover), clearColorSpec(), colorSpecFromRole(ColorRole::OnHover)),
          .pressed = makeState(
              colorSpecFromRole(ColorRole::SurfaceVariant), clearColorSpec(), colorSpecFromRole(ColorRole::OnSurface)
          ),
          .disabled =
              makeState(clearColorSpec(), clearColorSpec(), colorSpecFromRole(ColorRole::OnSurface, kDisabledAlpha)),
          .selected = selectedState(),
      };
    case ButtonVariant::Tab:
      return Button::ButtonPalette{
          .borderWidth = 0.0F,
          .normal = makeState(clearColorSpec(), clearColorSpec(), colorSpecFromRole(ColorRole::OnSurface)),
          .hover =
              makeState(colorSpecFromRole(ColorRole::Hover), clearColorSpec(), colorSpecFromRole(ColorRole::OnHover)),
          .pressed = makeState(
              colorSpecFromRole(ColorRole::SurfaceVariant), clearColorSpec(), colorSpecFromRole(ColorRole::OnSurface)
          ),
          .disabled = makeState(clearColorSpec(), clearColorSpec(), colorSpecFromRole(ColorRole::OnSurface)),
          .selected = std::nullopt,
      };
    case ButtonVariant::TabActive:
      return Button::ButtonPalette{
          .borderWidth = 0.0F,
          .normal = makeState(
              colorSpecFromRole(ColorRole::Primary), clearColorSpec(), colorSpecFromRole(ColorRole::OnPrimary)
          ),
          .hover = makeState(
              colorSpecFromRole(ColorRole::Primary), clearColorSpec(), colorSpecFromRole(ColorRole::OnPrimary)
          ),
          .pressed = makeState(
              colorSpecFromRole(ColorRole::Primary), clearColorSpec(), colorSpecFromRole(ColorRole::OnPrimary)
          ),
          .disabled = makeState(
              colorSpecFromRole(ColorRole::Primary, kDisabledAlpha), clearColorSpec(),
              colorSpecFromRole(ColorRole::OnPrimary)
          ),
          .selected = std::nullopt,
      };
    }

    return {};
  }

} // namespace

Button::ButtonPalette Button::defaultPalette(ButtonVariant variant) { return paletteForVariant(variant); }

Button::Button() {
  setAlign(FlexAlign::Center);
  setMinHeight(Style::controlHeightSm);
  setPadding(Style::spaceSm);
  setRadius(Style::scaledRadiusMd());

  auto area = std::make_unique<InputArea>();
  area->setOnEnter([this](const InputArea::PointerData& /*data*/) {
    applyVisualState();
    if (m_onEnter) {
      m_onEnter();
    }
  });
  area->setOnLeave([this]() {
    applyVisualState();
    if (m_onLeave) {
      m_onLeave();
    }
  });
  area->setOnPress([this](const InputArea::PointerData& data) {
    applyVisualState();
    if (m_onPress) {
      m_onPress(data.localX, data.localY, data.pressed);
    }
  });
  area->setOnMotion([this](const InputArea::PointerData& data) {
    if (m_onMotion) {
      m_onMotion();
    }
    if (m_onPointerMotion) {
      m_onPointerMotion(data.localX, data.localY);
    }
  });
  area->setOnClick([this](const InputArea::PointerData& data) {
    if (!m_enabled) {
      return;
    }
    if (data.button == BTN_RIGHT && (m_onRightClick || m_onRightClickWithPointer)) {
      if (m_onRightClickWithPointer) {
        m_onRightClickWithPointer(data.sceneX, data.sceneY, data.serial, data.time);
      } else {
        m_onRightClick();
      }
    } else if (data.button == BTN_LEFT && m_onClick) {
      m_onClick();
    }
  });
  area->setFocusable(true);
  area->setOnFocusGain([this]() { applyVisualState(); });
  area->setOnFocusLoss([this]() { applyVisualState(); });
  area->setOnKeyDown([this](const InputArea::KeyData& key) {
    if (!key.pressed || !m_enabled || !m_onClick) {
      return;
    }
    if (KeybindMatcher::matches(KeybindAction::Validate, key.sym, key.modifiers)) {
      m_onClick();
    }
  });
  area->setEnabled(false);
  m_inputArea = static_cast<InputArea*>(addChild(std::move(area)));
  m_inputArea->setParticipatesInLayout(false);
  m_inputArea->setZIndex(1);
  m_inputArea->setPosition(0.0F, 0.0F);
  m_inputArea->setFrameSize(width(), height());

  applyVariant();
  m_paletteConn = paletteChanged().connect([this] {
    // Re-derive color slots from the (possibly updated) palette and push
    // them immediately if no hover/press animation is in flight. Otherwise
    // the running animation keeps its old snapshot for one more tick and
    // the next applyVisualState() will resync.
    applyVariant();
  });
  m_buttonBordersConn =
      Style::buttonBordersChanged().connect([this] { setBorder(m_targetBorder, effectiveBorderWidth()); });
}

Button::~Button() {
  if (m_animId != 0 && animationManager() != nullptr) {
    animationManager()->cancel(m_animId);
    m_animId = 0;
  }
}

void Button::setText(std::string_view text) {
  // A glyph-only button must not grow the text-tier chrome (control height,
  // wider padding) just because its text was cleared: creating the label is
  // what flips the size tier, so skip it for empty text.
  if (text.empty() && m_label == nullptr) {
    return;
  }
  ensureLabel();
  m_label->setText(text);
  m_label->setVisible(!text.empty());
}

void Button::setGlyph(std::string_view name) {
  ensureGlyph();
  m_glyph->setGlyph(name);
}

void Button::setFontSize(float size) {
  ensureLabel();
  m_label->setFontSize(size);
  if (m_glyph != nullptr) {
    m_glyph->setGlyphSize(size);
  }
}

void Button::setGlyphSize(float size) {
  ensureGlyph();
  m_glyph->setGlyphSize(size);
}

void Button::setControlHeight(float height) {
  const float pinned = std::max(1.0F, height);
  setMinHeight(pinned);
  setMaxHeight(pinned);
}

void Button::setOnClick(std::function<void()> callback) {
  m_onClick = std::move(callback);
  refreshInputAreaEnabled();
}

void Button::setOnRightClick(std::function<void()> callback) {
  m_onRightClick = std::move(callback);
  if (m_inputArea != nullptr) {
    m_inputArea->setAcceptedButtons(
        m_onRightClick ? InputArea::buttonMask({BTN_LEFT, BTN_RIGHT}) : InputArea::buttonMask(BTN_LEFT)
    );
  }
  refreshInputAreaEnabled();
}

void Button::setOnRightClickWithPointer(
    std::function<void(float sceneX, float sceneY, std::uint32_t serial, std::uint32_t time)> callback
) {
  m_onRightClickWithPointer = std::move(callback);
  if (m_inputArea != nullptr) {
    m_inputArea->setAcceptedButtons(
        (m_onRightClick || m_onRightClickWithPointer) ? InputArea::buttonMask({BTN_LEFT, BTN_RIGHT})
                                                      : InputArea::buttonMask(BTN_LEFT)
    );
  }
  refreshInputAreaEnabled();
}

void Button::setOnPress(std::function<void(float, float, bool)> callback) {
  m_onPress = std::move(callback);
  refreshInputAreaEnabled();
}

void Button::setOnMotion(std::function<void()> callback) {
  m_onMotion = std::move(callback);
  refreshInputAreaEnabled();
}

void Button::setOnPointerMotion(std::function<void(float, float)> callback) {
  m_onPointerMotion = std::move(callback);
  refreshInputAreaEnabled();
}

void Button::setOnEnter(std::function<void()> callback) {
  m_onEnter = std::move(callback);
  refreshInputAreaEnabled();
}

void Button::setOnLeave(std::function<void()> callback) {
  m_onLeave = std::move(callback);
  refreshInputAreaEnabled();
}

void Button::setHoverSuppressed(bool suppressed) {
  if (m_hoverSuppressed == suppressed) {
    return;
  }
  m_hoverSuppressed = suppressed;
  applyVisualState();
}

void Button::setHoveredVisual(bool hovered) {
  if (m_hoveredVisual == hovered) {
    return;
  }
  m_hoveredVisual = hovered;
  applyVisualState();
}

void Button::setPressedVisual(bool pressed) {
  if (m_pressedVisual == pressed) {
    return;
  }
  m_pressedVisual = pressed;
  applyVisualState();
}

void Button::setCursorShape(std::uint32_t shape) {
  if (m_inputArea != nullptr) {
    m_inputArea->setCursorShape(shape);
  }
}

void Button::setBadge(std::string_view text) {
  ensureBadge();
  m_badgeLabel->setText(text);
  m_badge->setVisible(!text.empty());
}

void Button::setBadgeFontSize(float size) {
  ensureBadge();
  m_badgeLabel->setFontSize(size);
}

void Button::setTooltip(std::string_view text) {
  if (m_inputArea == nullptr) {
    return;
  }
  if (text.empty()) {
    m_inputArea->clearTooltip();
  } else {
    m_inputArea->setTooltip(std::string(text));
  }
}

void Button::setTabStop(bool tabStop) {
  if (m_inputArea != nullptr) {
    m_inputArea->setTabStop(tabStop);
  }
}

void Button::setKeyboardFocusHint(bool hint) {
  if (m_keyboardFocusHint == hint) {
    return;
  }
  m_keyboardFocusHint = hint;
  applyVisualState();
}

void Button::ensureBadge() {
  if (m_badge != nullptr) {
    return;
  }
  auto badge = std::make_unique<Flex>();
  badge->setDirection(FlexDirection::Horizontal);
  badge->setAlign(FlexAlign::Center);
  badge->setJustify(FlexJustify::Center);
  badge->setPadding(2.0F, Style::spaceXs);
  badge->setRadius(Style::scaledRadiusSm());
  badge->setParticipatesInLayout(false);
  badge->setVisible(false);

  auto label = std::make_unique<Label>();
  label->setFontSize(Style::fontSizeCaption * 0.85F);
  m_badgeLabel = static_cast<Label*>(badge->addChild(std::move(label)));

  m_badge = static_cast<Flex*>(addChild(std::move(badge)));
}

void Button::updateInputArea() {
  if (m_inputArea != nullptr) {
    m_inputArea->setPosition(0.0F, 0.0F);
    m_inputArea->setFrameSize(width(), height());
  }
}

bool Button::hovered() const noexcept { return m_inputArea != nullptr && m_inputArea->hovered(); }

bool Button::pressed() const noexcept { return m_inputArea != nullptr && m_inputArea->pressed(); }

void Button::setEnabled(bool enabled) {
  if (m_enabled == enabled) {
    return;
  }
  m_enabled = enabled;
  refreshInputAreaEnabled();
  applyVisualState();
}

void Button::setSelected(bool selected) {
  if (m_selected == selected) {
    return;
  }
  m_selected = selected;
  applyVisualState();
  markPaintDirty();
}

void Button::setContentAlign(ButtonContentAlign align) { m_contentAlign = align; }

void Button::setVariant(ButtonVariant variant) {
  if (m_variant == variant) {
    return;
  }
  m_variant = variant;
  m_customPalette.reset();
  applyVariant();
}

void Button::setCustomPalette(ButtonPalette customPalette) {
  m_customPalette = customPalette;
  applyVariant();
}

void Button::setSurfaceOpacity(float opacity) {
  const float clamped = std::clamp(opacity, 0.0F, 1.0F);
  if (m_surfaceOpacity == clamped) {
    return;
  }
  m_surfaceOpacity = clamped;
  applyVariant();
}

void Button::applyVariant() {
  m_palette = m_customPalette.value_or(paletteForVariant(m_variant));
  if (m_surfaceOpacity < 1.0F) {
    m_palette.normal.bg.alpha *= m_surfaceOpacity;
    m_palette.disabled.bg.alpha *= m_surfaceOpacity;
  }
  setBorder(m_palette.normal.border, effectiveBorderWidth());

  // Only seed targets before the first visual state application. Once the
  // button has been painted, applyVisualState() must compare against the
  // previous targets so variant/palette changes actually propagate.
  if (!m_visualStateInitialized) {
    m_targetBg = resolveColorSpec(m_palette.normal.bg);
    m_targetBorder = resolveColorSpec(m_palette.normal.border);
    m_targetLabel = resolveColorSpec(m_palette.normal.label);
  }
  applyVisualState();
}

void Button::refreshInputAreaEnabled() {
  if (m_inputArea != nullptr) {
    m_inputArea->setEnabled(
        m_enabled
        && (static_cast<bool>(m_onClick)
            || static_cast<bool>(m_onMotion)
            || static_cast<bool>(m_onPointerMotion)
            || static_cast<bool>(m_onPress)
            || static_cast<bool>(m_onEnter)
            || static_cast<bool>(m_onLeave)
            || static_cast<bool>(m_onRightClick)
            || static_cast<bool>(m_onRightClickWithPointer))
    );
  }
}

void Button::ensureLabel() {
  if (m_label != nullptr) {
    return;
  }
  auto label = std::make_unique<Label>();
  m_label = static_cast<Label*>(addChild(std::move(label)));
  setMinHeight(Style::controlHeight);
  setPadding(Style::spaceSm, Style::spaceMd);
  if (m_glyph != nullptr) {
    setDirection(FlexDirection::Horizontal);
    setGap(Style::spaceXs);
  }
  applyColors(m_targetBg, m_targetBorder, m_targetLabel);
}

void Button::ensureGlyph() {
  if (m_glyph != nullptr) {
    return;
  }
  // insertChildAt so the glyph lands before the label in the children vector,
  // which is what Flex iterates to assign layout positions
  if (m_label != nullptr) {
    auto& kids = children();
    std::size_t labelIndex = 0;
    for (std::size_t i = 0; i < kids.size(); ++i) {
      if (kids[i].get() == m_label) {
        labelIndex = i;
        break;
      }
    }
    auto glyph = std::make_unique<Glyph>();
    m_glyph = static_cast<Glyph*>(insertChildAt(labelIndex, std::move(glyph)));
  } else {
    auto glyph = std::make_unique<Glyph>();
    m_glyph = static_cast<Glyph*>(addChild(std::move(glyph)));
  }
  if (m_label != nullptr) {
    setDirection(FlexDirection::Horizontal);
    setGap(Style::spaceXs);
  }
  applyColors(m_targetBg, m_targetBorder, m_targetLabel);
}

void Button::applyColors(const Color& bg, const Color& border, const Color& label) {
  setFill(bg);
  setBorder(border, effectiveBorderWidth());
  if (m_label != nullptr) {
    m_label->setColor(label);
  }
  if (m_glyph != nullptr) {
    m_glyph->setColor(label);
  }
  for (auto& child : children()) {
    if (child.get() == m_label || child.get() == m_glyph || child.get() == m_badge) {
      continue;
    }
    if (auto* lbl = dynamic_cast<Label*>(child.get())) {
      lbl->setColor(label);
    } else if (auto* gl = dynamic_cast<Glyph*>(child.get())) {
      gl->setColor(label);
    }
  }
  if (m_badge != nullptr) {
    m_badge->setFill(Color{label.r, label.g, label.b, label.a * 0.85F});
    if (m_badgeLabel != nullptr) {
      m_badgeLabel->setColor(bg);
    }
  }
  m_visualStateInitialized = true;
}

float Button::effectiveBorderWidth() const noexcept {
  return Style::buttonBordersEnabled() ? m_palette.borderWidth : 0.0F;
}

void Button::resolveVisualStateColors(Color& targetBg, Color& targetBorder, Color& targetLabel) const {
  const bool isKeyboardFocused = m_enabled && m_keyboardFocusHint;
  const bool isInputFocused = m_enabled && m_inputArea != nullptr && m_inputArea->focused();
  const bool keyboardNavFocus = isKeyboardFocused
      && (m_variant == ButtonVariant::TabActive
          || m_variant == ButtonVariant::Tab
          || m_variant == ButtonVariant::Ghost);
  bool isHovered = m_enabled && (m_hoveredVisual || (!m_hoverSuppressed && hovered()));
  if (isInputFocused && !keyboardNavFocus) {
    isHovered = true;
  }
  bool isPressed = m_enabled && (m_pressedVisual || pressed());
  bool isSelected = m_enabled && m_selected;

  if (!m_enabled) {
    targetBg = resolveColorSpec(m_palette.disabled.bg);
    targetBorder = resolveColorSpec(m_palette.disabled.border);
    targetLabel = resolveColorSpec(m_palette.disabled.label);
  } else if (isPressed) {
    targetBg = resolveColorSpec(m_palette.pressed.bg);
    targetBorder = resolveColorSpec(m_palette.pressed.border);
    targetLabel = resolveColorSpec(m_palette.pressed.label);
  } else if (isSelected && m_palette.selected.has_value()) {
    targetBg = resolveColorSpec(m_palette.selected->bg);
    targetBorder = resolveColorSpec(m_palette.selected->border);
    targetLabel = resolveColorSpec(m_palette.selected->label);
  } else if (keyboardNavFocus) {
    targetBg = resolveColorSpec(colorSpecFromRole(ColorRole::Secondary));
    targetBorder = resolveColorSpec(clearColorSpec());
    targetLabel = resolveColorSpec(colorSpecFromRole(ColorRole::OnSecondary));
  } else if (isHovered || isSelected) {
    targetBg = resolveColorSpec(m_palette.hover.bg);
    targetBorder = resolveColorSpec(m_palette.hover.border);
    targetLabel = resolveColorSpec(m_palette.hover.label);
  } else {
    targetBg = resolveColorSpec(m_palette.normal.bg);
    targetBorder = resolveColorSpec(m_palette.normal.border);
    targetLabel = resolveColorSpec(m_palette.normal.label);
  }
}

void Button::applyVisualState() {
  Color targetBg;
  Color targetBorder;
  Color targetLabel;
  resolveVisualStateColors(targetBg, targetBorder, targetLabel);

  if (!m_visualStateInitialized) {
    m_targetBg = targetBg;
    m_targetBorder = targetBorder;
    m_targetLabel = targetLabel;
    applyColors(targetBg, targetBorder, targetLabel);
    return;
  }

  if (targetBg == m_targetBg && targetBorder == m_targetBorder && targetLabel == m_targetLabel) {
    return;
  }

  if (animationManager() == nullptr) {
    applyColors(targetBg, targetBorder, targetLabel);
    m_targetBg = targetBg;
    m_targetBorder = targetBorder;
    m_targetLabel = targetLabel;
    return;
  }

  // Snapshot current display colors as the animation start point
  m_fromBg = m_targetBg;
  m_fromBorder = m_targetBorder;
  m_fromLabel = m_targetLabel;
  m_targetBg = targetBg;
  m_targetBorder = targetBorder;
  m_targetLabel = targetLabel;

  if (m_animId != 0) {
    animationManager()->cancel(m_animId);
  }

  m_animId = animationManager()->animate(
      0.0F, 1.0F, Style::animFast, Easing::EaseOutCubic,
      [this](float t) {
        applyColors(
            lerpColor(m_fromBg, m_targetBg, t), lerpColor(m_fromBorder, m_targetBorder, t),
            lerpColor(m_fromLabel, m_targetLabel, t)
        );
      },
      [this]() { m_animId = 0; }
  );
  markPaintDirty();
}

void Button::applyLabelMaxWidth() {
  if (m_label == nullptr) {
    return;
  }
  const float maxBtnWidth = maxWidth();
  if (maxBtnWidth > 0.0F) {
    const float padding = paddingLeft() + paddingRight();
    const float glyphW = (m_glyph != nullptr && m_glyph->visible()) ? m_glyph->width() + gap() : 0.0F;
    m_label->setMaxWidth(std::max(0.0F, maxBtnWidth - padding - glyphW));
    m_label->setEllipsize(TextEllipsize::End);
  } else {
    m_label->setMaxWidth(0.0F);
  }
}

void Button::doLayout(Renderer& renderer) {
  const bool useCurrentSize = arrangingByLayout() || !sizeAssignedByLayout();
  const float assignedWidth = useCurrentSize ? width() : 0.0F;
  const float assignedHeight = useCurrentSize ? height() : 0.0F;
  const bool hasVisibleLabel = m_label != nullptr && m_label->visible();
  const bool glyphOnly = m_glyph != nullptr && !hasVisibleLabel;

  if (m_label != nullptr) {
    applyLabelMaxWidth();
    m_label->measure(renderer);
  }
  if (m_glyph != nullptr) {
    m_glyph->measure(renderer);
  }

  Flex::doLayout(renderer);

  // Buttons are often sized by a parent stretch pass. Preserve that assigned
  // box instead of collapsing back to intrinsic content width.
  if (assignedWidth > 0.0F || assignedHeight > 0.0F) {
    setSizeFromLayout(std::max(width(), assignedWidth), std::max(height(), assignedHeight));
  }

  if (glyphOnly && m_contentAlign == ButtonContentAlign::Center) {
    const bool hasAssignedWidth = assignedWidth > 0.0F;
    if (!hasAssignedWidth) {
      const float squareSize = std::max(width(), height());
      setSizeFromLayout(squareSize, squareSize);
    }
  }

  // After Flex layout the content row is leading-anchored inside the padding.
  // Shift the whole group to honor m_contentAlign (Start leaves it as-is).
  if (m_contentAlign != ButtonContentAlign::Start) {
    float contentLeft = 0.0F;
    float contentRight = 0.0F;
    float contentTop = 0.0F;
    float contentBottom = 0.0F;
    bool haveContent = false;

    for (auto& child : children()) {
      Node* node = child.get();
      if (node == nullptr || !node->visible() || !node->participatesInLayout() || node->zIndex() < 0) {
        continue;
      }
      const float left = node->x();
      const float right = node->x() + node->width();
      const float top = node->y();
      const float bottom = node->y() + node->height();
      if (!haveContent) {
        contentLeft = left;
        contentRight = right;
        contentTop = top;
        contentBottom = bottom;
        haveContent = true;
      } else {
        contentLeft = std::min(contentLeft, left);
        contentRight = std::max(contentRight, right);
        contentTop = std::min(contentTop, top);
        contentBottom = std::max(contentBottom, bottom);
      }
    }

    if (haveContent) {
      const float contentWidth = contentRight - contentLeft;
      const float contentHeight = contentBottom - contentTop;
      float targetLeft = 0.0F;
      if (m_contentAlign == ButtonContentAlign::Center) {
        targetLeft = std::round((width() - contentWidth) * 0.5F);
      } else { // End
        targetLeft = std::round(Style::rtl() ? paddingLeft() : width() - contentWidth - paddingRight());
      }
      const float shiftX = targetLeft - contentLeft;
      const float targetTop = std::round((height() - contentHeight) * 0.5F);
      const float shiftY = targetTop - contentTop;
      if (std::abs(shiftX) > 0.01F || std::abs(shiftY) > 0.01F) {
        for (auto& child : children()) {
          Node* node = child.get();
          if (node == nullptr || !node->visible() || !node->participatesInLayout() || node->zIndex() < 0) {
            continue;
          }
          node->setPosition(node->x() + shiftX, node->y() + shiftY);
        }
      }
    }
  }

  if (m_inputArea != nullptr) {
    m_inputArea->setPosition(0.0F, 0.0F);
    m_inputArea->setSize(width(), height());
  }

  if (m_badge != nullptr && m_badge->visible()) {
    m_badgeLabel->measure(renderer);
    m_badge->setMinWidth(0.0F);
    m_badge->layout(renderer);
    m_badge->setMinWidth(m_badge->height());
    m_badge->layout(renderer);
    const float margin = Style::spaceXs;
    m_badge->setPosition(width() - m_badge->width() - margin, margin);
  }

  // Only apply visual state if no animation is in progress — a running
  // animation already owns the color transition and re-calling here would
  // reset its from/to snapshot, collapsing the animation to a no-op.
  if (m_animId == 0) {
    applyVisualState();
  }
}

LayoutSize Button::doMeasure(Renderer& renderer, const LayoutConstraints& constraints) {
  applyLabelMaxWidth();
  return measureByLayout(renderer, constraints);
}

void Button::doArrange(Renderer& renderer, const LayoutRect& rect) { arrangeByLayout(renderer, rect); }

std::vector<std::vector<std::unique_ptr<Button>>>
wrapButtonsIntoRows(Renderer& renderer, std::vector<std::unique_ptr<Button>>& buttons, float maxWidth, float gap) {
  std::vector<std::vector<std::unique_ptr<Button>>> rows;
  if (buttons.empty()) {
    return rows;
  }

  std::vector<std::unique_ptr<Button>> currentRow;
  float currentRowWidth = 0.0F;

  for (auto& button : buttons) {
    if (!button) {
      continue;
    }
    const LayoutSize measured = button->measure(renderer, LayoutConstraints{});
    const float btnWidth = measured.width;

    if (!currentRow.empty() && currentRowWidth + gap + btnWidth > maxWidth) {
      rows.push_back(std::move(currentRow));
      currentRow.clear();
      currentRowWidth = 0.0F;
    }

    if (currentRow.empty()) {
      currentRowWidth = btnWidth;
    } else {
      currentRowWidth += gap + btnWidth;
    }
    currentRow.push_back(std::move(button));
  }

  if (!currentRow.empty()) {
    rows.push_back(std::move(currentRow));
  }

  buttons.clear();
  return rows;
}

void populateRowContainer(
    Flex& container, std::vector<std::vector<std::unique_ptr<Button>>> rows, float maxWidth, float gap
) {
  for (auto& rowButtons : rows) {
    auto row = std::make_unique<Flex>();
    row->setDirection(FlexDirection::Horizontal);
    row->setAlign(FlexAlign::Center);
    row->setGap(gap);
    row->setFillWidth(true);
    for (auto& btn : rowButtons) {
      if (rowButtons.size() == 1) {
        btn->setMaxWidth(maxWidth);
      } else {
        btn->setMaxWidth(0.0F);
      }
      btn->setFlexGrow(1.0F);
      row->addChild(std::move(btn));
    }
    container.addChild(std::move(row));
  }
}
