// Copyright 2023 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/ash/arc/input_overlay/ui/edit_label.h"

#include "ash/accessibility/accessibility_controller.h"
#include "ash/bubble/bubble_utils.h"
#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/shell.h"
#include "ash/style/typography.h"
#include "base/notreached.h"
#include "base/strings/utf_string_conversions.h"
#include "base/time/time.h"
#include "chrome/browser/ash/arc/input_overlay/actions/action.h"
#include "chrome/browser/ash/arc/input_overlay/actions/input_element.h"
#include "chrome/browser/ash/arc/input_overlay/arc_input_overlay_metrics.h"
#include "chrome/browser/ash/arc/input_overlay/constants.h"
#include "chrome/browser/ash/arc/input_overlay/display_overlay_controller.h"
#include "chrome/browser/ash/arc/input_overlay/ui/action_view_list_item.h"
#include "chrome/browser/ash/arc/input_overlay/ui/edit_labels.h"
#include "chrome/browser/ash/arc/input_overlay/ui/ui_utils.h"
#include "chrome/browser/ash/arc/input_overlay/util.h"
#include "chrome/grit/generated_resources.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "ui/accessibility/ax_enums.mojom.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/base/models/image_model.h"
#include "ui/chromeos/styles/cros_tokens_color_mappings.h"
#include "ui/compositor/layer.h"
#include "ui/gfx/geometry/transform.h"
#include "ui/gfx/geometry/transform_util.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/animation/animation_builder.h"
#include "ui/views/background.h"
#include "ui/views/border.h"
#include "ui/views/controls/button/button.h"
#include "ui/views/view_utils.h"

