// Copyright 2016 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/intent_picker_bubble_view.h"

#include <algorithm>
#include <string_view>
#include <utility>

#include "base/auto_reset.h"
#include "base/check_op.h"
#include "base/debug/dump_without_crashing.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/i18n/rtl.h"
#include "base/strings/utf_string_conversions.h"
#include "build/build_config.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/ui/color/chrome_color_id.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/chrome_typography.h"
#include "chrome/browser/ui/views/controls/hover_button.h"
#include "chrome/browser/ui/views/toolbar/toolbar_view.h"
#include "chrome/browser/web_applications/link_capturing_features.h"
#include "chrome/common/chrome_features.h"
#include "chrome/grit/generated_resources.h"
#include "components/services/app_service/public/cpp/intent_util.h"
#include "components/url_formatter/elide_url.h"
#include "content/public/browser/navigation_handle.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/accessibility/ax_action_data.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/base/mojom/dialog_button.mojom.h"
#include "ui/color/color_id.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/text_constants.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/animation/ink_drop_host.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/controls/button/checkbox.h"
#include "ui/views/controls/button/image_button.h"
#include "ui/views/controls/focus_ring.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/controls/separator.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/layout_types.h"
#include "ui/views/layout/table_layout.h"
#include "ui/views/layout/table_layout_view.h"
#include "ui/views/style/typography.h"
#include "ui/views/style/typography_provider.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_utils.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "ui/chromeos/devicetype_utils.h"
#endif

namespace {

constexpr char kInvalidLaunchName[] = "";

constexpr int kGridItemPreferredSize = 96;
constexpr int kGridItemsPerRow = 3;
constexpr int kGridInteriorColumnPadding = 8;
constexpr int kGridInteriorRowPadding = 8;
constexpr int kGridExteriorColumnPadding = 8;

constexpr int kGridItemTopInset = 12;
constexpr int kGridItemInset = 2;
constexpr int kGridItemInteriorPadding = 8;
constexpr int kGridItemBorderRadius = 4;
constexpr int kGridItemGroupId = 1;

bool g_auto_accept_intent_picker_bubble_for_testing = false;
bool g_auto_cancel_intent_picker_bubble_for_testing = false;

bool IsKeyboardCodeArrow(ui::KeyboardCode key_code) {
  return key_code == ui::VKEY_UP || key_code == ui::VKEY_DOWN ||
         key_code == ui::VKEY_RIGHT || key_code == ui::VKEY_LEFT;
}

bool IsDoubleClick(const ui::Event& event) {
  return (event.IsMouseEvent() && event.AsMouseEvent()->GetClickCount() == 2) ||
         (event.IsGestureEvent() &&
          event.AsGestureEvent()->details().tap_count() == 2);
}

// Callback for when an app is selected in the app list. First parameter is the
// index, second parameter is true if the dialog should be immediately accepted.
using AppSelectedCallback =
    base::RepeatingCallback<void(std::optional<size_t>, bool)>;

// Grid view:

// A Button which displays an app icon and name, as part of a grid layout of
// apps.
class IntentPickerAppGridButton : public views::Button {
  METADATA_HEADER(IntentPickerAppGridButton, views::Button)

 public:
  // Callback for when this app is selected. Parameter is true if the dialog
  // should be immediately accepted.
  using ButtonSelectedCallback = base::RepeatingCallback<void(bool)>;

