// Copyright 2021 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/action_label.h"

#include <set>

#include "ash/resources/vector_icons/vector_icons.h"
#include "ash/style/style_util.h"
#include "base/compiler_specific.h"
#include "base/notimplemented.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ash/arc/input_overlay/actions/action.h"
#include "chrome/browser/ash/arc/input_overlay/constants.h"
#include "chrome/browser/ash/arc/input_overlay/ui/action_view.h"
#include "chrome/browser/ash/arc/input_overlay/ui/touch_point.h"
#include "chrome/browser/ash/arc/input_overlay/ui/ui_utils.h"
#include "chrome/browser/ash/arc/input_overlay/util.h"
#include "chromeos/strings/grit/chromeos_strings.h"
#include "third_party/skia/include/core/SkColor.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/color/color_id.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/gfx/color_palette.h"
#include "ui/gfx/color_utils.h"
#include "ui/gfx/geometry/point.h"
#include "ui/gfx/paint_vector_icon.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/background.h"
#include "ui/views/controls/highlight_path_generator.h"
#include "ui/views/view_utils.h"

namespace arc::input_overlay {
namespace {

constexpr char kFontStyle[] = "Google Sans";
constexpr int kIconSize = 20;

// About colors.
constexpr SkColor kBackgroundColorDefault = SK_ColorWHITE;
constexpr SkColor kTextColorDefault = gfx::kGoogleGrey900;
constexpr SkColor kEditedUnboundBgColor = gfx::kGoogleRed300;

// UI specs.
constexpr gfx::Size kLabelSize(22, 22);
constexpr int kCornerRadius = 4;
constexpr int kFontSize = 14;
constexpr int kSideInset = 4;
// For ActionMove.
constexpr int kCrossPadding =
    9;  // 4 + 4(kCrossOutsideStrokeThickness) + 1(kCrossInsideStrokeThickness)

// TODO(b/241966781): remove this and replace it with image asset.
constexpr char16_t kMouseCursorLock[] = u"mouse cursor lock (esc)";

bool IsLeft(TapLabelPosition position) {
  return position == TapLabelPosition::kTopLeft ||
         position == TapLabelPosition::kBottomLeft;
}

bool IsRight(TapLabelPosition position) {
  return !IsLeft(position) && position != TapLabelPosition::kNone;
}

bool IsTop(TapLabelPosition position) {
  return position == TapLabelPosition::kTopLeft ||
         position == TapLabelPosition::kTopRight;
}

bool IsBottom(TapLabelPosition position) {
  return !IsTop(position) && position != TapLabelPosition::kNone;
}

class ActionLabelTap : public ActionLabel {
  METADATA_HEADER(ActionLabelTap, ActionLabel)

 public:
  ActionLabelTap(MouseAction mouse_action, TapLabelPosition label_position)
      : ActionLabel(mouse_action), label_position_(label_position) {
    DCHECK(mouse_action == MouseAction::PRIMARY_CLICK ||
           mouse_action == MouseAction::SECONDARY_CLICK);
  }

  ActionLabelTap(const std::u16string& text, TapLabelPosition label_position)
      : ActionLabel(text), label_position_(label_position) {}

  ~ActionLabelTap() override = default;

