// Copyright 2017 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/payments/editor_view_controller.h"

#include <algorithm>
#include <map>
#include <memory>
#include <utility>

#include "chrome/browser/ui/views/chrome_layout_provider.h"
#include "chrome/browser/ui/views/chrome_typography.h"
#include "chrome/browser/ui/views/payments/payment_request_dialog_view.h"
#include "chrome/browser/ui/views/payments/payment_request_dialog_view_ids.h"
#include "chrome/browser/ui/views/payments/payment_request_sheet_controller.h"
#include "chrome/browser/ui/views/payments/payment_request_views_util.h"
#include "chrome/browser/ui/views/payments/validating_combobox.h"
#include "chrome/browser/ui/views/payments/validating_textfield.h"
#include "components/strings/grit/components_strings.h"
#include "ui/base/ime/text_input_type.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/color/color_id.h"
#include "ui/gfx/geometry/insets.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/md_text_button.h"
#include "ui/views/controls/label.h"
#include "ui/views/controls/styled_label.h"
#include "ui/views/controls/textfield/textfield.h"
#include "ui/views/layout/box_layout.h"
#include "ui/views/layout/box_layout_view.h"
#include "ui/views/layout/fill_layout.h"
#include "ui/views/layout/table_layout_view.h"
#include "ui/views/metadata/view_factory.h"
#include "ui/views/view.h"

namespace payments {
namespace {

constexpr int kErrorLabelTopPadding = 6;

std::unique_ptr<views::Label> CreateErrorLabel(const std::u16string& error,
                                               autofill::FieldType type) {
  return views::Builder<views::Label>()
      .SetText(error)
      .SetTextContext(CONTEXT_DIALOG_BODY_TEXT_SMALL)
      .SetID(static_cast<int>(DialogViewID::ERROR_LABEL_OFFSET) + type)
      .SetMultiLine(true)
      .SetHorizontalAlignment(gfx::ALIGN_LEFT)
      .SetBorder(views::CreateEmptyBorder(
          gfx::Insets::TLBR(kErrorLabelTopPadding, 0, 0, 0)))
      .SetEnabledColor(ui::kColorAlertHighSeverity)
      .Build();
}

}  // namespace

EditorViewController::EditorViewController(
    base::WeakPtr<PaymentRequestSpec> spec,
    base::WeakPtr<PaymentRequestState> state,
    base::WeakPtr<PaymentRequestDialogView> dialog,
    BackNavigationType back_navigation_type,
    bool is_incognito)
    : PaymentRequestSheetController(spec, state, dialog),
      back_navigation_type_(back_navigation_type),
      is_incognito_(is_incognito) {}

EditorViewController::~EditorViewController() {
  ClearViewPointers();
}

void EditorViewController::DisplayErrorMessageForField(
    autofill::FieldType type,
    const std::u16string& error_message) {
  AddOrUpdateErrorMessageForField(type, error_message);
  RelayoutPane();
}

// static
int EditorViewController::GetInputFieldViewId(autofill::FieldType type) {
  return static_cast<int>(DialogViewID::INPUT_FIELD_TYPE_OFFSET) +
         static_cast<int>(type);
}

std::unique_ptr<views::View> EditorViewController::CreateHeaderView() {
  return nullptr;
}

std::unique_ptr<views::View> EditorViewController::CreateCustomFieldView(
    autofill::FieldType type,
    views::View** focusable_field,
    bool* valid,
    std::u16string* error_message) {
  return nullptr;
}

std::unique_ptr<views::View> EditorViewController::CreateExtraViewForField(
    autofill::FieldType type) {
  return nullptr;
}

bool EditorViewController::ValidateInputFields() {
  for (const auto& field : text_fields()) {
    if (!field.first->IsValid()) {
      return false;
    }
  }
  for (const auto& field : comboboxes()) {
    if (!field.first->IsValid()) {
      return false;
    }
  }
  return true;
}

void EditorViewController::Stop() {
  PaymentRequestSheetController::Stop();
  ClearViewPointers();
}

std::u16string EditorViewController::GetPrimaryButtonLabel() {
  return l10n_util::GetStringUTF16(IDS_DONE);
}

PaymentRequestSheetController::ButtonCallback
EditorViewController::GetPrimaryButtonCallback() {
  return base::BindRepeating(&EditorViewController::SaveButtonPressed,
                             base::Unretained(this));
}

int EditorViewController::GetPrimaryButtonId() {
  return static_cast<int>(DialogViewID::EDITOR_SAVE_BUTTON);
}

bool EditorViewController::GetPrimaryButtonEnabled() {
  return true;
}

bool EditorViewController::ShouldShowSecondaryButton() {
  // Do not show the "Cancel Payment" button.
  return false;
}

void EditorViewController::FillContentView(views::View* content_view) {
  auto layout = std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kVertical);
  layout->set_main_axis_alignment(views::BoxLayout::MainAxisAlignment::kStart);
  layout->set_cross_axis_alignment(
      views::BoxLayout::CrossAxisAlignment::kStretch);
  content_view->SetLayoutManager(std::move(layout));
  // No insets. Child views below are responsible for their padding.

