// Copyright 2025 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/webauthn/combined_selector_views.h"

#include <algorithm>
#include <cstddef>
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include <vector>

#include "base/strings/string_util.h"
#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/webauthn/sheet_models.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/events/event.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/gfx/text_constants.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/ink_drop.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/checkbox.h"
#include "ui/views/controls/button/radio_button.h"
#include "ui/views/controls/image_view.h"
#include "ui/views/controls/label.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/fill_layout.h"
#include "ui/views/layout/layout_provider.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/view.h"
#include "ui/views/view_class_properties.h"
#include "ui/views/view_utils.h"

namespace {

// This value is used to group all `CombinedSelectorRadioButton` which are under
// the same `CombinedSelectorRowView`. The grouping is used for traversal and
// selection. The value is selected voluntarily in order not to conflict
// grouping from any parent views.
constexpr static int kGroupId = 1327;

// Calculates the texts to be displayed for a given mechanism.
// The texts are returned in the order they should be displayed in the
// CombinedSelectorRowView.
// - The first text is the name of the mechanism (title).
// - The second text (if present) is the display name or the
//   origin if it's a password mechanism.
// - The last text is the description (e.g.
// "IDS_PASSWORD_MANAGER_PASSKEY_FROM_GOOGLE_PASSWORD_MANAGER").
std::vector<std::u16string_view> GetTextsForMechanism(
    const AuthenticatorRequestDialogModel::Mechanism& mechanism) {
  std::vector<std::u16string_view> texts;
  if (mechanism.display_name.empty() ||
      mechanism.display_name == mechanism.name) {
    texts.push_back(mechanism.name);
  } else {
    texts.push_back(mechanism.display_name);
    texts.push_back(mechanism.name);
  }
  if (const auto* password =
          std::get_if<AuthenticatorRequestDialogModel::Mechanism::Password>(
              &mechanism.type)) {
    if (password->value().origin) {
      texts.push_back(*password->value().origin);
    }
  }
  texts.push_back(mechanism.description);
  return texts;
}

}  // namespace

CombinedSelectorRadioButton::CombinedSelectorRadioButton(Delegate* delegate,
                                                         int index)
    : views::RadioButton(u"", kGroupId), delegate_(delegate), index_(index) {}

views::View* CombinedSelectorRadioButton::GetSelectedViewForGroup(int group) {
  Views views;
  GetRadioButtonsInList(group, &views);

  const auto i = std::ranges::find_if(views, [](const views::View* view) {
    return static_cast<const CombinedSelectorRadioButton*>(view)->GetChecked();
  });
  return (i == views.cend()) ? nullptr : *i;
}

void CombinedSelectorRadioButton::SetChecked(bool checked) {
  if (checked == RadioButton::GetChecked()) {
    return;
  }
  if (checked) {
    Views other;
    GetRadioButtonsInList(GetGroup(), &other);
    for (views::View* peer : other) {
      if (peer != this && IsViewClass<CombinedSelectorRadioButton>(peer)) {
        static_cast<CombinedSelectorRadioButton*>(peer)->SetChecked(false);
      }
    }
    delegate_->OnRadioButtonChecked(index_);
    RequestFocus();
  }
  Checkbox::SetChecked(checked);
}

void CombinedSelectorRadioButton::GetRadioButtonsInList(int group,
                                                        Views* views) {
  auto* row_view = parent();
  if (!row_view) {
    return;
  }
  auto* list_view = row_view->parent();
  if (!list_view) {
    return;
  }
  list_view->GetViewsInGroup(group, views);
}

bool CombinedSelectorRadioButton::SkipDefaultKeyEventProcessing(
    const ui::KeyEvent& event) {
  // The radio button would show the ink drop on return key press. Since the
  // radio buttons in the combined selector are tab focusable
  // (IsGroupFocusTraversable), this is not required. The return key should not
  // be handled by the radio button.
  return event.key_code() == ui::VKEY_RETURN
             ? false
             : RadioButton::SkipDefaultKeyEventProcessing(event);
}

BEGIN_METADATA(CombinedSelectorRadioButton)
END_METADATA

CombinedSelectorTextColumnView::CombinedSelectorTextColumnView(
    const std::vector<std::u16string_view> texts) {
  AddColumn(views::LayoutAlignment::kStart, views::LayoutAlignment::kCenter,
            1.0f, views::TableLayout::ColumnSize::kFixed, 0, 0);
  AddRows(texts.size(), views::TableLayout::kFixedSize);
  for (size_t i = 0; i < texts.size(); i++) {
    auto* label_view = AddChildView(std::make_unique<views::Label>(
        texts.at(i), views::style::CONTEXT_LABEL,
        i == 0 ? views::style::STYLE_BODY_3_MEDIUM
               : views::style::STYLE_BODY_4));
    label_view->SetHorizontalAlignment(gfx::ALIGN_LEFT);
  }
}

BEGIN_METADATA(CombinedSelectorTextColumnView)
END_METADATA