  void UpdateBounds() override {
    SetBorder(views::CreateEmptyBorder(gfx::Insets::VH(0, kSideInset)));
    const auto label_size = CalculatePreferredSize({});
    SetSize(label_size);
    // Label position is not set yet.
    if (label_position_ == TapLabelPosition::kNone) {
      return;
    }

    auto* action_view = GetParent();

    switch (label_position_) {
      case TapLabelPosition::kBottomLeft:
        SetPosition(
            gfx::Point(0, touch_point_size_.height() + kOffsetToTouchPoint));
        action_view->SetTouchPointCenter(
            gfx::Point(label_size.width() + touch_point_size_.width() / 2 +
                           kOffsetToTouchPoint,
                       touch_point_size_.height() / 2));
        break;
      case TapLabelPosition::kBottomRight:
        SetPosition(
            gfx::Point(touch_point_size_.width() + kOffsetToTouchPoint,
                       touch_point_size_.height() + kOffsetToTouchPoint));
        action_view->SetTouchPointCenter(gfx::Point(
            touch_point_size_.width() / 2, touch_point_size_.height() / 2));
        break;
      case TapLabelPosition::kTopLeft:
        SetPosition(gfx::Point());
        action_view->SetTouchPointCenter(
            gfx::Point(label_size.width() + kOffsetToTouchPoint +
                           touch_point_size_.width() / 2,
                       label_size.height() + kOffsetToTouchPoint +
                           touch_point_size_.height() / 2));
        break;
      case TapLabelPosition::kTopRight:
        SetPosition(
            gfx::Point(touch_point_size_.width() + kOffsetToTouchPoint, 0));
        action_view->SetTouchPointCenter(gfx::Point(
            touch_point_size_.width() / 2, label_size.height() +
                                               kOffsetToTouchPoint +
                                               touch_point_size_.height() / 2));
        break;
      default:
        NOTREACHED();
    }
  }

  void UpdateLabelPositionType(TapLabelPosition label_position) override {
    if (label_position_ == label_position) {
      return;
    }

    parent()->SetPosition(
        CalculateParentPositionWithFixedTouchPoint(label_position));
    label_position_ = label_position;
    UpdateBounds();
  }

 private:
  gfx::Point CalculateParentPositionWithFixedTouchPoint(
      TapLabelPosition label_position) {
    DCHECK_NE(label_position_, label_position);
    DCHECK_NE(label_position, TapLabelPosition::kNone);
    auto* action_view = GetParent();
    auto fix_pos = action_view->GetTouchCenterInWindow();
    fix_pos.Offset(-touch_point_size_.width() / 2,
                   -touch_point_size_.height() / 2);
    fix_pos.SetToMax(gfx::Point());
    auto new_pos = action_view->origin();
    const auto& label_size = size();

    if (IsLeft(label_position_) && IsRight(label_position)) {
      new_pos.set_x(fix_pos.x());
    } else if (!IsLeft(label_position_) && IsLeft(label_position)) {
      new_pos.set_x(std::max(
          0, fix_pos.x() - (label_size.width() + kOffsetToTouchPoint)));
    }

    if (IsTop(label_position_) && IsBottom(label_position)) {
      new_pos.set_y(fix_pos.y());
    } else if (!IsTop(label_position_) && IsTop(label_position)) {
      new_pos.set_y(std::max(
          0, fix_pos.y() - (label_size.height() + kOffsetToTouchPoint)));
    }

    return new_pos;
  }

  TapLabelPosition label_position_ = TapLabelPosition::kNone;
};

BEGIN_METADATA(ActionLabelTap)
END_METADATA

class ActionLabelMove : public ActionLabel {
  METADATA_HEADER(ActionLabelMove, ActionLabel)

 public:
  ActionLabelMove(const std::u16string& text, size_t index)
      : ActionLabel(text, index) {}
  explicit ActionLabelMove(MouseAction mouse) : ActionLabel(mouse) {}

  ~ActionLabelMove() override = default;

  void UpdateBounds() override {
    SetBorder(views::CreateEmptyBorder(gfx::Insets::VH(0, 0)));
    auto label_size = CalculatePreferredSize({});
    SetSize(label_size);
    // TODO(b/241966781): Mouse is not supported yet.
    DCHECK_EQ(mouse_action_, MouseAction::NONE);
    auto center = touch_point_size_.width() / 2;
    int offset_to_center =
        touch_point_size_.width() / 2 - kCrossPadding - label_size.height() / 2;
    int x = center + UNSAFE_TODO(kDirection[index_])[0] * offset_to_center -
            label_size.width() / 2;
    int y = center + UNSAFE_TODO(kDirection[index_])[1] * offset_to_center -
            label_size.height() / 2;
    SetPosition(gfx::Point(x, y));
    GetParent()->SetTouchPointCenter(gfx::Point(center, center));
  }

