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

#include "content/browser/renderer_host/input/touch_selection_controller_client_aura.h"

#include <set>

#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/observer_list.h"
#include "content/browser/renderer_host/render_widget_host_delegate.h"
#include "content/browser/renderer_host/render_widget_host_impl.h"
#include "content/browser/renderer_host/render_widget_host_view_aura.h"
#include "content/public/browser/context_menu_params.h"
#include "content/public/browser/render_view_host.h"
#include "third_party/blink/public/mojom/input/input_handler.mojom-shared.h"
#include "ui/aura/client/cursor_client.h"
#include "ui/aura/env.h"
#include "ui/aura/window.h"
#include "ui/base/clipboard/clipboard.h"
#include "ui/base/data_transfer_policy/data_transfer_endpoint.h"
#include "ui/base/mojom/menu_source_type.mojom.h"
#include "ui/base/ui_base_features.h"
#include "ui/events/event_observer.h"
#include "ui/gfx/geometry/point_conversions.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/geometry/size_conversions.h"
#include "ui/strings/grit/ui_strings.h"
#include "ui/touch_selection/touch_editing_controller.h"
#include "ui/touch_selection/touch_handle_drawable_aura.h"
#include "ui/touch_selection/touch_selection_magnifier_aura.h"
#include "ui/touch_selection/touch_selection_menu_runner.h"