  IntentPickerAppGridButton(ButtonSelectedCallback selected_callback,
                            const ui::ImageModel& icon_model,
                            const std::string& display_name)
      : views::Button(base::BindRepeating(&IntentPickerAppGridButton::OnPressed,
                                          base::Unretained(this))),
        selected_callback_(std::move(selected_callback)) {
    auto* layout = SetLayoutManager(std::make_unique<views::BoxLayout>(
        views::BoxLayout::Orientation::kVertical,
        gfx::Insets::TLBR(kGridItemTopInset, kGridItemInset, kGridItemInset,
                          kGridItemInset),
        kGridItemInteriorPadding, true));
    layout->set_main_axis_alignment(
        views::BoxLayout::MainAxisAlignment::kStart);
    layout->set_cross_axis_alignment(
        views::BoxLayout::CrossAxisAlignment::kCenter);

    auto* icon_view =
        AddChildView(std::make_unique<views::ImageView>(icon_model));
    icon_view->SetCanProcessEventsWithinSubtree(false);

    name_label_ = AddChildView(std::make_unique<views::Label>(
        base::UTF8ToUTF16(display_name), views::style::CONTEXT_BUTTON));
    name_label_->SetMultiLine(true);
    name_label_->SetMaxLines(2);
    name_label_->SetMaximumWidth(kGridItemPreferredSize);
    name_label_->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_CENTER);
    name_label_->SetVerticalAlignment(gfx::VerticalAlignment::ALIGN_TOP);

    SetFocusBehavior(FocusBehavior::ALWAYS);
    GetViewAccessibility().SetRole(ax::mojom::Role::kRadioButton);
    GetViewAccessibility().SetCheckedState(
        selected_ ? ax::mojom::CheckedState::kTrue
                  : ax::mojom::CheckedState::kFalse);
    UpdateAccessibleName();
    name_label_changed_subscription_ = name_label_->AddTextChangedCallback(
        base::BindRepeating(&IntentPickerAppGridButton::UpdateAccessibleName,
                            base::Unretained(this)));
    SetPreferredSize(gfx::Size(kGridItemPreferredSize, kGridItemPreferredSize));

    SetGroup(kGridItemGroupId);
  }
  IntentPickerAppGridButton(const IntentPickerAppGridButton&) = delete;
  IntentPickerAppGridButton& operator=(const IntentPickerAppGridButton&) =
      delete;
  ~IntentPickerAppGridButton() override = default;

  void SetSelected(bool selected) {
    selected_ = selected;
    UpdateBackground();
    GetViewAccessibility().SetCheckedState(
        selected_ ? ax::mojom::CheckedState::kTrue
                  : ax::mojom::CheckedState::kFalse);
  }

  // views::Button:
  void StateChanged(ButtonState old_state) override { UpdateBackground(); }

  bool IsGroupFocusTraversable() const override { return false; }
  views::View* GetSelectedViewForGroup(int group) override {
    if (group != kGridItemGroupId) {
      return nullptr;
    }

    Views siblings = parent()->children();
    auto it = std::ranges::find_if(siblings, [](views::View* v) {
      return views::AsViewClass<IntentPickerAppGridButton>(v)->selected_;
    });

    return it != siblings.end() ? *it : nullptr;
  }
  void OnFocus() override {
    Button::OnFocus();
    if (select_on_focus_) {
      selected_callback_.Run(false);
    }
  }
  bool HandleAccessibleAction(const ui::AXActionData& action_data) override {
    if (action_data.action == ax::mojom::Action::kFocus) {
      base::AutoReset<bool> reset(&select_on_focus_, false);
      RequestFocus();
      return true;
    }
    return Button::HandleAccessibleAction(action_data);
  }

 private:
  void UpdateBackground() {
    ui::ColorId color;
    if (selected_ || GetState() == ButtonState::STATE_PRESSED) {
      color = kColorIntentPickerItemBackgroundSelected;
    } else if (GetState() == ButtonState::STATE_HOVERED) {
      color = kColorIntentPickerItemBackgroundHovered;
    } else {
      SetBackground(nullptr);
      return;
    }

    SetBackground(
        views::CreateRoundedRectBackground(color, kGridItemBorderRadius));
  }

  void OnPressed(const ui::Event& event) {
    bool should_open = IsDoubleClick(event) ||
                       (event.IsKeyEvent() &&
                        event.AsKeyEvent()->key_code() == ui::VKEY_RETURN);
    selected_callback_.Run(should_open);
  }