  // An editor can optionally have a header view specific to it.
  if (std::unique_ptr<views::View> header_view = CreateHeaderView()) {
    content_view->AddChildView(std::move(header_view));
  }

  // The heart of the editor dialog: all the input fields with their labels.
  content_view->AddChildView(CreateEditorView());
}

bool EditorViewController::ShouldAccelerateEnterKey() {
  // We allow the user to confirm their details by pressing 'enter' irregardless
  // of which edit field is currently focused, for quicker navigation through
  // the form.
  return true;
}

void EditorViewController::UpdateEditorView() {
  // `UpdateContentView` removes all children, so reset pointers to them.
  ClearViewPointers();
  UpdateContentView();
  UpdateFocus(GetFirstFocusedView());
  dialog()->EditorViewUpdated();
}

views::View* EditorViewController::GetFirstFocusedView() {
  if (initial_focus_field_view_) {
    return initial_focus_field_view_;
  }
  return PaymentRequestSheetController::GetFirstFocusedView();
}

std::unique_ptr<ValidatingCombobox>
EditorViewController::CreateComboboxForField(const EditorField& field,
                                             std::u16string* error_message) {
  std::unique_ptr<ValidationDelegate> delegate =
      CreateValidationDelegate(field);
  ValidationDelegate* delegate_ptr = delegate.get();
  std::unique_ptr<ValidatingCombobox> combobox =
      std::make_unique<ValidatingCombobox>(GetComboboxModelForType(field.type),
                                           std::move(delegate));
  combobox->GetViewAccessibility().SetName(field.label);

  std::u16string initial_value = GetInitialValueForType(field.type);
  if (!initial_value.empty()) {
    combobox->SelectValue(initial_value);
  }
  if (IsEditingExistingItem()) {
    combobox->SetInvalid(
        !delegate_ptr->IsValidCombobox(combobox.get(), error_message));
  }

  // Using autofill field type as a view ID.
  combobox->SetID(GetInputFieldViewId(field.type));
  combobox->SetCallback(
      base::BindRepeating(&EditorViewController::OnPerformAction,
                          base::Unretained(this), combobox.get()));
  comboboxes_.insert(std::make_pair(combobox.get(), field));
  return combobox;
}

void EditorViewController::ContentsChanged(views::Textfield* sender,
                                           const std::u16string& new_contents) {
  ValidatingTextfield* sender_cast = static_cast<ValidatingTextfield*>(sender);
  sender_cast->OnContentsChanged();
  primary_button()->SetEnabled(ValidateInputFields());
}

void EditorViewController::OnPerformAction(ValidatingCombobox* sender) {
  static_cast<ValidatingCombobox*>(sender)->OnContentsChanged();
  primary_button()->SetEnabled(ValidateInputFields());
}

std::unique_ptr<views::View> EditorViewController::CreateEditorView() {
  std::unique_ptr<views::View> editor_view = std::make_unique<views::View>();

  // All views have fixed size except the Field which stretches. The fixed
  // padding at the end is computed so that Field views have a minimum of
  // 176/272dp (short/long fields) as per spec.
  // ___________________________________________________________________________
  // |Label | 16dp pad | Field (flex) | 8dp pad | Extra View | Computed Padding|
  // |______|__________|______________|_________|____________|_________________|
  constexpr int kInputRowSpacing = 12;

  editor_view->SetBorder(views::CreateEmptyBorder(gfx::Insets::TLBR(
      kInputRowSpacing, payments::kPaymentRequestRowHorizontalInsets, 0,
      payments::kPaymentRequestRowHorizontalInsets)));

  editor_view->SetLayoutManager(std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kVertical, gfx::Insets(),
      kInputRowSpacing));

  views::View* first_field = nullptr;
  for (const auto& field : GetFieldDefinitions()) {
    bool valid = false;
    views::View* focusable_field =
        CreateInputField(editor_view.get(), field, &valid);
    if (!first_field) {
      first_field = focusable_field;
    }
    if (!initial_focus_field_view_ && !valid) {
      initial_focus_field_view_ = focusable_field;
    }
  }

  if (!initial_focus_field_view_) {
    initial_focus_field_view_ = first_field;
  }

  // Validate all fields and disable the primary (Done) button if necessary.
  primary_button()->SetEnabled(ValidateInputFields());

  // Adds the "* indicates a required field" label in "hint" grey text.
  editor_view->AddChildView(CreateHintLabel(
      l10n_util::GetStringUTF16(IDS_PAYMENTS_REQUIRED_FIELD_MESSAGE),
      gfx::HorizontalAlignment::ALIGN_LEFT));

  return editor_view;
}