namespace arc::input_overlay {

namespace {

constexpr float kCornerRadius = 8.0f;
constexpr int kLabelSize = 32;

constexpr ui::ColorId kPenIconColor = cros_tokens::kCrosSysOnPrimaryContainer;

// Pulse animation specs.
constexpr int kPulseTimes = 3;
constexpr int kPulseExtraHalfSize = 32;
constexpr base::TimeDelta kPulseDuration = base::Seconds(2);

// Returns "<up/down/left/right>" for `direction`.
std::u16string GetAccessibleNameSuffixForDirection(Direction direction) {
  switch (direction) {
    case Direction::kUp:
      return l10n_util ::GetStringUTF16(
          IDS_INPUT_OVERLAY_JOYSTICK_DIRECTION_UP_A11Y_LABEL);
    case Direction::kLeft:
      return l10n_util ::GetStringUTF16(
          IDS_INPUT_OVERLAY_JOYSTICK_DIRECTION_LEFT_A11Y_LABEL);
    case Direction::kDown:
      return l10n_util ::GetStringUTF16(
          IDS_INPUT_OVERLAY_JOYSTICK_DIRECTION_DOWN_A11Y_LABEL);
    case Direction::kRight:
      return l10n_util ::GetStringUTF16(
          IDS_INPUT_OVERLAY_JOYSTICK_DIRECTION_RIGHT_A11Y_LABEL);
    default:
      NOTREACHED();
  }
}

}  // namespace

EditLabel::EditLabel(DisplayOverlayController* controller,
                     Action* action,
                     bool for_editing_list,
                     size_t index)
    : views::LabelButton(),
      controller_(controller),
      action_(action),
      for_editing_list_(for_editing_list),
      direction_index_(static_cast<Direction>(index)) {
  Init();
}

EditLabel::~EditLabel() = default;

void EditLabel::OnActionInputBindingUpdated() {
  SetLabelContent();
}

bool EditLabel::IsInputUnbound() {
  return GetText().compare(kUnknownBind) == 0 || GetText().empty();
}

void EditLabel::RemoveNewState() {
  SetLabelContent();
}

void EditLabel::PerformPulseAnimation(int pulse_count) {
  // Destroy the pulse layer if it pulses after `kPulseTimes` times.
  if (pulse_count >= kPulseTimes) {
    pulse_layer_.reset();
    return;
  }

  auto* widget = GetWidget();
  DCHECK(widget);

  // Initiate pulse layer if it starts to pulse for the first time.
  if (pulse_count == 0) {
    pulse_layer_ = std::make_unique<ui::LayerSolidColor>();
    widget->GetLayer()->Add(pulse_layer_.get());
    pulse_layer_->SetColor(
        SkColor4f::FromColor(widget->GetColorProvider()->GetColor(
            cros_tokens::kCrosSysHighlightText)));
  }

  DCHECK(pulse_layer_);

  // Initial bounds in its widget coordinate.
  auto view_bounds = ConvertRectToWidget(gfx::Rect(size()));

  // Set initial properties.
  pulse_layer_->SetBounds(view_bounds);
  pulse_layer_->SetOpacity(1.0f);
  pulse_layer_->SetRoundedCornerRadius(gfx::RoundedCornersF(kCornerRadius));

  // Animate from a square to a circle with larger target bounds and to a
  // smaller opacity.
  view_bounds.Outset(kPulseExtraHalfSize);

  views::AnimationBuilder()
      .SetPreemptionStrategy(
          ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET)
      .OnEnded(base::BindOnce(&EditLabel::PerformPulseAnimation,
                              base::Unretained(this), pulse_count + 1))
      .Once()
      .SetDuration(kPulseDuration)
      .SetBounds(pulse_layer_.get(), view_bounds,
                 gfx::Tween::ACCEL_0_40_DECEL_100)
      .SetOpacity(pulse_layer_.get(), /*opacity=*/0.0f,
                  gfx::Tween::ACCEL_0_80_DECEL_80)
      .SetRoundedCorners(
          pulse_layer_.get(),
          gfx::RoundedCornersF(kPulseExtraHalfSize + kLabelSize / 2.0f),
          gfx::Tween::ACCEL_0_40_DECEL_100);
}

void EditLabel::Init() {
  SetHorizontalAlignment(gfx::ALIGN_CENTER);
  SetPreferredSize(gfx::Size(kLabelSize, kLabelSize));
  SetFocusBehavior(FocusBehavior::ALWAYS);
  SetInstallFocusRingOnFocus(false);
  SetRequestFocusOnPress(true);
  SetAnimateOnStateChange(false);
  SetHotTracked(false);
  SetShowInkDropWhenHotTracked(false);
  SetHasInkDropActionOnClick(false);
  ash::bubble_utils::ApplyStyle(label(), ash::TypographyToken::kCrosHeadline1,
                                cros_tokens::kCrosSysOnPrimaryContainer);
  SetLabelContent();
}

void EditLabel::SetLabelContent() {
  DCHECK(!action_->IsDeleted());
  const auto& keys = action_->GetCurrentDisplayedInput().keys();
  DCHECK(size_t(direction_index_) < keys.size());
  std::u16string output_string = GetDisplayText(keys[size_t(direction_index_)]);
  if (action_->is_new() && output_string == kUnknownBind) {
    output_string = u"";
  }

  // Clear icon if it is a valid key for new action.
  SetImageModel(views::Button::STATE_NORMAL,
                output_string.empty()
                    ? ui::ImageModel::FromVectorIcon(
                          ash::kGameControlsEditPenIcon, kPenIconColor)
                    : ui::ImageModel());
  // Set text label by `output_string` even it is empty to clear the text label.
  SetTextLabel(output_string);
}

void EditLabel::SetTextLabel(const std::u16string& text) {
  SetText(text);
  UpdateAccessibleName();

  SetBackground(views::CreateRoundedRectBackground(
      text == kUnknownBind && !action_->is_new()
          ? cros_tokens::kCrosSysErrorHighlight
          : cros_tokens::kCrosSysHighlightShape,
      kCornerRadius));
  if (HasFocus()) {
    SetToFocused();
  } else {
    SetToDefault();
  }
}

void EditLabel::SetNameTagState(bool is_error,
                                const std::u16string& error_tooltip) {
  DCHECK(parent());
  auto* parent_view = views::AsViewClass<EditLabels>(parent());
  parent_view->SetNameTagState(is_error, error_tooltip);
}

void EditLabel::UpdateAccessibleName() {
  const std::u16string a11y_name(
      GetDisplayTextAccessibleName(std::u16string(label()->GetText())));
  const bool unassigned =
      a11y_name.empty() || a11y_name.compare(kUnknownBind) == 0;
  const std::u16string suffix_instruction = l10n_util::GetStringUTF16(
      unassigned
          ? IDS_INPUT_OVERLAY_EDIT_LABEL_KEYBOARD_ASSIGN_INSTRUCTION_A11Y_LABEL
          : IDS_INPUT_OVERLAY_EDIT_LABEL_KEYBOARD_REASSIGN_INSTRUCTION_A11Y_LABEL);

  switch (action_->GetType()) {
    case ActionType::TAP:
      if (unassigned) {
        GetViewAccessibility().SetName(l10n_util::GetStringFUTF16(
            IDS_INPUT_OVERLAY_EDIT_LABEL_BUTTON_KEYBOARD_UNASSIGNED_A11Y_TPL,
            suffix_instruction));
      } else {
        GetViewAccessibility().SetName(l10n_util::GetStringFUTF16(
            IDS_INPUT_OVERLAY_EDIT_LABEL_BUTTON_KEYBOARD_A11Y_TPL, a11y_name,
            suffix_instruction));
      }
      break;
    case ActionType::MOVE: {
      const std::u16string direction =
          GetAccessibleNameSuffixForDirection(direction_index_);
      if (unassigned) {
        GetViewAccessibility().SetName(l10n_util::GetStringFUTF16(
            IDS_INPUT_OVERLAY_EDIT_LABEL_JOYSTICK_KEYBOARD_UNASSIGNED_A11Y_TPL,
            direction, suffix_instruction));
      } else {
        GetViewAccessibility().SetName(l10n_util::GetStringFUTF16(
            IDS_INPUT_OVERLAY_EDIT_LABEL_JOYSTICK_KEYBOARD_A11Y_TPL, a11y_name,
            direction, suffix_instruction));
      }
      break;
    }
    default:
      NOTREACHED();
  }
}

void EditLabel::ChangeFocusToNextLabel() {
  DCHECK(parent());
  if (auto* parent_view = views::AsViewClass<EditLabels>(parent())) {
    parent_view->FocusLabel();
  }
}

void EditLabel::SetToDefault() {
  SetEnabledTextColors(IsInputUnbound() && !action_->is_new()
                           ? cros_tokens::kCrosSysError
                           : cros_tokens::kCrosSysOnPrimaryContainer);
  SetBorder(nullptr);
}

void EditLabel::SetToFocused() {
  SetEnabledTextColors(IsInputUnbound() && !action_->is_new()
                           ? cros_tokens::kCrosSysError
                           : cros_tokens::kCrosSysOnSurface);
  SetBorder(views::CreateRoundedRectBorder(
      /*thickness=*/2, kCornerRadius, cros_tokens::kCrosSysPrimary));
}

void EditLabel::OnFocus() {
  LabelButton::OnFocus();
  if (action_->is_new()) {
    // Hide the pen icon once the label is focused to edit.
    SetImageModel(views::Button::STATE_NORMAL, ui::ImageModel());
  }
  SetToFocused();
  if (for_editing_list_) {
    controller_->AddActionHighlightWidget(action_);
    RecordEditingListFunctionTriggered(controller_->GetPackageName(),
                                       EditingListFunction::kEditLabelFocused);
  } else {
    RecordButtonOptionsMenuFunctionTriggered(
        controller_->GetPackageName(),
        ButtonOptionsMenuFunction::kEditLabelFocused);
  }
}

void EditLabel::OnBlur() {
  LabelButton::OnBlur();
  // `OnBlur()` will be called before removing this view. This view is removed
  // after changing action type and previous `action_` may be invalid. If
  // `action_` is deleted, there is no need to update the content. This view
  // will be removed after this.
  if (!controller_->IsActiveAction(action_)) {
    return;
  }

  if (action_->is_new() && GetText().empty()) {
    SetImageModel(views::Button::STATE_NORMAL,
                  ui::ImageModel::FromVectorIcon(ash::kGameControlsEditPenIcon,
                                                 kPenIconColor));
  }
  SetToDefault();
  // Reset the error state if an reserved key was pressed.
  SetNameTagState(/*is_error=*/false, u"");

  if (!for_editing_list_) {
    return;
  }

  if (auto* list_item = controller_->GetEditingListItemForAction(action_);
      !list_item || !list_item->IsMouseHovered()) {
    controller_->HideActionHighlightWidgetForAction(action_);
  }
}

bool EditLabel::OnKeyPressed(const ui::KeyEvent& event) {
  auto code = event.code();
  std::u16string new_bind = GetDisplayText(code);
  // Don't show error when the same key is pressed.
  if (GetText() == new_bind) {
    SetNameTagState(/*is_error=*/false, u"");
    ChangeFocusToNextLabel();
    return true;
  }

  // Show error when the reserved keys and modifier keys are pressed.
  if ((!action_->support_modifier_key() &&
       ModifierDomCodeToEventFlag(code) != ui::EF_NONE) ||
      IsReservedDomCode(code)) {
    SetNameTagState(
        /*is_error=*/true,
        l10n_util::GetStringUTF16(IDS_INPUT_OVERLAY_EDIT_RESERVED_KEYS));
    ash::Shell::Get()
        ->accessibility_controller()
        ->TriggerAccessibilityAlertWithMessage(
            l10n_util::GetStringUTF8(IDS_INPUT_OVERLAY_EDIT_RESERVED_KEYS));
    return false;
  }

  SetTextLabel(new_bind);
  const std::string& package_name = controller_->GetPackageName();
  if (for_editing_list_) {
    RecordEditingListFunctionTriggered(package_name,
                                       EditingListFunction::kKeyAssigned);
  } else {
    RecordButtonOptionsMenuFunctionTriggered(
        package_name, ButtonOptionsMenuFunction::kKeyAssigned);
  }

  std::unique_ptr<InputElement> input;
  switch (action_->GetType()) {
    case ActionType::TAP:
      input = InputElement::CreateActionTapKeyElement(code);
      break;
    case ActionType::MOVE: {
      const auto& input_binding = action_->GetCurrentDisplayedInput();
      auto new_keys = input_binding.keys();
      // If there is duplicated key in its own action, unset the key.
      const int unassigned_index = input_binding.GetIndexOfKey(code);
      if (unassigned_index != -1 &&
          size_t(unassigned_index) != size_t(direction_index_)) {
        new_keys[unassigned_index] = ui::DomCode::NONE;
      }
      // Set the new key.
      new_keys[size_t(direction_index_)] = code;
      input = InputElement::CreateActionMoveKeyElement(new_keys);
      break;
    }
    default:
      NOTREACHED();
  }
  DCHECK(input);
  controller_->OnInputBindingChange(action_, std::move(input));
  ChangeFocusToNextLabel();
  return true;
}

BEGIN_METADATA(EditLabel)
END_METADATA

}  // namespace arc::input_overlay