  // Updates the accessible name of the bubble in the ViewsAX cache.
  void UpdateAccessibleName() {
    CHECK(name_label_);
    GetViewAccessibility().SetName(std::u16string(name_label_->GetText()));
  }

  bool selected_ = false;
  bool select_on_focus_ = true;
  raw_ptr<views::Label> name_label_;
  ButtonSelectedCallback selected_callback_;
  base::CallbackListSubscription name_label_changed_subscription_;
};

BEGIN_METADATA(IntentPickerAppGridButton)
END_METADATA

// Displays a list of apps as a grid of buttons.
class IntentPickerAppGridView
    : public IntentPickerBubbleView::IntentPickerAppsView {
  METADATA_HEADER(IntentPickerAppGridView,
                  IntentPickerBubbleView::IntentPickerAppsView)

 public:
  IntentPickerAppGridView(
      const std::vector<IntentPickerBubbleView::AppInfo>& apps,
      AppSelectedCallback selected_callback)
      : selected_callback_(selected_callback) {
    auto table_view = std::make_unique<views::TableLayoutView>();
    table_view->SetID(IntentPickerBubbleView::ViewId::kItemContainer);

    table_view->AddPaddingColumn(views::TableLayout::kFixedSize,
                                 kGridExteriorColumnPadding);
    for (int i = 0; i < kGridItemsPerRow; i++) {
      table_view->AddColumn(views::LayoutAlignment::kCenter,
                            views::LayoutAlignment::kStart,
                            views::TableLayout::kFixedSize,
                            views::TableLayout::ColumnSize::kUsePreferred,
                            /*fixed_width=*/0, /*min_width=*/0);
      if (i < kGridItemsPerRow - 1) {
        table_view->AddPaddingColumn(views::TableLayout::kFixedSize,
                                     kGridInteriorColumnPadding);
      }
    }
    table_view->AddPaddingColumn(views::TableLayout::kFixedSize,
                                 kGridExteriorColumnPadding);

    // Add padding to the exterior of the grid so that the focus ring on app
    // items is not clipped.
    constexpr int kFocusRingPadding = views::FocusRing::kDefaultHaloInset +
                                      views::FocusRing::kDefaultHaloThickness;

    int row_count = (apps.size() - 1) / kGridItemsPerRow + 1;
    table_view->AddPaddingRow(views::TableLayout::kFixedSize,
                              kFocusRingPadding);
    for (int i = 0; i < row_count; i++) {
      table_view->AddRows(1, views::TableLayout::kFixedSize);
      if (i < row_count - 1) {
        table_view->AddPaddingRow(views::TableLayout::kFixedSize,
                                  kGridInteriorRowPadding);
      }
    }
    table_view->AddPaddingRow(views::TableLayout::kFixedSize,
                              kFocusRingPadding);

    for (size_t i = 0; i < apps.size(); i++) {
      auto app_button = std::make_unique<IntentPickerAppGridButton>(
          base::BindRepeating(
              &IntentPickerAppGridView::SetSelectedIndexInternal,
              base::Unretained(this), i),
          apps[i].icon_model, apps[i].display_name);
      table_view->AddChildView(std::move(app_button));
    }

    SetContents(std::move(table_view));
    // Clip height so that at most two rows are visible, with a peek of the
    // third if it exists.
    ClipHeightTo(kGridItemPreferredSize, kGridItemPreferredSize * 2.5f);
  }

  void SetSelectedIndex(std::optional<size_t> index) override {
    SetSelectedIndexInternal(index, false);
  }

  std::optional<size_t> GetSelectedIndex() const override {
    return selected_app_index_;
  }

 private:
  void SetSelectedIndexInternal(std::optional<size_t> new_index,
                                bool accepted) {
    if (selected_app_index_.has_value()) {
      GetButtonAtIndex(selected_app_index_.value())->SetSelected(false);
    }
    if (new_index.has_value()) {
      GetButtonAtIndex(new_index.value())->SetSelected(true);
    }

    if (selected_app_index_.has_value() && new_index.has_value() &&
        GetButtonAtIndex(selected_app_index_.value())->HasFocus()) {
      GetButtonAtIndex(new_index.value())->RequestFocus();
    }

    selected_app_index_ = new_index;

    selected_callback_.Run(new_index, accepted);
  }

  IntentPickerAppGridButton* GetButtonAtIndex(size_t index) {
    const auto& children = contents()->children();
    return views::AsViewClass<IntentPickerAppGridButton>(children[index]);
  }

  AppSelectedCallback selected_callback_;

  std::optional<size_t> selected_app_index_ = 0;
};

