// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chrome/browser/ui/views/tabs/tab_group_header.h"

#include <memory>
#include <string>
#include <string_view>
#include <utility>

#include "base/memory/raw_ptr.h"
#include "build/build_config.h"
#include "chrome/app/vector_icons/vector_icons.h"
#include "chrome/browser/tab_group_sync/tab_group_sync_service_factory.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/tabs/saved_tab_groups/saved_tab_group_utils.h"
#include "chrome/browser/ui/tabs/tab_group_attention_indicator.h"
#include "chrome/browser/ui/tabs/tab_group_data.h"
#include "chrome/browser/ui/tabs/tab_group_features.h"
#include "chrome/browser/ui/tabs/tab_style.h"
#include "chrome/browser/ui/tabs/vertical_tab_strip_state_controller.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/tabs/groups/tab_group_accessibility.h"
#include "chrome/browser/ui/views/tabs/groups/tab_group_editor_bubble_tracker.h"
#include "chrome/browser/ui/views/tabs/groups/tab_group_editor_bubble_view.h"
#include "chrome/browser/ui/views/tabs/shared/tab_strip_types.h"
#include "chrome/browser/ui/views/tabs/tab_group_style.h"
#include "chrome/browser/ui/views/tabs/tab_group_underline.h"
#include "chrome/browser/ui/views/tabs/tab_slot_controller.h"
#include "chrome/browser/ui/views/tabs/tab_slot_view.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/browser/ui/views/tabs/tab_strip_controller.h"
#include "chrome/grit/generated_resources.h"
#include "components/data_sharing/public/features.h"
#include "components/saved_tab_groups/public/tab_group_sync_service.h"
#include "components/strings/grit/components_strings.h"
#include "components/tab_groups/tab_group_id.h"
#include "components/tabs/public/tab_group.h"
#include "components/vector_icons/vector_icons.h"
#include "third_party/skia/include/core/SkPath.h"
#include "third_party/skia/include/core/SkRRect.h"
#include "ui/base/interaction/element_identifier.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/mojom/menu_source_type.mojom-shared.h"
#include "ui/base/ui_base_features.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/geometry/skia_conversions.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/layout/flex_layout.h"
#include "ui/views/layout/flex_layout_types.h"
#include "ui/views/view.h"
#include "ui/views/view_class_properties.h"

#if BUILDFLAG(IS_MAC)
#include "base/i18n/break_iterator.h"
#include "base/i18n/char_iterator.h"
#include "base/i18n/rtl.h"
#include "third_party/icu/source/common/unicode/uchar.h"
#endif

namespace {

// The amount of padding between the label and the sync icon.
constexpr int kSyncIconPaddingFromLabel = 2;

bool SupportsDataSharing() {
  return data_sharing::features::IsDataSharingFunctionalityEnabled();
}

#if BUILDFLAG(IS_MAC)
// Returns true if `text` renders with Apple Color Emoji on macOS.
// This intentionally uses a conservative heuristic: only default
// emoji-presentation codepoints and regional indicators (flag sequences).
//
// We intentionally do not treat Variation Selector-16 (U+FE0F) alone as a
// trigger. Some text-default symbols followed by VS-16 can still render with
// text-like metrics in this UI context (e.g. U+2666 U+FE0F), and applying the
// 1px compensation to them introduces a visible right bias.
bool RendersAsColorEmoji(std::u16string_view text) {
  for (base::i18n::UTF16CharIterator iter(text); !iter.end(); iter.Advance()) {
    const UChar32 codepoint = iter.get();
    if (u_hasBinaryProperty(codepoint, UCHAR_EMOJI_PRESENTATION) ||
        u_hasBinaryProperty(codepoint, UCHAR_REGIONAL_INDICATOR)) {
      return true;
    }
  }
  return false;
}

// Returns true if `text` consists of exactly one grapheme cluster (a single
// user-perceived character). This includes multi-codepoint sequences such as
// emoji with skin tone modifiers, ZWJ sequences, and regional indicator
// pairs.
bool IsSingleGrapheme(std::u16string_view text) {
  if (text.empty()) {
    return false;
  }
  base::i18n::BreakIterator graphemes(
      text, base::i18n::BreakIterator::BREAK_CHARACTER);
  if (!graphemes.Init() || !graphemes.Advance()) {
    return false;
  }
  return !graphemes.Advance();
}
#endif

// Returns a horizontal pixel offset to apply to the tab group title for
// visual centering, accounting for RTL. Most titles return 0 (advance-width
// centering is correct). On macOS, Apple Color Emoji glyphs have asymmetric
// side bearings — the colored ink sits slightly left of the glyph's advance
// box — so a single-emoji title centered using advance-width metrics looks
// ~1px left of the chip's geometric center. Shift such titles right by 1px
// to compensate. Under RTL the offset is negated so the on-screen visual
// direction (rightward shift) is preserved after framework mirroring.
int GetTitleVisualCenteringOffset(std::u16string_view title) {
#if BUILDFLAG(IS_MAC)
  if (IsSingleGrapheme(title) && RendersAsColorEmoji(title)) {
    return base::i18n::IsRTL() ? -1 : 1;
  }
#endif
  return 0;
}

class TabGroupHighlightPathGenerator : public views::HighlightPathGenerator {
 public:
  TabGroupHighlightPathGenerator(const views::View* chip,
                                 const views::View* title,
                                 const TabGroupStyle& style)
      : chip_(chip), title_(title), style_(style) {}
  TabGroupHighlightPathGenerator(const TabGroupHighlightPathGenerator&) =
      delete;
  TabGroupHighlightPathGenerator& operator=(
      const TabGroupHighlightPathGenerator&) = delete;