// Each input field is a 4-quadrant grid.
// +----------------------------------------------------------+
// | Field Label           | Input field (textfield/combobox) |
// |_______________________|__________________________________|
// |   (empty)             | Error label                      |
// +----------------------------------------------------------+
views::View* EditorViewController::CreateInputField(views::View* editor_view,
                                                    const EditorField& field,
                                                    bool* valid) {
  constexpr int kShortFieldMinimumWidth = 176;
  constexpr int kLabelWidth = 140;
  constexpr int kLongFieldMinimumWidth = 272;
  // This is the horizontal padding between the label and the field.
  const int label_input_field_horizontal_padding =
      ChromeLayoutProvider::Get()->GetDistanceMetric(
          views::DISTANCE_RELATED_CONTROL_HORIZONTAL);
  // The horizontal padding between the field and the extra view.
  constexpr int kFieldExtraViewHorizontalPadding = 8;

  auto* field_view =
      editor_view->AddChildView(std::make_unique<views::TableLayoutView>());

  // Label.
  field_view->AddColumn(views::LayoutAlignment::kStart,
                        views::LayoutAlignment::kCenter,
                        views::TableLayout::kFixedSize,
                        views::TableLayout::ColumnSize::kFixed, kLabelWidth, 0);
  field_view->AddPaddingColumn(views::TableLayout::kFixedSize,
                               label_input_field_horizontal_padding);
  // Field.
  field_view->AddColumn(views::LayoutAlignment::kStretch,
                        views::LayoutAlignment::kStretch, 1.0,
                        views::TableLayout::ColumnSize::kUsePreferred, 0, 0);
  field_view->AddPaddingColumn(views::TableLayout::kFixedSize,
                               kFieldExtraViewHorizontalPadding);

  int extra_view_width, padding, field_width;
  if (field.length_hint == EditorField::LengthHint::HINT_SHORT) {
    field_width = kShortFieldMinimumWidth;
    extra_view_width =
        ComputeWidestExtraViewWidth(EditorField::LengthHint::HINT_SHORT);
    // The padding at the end is fixed, computed to make sure the short field
    // maintains its minimum width.
  } else {
    field_width = kLongFieldMinimumWidth;
    extra_view_width =
        ComputeWidestExtraViewWidth(EditorField::LengthHint::HINT_LONG);
  }
  // The padding at the end is fixed, computed to make sure the long field
  // maintains its minimum width.
  padding = kDialogMinWidth - field_width - kLabelWidth -
            (2 * kPaymentRequestRowHorizontalInsets) -
            label_input_field_horizontal_padding -
            kFieldExtraViewHorizontalPadding - extra_view_width;

  // Extra view.
  field_view->AddColumn(
      views::LayoutAlignment::kStart, views::LayoutAlignment::kCenter,
      views::TableLayout::kFixedSize, views::TableLayout::ColumnSize::kFixed,
      extra_view_width, 0);

  field_view->AddPaddingColumn(views::TableLayout::kFixedSize, padding);

  field_view->AddRows(1, views::TableLayout::kFixedSize);

  std::unique_ptr<views::Label> label = std::make_unique<views::Label>(
      field.required ? field.label + u"*" : field.label);

  label->SetMultiLine(true);
  label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
  field_view->AddChildView(std::move(label));

  views::View* focusable_field = nullptr;
  constexpr int kInputFieldHeight = 28;

  std::u16string error_message;
  switch (field.control_type) {
    case EditorField::ControlType::TEXTFIELD:
    case EditorField::ControlType::TEXTFIELD_NUMBER: {
      std::unique_ptr<ValidationDelegate> validation_delegate =
          CreateValidationDelegate(field);
      ValidationDelegate* delegate_ptr = validation_delegate.get();

      std::u16string initial_value = GetInitialValueForType(field.type);
      auto text_field =
          std::make_unique<ValidatingTextfield>(std::move(validation_delegate));
      // Set the initial value and validity state.
      text_field->SetText(initial_value);
      text_field->GetViewAccessibility().SetName(field.label);
      *valid = IsEditingExistingItem() &&
               delegate_ptr->IsValidTextfield(text_field.get(), &error_message);
      if (IsEditingExistingItem()) {
        text_field->SetInvalid(!(*valid));
      }

      if (field.control_type == EditorField::ControlType::TEXTFIELD_NUMBER) {
        text_field->SetTextInputType(ui::TextInputType::TEXT_INPUT_TYPE_NUMBER);
      }
      text_field->set_controller(this);
      // Using autofill field type as a view ID (for testing).
      text_field->SetID(GetInputFieldViewId(field.type));
      text_fields_.insert(std::make_pair(text_field.get(), field));

      focusable_field = field_view->AddChildView(std::move(text_field));
      focusable_field->SetPreferredSize(
          gfx::Size(field_width, kInputFieldHeight));
      break;
    }
    case EditorField::ControlType::COMBOBOX: {
      std::unique_ptr<ValidatingCombobox> combobox =
          CreateComboboxForField(field, &error_message);

      *valid = combobox->IsValid();

      focusable_field = field_view->AddChildView(std::move(combobox));
      focusable_field->SetPreferredSize(
          gfx::Size(field_width, kInputFieldHeight));
      break;
    }
    case EditorField::ControlType::CUSTOMFIELD: {
      std::unique_ptr<views::View> custom_view = CreateCustomFieldView(
          field.type, &focusable_field, valid, &error_message);
      DCHECK(custom_view);

      field_view->AddChildView(std::move(custom_view));
      field_view->SetPreferredSize(gfx::Size(field_width, kInputFieldHeight));
      break;
    }
    case EditorField::ControlType::READONLY_LABEL: {
      std::unique_ptr<views::Label> readonly_label =
          std::make_unique<views::Label>(GetInitialValueForType(field.type));
      readonly_label->SetID(GetInputFieldViewId(field.type));
      readonly_label->SetHorizontalAlignment(gfx::ALIGN_LEFT);
      field_view->AddChildView(std::move(readonly_label));
      field_view->SetPreferredSize(gfx::Size(field_width, kInputFieldHeight));
      break;
    }
  }

  // If an extra view needs to go alongside the input field view, add it to the
  // last column.
  std::unique_ptr<views::View> extra_view = CreateExtraViewForField(field.type);
  field_view->AddChildView(extra_view ? std::move(extra_view)
                                      : std::make_unique<views::View>());

  // Error view.
  field_view->AddRows(1, views::TableLayout::kFixedSize);
  // Skip the first label column.
  field_view->AddChildView(std::make_unique<views::View>());
  auto error_label_view = std::make_unique<views::View>();
  error_label_view->SetLayoutManager(std::make_unique<views::FillLayout>());
  error_labels_[field.type] = error_label_view.get();
  if (IsEditingExistingItem() && !error_message.empty()) {
    AddOrUpdateErrorMessageForField(field.type, error_message);
  }

  field_view->AddChildView(std::move(error_label_view));

  return focusable_field;
}