BEGIN_METADATA(IntentPickerAppGridView)
ADD_PROPERTY_METADATA(std::optional<size_t>, SelectedIndex)
END_METADATA

// List view:

// A button that represents a candidate intent handler.
class IntentPickerLabelButton : public views::LabelButton {
  METADATA_HEADER(IntentPickerLabelButton, views::LabelButton)

 public:
  IntentPickerLabelButton(PressedCallback callback,
                          const ui::ImageModel& icon_model,
                          const std::string& display_name)
      : LabelButton(std::move(callback),
                    base::UTF8ToUTF16(std::string_view(display_name))) {
    SetHorizontalAlignment(gfx::ALIGN_LEFT);
    if (!icon_model.IsEmpty()) {
      SetImageModel(views::ImageButton::STATE_NORMAL, icon_model);
    }
    auto* provider = ChromeLayoutProvider::Get();
    SetBorder(views::CreateEmptyBorder(gfx::Insets::VH(
        provider->GetDistanceMetric(DISTANCE_CONTENT_LIST_VERTICAL_MULTI),
        provider->GetInsetsMetric(views::INSETS_DIALOG).left())));
    views::InkDrop::Get(this)->SetMode(views::InkDropHost::InkDropMode::ON);
    views::InkDrop::Get(this)->SetBaseColor(
        views::TypographyProvider::Get().GetColorId(
            views::style::CONTEXT_BUTTON, views::style::STYLE_SECONDARY));
  }
  IntentPickerLabelButton(const IntentPickerLabelButton&) = delete;
  IntentPickerLabelButton& operator=(const IntentPickerLabelButton&) = delete;
  ~IntentPickerLabelButton() override = default;

  void MarkAsUnselected(const ui::Event* event) {
    views::InkDrop::Get(this)->AnimateToState(
        views::InkDropState::HIDDEN, ui::LocatedEvent::FromIfValid(event));
  }

  void MarkAsSelected(const ui::Event* event) {
    views::InkDrop::Get(this)->AnimateToState(
        views::InkDropState::ACTIVATED, ui::LocatedEvent::FromIfValid(event));
  }
};

BEGIN_METADATA(IntentPickerLabelButton)
END_METADATA