  // views::HighlightPathGenerator:
  SkPath GetHighlightPath(const views::View* view) override {
    return SkPath::RRect(SkRRect::MakeRectXY(
        gfx::RectToSkRect(chip_->bounds()),
        style_->GetHighlightPathGeneratorCornerRadius(title_),
        style_->GetHighlightPathGeneratorCornerRadius(title_)));
  }

 private:
  const raw_ptr<const views::View, AcrossTasksDanglingUntriaged> chip_;
  const raw_ptr<const views::View, AcrossTasksDanglingUntriaged> title_;
  const raw_ref<const TabGroupStyle> style_;
};

}  // namespace

TabGroupHeader::TabGroupHeader(TabSlotController& tab_slot_controller,
                               const tab_groups::TabGroupId& group,
                               const TabGroupStyle& style)
    : HoverCardAnchorTarget(this),
      tab_slot_controller_(tab_slot_controller),
      title_chip_(AddChildView(std::make_unique<views::View>())),
      title_(title_chip_->AddChildView(std::make_unique<views::Label>())),
      sync_icon_(
          title_chip_->AddChildView(std::make_unique<views::ImageView>())),
      attention_indicator_(
          title_chip_->AddChildView(std::make_unique<views::ImageView>())),
      group_style_(style),
      tab_style_(TabStyle::Get()),
      group_title_(u""),
      color_(tab_slot_controller_->GetPaintedGroupColor(
          tab_slot_controller_->GetGroupColorId(group))),
      is_collapsed_(tab_slot_controller_->IsGroupCollapsed(group)),
      editor_bubble_tracker_(tabs::VerticalTabStripStateController::From(
          tab_slot_controller_->GetBrowserWindowInterface())) {
  editor_bubble_opened_subscription_ =
      editor_bubble_tracker_.RegisterOnBubbleOpened(
          base::BindRepeating(&TabSlotController::NotifyTabstripBubbleOpened,
                              base::Unretained(tab_slot_controller_)));
  editor_bubble_closed_subscription_ =
      editor_bubble_tracker_.RegisterOnBubbleClosed(
          base::BindRepeating(&TabSlotController::NotifyTabstripBubbleClosed,
                              base::Unretained(tab_slot_controller_)));

  TabGroup* const tab_group = tab_slot_controller_->GetTabGroup(group);
  if (tab_group) {
    tab_group_data_observer_ =
        std::make_unique<tabs::TabGroupDataObserver>(tab_group);
    SetHoverCardDataFrom(tab_group_data_observer_->tab_group_data());
    tab_group_data_observer_subscription_ =
        tab_group_data_observer_->RegisterTabGroupDataChangedCallback(
            base::BindRepeating(&TabGroupHeader::OnTabGroupDataChanged,
                                base::Unretained(this)));
  }
}

TabGroupHeader::~TabGroupHeader() {
  if (tab_slot_controller_->HoverCardIsShowing(this)) {
    tab_slot_controller_->UpdateHoverCard(
        nullptr, TabSlotController::HoverCardUpdateType::kTabRemoved);
  }
}

