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

#include "ash/login/ui/access_code_input.h"

#include <string>
#include <string_view>

#include "ash/strings/grit/ash_strings.h"
#include "ash/style/system_textfield.h"
#include "base/notimplemented.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.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_impl_macros.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/color/color_id.h"
#include "ui/compositor/layer.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/range/range.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/layout/box_layout.h"
#include "ui/views/layout/fill_layout.h"

namespace ash {

namespace {
// Identifier for focus traversal.
constexpr int kFixedLengthInputGroup = 1;

constexpr int kAccessCodeFlexLengthWidthDp = 192;
constexpr int kAccessCodeFlexUnderlineThicknessDp = 1;
constexpr int kAccessCodeFontSizeDeltaDp = 4;
constexpr int kObscuredGlyphSpacingDp = 6;

constexpr int kAccessCodeInputFieldWidthDp = 24;
constexpr int kAccessCodeBetweenInputFieldsGapDp = 8;

// A border impl that draw the underline for pin field. This uses Border as
// ash::SystemTextfield does not allow custom background.
class PinCodeUnderline : public views::Border {
 public:
  PinCodeUnderline(int thickness_dp, const ui::ColorId color_id)
      : thickness_dp_(thickness_dp) {
    SetColor(color_id);
  }
  PinCodeUnderline(const PinCodeUnderline&) = delete;
  PinCodeUnderline& operator=(const PinCodeUnderline&) = delete;
  ~PinCodeUnderline() override = default;

  // views::Border:
  void Paint(const views::View& view, gfx::Canvas* canvas) override {
    gfx::Rect bounds = view.GetLocalBounds();
    bounds.set_y(bounds.height() - thickness_dp_);
    bounds.set_height(thickness_dp_);
    auto bg_color = color().ResolveToSkColor(view.GetColorProvider());
    canvas->FillRect(bounds, bg_color);
  }

  gfx::Insets GetInsets() const override {
    return gfx::Insets::TLBR(0, 0, thickness_dp_, 0);
  }
  gfx::Size GetMinimumSize() const override { return gfx::Size(); }