class IntentPickerAppListView
    : public IntentPickerBubbleView::IntentPickerAppsView {
  METADATA_HEADER(IntentPickerAppListView,
                  IntentPickerBubbleView::IntentPickerAppsView)

 public:
  IntentPickerAppListView(
      const std::vector<IntentPickerBubbleView::AppInfo>& apps,
      AppSelectedCallback selected_callback)
      : selected_callback_(selected_callback) {
    auto scrollable_view = std::make_unique<views::View>();
    scrollable_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
        views::BoxLayout::Orientation::kVertical));
    scrollable_view->SetID(IntentPickerBubbleView::ViewId::kItemContainer);

    for (size_t i = 0; i < apps.size(); i++) {
      auto app_button = std::make_unique<IntentPickerLabelButton>(
          base::BindRepeating(&IntentPickerAppListView::OnAppPressed,
                              base::Unretained(this), i),
          apps[i].icon_model, apps[i].display_name);
      scrollable_view->AddChildViewAt(std::move(app_button), i);
    }

    SetBackgroundColor(ui::kColorBubbleBackground);
    SetContents(std::move(scrollable_view));
    DCHECK(!contents()->children().empty());
    const int row_height =
        contents()->children().front()->GetPreferredSize().height();
    // Use |kMaxAppResults| as a measure of how many apps we want to show.
    constexpr int kMaxAppResults = 3;
    ClipHeightTo(row_height, (kMaxAppResults + 0.5) * row_height);
  }

  ~IntentPickerAppListView() override = default;

  void SetSelectedIndex(std::optional<size_t> index) override {
    DCHECK(index.has_value());  // List-style intent picker does not support
                                // having no selection.
    SetSelectedAppIndex(index.value(), nullptr);
  }

  std::optional<size_t> GetSelectedIndex() const override {
    return selected_app_index_;
  }

  void OnKeyEvent(ui::KeyEvent* event) override {
    if (!IsKeyboardCodeArrow(event->key_code()) ||
        event->type() != ui::EventType::kKeyReleased) {
      return;
    }

    int delta = 0;
    switch (event->key_code()) {
      case ui::VKEY_UP:
        delta = -1;
        break;
      case ui::VKEY_DOWN:
        delta = 1;
        break;
      case ui::VKEY_LEFT:
        delta = base::i18n::IsRTL() ? 1 : -1;
        break;
      case ui::VKEY_RIGHT:
        delta = base::i18n::IsRTL() ? -1 : 1;
        break;
      default:
        NOTREACHED();
    }

    SetSelectedAppIndex(CalculateNextAppIndex(delta), nullptr);
    AdjustScrollViewVisibleRegion();

    ScrollView::OnKeyEvent(event);
  }

 private:
  void OnAppPressed(size_t index, const ui::Event& event) {
    SetSelectedAppIndex(index, &event);
  }

  void SetSelectedAppIndex(size_t index, const ui::Event* event) {
    GetIntentPickerLabelButtonAt(selected_app_index_)
        ->MarkAsUnselected(nullptr);
    selected_app_index_ = index;
    GetIntentPickerLabelButtonAt(selected_app_index_)->MarkAsSelected(event);

    bool accepted = false;
    if (event && IsDoubleClick(*event)) {
      accepted = true;
    }

    selected_callback_.Run(index, accepted);
  }

  size_t CalculateNextAppIndex(int delta) {
    size_t size = contents()->children().size();
    return static_cast<size_t>((selected_app_index_ + delta) % size);
  }

  void AdjustScrollViewVisibleRegion() {
    views::ScrollBar* bar = vertical_scroll_bar();
    if (bar) {
      const int row_height =
          contents()->children().front()->GetPreferredSize().height();
      ScrollToPosition(bar, (selected_app_index_ - 1) * row_height);
    }
  }

  IntentPickerLabelButton* GetIntentPickerLabelButtonAt(size_t index) {
    const auto& children = contents()->children();
    DCHECK_LT(index, children.size());
    return views::AsViewClass<IntentPickerLabelButton>(children[index]);
  }

  AppSelectedCallback selected_callback_;

  size_t selected_app_index_ = 0;
};

BEGIN_METADATA(IntentPickerAppListView)
ADD_PROPERTY_METADATA(std::optional<size_t>, SelectedIndex)
END_METADATA

}  // namespace

// static
IntentPickerBubbleView* IntentPickerBubbleView::intent_picker_bubble_ = nullptr;