int EditorViewController::ComputeWidestExtraViewWidth(
    EditorField::LengthHint size) {
  int widest_column_width = 0;

  for (const auto& field : GetFieldDefinitions()) {
    if (field.length_hint != size) {
      continue;
    }

    std::unique_ptr<views::View> extra_view =
        CreateExtraViewForField(field.type);
    if (!extra_view) {
      continue;
    }
    widest_column_width =
        std::max(extra_view->GetPreferredSize().width(), widest_column_width);
  }
  return widest_column_width;
}

void EditorViewController::AddOrUpdateErrorMessageForField(
    autofill::FieldType type,
    const std::u16string& error_message) {
  const auto& label_view_it = error_labels_.find(type);
  CHECK(label_view_it != error_labels_.end());

  if (error_message.empty()) {
    label_view_it->second->RemoveAllChildViews();
  } else {
    if (label_view_it->second->children().empty()) {
      // If there was no error label view, add it.
      label_view_it->second->AddChildView(
          CreateErrorLabel(error_message, type));
    } else {
      // The error view is the only child, and has a Label as only child itself.
      static_cast<views::Label*>(label_view_it->second->children().front())
          ->SetText(error_message);
    }
  }
}

void EditorViewController::SaveButtonPressed(const ui::Event& event) {
  if (!ValidateModelAndSave()) {
    return;
  }
  if (back_navigation_type_ == BackNavigationType::kOneStep) {
    dialog()->GoBack();
  } else {
    DCHECK_EQ(BackNavigationType::kPaymentSheet, back_navigation_type_);
    dialog()->GoBackToPaymentSheet();
  }
}

void EditorViewController::ClearViewPointers() {
  for (const auto& [textfield, _] : text_fields_) {
    textfield->set_controller(nullptr);
  }
  text_fields_.clear();
  comboboxes_.clear();
  initial_focus_field_view_ = nullptr;
}

}  // namespace payments