void TabGroupHeader::Init(const tab_groups::TabGroupId& group) {
  SetGroup(group);
  set_context_menu_controller(this);

  // Disable events processing (like tooltip handling)
  // for children of TabGroupHeader.
  title_chip_->SetCanProcessEventsWithinSubtree(false);

  title_->SetCollapseWhenHidden(true);
  title_->SetAutoColorReadabilityEnabled(false);
  title_->SetHorizontalAlignment(gfx::ALIGN_LEFT);
  title_->SetElideBehavior(gfx::FADE_TAIL);
  title_->SetLineHeight(20);

  // Enable keyboard focus.
  SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
  views::FocusRing::Install(this);
  views::HighlightPathGenerator::Install(
      this, std::make_unique<TabGroupHighlightPathGenerator>(
                title_chip_, title_, *group_style_));
  // The tab group gets painted with a solid color that may not contrast well
  // with the focus indicator, so draw an outline around the focus ring for it
  // to contrast with the solid color.
  SetProperty(views::kDrawFocusRingBackgroundOutline, true);

  SetProperty(views::kElementIdentifierKey, kTabGroupHeaderElementId);
  attention_indicator_->SetProperty(views::kElementIdentifierKey,
                                    kTabGroupHeaderAttentionIndicatorElementId);

  SetEventTargeter(std::make_unique<views::ViewTargeter>(this));

  SetCollapsedState();
  UpdateIsCollapsed();

  GetViewAccessibility().SetRole(ax::mojom::Role::kTabList);
  GetViewAccessibility().SetIsEditable(true);

  title_text_changed_subscription_ =
      title_->AddTextChangedCallback(base::BindRepeating(
          &TabGroupHeader::UpdateTooltipText, base::Unretained(this)));

  UpdateTooltipText();
}

bool TabGroupHeader::OnKeyPressed(const ui::KeyEvent& event) {
  if ((event.key_code() == ui::VKEY_SPACE ||
       event.key_code() == ui::VKEY_RETURN) &&
      !editor_bubble_tracker_.is_open() &&
      tab_slot_controller_->GetFocusedGroup() != group()) {
    tab_slot_controller_->ToggleTabGroupCollapsedState(
        group().value(), ToggleTabGroupCollapsedStateOrigin::kKeyboard);
    views::ElementTrackerViews::GetInstance()->NotifyViewActivated(
        kTabGroupHeaderElementId, this);
    return true;
  }

  constexpr int kModifiedFlag =
#if BUILDFLAG(IS_MAC)
      ui::EF_COMMAND_DOWN;
#else
      ui::EF_CONTROL_DOWN;
#endif

  if (event.type() == ui::EventType::kKeyPressed &&
      (event.flags() & kModifiedFlag)) {
    if (event.key_code() == ui::VKEY_RIGHT) {
      tab_slot_controller_->ShiftGroupRight(group().value());
      return true;
    }
    if (event.key_code() == ui::VKEY_LEFT) {
      tab_slot_controller_->ShiftGroupLeft(group().value());
      return true;
    }
  }

  return false;
}

bool TabGroupHeader::OnMousePressed(const ui::MouseEvent& event) {
  // Ignore the click if the editor is already open. Do this so clicking
  // on us again doesn't re-trigger the editor.
  //
  // Though the bubble is deactivated before we receive a mouse event,
  // the actual widget destruction happens in a posted task. That task
  // gets run after we receive the mouse event. If this sounds brittle,
  // that's because it is!
  if (editor_bubble_tracker_.is_open()) {
    return false;
  }

  tab_slot_controller_->UpdateHoverCard(
      nullptr, TabSlotController::HoverCardUpdateType::kEvent);

  // Allow a right click from touch to drag, which corresponds to a long click.
  if (event.IsOnlyLeftMouseButton() ||
      (event.IsOnlyRightMouseButton() && event.flags() & ui::EF_FROM_TOUCH)) {
    tab_slot_controller_->MaybeStartDrag(
        this, event, tab_slot_controller_->GetSelectionModel());

    return true;
  }

  return false;
}

bool TabGroupHeader::OnMouseDragged(const ui::MouseEvent& event) {
  // TODO: ensure ignoring return value is ok.
  std::ignore = tab_slot_controller_->ContinueDrag(this, event);
  return true;
}

void TabGroupHeader::OnMouseReleased(const ui::MouseEvent& event) {
  if (!dragging()) {
    bool open_editor_bubble =
        event.IsRightMouseButton() && !editor_bubble_tracker_.is_open();
    bool toggle_collapse = event.IsLeftMouseButton() &&
                           tab_slot_controller_->GetFocusedGroup() != group();

    if (open_editor_bubble) {
      editor_bubble_tracker_.Opened(TabGroupEditorBubbleView::Show(
          tab_slot_controller_->GetBrowserWindowInterface(), group().value(),
          /*anchor_view=*/this, /*anchor_rect=*/std::nullopt,
          /*stop_context_menu_propagation=*/false));
    } else if (toggle_collapse) {
      tab_slot_controller_->ToggleTabGroupCollapsedState(
          group().value(), ToggleTabGroupCollapsedStateOrigin::kMouse);
      views::ElementTrackerViews::GetInstance()->NotifyViewActivated(
          kTabGroupHeaderElementId, this);
    }
  }

  tab_slot_controller_->EndDrag(EndDragReason::kComplete);
}