  void UpdateLabelPositionType(TapLabelPosition label_position) override {}
};

BEGIN_METADATA(ActionLabelMove)
END_METADATA

}  // namespace

std::vector<raw_ptr<ActionLabel, VectorExperimental>> ActionLabel::Show(
    views::View* parent,
    ActionType action_type,
    const InputElement& input_element,
    TapLabelPosition label_position) {
  std::vector<raw_ptr<ActionLabel, VectorExperimental>> labels;
  gfx::Size touch_point_size;

  switch (action_type) {
    case ActionType::TAP:
      if (IsKeyboardBound(input_element)) {
        DCHECK_EQ(1u, input_element.keys().size());
        labels.emplace_back(
            parent->AddChildView(std::make_unique<ActionLabelTap>(
                GetDisplayText(input_element.keys()[0]), label_position)));
      } else if (IsMouseBound(input_element)) {
        labels.emplace_back(
            parent->AddChildView(std::make_unique<ActionLabelTap>(
                input_element.mouse_action(), label_position)));
      } else {
        labels.emplace_back(parent->AddChildView(
            std::make_unique<ActionLabelTap>(kUnknownBind, label_position)));
      }
      touch_point_size = TouchPoint::GetSize(ActionType::TAP);
      break;

    case ActionType::MOVE:
      if (IsKeyboardBound(input_element)) {
        const auto& keys = input_element.keys();
        for (size_t i = 0; i < kActionMoveKeysSize; i++) {
          labels.emplace_back(parent->AddChildView(
              std::make_unique<ActionLabelMove>(GetDisplayText(keys[i]), i)));
        }
      } else if (IsMouseBound(input_element)) {
        labels.emplace_back(parent->AddChildView(
            std::make_unique<ActionLabelMove>(kMouseCursorLock, 0)));
        NOTIMPLEMENTED();
      } else {
        for (size_t i = 0; i < kActionMoveKeysSize; i++) {
          labels.emplace_back(parent->AddChildView(
              std::make_unique<ActionLabelMove>(kUnknownBind, i)));
        }
      }
      touch_point_size = TouchPoint::GetSize(ActionType::MOVE);
      break;

    default:
      NOTREACHED();
  }

  for (arc::input_overlay::ActionLabel* label : labels) {
    label->Init();
    label->set_touch_point_size(touch_point_size);
  }

  return labels;
}

void ActionLabel::Init() {
  SetHorizontalAlignment(gfx::ALIGN_CENTER);
  SetBorder(views::CreateEmptyBorder(gfx::Insets::VH(0, kSideInset)));
  GetViewAccessibility().SetRole(ax::mojom::Role::kLabelText);
  GetViewAccessibility().SetName(CalculateAccessibleName());
}

ActionLabel::ActionLabel(MouseAction mouse_action)
    : mouse_action_(mouse_action) {}

ActionLabel::ActionLabel(const std::u16string& text, size_t index)
    : views::LabelButton(base::BindRepeating(&ActionLabel::OnButtonPressed,
                                             base::Unretained(this)),
                         text),
      index_(index) {
  DCHECK(index_ >= 0 && index_ < kActionMoveKeysSize);
}

ActionLabel::~ActionLabel() = default;

void ActionLabel::SetTextActionLabel(const std::u16string& text) {
  label()->SetText(text);
  GetViewAccessibility().SetName(CalculateAccessibleName());

  if (text == kUnknownBind && !GetParent()->action()->is_new()) {
    SetToEditUnbindInput();
  } else {
    SetToEditDefault();
  }
}

void ActionLabel::SetImageActionLabel(MouseAction mouse_action) {
  set_mouse_action(mouse_action);
  GetViewAccessibility().SetName(CalculateAccessibleName());
}

void ActionLabel::SetDisplayMode(DisplayMode mode) {
  switch (mode) {
    case DisplayMode::kView:
      SetToViewMode();
      SetFocusBehavior(FocusBehavior::NEVER);
      break;
    case DisplayMode::kEdit:
      SetToEditDefault();
      SetFocusBehavior(FocusBehavior::ACCESSIBLE_ONLY);
      break;
    default:
      NOTREACHED();
  }
}

void ActionLabel::RemoveNewState() {
  SetBackgroundForEdit();
}

ActionView* ActionLabel::GetParent() {
  auto* view = views::AsViewClass<ActionView>(parent());
  DCHECK(view);
  return view;
}

gfx::Size ActionLabel::CalculatePreferredSize(
    const views::SizeBounds& available_size) const {
  auto size = LabelButton::CalculatePreferredSize(available_size);
  size.SetToMax(kLabelSize);
  return size;
}

void ActionLabel::ChildPreferredSizeChanged(View* child) {
  UpdateBounds();
  LabelButton::ChildPreferredSizeChanged(this);
}

void ActionLabel::OnButtonPressed() {
  GetParent()->ShowButtonOptionsMenu();
}

void ActionLabel::SetToViewMode() {
  display_mode_ = DisplayMode::kView;
  SetInstallFocusRingOnFocus(false);
  label()->SetFontList(gfx::FontList({kFontStyle}, gfx::Font::NORMAL, kFontSize,
                                     gfx::Font::Weight::BOLD));
  SetEnabledTextColors(kTextColorDefault);

  if (mouse_action_ != MouseAction::NONE) {
    if (mouse_action_ == MouseAction::PRIMARY_CLICK) {
      auto left_click_icon = ui::ImageModel::FromVectorIcon(
          ash::kMouseLeftClickViewIcon, gfx::kPlaceholderColor, kIconSize);
      SetImageModel(views::Button::STATE_NORMAL, left_click_icon);
    } else {
      auto right_click_icon = ui::ImageModel::FromVectorIcon(
          ash::kMouseRightClickViewIcon, gfx::kPlaceholderColor, kIconSize);
      SetImageModel(views::Button::STATE_NORMAL, right_click_icon);
    }
  }

  SetBackground(views::CreateRoundedRectBackground(kBackgroundColorDefault,
                                                   kCornerRadius));
  SetPreferredSize(CalculatePreferredSize({}));
}

void ActionLabel::SetToEditDefault() {
  label()->SetFontList(gfx::FontList({kFontStyle}, gfx::Font::NORMAL, kFontSize,
                                     gfx::Font::Weight::BOLD));
  SetEnabledTextColors(kTextColorDefault);
  SetBackgroundForEdit();
}

void ActionLabel::SetToEditUnbindInput() {
  SetPreferredSize(CalculatePreferredSize({}));
  SetBackground(
      views::CreateRoundedRectBackground(kEditedUnboundBgColor, kCornerRadius));
}

void ActionLabel::SetBackgroundForEdit() {
  SetBackground(views::CreateRoundedRectBackground(
      IsInputUnbound() && !(GetParent()->action()->is_new())
          ? kEditedUnboundBgColor
          : kBackgroundColorDefault,
      kCornerRadius));
}

bool ActionLabel::IsInputUnbound() {
  return GetText().compare(kUnknownBind) == 0;
}

std::u16string ActionLabel::CalculateAccessibleName() {
  if (mouse_action_ != MouseAction::NONE) {
    // TODO(accessibility): The accessible name is expected to be end-user
    // consumable.
    return base::UTF8ToUTF16(GetClassName());
  }

  return l10n_util::GetStringUTF16(IDS_INPUT_OVERLAY_KEYMAPPING_KEY)
      .append(u" ")
      .append(GetDisplayTextAccessibleName(std::u16string(label()->GetText())));
}

BEGIN_METADATA(ActionLabel)
END_METADATA

}  // namespace arc::input_overlay