// static
views::Widget* IntentPickerBubbleView::ShowBubble(
    views::BubbleAnchor bubble_anchor,
    std::optional<ui::ElementIdentifier> highlighted_element,
    BubbleType bubble_type,
    content::WebContents* web_contents,
    std::vector<AppInfo> app_info,
    bool show_stay_in_chrome,
    bool show_remember_selection,
    const std::optional<url::Origin>& initiating_origin,
    IntentPickerResponse intent_picker_cb) {
  if (intent_picker_bubble_) {
    intent_picker_bubble_->CloseBubble();
  }
  intent_picker_bubble_ = new IntentPickerBubbleView(
      bubble_anchor, bubble_type, std::move(app_info),
      std::move(intent_picker_cb), web_contents, show_stay_in_chrome,
      show_remember_selection, initiating_origin);
  if (highlighted_element) {
    intent_picker_bubble_->SetHighlightedElement(*highlighted_element);
  }
  intent_picker_bubble_->Initialize();
  views::Widget* widget =
      views::BubbleDialogDelegateView::CreateBubble(intent_picker_bubble_);

  DCHECK(intent_picker_bubble_->HasCandidates());
  intent_picker_bubble_->ShowForReason(DisplayReason::USER_GESTURE);

  intent_picker_bubble_->SelectDefaultItem();
  if (g_auto_accept_intent_picker_bubble_for_testing) {
    intent_picker_bubble_->AcceptDialog();
  }
  if (g_auto_cancel_intent_picker_bubble_for_testing) {
    CloseCurrentBubble();
  }
  return widget;
}

// static
base::AutoReset<bool>
IntentPickerBubbleView::SetAutoAcceptIntentPickerBubbleForTesting() {
  return base::AutoReset<bool>(&g_auto_accept_intent_picker_bubble_for_testing,
                               true);
}

// static
base::AutoReset<bool>
IntentPickerBubbleView::SetAutoCancelIntentPickerBubbleForTesting() {
  return base::AutoReset<bool>(&g_auto_cancel_intent_picker_bubble_for_testing,
                               true);
}

// static
void IntentPickerBubbleView::CloseCurrentBubble() {
  if (intent_picker_bubble_) {
    intent_picker_bubble_->CloseBubble();
  }
}

void IntentPickerBubbleView::CloseBubble() {
  ClearIntentPickerBubbleView();
  LocationBarBubbleDelegateView::CloseBubble();
}

void IntentPickerBubbleView::OnDialogAccepted() {
  bool should_persist = remember_selection_checkbox_ &&
                        remember_selection_checkbox_->GetChecked();
  auto selected_index = GetSelectedIndex();
  // Dialog cannot be accepted when there is no selection.
  DCHECK(selected_index.has_value());
  RunCallbackAndCloseBubble(app_info_[selected_index.value()].launch_name,
                            app_info_[selected_index.value()].type,
                            apps::IntentPickerCloseReason::OPEN_APP,
                            should_persist);
}

void IntentPickerBubbleView::OnDialogCancelled() {
  const char* launch_name = apps_util::kUseBrowserForLink;
  bool should_persist = remember_selection_checkbox_ &&
                        remember_selection_checkbox_->GetChecked();
  RunCallbackAndCloseBubble(launch_name, apps::PickerEntryType::kUnknown,
                            apps::IntentPickerCloseReason::STAY_IN_CHROME,
                            should_persist);
}

void IntentPickerBubbleView::OnDialogClosed() {
  // Whenever closing the bubble without pressing |Just once| or |Always| we
  // need to report back that the user didn't select anything.
  RunCallbackAndCloseBubble(kInvalidLaunchName, apps::PickerEntryType::kUnknown,
                            apps::IntentPickerCloseReason::DIALOG_DEACTIVATED,
                            false);
}

bool IntentPickerBubbleView::ShouldShowCloseButton() const {
  return true;
}

void IntentPickerBubbleView::SelectDefaultItem() {
  if (use_grid_view_ && app_info_.size() > 1) {
    apps_view_->SetSelectedIndex(std::nullopt);
    // The default button is disabled in this case. Clear the focus so it
    // returns to the window, as if there was no default button in the first
    // place.
    GetWidget()->GetFocusManager()->ClearFocus();
  } else {
    apps_view_->SetSelectedIndex(0);
  }
}

std::optional<size_t> IntentPickerBubbleView::GetSelectedIndex() const {
  return apps_view_->GetSelectedIndex();
}

std::u16string IntentPickerBubbleView::GetWindowTitle() const {
  return l10n_util::GetStringUTF16(
      use_grid_view_ ? IDS_INTENT_PICKER_BUBBLE_VIEW_OPEN_IN_APP
                     : IDS_INTENT_PICKER_BUBBLE_VIEW_OPEN_WITH);
}