void TabGroupHeader::OnMouseEntered(const ui::MouseEvent& event) {
  if (features::IsTabGroupHoverCardsEnabled()) {
    tab_slot_controller_->UpdateHoverCard(
        this, TabSlotController::HoverCardUpdateType::kHover);
  } else {
    tab_slot_controller_->UpdateHoverCard(
        nullptr, TabSlotController::HoverCardUpdateType::kHover);
  }
}

void TabGroupHeader::OnGestureEvent(ui::GestureEvent* event) {
  tab_slot_controller_->UpdateHoverCard(
      nullptr, TabSlotController::HoverCardUpdateType::kEvent);
  switch (event->type()) {
    case ui::EventType::kGestureTap:
      if (tab_slot_controller_->GetFocusedGroup() != group()) {
        tab_slot_controller_->ToggleTabGroupCollapsedState(
            group().value(), ToggleTabGroupCollapsedStateOrigin::kGesture);
        views::ElementTrackerViews::GetInstance()->NotifyViewActivated(
            kTabGroupHeaderElementId, this);
      }
      break;
    case ui::EventType::kGestureLongTap: {
      editor_bubble_tracker_.Opened(TabGroupEditorBubbleView::Show(
          tab_slot_controller_->GetBrowserWindowInterface(), group().value(),
          /*anchor_view=*/this, /*anchor_rect=*/std::nullopt,
          /*stop_context_menu_propagation=*/false));
      break;
    }
    case ui::EventType::kGestureScrollBegin: {
      tab_slot_controller_->MaybeStartDrag(
          this, *event, tab_slot_controller_->GetSelectionModel());
      break;
    }
    default:
      break;
  }
  event->SetHandled();
}

void TabGroupHeader::OnFocus() {
  View::OnFocus();

  if (features::IsTabGroupHoverCardsEnabled()) {
    tab_slot_controller_->UpdateHoverCard(
        this, TabSlotController::HoverCardUpdateType::kFocus);
  } else {
    tab_slot_controller_->UpdateHoverCard(
        nullptr, TabSlotController::HoverCardUpdateType::kFocus);
  }
}

void TabGroupHeader::OnBlur() {
  if (features::IsTabGroupHoverCardsEnabled() &&
      !tab_slot_controller_->IsFocusInTabStrip()) {
    tab_slot_controller_->UpdateHoverCard(
        nullptr, TabSlotController::HoverCardUpdateType::kFocus);
  }
}

void TabGroupHeader::OnThemeChanged() {
  TabSlotView::OnThemeChanged();
  VisualsChanged();
}

TabSlotView::ViewType TabGroupHeader::GetTabSlotViewType() const {
  return TabSlotView::ViewType::kTabGroupHeader;
}

TabSizeInfo TabGroupHeader::GetTabSizeInfo() const {
  TabSizeInfo size_info;
  // Group headers have a fixed width based on `title_`'s width.
  const int width = GetDesiredWidth();
  size_info.pinned_tab_width = width;
  size_info.min_active_width = width;
  size_info.min_inactive_width = width;
  size_info.standard_width = width;
  return size_info;
}

gfx::Rect TabGroupHeader::GetAnchorBoundsInScreen() const {
  // Skip the insetting in TabSlotView::GetAnchorBoundsInScreen(). In this
  // context insetting makes the anchored bubble partially cut into the tab
  // outline.
  // TODO(crbug.com/40803556): See if the layout of TabGroupHeader can be
  // unified with tabs so that bounds do not need to be calculated differently
  // between tabs and headers. As of writing this, hover cards to not cut into
  // the tab outline but without this change TabGroupEditorBubbleView does.
  return View::GetAnchorBoundsInScreen();
}

bool TabGroupHeader::NeedsToShowThumbnail() const {
  return false;
}

bool TabGroupHeader::IsValidHoverCardTarget() const {
  DCHECK(features::IsTabGroupHoverCardsEnabled());
  return group().has_value() &&
         tab_slot_controller_->GetTabGroup(group().value()) != nullptr;
}

views::BubbleAnchor TabGroupHeader::GetAnchor() {
  return views::BubbleAnchor(this);
}