 private:
  int thickness_dp_;
};

std::unique_ptr<views::Border> CreatePinCodeBorder(int thickness_dp,
                                                   const ui::ColorId color_id) {
  return std::make_unique<PinCodeUnderline>(thickness_dp, color_id);
}

}  // namespace

BEGIN_METADATA(AccessCodeInput)
END_METADATA

FlexCodeInput::FlexCodeInput(OnInputChange on_input_change,
                             OnEnter on_enter,
                             OnEscape on_escape,
                             bool obscure_pin)
    : on_input_change_(std::move(on_input_change)),
      on_enter_(std::move(on_enter)),
      on_escape_(std::move(on_escape)) {
  DCHECK(on_input_change_);
  DCHECK(on_enter_);
  DCHECK(on_escape_);

  SetLayoutManager(std::make_unique<views::FillLayout>());

  const ui::ColorId input_color_id =
      static_cast<ui::ColorId>(cros_tokens::kCrosSysOnSurface);
  code_field_ = AddChildView(
      std::make_unique<SystemTextfield>(SystemTextfield::Type::kMedium));
  code_field_->set_controller(this);
  code_field_->SetFontList(views::Textfield::GetDefaultFontList().Derive(
      kAccessCodeFontSizeDeltaDp, gfx::Font::FontStyle::NORMAL,
      gfx::Font::Weight::NORMAL));
  code_field_->SetBorder(views::CreateSolidSidedBorder(
      gfx::Insets::TLBR(0, 0, kAccessCodeFlexUnderlineThicknessDp, 0),
      input_color_id));
  code_field_->SetBackgroundEnabled(false);
  code_field_->SetFocusBehavior(FocusBehavior::ALWAYS);
  code_field_->SetPreferredSize(
      gfx::Size(kAccessCodeFlexLengthWidthDp, kAccessCodeInputFieldHeightDp));
  SetInputColorId(input_color_id);

  if (obscure_pin) {
    code_field_->SetTextInputType(ui::TEXT_INPUT_TYPE_PASSWORD);
    code_field_->SetObscuredGlyphSpacing(kObscuredGlyphSpacingDp);
  } else {
    code_field_->SetTextInputType(ui::TEXT_INPUT_TYPE_TEXT);
  }
}

FlexCodeInput::~FlexCodeInput() = default;

void FlexCodeInput::InsertDigit(int value) {
  DCHECK_LE(0, value);
  DCHECK_GE(9, value);
  if (code_field_->GetEnabled()) {
    code_field_->SetText(
        base::StrCat({code_field_->GetText(), base::NumberToString16(value)}));
    on_input_change_.Run(true);
  }
}

void FlexCodeInput::Backspace() {
  // Instead of just adjusting code_field_ text directly, fire a backspace key
  // event as this handles the various edge cases (ie, selected text).

  // views::Textfield::OnKeyPressed is private, so we call it via views::View.
  auto* view = static_cast<views::View*>(code_field_);
  view->OnKeyPressed(ui::KeyEvent(ui::EventType::kKeyPressed, ui::VKEY_BACK,
                                  ui::DomCode::BACKSPACE, ui::EF_NONE));
  view->OnKeyPressed(ui::KeyEvent(ui::EventType::kKeyReleased, ui::VKEY_BACK,
                                  ui::DomCode::BACKSPACE, ui::EF_NONE));
  // This triggers ContentsChanged(), which calls |on_input_change_|.
}

std::optional<std::string> FlexCodeInput::GetCode() const {
  std::u16string_view code = code_field_->GetText();
  if (!code.length()) {
    return std::nullopt;
  }
  return base::UTF16ToUTF8(code);
}

void FlexCodeInput::SetInputColorId(ui::ColorId color_id) {
  code_field_->SetTextColorId(color_id);
}

void FlexCodeInput::SetInputEnabled(bool input_enabled) {
  code_field_->SetEnabled(input_enabled);
}

void FlexCodeInput::SetReadOnly(bool read_only) {
  NOTIMPLEMENTED();
}

bool FlexCodeInput::IsReadOnly() const {
  NOTIMPLEMENTED();
  return false;
}

void FlexCodeInput::ClearInput() {
  code_field_->SetText(std::u16string());
  on_input_change_.Run(false);
}

void FlexCodeInput::RequestFocus() {
  code_field_->RequestFocus();
}

void FlexCodeInput::SetAccessibleNameOnTextfield(const std::u16string& name) {
  code_field_->GetViewAccessibility().SetName(name);
}

void FlexCodeInput::ContentsChanged(views::Textfield* sender,
                                    const std::u16string& new_contents) {
  const bool has_content = new_contents.length() > 0;
  on_input_change_.Run(has_content);
}

bool FlexCodeInput::HandleKeyEvent(views::Textfield* sender,
                                   const ui::KeyEvent& key_event) {
  // Only handle keys.
  if (key_event.type() != ui::EventType::kKeyPressed) {
    return false;
  }

  // Default handling for events with Alt modifier like spoken feedback.
  if (key_event.IsAltDown()) {
    return false;
  }

  // FlexCodeInput class responds to a limited subset of key press events.
  // All events not handled below are sent to |code_field_|.
  const ui::KeyboardCode key_code = key_event.key_code();

  // Allow using tab for keyboard navigation.
  if (key_code == ui::VKEY_TAB || key_code == ui::VKEY_BACKTAB) {
    return false;
  }

  if (key_code == ui::VKEY_RETURN) {
    if (GetCode().has_value()) {
      on_enter_.Run();
    }
    return true;
  }

  if (key_code == ui::VKEY_ESCAPE) {
    on_escape_.Run();
    return true;
  }

  return false;
}

BEGIN_METADATA(FlexCodeInput)
END_METADATA

AccessibleInputField::AccessibleInputField()
    : SystemTextfield(SystemTextfield::Type::kMedium) {
  // We want the PIN input field, an empty input field, to retain
  // NameFrom::kAttributeExplicitlyEmpty.
  GetViewAccessibility().SetName(
      std::string(), ax::mojom::NameFrom::kAttributeExplicitlyEmpty);
}

bool AccessibleInputField::IsGroupFocusTraversable() const {
  return false;
}

views::View* AccessibleInputField::GetSelectedViewForGroup(int group) {
  return parent() ? parent()->GetSelectedViewForGroup(group) : nullptr;
}

void AccessibleInputField::OnGestureEvent(ui::GestureEvent* event) {
  if (event->type() == ui::EventType::kGestureTap) {
    RequestFocusWithPointer(event->details().primary_pointer_type());
    return;
  }

  views::Textfield::OnGestureEvent(event);
}

BEGIN_METADATA(AccessibleInputField)
END_METADATA

FixedLengthCodeInput::FixedLengthCodeInput(int length,
                                           OnInputChange on_input_change,
                                           OnEnter on_enter,
                                           OnEscape on_escape,
                                           bool obscure_pin)
    : on_input_change_(std::move(on_input_change)),
      on_enter_(std::move(on_enter)),
      on_escape_(std::move(on_escape)),
      is_obscure_pin_(obscure_pin) {
  DCHECK_LT(0, length);
  DCHECK(on_input_change_);

  auto* layout = SetLayoutManager(std::make_unique<views::BoxLayout>(
      views::BoxLayout::Orientation::kHorizontal, gfx::Insets(),
      kAccessCodeBetweenInputFieldsGapDp));
  SetGroup(kFixedLengthInputGroup);
  SetPaintToLayer();
  layer()->SetFillsBoundsOpaquely(false);

  const ui::ColorId text_color_id =
      static_cast<ui::ColorId>(cros_tokens::kCrosSysOnSurface);

  for (int i = 0; i < length; ++i) {
    auto* field = new AccessibleInputField();
    views::FocusRing::Get(field)->SetHasFocusPredicate(
        base::BindRepeating([](const views::View* view) { return false; }));
    field->SetBackgroundEnabled(false);
    field->set_controller(this);
    field->SetPreferredSize(
        gfx::Size(kAccessCodeInputFieldWidthDp, kAccessCodeInputFieldHeightDp));
    field->SetHorizontalAlignment(gfx::HorizontalAlignment::ALIGN_CENTER);
    if (is_obscure_pin_) {
      field->SetTextInputType(ui::TEXT_INPUT_TYPE_PASSWORD);
    } else {
      field->SetTextInputType(ui::TEXT_INPUT_TYPE_NUMBER);
    }
    field->SetTextColorId(text_color_id);
    field->SetFontList(views::Textfield::GetDefaultFontList().Derive(
        kAccessCodeFontSizeDeltaDp, gfx::Font::FontStyle::NORMAL,
        gfx::Font::Weight::NORMAL));
    field->SetBorder(CreatePinCodeBorder(
        kAccessCodeInputFieldUnderlineThicknessDp, text_color_id));
    field->SetGroup(kFixedLengthInputGroup);

    // Ignores the a11y focus of |field| because the a11y needs to focus to the
    // FixedLengthCodeInput object.
    field->GetViewAccessibility().set_propagate_focus_to_ancestor(true);
    input_fields_.push_back(field);
    AddChildViewRaw(field);
    layout->SetFlexForView(field, 1);
  }
  text_value_for_a11y_ = std::u16string(length, ' ');

  GetViewAccessibility().SetRole(ax::mojom::Role::kTextField);
  GetViewAccessibility().SetName(l10n_util::GetStringUTF8(
      IDS_ASH_LOGIN_PARENT_ACCESS_GENERIC_DESCRIPTION));
  GetViewAccessibility().SetIsProtected(is_obscure_pin_);
  GetViewAccessibility().SetValue(text_value_for_a11y_);
  GetViewAccessibility().SetIsEditable(true);
  GetViewAccessibility().AddHTMLAttributes(std::make_pair("type", "tel"));
  OnTextSelectionChanged();
}

FixedLengthCodeInput::~FixedLengthCodeInput() = default;

// Inserts |value| into the |active_field_| and moves focus to the next field
// if it exists.
void FixedLengthCodeInput::InsertDigit(int value) {
  DCHECK_LE(0, value);
  DCHECK_GE(9, value);

  ActiveField()->SetText(base::NumberToString16(value));
  bool was_last_field = IsLastFieldActive();
  ResetTextValueForA11y();
  FocusNextField();
  NotifyAccessibilityEventDeprecated(ax::mojom::Event::kTextSelectionChanged,
                                     true);
  on_input_change_.Run(was_last_field, GetCode().has_value());
}

// Clears input from the |active_field_|. If |active_field| is empty moves
// focus to the previous field (if exists) and clears input there.
void FixedLengthCodeInput::Backspace() {
  // Ignore backspace on the first field, if empty.
  if (IsFirstFieldActive() && ActiveInput().empty()) {
    return;
  }

  if (ActiveInput().empty()) {
    FocusPreviousField();
  }

  ActiveField()->SetText(std::u16string());
  ResetTextValueForA11y();

  NotifyAccessibilityEventDeprecated(ax::mojom::Event::kTextSelectionChanged,
                                     true);
  on_input_change_.Run(IsLastFieldActive(), false /*complete*/);
}

// Returns access code as string if all fields contain input.
std::optional<std::string> FixedLengthCodeInput::GetCode() const {
  std::string result;
  size_t length;
  for (ash::AccessibleInputField* field : input_fields_) {
    length = field->GetText().length();
    if (!length) {
      return std::nullopt;
    }

    DCHECK_EQ(1u, length);
    base::StrAppend(&result, {base::UTF16ToUTF8(field->GetText())});
  }
  return result;
}

void FixedLengthCodeInput::SetInputColorId(ui::ColorId color_id) {
  const ui::ColorId error_color_id =
      static_cast<ui::ColorId>(cros_tokens::kCrosSysError);

  for (ash::AccessibleInputField* field : input_fields_) {
    field->SetTextColorId(color_id);
    // We don't update the underline color to red.
    if (color_id != error_color_id) {
      field->SetBorder(CreatePinCodeBorder(
          kAccessCodeInputFieldUnderlineThicknessDp, error_color_id));
    }
  }
}

bool FixedLengthCodeInput::IsGroupFocusTraversable() const {
  return false;
}

views::View* FixedLengthCodeInput::GetSelectedViewForGroup(int group) {
  return ActiveField();
}

void FixedLengthCodeInput::RequestFocus() {
  ActiveField()->RequestFocus();
  OnTextSelectionChanged();
}

void FixedLengthCodeInput::ResetTextValueForA11y() {
  std::u16string result;

  for (size_t i = 0; i < input_fields_.size(); ++i) {
    if (input_fields_[i]->GetText().empty()) {
      result.push_back(' ');
    } else {
      result.push_back(is_obscure_pin_ ? u'•' : input_fields_[i]->GetText()[0]);
    }
  }

  text_value_for_a11y_ = result;
  GetViewAccessibility().SetValue(text_value_for_a11y_);
  OnTextSelectionChanged();
}

gfx::Range FixedLengthCodeInput::GetSelectedRangeOfTextValueForA11y() {
  int text_sel_start = active_input_index_;
  bool empty = text_value_for_a11y_[text_sel_start] == ' ';
  int text_sel_end = text_sel_start + (empty ? 0 : 1);
  return gfx::Range(text_sel_start, text_sel_end);
}

void FixedLengthCodeInput::OnTextSelectionChanged() {
  const gfx::Range& range = GetSelectedRangeOfTextValueForA11y();
  GetViewAccessibility().SetTextSelStart(range.start());
  GetViewAccessibility().SetTextSelEnd(range.end());
}

bool FixedLengthCodeInput::HandleKeyEvent(views::Textfield* sender,
                                          const ui::KeyEvent& key_event) {
  if (key_event.type() != ui::EventType::kKeyPressed) {
    return false;
  }

  // Default handling for events with Alt modifier like spoken feedback.
  if (key_event.IsAltDown()) {
    return false;
  }

  // Default handling for events with Control modifier like sign out.
  if (key_event.IsControlDown()) {
    return false;
  }

  if (sender->GetReadOnly()) {
    return false;
  }

  // FixedLengthCodeInput class responds to limited subset of key press
  // events. All key pressed events not handled below are ignored.
  const ui::KeyboardCode key_code = key_event.key_code();
  if (key_code == ui::VKEY_TAB || key_code == ui::VKEY_BACKTAB) {
    // Allow using tab for keyboard navigation.
    return false;
  } else if (key_code == ui::VKEY_PROCESSKEY) {
    // Default handling for keyboard events that are not generated by physical
    // key press. This can happen, for example, when virtual keyboard button
    // is pressed.
    return false;
  } else if (key_code >= ui::VKEY_0 && key_code <= ui::VKEY_9) {
    InsertDigit(key_code - ui::VKEY_0);
  } else if (key_code >= ui::VKEY_NUMPAD0 && key_code <= ui::VKEY_NUMPAD9) {
    InsertDigit(key_code - ui::VKEY_NUMPAD0);
  } else if (key_code == ui::VKEY_LEFT && arrow_navigation_allowed_) {
    FocusPreviousField();
    NotifyAccessibilityEventDeprecated(ax::mojom::Event::kTextSelectionChanged,
                                       true);
  } else if (key_code == ui::VKEY_RIGHT && arrow_navigation_allowed_) {
    // Do not allow to leave empty field when moving focus with arrow key.
    if (!ActiveInput().empty()) {
      FocusNextField();
      NotifyAccessibilityEventDeprecated(
          ax::mojom::Event::kTextSelectionChanged, true);
    }
  } else if (key_code == ui::VKEY_BACK) {
    Backspace();
  } else if (key_code == ui::VKEY_RETURN) {
    if (GetCode().has_value()) {
      on_enter_.Run();
    }
  } else if (key_code == ui::VKEY_ESCAPE) {
    on_escape_.Run();
  }

  return true;
}

void FixedLengthCodeInput::ContentsChanged(views::Textfield* sender,
                                           const std::u16string& new_contents) {
  if (new_contents.empty()) {
    return;
  }
  // Called when a character or text is inserted from the virtual keyboard.
  if (new_contents.size() > 1) {
    sender->SetText(std::u16string());
    return;
  }
  unsigned new_digit = 0;
  // If a non-numeric character is inserted from the virtual keyboard, clear it.
  if (!base::StringToUint(new_contents, &new_digit) ||
      !base::IsValueInRangeForNumericType<uint8_t>(new_digit)) {
    sender->SetText(std::u16string());
    return;
  }
  bool was_last_field = IsLastFieldActive();
  ResetTextValueForA11y();
  FocusNextField();
  NotifyAccessibilityEventDeprecated(ax::mojom::Event::kTextSelectionChanged,
                                     true);
  on_input_change_.Run(was_last_field, GetCode().has_value());
}

bool FixedLengthCodeInput::HandleMouseEvent(views::Textfield* sender,
                                            const ui::MouseEvent& mouse_event) {
  if (!(mouse_event.IsOnlyLeftMouseButton() ||
        mouse_event.IsOnlyRightMouseButton())) {
    return false;
  }

  // Move focus to the field that was selected with mouse input.
  for (size_t i = 0; i < input_fields_.size(); ++i) {
    if (input_fields_[i] == sender) {
      active_input_index_ = i;
      RequestFocus();
      NotifyAccessibilityEventDeprecated(
          ax::mojom::Event::kTextSelectionChanged, true);
      break;
    }
  }

  return true;
}

bool FixedLengthCodeInput::HandleGestureEvent(
    views::Textfield* sender,
    const ui::GestureEvent& gesture_event) {
  if (gesture_event.details().type() != ui::EventType::kGestureTap) {
    return false;
  }

  // Move focus to the field that was selected with gesture.
  for (size_t i = 0; i < input_fields_.size(); ++i) {
    if (input_fields_[i] == sender) {
      active_input_index_ = i;
      RequestFocus();
      NotifyAccessibilityEventDeprecated(
          ax::mojom::Event::kTextSelectionChanged, true);
      break;
    }
  }

  return true;
}

void FixedLengthCodeInput::SetInputEnabled(bool input_enabled) {
  for (ash::AccessibleInputField* field : input_fields_) {
    field->SetEnabled(input_enabled);
  }
}

void FixedLengthCodeInput::SetReadOnly(bool read_only) {
  const ui::ColorId underline_color_id =
      static_cast<ui::ColorId>(cros_tokens::kCrosSysOnSurface);
  for (ash::AccessibleInputField* field : input_fields_) {
    field->SetReadOnly(read_only);
    field->SetBackground(nullptr);
    field->SetBorder(CreatePinCodeBorder(
        kAccessCodeInputFieldUnderlineThicknessDp, underline_color_id));
    field->SetCursorEnabled(!read_only);
  }
}

bool FixedLengthCodeInput::IsReadOnly() const {
  if (!input_fields_.empty()) {
    // As SetReadOnly above propagates flag to all fields, just
    // check the first field here instead of implementing complex
    // combining logic.
    return static_cast<views::SelectionControllerDelegate*>(input_fields_[0])
        ->IsReadOnly();
  }
  return false;
}

void FixedLengthCodeInput::ClearInput() {
  for (ash::AccessibleInputField* field : input_fields_) {
    field->SetText(std::u16string());
  }
  active_input_index_ = 0;
  text_value_for_a11y_.clear();
  GetViewAccessibility().RemoveValue();
  RequestFocus();
}

bool FixedLengthCodeInput::IsEmpty() const {
  for (ash::AccessibleInputField* field : input_fields_) {
    if (field->GetText().length()) {
      return false;
    }
  }
  return true;
}

void FixedLengthCodeInput::SetAllowArrowNavigation(bool allowed) {
  arrow_navigation_allowed_ = allowed;
}

void FixedLengthCodeInput::FocusPreviousField() {
  if (active_input_index_ == 0) {
    return;
  }

  --active_input_index_;
  RequestFocus();
}

void FixedLengthCodeInput::FocusNextField() {
  if (IsLastFieldActive()) {
    return;
  }

  ++active_input_index_;
  RequestFocus();
}

bool FixedLengthCodeInput::IsLastFieldActive() const {
  return active_input_index_ == (static_cast<int>(input_fields_.size()) - 1);
}

bool FixedLengthCodeInput::IsFirstFieldActive() const {
  return active_input_index_ == 0;
}

AccessibleInputField* FixedLengthCodeInput::ActiveField() const {
  return input_fields_[active_input_index_];
}

std::u16string_view FixedLengthCodeInput::ActiveInput() const {
  return ActiveField()->GetText();
}

BEGIN_METADATA(FixedLengthCodeInput)
END_METADATA

}  // namespace ash