IntentPickerBubbleView::IntentPickerBubbleView(
    views::BubbleAnchor bubble_anchor,
    BubbleType bubble_type,
    std::vector<AppInfo> app_info,
    IntentPickerResponse intent_picker_cb,
    content::WebContents* web_contents,
    bool show_stay_in_chrome,
    bool show_remember_selection,
    const std::optional<url::Origin>& initiating_origin)
    : LocationBarBubbleDelegateView(bubble_anchor, web_contents),
      intent_picker_cb_(std::move(intent_picker_cb)),
      app_info_(std::move(app_info)),
      use_grid_view_(apps::features::ShouldShowLinkCapturingUX() &&
                     bubble_type == BubbleType::kLinkCapturing),
      show_stay_in_chrome_(show_stay_in_chrome && !use_grid_view_),
      show_remember_selection_(show_remember_selection),
      bubble_type_(bubble_type),
      initiating_origin_(initiating_origin) {
  SetButtons(show_stay_in_chrome_
                 ? static_cast<int>(ui::mojom::DialogButton::kOk) |
                       static_cast<int>(ui::mojom::DialogButton::kCancel)
                 : static_cast<int>(ui::mojom::DialogButton::kOk));
  SetButtonLabel(ui::mojom::DialogButton::kOk,
                 l10n_util::GetStringUTF16(IDS_INTENT_PICKER_BUBBLE_VIEW_OPEN));
  SetButtonLabel(
      ui::mojom::DialogButton::kCancel,
      l10n_util::GetStringUTF16(IDS_INTENT_PICKER_BUBBLE_VIEW_STAY_IN_CHROME));
  SetAcceptCallback(base::BindOnce(&IntentPickerBubbleView::OnDialogAccepted,
                                   base::Unretained(this)));
  SetCancelCallback(base::BindOnce(&IntentPickerBubbleView::OnDialogCancelled,
                                   base::Unretained(this)));
  SetCloseCallback(base::BindOnce(&IntentPickerBubbleView::OnDialogClosed,
                                  base::Unretained(this)));

  // Intent picker bubbles are handled in `intent_picker_helpers`, where they
  // get closed on each navigation start and should stay open until after
  // navigation finishes.
  SetCloseOnMainFrameOriginNavigation(false);
  // Margins are manually added in Initialize().
  set_margins(gfx::Insets());
}

IntentPickerBubbleView::~IntentPickerBubbleView() {
  SetLayoutManager(nullptr);
}

// If the widget gets closed without an app being selected we still need to use
// the callback so the caller can Resume the navigation.
void IntentPickerBubbleView::OnWidgetDestroying(views::Widget* widget) {
  RunCallbackAndCloseBubble(kInvalidLaunchName, apps::PickerEntryType::kUnknown,
                            apps::IntentPickerCloseReason::DIALOG_DEACTIVATED,
                            false);
}

void IntentPickerBubbleView::OnAppSelected(std::optional<size_t> index,
                                           bool accepted) {
  SetButtonEnabled(ui::mojom::DialogButton::kOk, index.has_value());

  if (index.has_value()) {
    UpdateCheckboxState(index.value());
  }

  if (accepted) {
    DCHECK(index.has_value());
    AcceptDialog();
  }
}