views::BubbleBorder::Arrow TabGroupHeader::GetAnchorPosition() const {
  return views::BubbleBorder::TOP_LEFT;
}

void TabGroupHeader::OnGroupContentsChanged() {
  UpdateAccessibleName();
  UpdateTooltipText();
}

void TabGroupHeader::ShowContextMenuForViewImpl(
    views::View* source,
    const gfx::Point& point,
    ui::mojom::MenuSourceType source_type) {
  // Right click toggles ShowContextMenuForViewImpl, which we dont want to occur
  // if the left click should toggle the context menu.
  if (editor_bubble_tracker_.is_open()) {
    return;
  }

  // When the context menu is triggered via keyboard, the keyboard event
  // propagates to the textfield inside the Editor Bubble. In those cases, we
  // want to tell the Editor Bubble to stop the event by setting
  // stop_context_menu_propagation to true.
  //
  // However, when the context menu is triggered via mouse, the same event
  // sequence doesn't happen. Stopping the context menu propagation in that case
  // would artificially hide the textfield's context menu the first time the
  // user tried to access it. So we don't want to stop the context menu
  // propagation if this call is reached via mouse.
  //
  // Notably, event behavior with a mouse is inconsistent depending on
  // OS. When not on Mac, the OnMouseReleased() event happens first and opens
  // the Editor Bubble early, preempting the Show() call below. On Mac, the
  // ShowContextMenu() event happens first and the Show() call is made here.
  //
  // So, because of the event order on non-Mac, and because there is no native
  // way to open a context menu via keyboard on Mac, we assume that we've
  // reached this function via mouse if and only if the current OS is Mac.
  // Therefore, we don't stop the menu propagation in that case.
  constexpr bool kStopContextMenuPropagation =
#if BUILDFLAG(IS_MAC)
      false;
#else
      true;
#endif

  editor_bubble_tracker_.Opened(TabGroupEditorBubbleView::Show(
      tab_slot_controller_->GetBrowserWindowInterface(), group().value(), this,
      std::nullopt, kStopContextMenuPropagation));
}

bool TabGroupHeader::DoesIntersectRect(const views::View* target,
                                       const gfx::Rect& rect) const {
  // Tab group headers are only highlighted with a tab shape while dragging, so
  // visually the header is basically a rectangle between two tab separators.
  // The distance from the endge of the view to the tab separator is half of the
  // overlap distance. We should only accept events between the separators.
  const views::Widget* widget = GetWidget();
  bool extend_hittest = widget->IsMaximized() || widget->IsFullscreen();

  gfx::Rect contents_rect = GetLocalBounds();
  contents_rect.Inset(gfx::Insets::TLBR(
      extend_hittest ? 0 : GetLayoutConstant(LayoutConstant::kTabStripPadding),
      tab_style_->GetTabOverlap() / 2, 0, tab_style_->GetTabOverlap() / 2));
  return contents_rect.Intersects(rect);
}

void TabGroupHeader::VisualsChanged() {
  // TODO(crbug.com/372296676): Make TabGroupHeader observe the group for
  // changes to cut down on the number of times we recalculate the view.
  const tab_groups::TabGroupId tab_group_id = group().value();
  group_title_ = tab_slot_controller_->GetGroupTitle(tab_group_id);

  color_ = tab_slot_controller_->GetPaintedGroupColor(
      tab_slot_controller_->GetGroupColorId(tab_group_id));
  should_show_header_icon_ = ShouldShowHeaderIcon();

  // Update collapsed state before changing any UI.
  SetCollapsedState();

  UpdateTitleView();
  UpdateSyncIconView();
  UpdateAttentionIndicatorView();
  if (group_title_.empty()) {
    CreateHeaderWithoutTitle();
  } else {
    CreateHeaderWithTitle();
  }

  // Expand the clip by 1 DIP to prevent clipping the title chip's anti-aliased
  // rrect background at fractional display scales. This won't paint over
  // anything else since nothing is painted outside the background region.
  // See https://crbug.com/40662024 for more details.
  gfx::Rect clip_bounds = title_chip_->GetLocalBounds();
  clip_bounds.Outset(1);
  title_chip_->SetClipPath(SkPath::Rect(gfx::RectToSkRect(clip_bounds)));

  if (views::FocusRing::Get(this)) {
    views::FocusRing::Get(this)->DeprecatedLayoutImmediately();
  }

  UpdateIsCollapsed();
  UpdateAccessibleName();
}

int TabGroupHeader::GetCollapsedHeaderWidth() const {
  return GetTabSizeInfo().standard_width;
}