CombinedSelectorRowView::CombinedSelectorRowView(
    const ui::ImageModel& icon,
    const std::vector<std::u16string_view> texts,
    RadioStatus radio_status,
    bool enabled,
    CombinedSelectorRadioButton::Delegate* radio_delegate,
    int index)
    : radio_status_(radio_status), enabled_(enabled) {
  SetEnabled(enabled);

  GetViewAccessibility().SetRole(radio_status != RadioStatus::kNone
                                     ? ax::mojom::Role::kRadioButton
                                     : ax::mojom::Role::kButton);
  GetViewAccessibility().SetName(base::JoinString(texts, u"\n"));
  const int horizontal_padding = radio_status == RadioStatus::kNone ? 0 : 16;
  SetBorder(views::CreateEmptyBorder(gfx::Insets::VH(8, horizontal_padding)));

  const int icon_padding = ChromeLayoutProvider::Get()->GetDistanceMetric(
      views::DISTANCE_RELATED_LABEL_HORIZONTAL);
  AddColumn(views::LayoutAlignment::kCenter, views::LayoutAlignment::kCenter,
            views::TableLayout::kFixedSize,
            views::TableLayout::ColumnSize::kUsePreferred,
            /*fixed_width=*/0,
            /*min_width=*/0)
      .AddPaddingColumn(views::TableLayout::kFixedSize, icon_padding);
  AddColumn(views::LayoutAlignment::kStretch, views::LayoutAlignment::kStretch,
            /*horizontal_resize=*/1.0,
            views::TableLayout::ColumnSize::kUsePreferred,
            /*fixed_width=*/0,
            /*min_width=*/0);
  if (radio_status != RadioStatus::kNone) {
    AddPaddingColumn(views::TableLayout::kFixedSize, icon_padding)
        .AddColumn(views::LayoutAlignment::kCenter,
                   views::LayoutAlignment::kCenter,
                   views::TableLayout::kFixedSize,
                   views::TableLayout::ColumnSize::kUsePreferred,
                   /*fixed_width=*/0,
                   /*min_width=*/0);
  }
  AddRows(1, views::TableLayout::kFixedSize);

  AddChildView(std::make_unique<views::ImageView>(icon));
  AddChildView(std::make_unique<CombinedSelectorTextColumnView>(texts));
  MaybeAddRadioButton(radio_delegate, index);
}

void CombinedSelectorRowView::MaybeAddRadioButton(
    CombinedSelectorRadioButton::Delegate* delegate,
    int index) {
  if (radio_status_ == RadioStatus::kNone) {
    return;
  }
  auto radio_button =
      std::make_unique<CombinedSelectorRadioButton>(delegate, index);
  radio_button->SetChecked(radio_status_ == RadioStatus::kSelected);
  radio_button->SetEnabled(enabled_);
  radio_button->GetViewAccessibility().SetName(*this);
  radio_button_ = AddChildView(std::move(radio_button));
}

void CombinedSelectorRowView::RequestFocus() {
  if (radio_button_) {
    radio_button_->RequestFocus();
  }
}

bool CombinedSelectorRowView::OnMousePressed(const ui::MouseEvent& event) {
  if (radio_button_ && event.IsOnlyLeftMouseButton()) {
    const gfx::Point center = radio_button_->GetLocalBounds().CenterPoint();
    ui::MouseEvent synthetic_press_event(
        ui::EventType::kMousePressed, center, center, event.time_stamp(),
        event.flags(), event.changed_button_flags());
    radio_button_->OnMousePressed(synthetic_press_event);
    return true;
  }
  return views::TableLayoutView::OnMousePressed(event);
}

void CombinedSelectorRowView::OnMouseReleased(const ui::MouseEvent& event) {
  if (radio_button_ && event.IsOnlyLeftMouseButton()) {
    const gfx::Point center = radio_button_->GetLocalBounds().CenterPoint();
    ui::MouseEvent synthetic_release_event(
        ui::EventType::kMouseReleased, center, center, event.time_stamp(),
        event.flags(), event.changed_button_flags());
    radio_button_->OnMouseReleased(synthetic_release_event);
    RequestFocus();
    return;
  }
  views::TableLayoutView::OnMouseReleased(event);  // Default handling.
}

BEGIN_METADATA(CombinedSelectorRowView)
END_METADATA

CombinedSelectorListView::CombinedSelectorListView(
    CombinedSelectorSheetModel* model,
    CombinedSelectorRadioButton::Delegate* delegate) {
  SetLayoutManager(std::make_unique<views::FillLayout>());
  auto* scroll_view = AddChildView(std::make_unique<views::ScrollView>());

  auto wrapper = std::make_unique<views::View>();
  wrapper->SetLayoutManager(std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kVertical, gfx::Insets(),
      /*between_child_spacing=*/kRowGap));
  size_t num_mechanisms = model->dialog_model()->mechanisms.size();

  if (num_mechanisms > 1) {
    wrapper->AddChildView(std::make_unique<views::Separator>());
  }

  for (size_t i = 0; i < num_mechanisms; i++) {
    if (i > 0) {
      wrapper->AddChildView(std::make_unique<views::Separator>());
    }
    const auto& mechanism = model->dialog_model()->mechanisms[i];
    auto image_model =
        ui::ImageModel::FromVectorIcon(*mechanism.icon, ui::kColorIcon, 20);
    auto texts = GetTextsForMechanism(mechanism);
    auto* row = wrapper->AddChildView(std::make_unique<CombinedSelectorRowView>(
        image_model, std::move(texts), model->GetSelectionStatus(i),
        !model->dialog_model()->ui_disabled_, delegate, i));
    if (model->GetSelectionStatus(i) ==
        CombinedSelectorSheetModel::SelectionStatus::kSelected) {
      selected_view_ = row;
    }

    SetOwnedGroup(kGroupId);
  }

  if (num_mechanisms > 1) {
    wrapper->AddChildView(std::make_unique<views::Separator>());
  }

  scroll_view->SetContents(std::move(wrapper));
  scroll_view->ClipHeightTo(kMaxRowHeight, 3 * kMaxRowHeight + 2 * kRowGap);
}

CombinedSelectorListView::~CombinedSelectorListView() = default;

void CombinedSelectorListView::RequestFocus() {
  if (selected_view_) {
    selected_view_->RequestFocus();
  }
}

BEGIN_METADATA(CombinedSelectorListView)
END_METADATA