void IntentPickerBubbleView::Initialize() {
  const bool show_origin =
      initiating_origin_ &&
      !initiating_origin_->IsSameOriginWith(
          web_contents()->GetPrimaryMainFrame()->GetLastCommittedOrigin());

  auto leading_content_type = use_grid_view_
                                  ? views::DialogContentType::kText
                                  : views::DialogContentType::kControl;
  auto trailing_content_type = (show_origin && !show_remember_selection_)
                                   ? views::DialogContentType::kText
                                   : views::DialogContentType::kControl;
  const auto* provider = ChromeLayoutProvider::Get();
  auto insets = provider->GetDialogInsetsForContentType(leading_content_type,
                                                        trailing_content_type);

  SetLayoutManager(std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kVertical,
      gfx::Insets::TLBR(insets.top(), 0, insets.bottom(), 0),
      provider->GetDistanceMetric(views::DISTANCE_UNRELATED_CONTROL_VERTICAL)));
  insets = gfx::Insets::TLBR(0, insets.left(), 0, insets.right());

  const int kMaxDialogWidth =
      provider->GetDistanceMetric(views::DISTANCE_BUBBLE_PREFERRED_WIDTH);

  // Create a container for all of the individual app views.
  if (use_grid_view_) {
    apps_view_ = AddChildView(std::make_unique<IntentPickerAppGridView>(
        app_info_, base::BindRepeating(&IntentPickerBubbleView::OnAppSelected,
                                       base::Unretained(this))));
  } else {
    apps_view_ = AddChildView(std::make_unique<IntentPickerAppListView>(
        app_info_, base::BindRepeating(&IntentPickerBubbleView::OnAppSelected,
                                       base::Unretained(this))));
  }

  if (show_origin) {
    std::u16string origin_text = l10n_util::GetStringFUTF16(
        IDS_INTENT_PICKER_BUBBLE_VIEW_INITIATING_ORIGIN,
        url_formatter::FormatOriginForSecurityDisplay(*initiating_origin_));
    auto* label = AddChildView(std::make_unique<views::Label>(
        origin_text, ChromeTextContext::CONTEXT_DIALOG_BODY_TEXT_SMALL,
        views::style::STYLE_SECONDARY));
    label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
    label->SetAllowCharacterBreak(true);
    label->SetMultiLine(true);
    label->SetMaximumWidth(kMaxDialogWidth - insets.width());
    label->SetProperty(views::kMarginsKey, insets);
  }

  if (show_remember_selection_) {
    if (!use_grid_view_) {
      AddChildView(std::make_unique<views::Separator>());
    }

    remember_selection_checkbox_ = AddChildView(
        std::make_unique<views::Checkbox>(l10n_util::GetStringUTF16(
            IDS_INTENT_PICKER_BUBBLE_VIEW_REMEMBER_SELECTION)));
    remember_selection_checkbox_->SetID(ViewId::kRememberCheckbox);
    remember_selection_checkbox_->SetProperty(views::kMarginsKey, insets);
  }
}

bool IntentPickerBubbleView::HasCandidates() const {
  return !app_info_.empty();
}

void IntentPickerBubbleView::RunCallbackAndCloseBubble(
    const std::string& launch_name,
    apps::PickerEntryType entry_type,
    apps::IntentPickerCloseReason close_reason,
    bool should_persist) {
  ClearIntentPickerBubbleView();
  if (!intent_picker_cb_.is_null()) {
    // Calling Run() will make |intent_picker_cb_| null.
    DCHECK(!should_persist || !launch_name.empty());
    std::move(intent_picker_cb_)
        .Run(launch_name, entry_type, close_reason, should_persist);
  }
}

void IntentPickerBubbleView::UpdateCheckboxState(size_t index) {
  if (!remember_selection_checkbox_) {
    return;
  }
  auto selected_app_type = app_info_[index].type;
  bool should_enable = selected_app_type != apps::PickerEntryType::kDevice;

  // Reset the checkbox state to the default unchecked if becomes disabled.
  if (!should_enable) {
    remember_selection_checkbox_->SetChecked(false);
  }
  remember_selection_checkbox_->SetEnabled(should_enable);
}

void IntentPickerBubbleView::ClearIntentPickerBubbleView() {
  // This is called asynchronously during OnWidgetDestroying, at which point
  // intent_picker_bubble_ may have already been cleared or set to something
  // else.
  if (intent_picker_bubble_ == this) {
    intent_picker_bubble_ = nullptr;
  }
}

BEGIN_METADATA(IntentPickerBubbleView)
END_METADATA

BEGIN_METADATA(IntentPickerBubbleView, IntentPickerAppsView)
END_METADATA