bool TabGroupHeader::ShouldShowAttentionIndicator() const {
  // Attention should only be shown if the group is collapsed.
  TabGroup* tab_group = tab_slot_controller_->GetTabGroup(group().value());
  if (!tab_group || tab_group->IsEmpty()) {
    return false;
  }

  return is_collapsed_ && tab_group->GetTabGroupFeatures()
                              ->attention_indicator()
                              ->GetHasAttention();
}

std::u16string_view TabGroupHeader::GetTitleTextForTesting() const {
  CHECK(title_);
  return title_->GetText();
}

int TabGroupHeader::GetDesiredWidth() const {
  const int overlap_margin = group_style_->GetTabGroupViewOverlap() * 2;
  return overlap_margin + title_chip_->width();
}

int TabGroupHeader::GetChipHeight() const {
  return group_style_->GetEmptyChipSize();
}

int TabGroupHeader::GetChipY() const {
  return group_style_->GetTitleChipOffset(std::nullopt).y();
}

bool TabGroupHeader::ShouldShowHeaderIcon() const {
  const bool supports_shared_groups = SupportsDataSharing();
  if (!supports_shared_groups) {
    return false;
  }

  tab_groups::TabGroupSyncService* tab_group_service =
      tab_slot_controller_->GetBrowserWindowInterface()
          ? tab_groups::TabGroupSyncServiceFactory::GetForProfile(
                tab_slot_controller_->GetBrowserWindowInterface()->GetProfile())
          : nullptr;
  if (!tab_group_service) {
    return false;
  }

  std::optional<tab_groups::SavedTabGroup> saved_group =
      tab_group_service->GetGroup(group().value());
  if (!saved_group) {
    return false;
  }

  if (supports_shared_groups) {
    // DataSharing shows a share icon if the group is shared.
    return saved_group->is_shared_tab_group();
  }

  // Show the V1 sync icon.
  return true;
}

void TabGroupHeader::SetCollapsedState() {
  const bool collapsed_state =
      tab_slot_controller_->IsGroupCollapsed(group().value());
  if (is_collapsed_ != collapsed_state) {
    is_collapsed_ = collapsed_state;
  }
}

void TabGroupHeader::UpdateTitleView() {
  title_->SetText(group_title_);

  if (!group_title_.empty()) {
    title_->SetEnabledColor(color_utils::GetColorWithMaxContrast(color_));
  }
}

void TabGroupHeader::UpdateSyncIconView() {
  sync_icon_->SetVisible(should_show_header_icon_);
  if (should_show_header_icon_) {
    bool use_share_icon = SupportsDataSharing();
    sync_icon_->SetImage(ui::ImageModel::FromVectorIcon(
        use_share_icon ? features::IsRoundedIconsEnabled() ? kGroupCustomIcon
                                                           : kPeopleGroupOldIcon
        : features::IsRoundedIconsEnabled() ? vector_icons::kSyncIcon
                                            : kTabGroupsSyncOldIcon,
        color_utils::GetColorWithMaxContrast(color_),
        group_style_->GetSyncIconWidth()));
  }
}

void TabGroupHeader::UpdateAttentionIndicatorView() {
  const bool supports_attention_indicator = SupportsDataSharing();
  if (!supports_attention_indicator) {
    attention_indicator_->SetVisible(false);
    return;
  }

  const bool should_show_attention_indicator = ShouldShowAttentionIndicator();
  attention_indicator_->SetVisible(should_show_attention_indicator);
  if (should_show_attention_indicator) {
    attention_indicator_->SetImage(ui::ImageModel::FromVectorIcon(
        kDefaultTouchFaviconMaskCustomIcon,
        color_utils::GetColorWithMaxContrast(color_),
        group_style_->GetAttentionIndicatorWidth()));
  }
}

void TabGroupHeader::UpdateIsCollapsed() {
  if (is_collapsed_) {
    GetViewAccessibility().SetIsCollapsed();
  } else {
    GetViewAccessibility().SetIsExpanded();
  }
}