namespace content {
namespace {

// Delay before showing the quick menu, in milliseconds.
constexpr int kQuickMenuDelayInMs = 100;

}  // namespace

// An aura::Env event observer that hides touch selection ui on mouse and
// keyboard events, including those targeting windows outside the client.
class TouchSelectionControllerClientAura::EnvEventObserver
    : public ui::EventObserver {
 public:
  EnvEventObserver(ui::TouchSelectionController* selection_controller,
                   aura::Window* window)
      : selection_controller_(selection_controller), window_(window) {
    // Observe certain event types sent to any event target, to hide this ui.
    aura::Env* env = aura::Env::GetInstance();
    std::set<ui::EventType> types = {
        ui::EventType::kMousePressed, ui::EventType::kMouseMoved,
        ui::EventType::kKeyPressed, ui::EventType::kMousewheel};
    env->AddEventObserver(this, env, types);
  }

  EnvEventObserver(const EnvEventObserver&) = delete;
  EnvEventObserver& operator=(const EnvEventObserver&) = delete;

  ~EnvEventObserver() override {
    aura::Env::GetInstance()->RemoveEventObserver(this);
  }

 private:
  // ui::EventObserver:
  void OnEvent(const ui::Event& event) override {
    // TODO(crbug.com/546298678): CHECK-exclusion: Convert to a CHECK once we
    // are confident it won't be triggered.
    DCHECK_NE(ui::TouchSelectionController::ActiveStatus::kInactive,
              selection_controller_->active_status());

    if (event.IsMouseEvent()) {
      // Check IsMouseEventsEnabled, except on Mus, where it's disabled on touch
      // events in this client, but not re-enabled on mouse events elsewhere.
      auto* cursor = aura::client::GetCursorClient(window_->GetRootWindow());
      if (cursor && !cursor->IsMouseEventsEnabled())
        return;

      // Windows OS unhandled WM_POINTER* may be redispatched as WM_MOUSE*.
      // Avoid adjusting the handles on synthesized events or events generated
      // from touch as this can clear an active selection generated by the pen.
      if ((event.flags() & (ui::EF_IS_SYNTHESIZED | ui::EF_FROM_TOUCH)) ||
          event.AsMouseEvent()->pointer_details().pointer_type ==
              ui::EventPointerType::kPen) {
        return;
      }
    }

    selection_controller_->HideAndDisallowShowingAutomatically();
  }

  raw_ptr<ui::TouchSelectionController> selection_controller_;
  raw_ptr<aura::Window> window_;
};

TouchSelectionControllerClientAura::TouchSelectionControllerClientAura(
    RenderWidgetHostViewAura* rwhva)
    : rwhva_(rwhva),
      internal_client_(rwhva),
      active_client_(&internal_client_),
      active_menu_client_(this),
      quick_menu_timer_(FROM_HERE,
                        base::Milliseconds(kQuickMenuDelayInMs),
                        base::BindRepeating(
                            &TouchSelectionControllerClientAura::ShowQuickMenu,
                            base::Unretained(this))),
      quick_menu_requested_(false),
      touch_down_(false),
      scroll_in_progress_(false),
      handle_drag_in_progress_(false),
      show_quick_menu_immediately_for_test_(false) {
  CHECK(rwhva_, base::NotFatalUntil::M152);
}

TouchSelectionControllerClientAura::~TouchSelectionControllerClientAura() {
  for (auto& observer : observers_)
    observer.OnManagerWillDestroy(this);
}

void TouchSelectionControllerClientAura::OnWindowMoved() {
  UpdateQuickMenu();
}

void TouchSelectionControllerClientAura::OnTouchDown() {
  touch_down_ = true;
  UpdateQuickMenu();
}

void TouchSelectionControllerClientAura::OnTouchUp() {
  touch_down_ = false;
  UpdateQuickMenu();
}

void TouchSelectionControllerClientAura::OnScrollStarted() {
  scroll_in_progress_ = true;
  rwhva_->selection_controller()->SetTemporarilyHidden(true);
  UpdateQuickMenu();
}

void TouchSelectionControllerClientAura::OnScrollCompleted() {
  scroll_in_progress_ = false;
  active_client_->DidScroll();
  rwhva_->selection_controller()->SetTemporarilyHidden(false);
  UpdateQuickMenu();
}

bool TouchSelectionControllerClientAura::HandleContextMenu(
    const ContextMenuParams& params,
    bool can_paste) {
  if (params.is_editable && params.selection_text.empty() &&
      (params.source_type == ui::mojom::MenuSourceType::kLongPress ||
       params.source_type == ui::mojom::MenuSourceType::kLongTap ||
       (::features::IsTouchTextEditingRedesignEnabled() &&
        params.source_type == ui::mojom::MenuSourceType::kTouch))) {
    if (IsQuickMenuAvailable(can_paste)) {
      if (::features::IsTouchTextEditingRedesignEnabled() &&
          params.source_type == ui::mojom::MenuSourceType::kTouch) {
        if (rwhva_->selection_controller()->active_status() ==
            ui::TouchSelectionController::ActiveStatus::kInactive) {
          rwhva_->selection_controller()->OnSelectionBoundsChanged(
              manager_selection_start_, manager_selection_end_);
        }
        quick_menu_requested_ = !quick_menu_requested_;
      } else {
        quick_menu_requested_ = true;
      }
      UpdateQuickMenu();
      return true;
    } else {
      if (::features::IsTouchTextEditingRedesignEnabled() &&
          params.source_type == ui::mojom::MenuSourceType::kTouch) {
        rwhva_->selection_controller()->HideAndDisallowShowingAutomatically();
        quick_menu_requested_ = false;
        UpdateQuickMenu();
        return true;
      }
    }
  }

  const bool from_touch =
      params.source_type == ui::mojom::MenuSourceType::kLongPress ||
      params.source_type == ui::mojom::MenuSourceType::kLongTap ||
      params.source_type == ui::mojom::MenuSourceType::kTouch;
  if (from_touch && !params.selection_text.empty()) {
    return true;
  }

  rwhva_->selection_controller()->HideAndDisallowShowingAutomatically();
  return false;
}

void TouchSelectionControllerClientAura::DidStopFlinging() {
  OnScrollCompleted();
}

void TouchSelectionControllerClientAura::OnSwipeToMoveCursorBegin() {
  GetTouchSelectionController()->OnSwipeToMoveCursorBegin();
  OnSelectionEvent(ui::INSERTION_HANDLE_DRAG_STARTED);
}

void TouchSelectionControllerClientAura::OnSwipeToMoveCursorEnd() {
  GetTouchSelectionController()->OnSwipeToMoveCursorEnd();
  OnSelectionEvent(ui::INSERTION_HANDLE_DRAG_STOPPED);
}

void TouchSelectionControllerClientAura::OnClientHitTestRegionUpdated(
    ui::TouchSelectionControllerClient* client) {
  if (client != active_client_ || !GetTouchSelectionController() ||
      GetTouchSelectionController()->active_status() ==
          ui::TouchSelectionController::ActiveStatus::kInactive) {
    return;
  }

  active_client_->DidScroll();
}

void TouchSelectionControllerClientAura::UpdateClientSelectionBounds(
    const gfx::SelectionBound& start,
    const gfx::SelectionBound& end) {
  UpdateClientSelectionBounds(start, end, &internal_client_, this);
}

void TouchSelectionControllerClientAura::UpdateClientSelectionBounds(
    const gfx::SelectionBound& start,
    const gfx::SelectionBound& end,
    ui::TouchSelectionControllerClient* client,
    ui::TouchSelectionMenuClient* menu_client) {
  if (client != active_client_ && (!start.HasHandle() || !start.visible()) &&
      (!end.HasHandle() || !end.visible()) &&
      (manager_selection_start_.HasHandle() ||
       manager_selection_end_.HasHandle())) {
    return;
  }

  active_client_ = client;
  active_menu_client_ = menu_client;
  manager_selection_start_ = start;
  manager_selection_end_ = end;
  // Notify TouchSelectionController if anything should change here. Only
  // update if the client is different and not making a change to empty, or
  // is the same client.
  GetTouchSelectionController()->OnSelectionBoundsChanged(start, end);
}

void TouchSelectionControllerClientAura::InvalidateClient(
    ui::TouchSelectionControllerClient* client) {
  CHECK(client != &internal_client_, base::NotFatalUntil::M152);
  if (client == active_client_) {
    GetTouchSelectionController()->HideAndDisallowShowingAutomatically();
    active_client_ = &internal_client_;
    active_menu_client_ = this;
  }
}

ui::TouchSelectionController*
TouchSelectionControllerClientAura::GetTouchSelectionController() {
  return rwhva_->selection_controller();
}

void TouchSelectionControllerClientAura::AddObserver(
    TouchSelectionControllerClientManager::Observer* observer) {
  observers_.AddObserver(observer);
}

void TouchSelectionControllerClientAura::RemoveObserver(
    TouchSelectionControllerClientManager::Observer* observer) {
  observers_.RemoveObserver(observer);
}

bool TouchSelectionControllerClientAura::IsQuickMenuAvailable(
    bool can_paste) const {
  return ui::TouchSelectionMenuRunner::GetInstance() &&
         ui::TouchSelectionMenuRunner::GetInstance()->IsMenuAvailable(
             active_menu_client_, can_paste);
}

void TouchSelectionControllerClientAura::ShowQuickMenu() {
  if (!ui::TouchSelectionMenuRunner::GetInstance()) {
    return;
  }

  ui::DataTransferEndpoint data_dst = ui::DataTransferEndpoint(
      ui::EndpointType::kDefault, {.notify_if_restricted = false});
  ui::Clipboard::GetForCurrentThread()->GetAllAvailableFormats(
      ui::ClipboardBuffer::kCopyPaste, data_dst,
      base::BindOnce(
          [](base::WeakPtr<TouchSelectionControllerClientAura> weak_this,
             base::flat_set<ui::ClipboardFormatType> formats) {
            if (!weak_this) {
              return;
            }
            bool can_paste =
                formats.contains(ui::ClipboardFormatType::PlainTextType());

            if (!weak_this->ShouldShowQuickMenu(can_paste)) {
              return;
            }

            // Don't show the menu if the selection bounds are zero, since this
            // usually means that touch selection is not active or that there
            // is no cursor or selection.
            if (::features::IsTouchTextEditingRedesignEnabled() &&
                weak_this->rwhva_->selection_controller()
                        ->GetRectBetweenBounds() == gfx::RectF()) {
              return;
            }

            gfx::Rect anchor_rect =
                gfx::ToRoundedRect(weak_this->rwhva_->selection_controller()
                                       ->GetVisibleRectBetweenBounds());

            // Clip the anchor rect to the rwhva bounds and only show the menu
            // if there is at least some (possibly zero-area) overlap. We use
            // `InclusiveIntersect` rather than checking `IsEmpty` here, since
            // we might want to show the menu even if the anchor rect is empty
            // (e.g. zero-width caret).
            if (!anchor_rect.InclusiveIntersect(
                    weak_this->rwhva_->GetNativeView()->bounds())) {
              return;
            }

            gfx::SizeF max_handle_size =
                weak_this->rwhva_->selection_controller()
                    ->GetStartHandleRect()
                    .size();
            max_handle_size.SetToMax(weak_this->rwhva_->selection_controller()
                                         ->GetEndHandleRect()
                                         .size());

            ui::TouchSelectionMenuRunner::GetInstance()->OpenMenu(
                weak_this->active_menu_client_->GetWeakPtr(),
                weak_this->rwhva_->ConvertRectToScreen(anchor_rect),
                gfx::ToRoundedSize(max_handle_size),
                weak_this->rwhva_->GetNativeView()->GetToplevelWindow(),
                can_paste);
          },
          menu_request_weak_ptr_factory_.GetWeakPtr()));
}

void TouchSelectionControllerClientAura::UpdateQuickMenu() {
  bool menu_is_showing =
      ui::TouchSelectionMenuRunner::GetInstance() &&
      ui::TouchSelectionMenuRunner::GetInstance()->IsRunning();

  // Hide the quick menu if there is any. This should happen even if the menu
  // should be shown again, in order to update its location or content.
  if (menu_is_showing) {
    ui::TouchSelectionMenuRunner::GetInstance()->CloseMenu();
  } else {
    quick_menu_timer_.Stop();
  }

  // Invalidate any in-flight OpenMenu async clipboard callbacks.
  menu_request_weak_ptr_factory_.InvalidateWeakPtrs();

  if (ShouldShowQuickMenu(/*can_paste=*/true)) {
    if (show_quick_menu_immediately_for_test_) {
      ShowQuickMenu();
    } else {
      quick_menu_timer_.Reset();
    }
  }
}

void TouchSelectionControllerClientAura::ShowMagnifier() {
  if (!::features::IsTouchTextEditingRedesignEnabled()) {
    return;
  }

  aura::Window* context = rwhva_->GetNativeView();
  aura::Window* root_window = context->GetRootWindow();
  CHECK(root_window, base::NotFatalUntil::M152);

  if (!touch_selection_magnifier_) {
    touch_selection_magnifier_ =
        std::make_unique<ui::TouchSelectionMagnifierAura>();
  }

  CHECK_NE(GetTouchSelectionController()->active_status(),
           ui::TouchSelectionController::ActiveStatus::kInactive,
           base::NotFatalUntil::M152);
  const gfx::SelectionBound& focus_bound_in_context =
      GetTouchSelectionController()->GetFocusBound();

  // Convert focus bound to root window coordinates.
  gfx::Point focus_start = focus_bound_in_context.edge_start_rounded();
  gfx::Point focus_end = focus_bound_in_context.edge_end_rounded();
  aura::Window::ConvertPointToTarget(context, root_window, &focus_start);
  aura::Window::ConvertPointToTarget(context, root_window, &focus_end);
  touch_selection_magnifier_->ShowFocusBound(root_window->layer(), focus_start,
                                             focus_end);
}

void TouchSelectionControllerClientAura::HideMagnifier() {
  touch_selection_magnifier_ = nullptr;
}

bool TouchSelectionControllerClientAura::SupportsAnimation() const {
  // We don't pass this to the active client, since it is assumed it will have
  // the same behaviour as the Aura client.
  return false;
}

bool TouchSelectionControllerClientAura::InternalClient::SupportsAnimation()
    const {
  NOTREACHED();
}

void TouchSelectionControllerClientAura::SetNeedsAnimate() {
  NOTREACHED();
}

void TouchSelectionControllerClientAura::InternalClient::SetNeedsAnimate() {
  NOTREACHED();
}

void TouchSelectionControllerClientAura::MoveCaret(
    const gfx::PointF& position) {
  active_client_->MoveCaret(position);
}

void TouchSelectionControllerClientAura::InternalClient::MoveCaret(
    const gfx::PointF& position) {
  RenderWidgetHostDelegate* host_delegate = rwhva_->host()->delegate();
  if (host_delegate)
    host_delegate->MoveCaret(gfx::ToRoundedPoint(position));
}

void TouchSelectionControllerClientAura::MoveRangeSelectionExtent(
    const gfx::PointF& extent) {
  active_client_->MoveRangeSelectionExtent(extent);
}

void TouchSelectionControllerClientAura::InternalClient::
    MoveRangeSelectionExtent(const gfx::PointF& extent) {
  RenderWidgetHostDelegate* host_delegate = rwhva_->host()->delegate();
  if (host_delegate)
    host_delegate->MoveRangeSelectionExtent(gfx::ToRoundedPoint(extent));
}

void TouchSelectionControllerClientAura::SelectBetweenCoordinates(
    const gfx::PointF& base,
    const gfx::PointF& extent) {
  active_client_->SelectBetweenCoordinates(base, extent);
}

void TouchSelectionControllerClientAura::InternalClient::
    SelectBetweenCoordinates(const gfx::PointF& base,
                             const gfx::PointF& extent) {
  RenderWidgetHostDelegate* host_delegate = rwhva_->host()->delegate();
  if (host_delegate) {
    host_delegate->SelectRange(gfx::ToRoundedPoint(base),
                               gfx::ToRoundedPoint(extent));
  }
}

void TouchSelectionControllerClientAura::OnSelectionEvent(
    ui::SelectionEventType event) {
  // This function (implicitly) uses active_menu_client_, so we don't go to the
  // active view for this.
  switch (event) {
    case ui::SELECTION_HANDLES_SHOWN:
      quick_menu_requested_ = true;
      [[fallthrough]];
    case ui::INSERTION_HANDLE_SHOWN:
      UpdateQuickMenu();
      env_event_observer_ = std::make_unique<EnvEventObserver>(
          rwhva_->selection_controller(), rwhva_->GetNativeView());
      break;
    case ui::SELECTION_HANDLES_CLEARED:
    case ui::INSERTION_HANDLE_CLEARED:
      env_event_observer_.reset();
      quick_menu_requested_ = false;
      UpdateQuickMenu();
      break;
    case ui::SELECTION_HANDLE_DRAG_STARTED:
    case ui::INSERTION_HANDLE_DRAG_STARTED:
      handle_drag_in_progress_ = true;
      UpdateQuickMenu();
      break;
    case ui::SELECTION_HANDLE_DRAG_STOPPED:
    case ui::INSERTION_HANDLE_DRAG_STOPPED:
      handle_drag_in_progress_ = false;
      UpdateQuickMenu();
      HideMagnifier();
      break;
    case ui::SELECTION_HANDLES_MOVED:
    case ui::INSERTION_HANDLE_MOVED:
      UpdateQuickMenu();
      if (handle_drag_in_progress_) {
        ShowMagnifier();
      }
      break;
    case ui::INSERTION_HANDLE_TAPPED:
      quick_menu_requested_ = !quick_menu_requested_;
      UpdateQuickMenu();
      break;
  }
}

void TouchSelectionControllerClientAura::InternalClient::OnSelectionEvent(
    ui::SelectionEventType event) {
  NOTREACHED();
}

void TouchSelectionControllerClientAura::OnDragUpdate(
    const ui::TouchSelectionDraggable::Type type,
    const gfx::PointF& position) {}

void TouchSelectionControllerClientAura::InternalClient::OnDragUpdate(
    const ui::TouchSelectionDraggable::Type type,
    const gfx::PointF& position) {
  NOTREACHED();
}

std::unique_ptr<ui::TouchHandleDrawable>
TouchSelectionControllerClientAura::CreateDrawable() {
  // This function is purely related to the top-level view's window, so it
  // is always handled here and never in
  // TouchSelectionControllerClientChildFrame.
  return std::unique_ptr<ui::TouchHandleDrawable>(
      new ui::TouchHandleDrawableAura(rwhva_->GetNativeView()));
}

void TouchSelectionControllerClientAura::DidScroll() {}

std::unique_ptr<ui::TouchHandleDrawable>
TouchSelectionControllerClientAura::InternalClient::CreateDrawable() {
  NOTREACHED();
}

// Since the top-level client can only ever have its selection position changed
// by a mainframe scroll, or an actual change in the selection, and since both
// of these will initiate a compositor frame and thus the regular update
// process, there is nothing to do here.
void TouchSelectionControllerClientAura::InternalClient::DidScroll() {}

bool TouchSelectionControllerClientAura::IsCommandIdEnabled(
    int command_id,
    bool can_paste) const {
  bool editable = rwhva_->GetTextInputType() != ui::TEXT_INPUT_TYPE_NONE;
  bool readable = rwhva_->GetTextInputType() != ui::TEXT_INPUT_TYPE_PASSWORD;
  bool has_selection = !rwhva_->GetSelectedText().empty();
  switch (command_id) {
    case std::to_underlying(ui::TouchEditable::MenuCommands::kCut):
      return editable && readable && has_selection;
    case std::to_underlying(ui::TouchEditable::MenuCommands::kCopy):
      return readable && has_selection;
    case std::to_underlying(ui::TouchEditable::MenuCommands::kPaste): {
      return editable && can_paste;
    }
    case std::to_underlying(ui::TouchEditable::MenuCommands::kSelectAll): {
      gfx::Range text_range;
      if (rwhva_->GetTextRange(&text_range)) {
        return text_range.length() > rwhva_->GetSelectedText().length();
      }
      return true;
    }
    case std::to_underlying(ui::TouchEditable::MenuCommands::kSelectWord): {
      gfx::Range text_range;
      if (rwhva_->GetTextRange(&text_range)) {
        return readable && !has_selection && !text_range.is_empty();
      }
      return readable && !has_selection;
    }
    default:
      return false;
  }
}

void TouchSelectionControllerClientAura::ExecuteCommand(int command_id,
                                                        int event_flags) {
  if (command_id !=
          std::to_underlying(ui::TouchEditable::MenuCommands::kSelectAll) &&
      command_id !=
          std::to_underlying(ui::TouchEditable::MenuCommands::kSelectWord)) {
    rwhva_->selection_controller()->HideAndDisallowShowingAutomatically();
  }
  RenderWidgetHostDelegate* host_delegate = rwhva_->host()->delegate();
  if (!host_delegate)
    return;

  switch (command_id) {
    case std::to_underlying(ui::TouchEditable::MenuCommands::kCut):
      host_delegate->Cut();
      break;
    case std::to_underlying(ui::TouchEditable::MenuCommands::kCopy):
      host_delegate->Copy();
      break;
    case std::to_underlying(ui::TouchEditable::MenuCommands::kPaste):
      host_delegate->Paste();
      break;
    case std::to_underlying(ui::TouchEditable::MenuCommands::kSelectAll):
      host_delegate->SelectAll();
      break;
    case std::to_underlying(ui::TouchEditable::MenuCommands::kSelectWord):
      host_delegate->SelectAroundCaret(
          blink::mojom::SelectionGranularity::kWord,
          /*should_show_handle=*/true,
          /*should_show_context_menu=*/false);
      break;
    default:
      NOTREACHED();
  }
}

void TouchSelectionControllerClientAura::RunContextMenu() {
  gfx::RectF anchor_rect =
      rwhva_->selection_controller()->GetVisibleRectBetweenBounds();
  gfx::PointF anchor_point =
      gfx::PointF(anchor_rect.CenterPoint().x(), anchor_rect.y());
  RenderWidgetHostImpl* host = rwhva_->host();
  host->ShowContextMenuAtPoint(gfx::ToRoundedPoint(anchor_point),
                               ui::mojom::MenuSourceType::kTouchEditMenu);

  // Hide selection handles after getting rect-between-bounds from touch
  // selection controller; otherwise, rect would be empty and the above
  // calculations would be invalid.
  rwhva_->selection_controller()->HideAndDisallowShowingAutomatically();
}

bool TouchSelectionControllerClientAura::ShouldShowQuickMenu(bool can_paste) {
  return quick_menu_requested_ && !touch_down_ && !scroll_in_progress_ &&
         !handle_drag_in_progress_ && IsQuickMenuAvailable(can_paste);
}

std::u16string TouchSelectionControllerClientAura::GetSelectedText() {
  return rwhva_->GetSelectedText();
}

}  // namespace content