void TabGroupHeader::CreateHeaderWithoutTitle() {
  const int chip_height = GetChipHeight();
  const int chip_y = GetChipY();
  const int empty_width = group_style_->GetEmptyChipSize();
  const gfx::Point empty_origin =
      group_style_->GetTitleChipOffset(std::nullopt);

  title_chip_->SetBounds(empty_origin.x(), chip_y, empty_width, chip_height);
  title_chip_->SetBackground(group_style_->GetEmptyTitleChipBackground(color_));

  const int sync_icon_width = group_style_->GetSyncIconWidth();

  if (should_show_header_icon_) {
    const bool should_show_attention_indicator = ShouldShowAttentionIndicator();
    if (should_show_attention_indicator) {
      const gfx::Insets title_chip_insets =
          group_style_->GetInsetsForHeaderChip();
      const int title_chip_vertical_inset = 0;
      gfx::Rect title_chip_bounds = title_chip_->GetLocalBounds();
      const int attention_indicator_width =
          group_style_->GetAttentionIndicatorWidth();

      // The total width of the title chip includes the horizontal
      // insets, the sync icon, and the attention indicator + its padding.
      title_chip_bounds.set_width(sync_icon_width + attention_indicator_width +
                                  kSyncIconPaddingFromLabel +
                                  title_chip_insets.width());
      title_chip_->SetBoundsRect(title_chip_bounds);

      sync_icon_->SetBounds(title_chip_insets.left(), title_chip_vertical_inset,
                            sync_icon_width, chip_height);

      attention_indicator_->SetBounds(
          sync_icon_->bounds().right() + kSyncIconPaddingFromLabel,
          title_chip_vertical_inset, attention_indicator_width, chip_height);
    } else {
      // The `sync_icon` by itself should be centered in the title chip.
      gfx::Rect sync_icon_bounds = title_chip_->GetLocalBounds();
      sync_icon_bounds.ClampToCenteredSize(
          gfx::Size(sync_icon_width, sync_icon_width));
      sync_icon_->SetBoundsRect(sync_icon_bounds);
    }
  } else {
    sync_icon_->SetBounds(0, 0, 0, 0);
    attention_indicator_->SetBounds(0, 0, 0, 0);
  }
}

void TabGroupHeader::CreateHeaderWithTitle() {
  // TODO(crbug.com/40893761): The math of the layout in this function is done
  // arithmetically and can be hard to understand. This should instead be done
  // by a layout manager.
  // Visual representation of tab group header:
  //             [            Total Content Width           ]
  // [Left Inset][Sync Icon][Padding][Group Title][Attention][Right Inset]
  const int sync_icon_width =
      should_show_header_icon_ ? group_style_->GetSyncIconWidth() : 0;
  const int padding_between_label_sync_icon =
      should_show_header_icon_ ? kSyncIconPaddingFromLabel : 0;
  // Only show attention indicator if header icon will show and
  // attention indicator is enabled.
  const bool should_show_attention_indicator =
      should_show_header_icon_ && ShouldShowAttentionIndicator();
  const int attention_indicator_width =
      should_show_attention_indicator
          ? group_style_->GetAttentionIndicatorWidth() +
                kSyncIconPaddingFromLabel
          : 0;
  const int attention_indicator_padding =
      should_show_attention_indicator ? kSyncIconPaddingFromLabel : 0;

  // The max width of the content should be half the standard tab width (not
  // counting overlap).
  const int text_max_width = (tab_style_->GetStandardWidth(/*is_split*/ false) -
                              tab_style_->GetTabOverlap()) /
                                 2 -
                             sync_icon_width - padding_between_label_sync_icon;
  const int text_width = std::min(
      title_->GetPreferredSize(views::SizeBounds(title_->width(), {})).width(),
      text_max_width);

  // Width of title chip should at least be the width of an empty title chip.
  const int total_content_width =
      sync_icon_width + padding_between_label_sync_icon + text_width +
      attention_indicator_width + attention_indicator_padding;
  const gfx::Insets title_chip_insets = group_style_->GetInsetsForHeaderChip();
  const int title_chip_width = std::max(
      group_style_->GetEmptyChipSize(),
      static_cast<float>(total_content_width + title_chip_insets.width()));

  const int chip_height = GetChipHeight();
  const int chip_y = GetChipY();
  const gfx::Point named_origin =
      group_style_->GetTitleChipOffset(std::nullopt);

  const int corner_radius = group_style_->GetChipCornerRadius();
  title_chip_->SetBounds(named_origin.x(), chip_y, title_chip_width,
                         chip_height);
  title_chip_->SetBackground(
      views::CreateRoundedRectBackground(color_, corner_radius));

  // Set the bounds of the sync icon first, followed by the title.
  const int start_of_sync_icon = title_chip_insets.left();
  const int title_chip_vertical_inset = 0;
  if (!should_show_header_icon_) {
    sync_icon_->SetBounds(0, 0, 0, 0);
    // If the title and its insets are narrower than the minimum width of the
    // chip, divide the whitespace remainder between the start and end of the
    // chip so the title appears centered.
    const int text_offset = std::max(
        0, (title_chip_width - text_width - title_chip_insets.width()) / 2);
    title_->SetBounds(title_chip_insets.left() + text_offset +
                          GetTitleVisualCenteringOffset(group_title_),
                      title_chip_vertical_inset, text_width, chip_height);
    attention_indicator_->SetBounds(0, 0, 0, 0);
  } else {
    sync_icon_->SetBounds(start_of_sync_icon, title_chip_vertical_inset,
                          sync_icon_width, chip_height);
    title_->SetBounds(
        sync_icon_->bounds().right() + padding_between_label_sync_icon,
        title_chip_vertical_inset, text_width, chip_height);
    if (should_show_attention_indicator) {
      attention_indicator_->SetBounds(
          title_->bounds().right() + kSyncIconPaddingFromLabel,
          title_chip_vertical_inset, attention_indicator_width, chip_height);
    } else {
      attention_indicator_->SetBounds(0, 0, 0, 0);
    }
  }
}

void TabGroupHeader::UpdateTooltipText() {
  if (!group().has_value() || features::IsTabGroupHoverCardsEnabled()) {
    return;
  }

  TabGroup* tab_group = tab_slot_controller_->GetTabGroup(group().value());
  if (!tab_group || tab_group->IsEmpty() || tab_group->ListTabs().is_empty()) {
    return;
  }

  if (!title_->GetText().empty()) {
    SetTooltipText(l10n_util::GetStringFUTF16(
        IDS_TAB_GROUPS_NAMED_GROUP_TOOLTIP, std::u16string(title_->GetText()),
        tab_slot_controller_->GetGroupContentString(group().value())));
  } else {
    SetTooltipText(l10n_util::GetStringFUTF16(
        IDS_TAB_GROUPS_UNNAMED_GROUP_TOOLTIP,
        tab_slot_controller_->GetGroupContentString(group().value())));
  }
}

void TabGroupHeader::UpdateAccessibleName() {
  if (features::IsTabGroupHoverCardsEnabled()) {
    GetViewAccessibility().SetName(tab_groups::GetHoverCardAccessibilityText(
        tab_group_data_observer_->tab_group_data()));
    return;
  }

  TabGroup* tab_group = tab_slot_controller_->GetTabGroup(group().value());
  if (tab_group && tab_group->ListTabs().length() == 0) {
    return;
  }

  std::u16string title(tab_slot_controller_->GetGroupTitle(group().value()));
  std::u16string contents =
      tab_slot_controller_->GetGroupContentString(group().value());
  std::u16string group_status = std::u16string();

// Windows screen reader properly announces the state set above in `node_data`
// and will read out the state change when the header's collapsed state is
// toggled. The state is added into the title for other platforms and the title
// will be reread with the updated state when the header's collapsed state is
// toggled.
#if !BUILDFLAG(IS_WIN)
  bool is_collapsed = tab_slot_controller_->IsGroupCollapsed(group().value());
  group_status = is_collapsed
                     ? l10n_util::GetStringUTF16(IDS_GROUP_AX_LABEL_COLLAPSED)
                     : l10n_util::GetStringUTF16(IDS_GROUP_AX_LABEL_EXPANDED);
#endif

  std::u16string shared_state = u"";

  if (SupportsDataSharing() && should_show_header_icon_) {
    shared_state = l10n_util::GetStringUTF16(IDS_SAVED_GROUP_AX_LABEL_SHARED);
    if (ShouldShowAttentionIndicator()) {
      group_status += u", " + l10n_util::GetStringUTF16(
                                  DATA_SHARING_GROUP_LABEL_NEW_ACTIVITY);
    }
  }

  std::u16string final_name;
  if (title.empty()) {
    final_name =
        l10n_util::GetStringFUTF16(IDS_GROUP_AX_LABEL_UNNAMED_GROUP_FORMAT,
                                   shared_state, contents, group_status);
  } else {
    final_name =
        l10n_util::GetStringFUTF16(IDS_GROUP_AX_LABEL_NAMED_GROUP_FORMAT,
                                   shared_state, title, contents, group_status);
  }
  GetViewAccessibility().SetName(final_name);
}

void TabGroupHeader::OnTabGroupDataChanged() {
  const tabs::TabGroupData& tab_group_data =
      tab_group_data_observer_->tab_group_data();
  SetHoverCardDataFrom(tab_group_data);
  UpdateAttentionIndicatorView();
  UpdateAccessibleName();
}

BEGIN_METADATA(TabGroupHeader)
ADD_READONLY_PROPERTY_METADATA(int, DesiredWidth)
END_METADATA
