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

#include "ui/touch_selection/touch_selection_controller.h"

#include <memory>

#if BUILDFLAG(IS_ARKWEB_EXT)
#include "arkweb/ohos_nweb_ex/build/features/features.h"
#endif
#include "base/auto_reset.h"
#include "base/check_op.h"
#include "base/metrics/user_metrics.h"
#include "base/notreached.h"
#include "build/build_config.h"
#include "ui/touch_selection/touch_selection_metrics.h"

#if BUILDFLAG(ARKWEB_MENU)
#include "base/logging.h"
#endif
#if BUILDFLAG(IS_ARKWEB)
#include "arkweb/chromium_ext/ui/touch_selection/touch_selection_controller_utils.h"
#endif

namespace ui {
namespace {

gfx::Vector2dF ComputeLineOffsetFromBottom(const gfx::SelectionBound& bound) {
  gfx::Vector2dF line_offset =
      gfx::ScaleVector2d(bound.edge_start() - bound.edge_end(), 0.5f);
  // An offset of 8 DIPs is sufficient for most line sizes. For small lines,
  // using half the line height avoids synthesizing a point on a line above
  // (or below) the intended line.
  const gfx::Vector2dF kMaxLineOffset(8.f, 8.f);
  line_offset.SetToMin(kMaxLineOffset);
  line_offset.SetToMax(-kMaxLineOffset);
  return line_offset;
}

TouchHandleOrientation ToTouchHandleOrientation(
    gfx::SelectionBound::Type type) {
  switch (type) {
    case gfx::SelectionBound::LEFT:
      return TouchHandleOrientation::LEFT;
    case gfx::SelectionBound::RIGHT:
      return TouchHandleOrientation::RIGHT;
    case gfx::SelectionBound::CENTER:
      return TouchHandleOrientation::CENTER;
    case gfx::SelectionBound::HIDDEN:
      return TouchHandleOrientation::UNDEFINED;
    case gfx::SelectionBound::EMPTY:
      return TouchHandleOrientation::UNDEFINED;
  }
  NOTREACHED() << "Invalid selection bound type: " << type;
}

}  // namespace

TouchSelectionController::TouchSelectionController(
    TouchSelectionControllerClient* client,
    const Config& config)
    : client_(client),
      config_(config),
      response_pending_input_event_(INPUT_EVENT_TYPE_NONE),
      start_orientation_(TouchHandleOrientation::UNDEFINED),
      end_orientation_(TouchHandleOrientation::UNDEFINED),
      active_status_(INACTIVE),
      temporarily_hidden_(false),
      anchor_drag_to_selection_start_(false),
      longpress_drag_selector_(this),
      selection_handle_dragged_(false),
      consume_touch_sequence_(false),
      show_touch_handles_(false) {
  DCHECK(client_);
#if BUILDFLAG(IS_ARKWEB)
  utils_ = std::make_unique<TouchSelectionControllerUtils>(this);
#endif
}

TouchSelectionController::~TouchSelectionController() {
}

void TouchSelectionController::OnSelectionBoundsChanged(
    const gfx::SelectionBound& start,
    const gfx::SelectionBound& end) {
  if (start == start_ && end_ == end) {
#if BUILDFLAG(ARKWEB_MENU)
    utils_->SetResetSelectionTemporarily(false);
#endif
    return;
  }

  if (!start.HasHandle() || !end.HasHandle() || !show_touch_handles_) {
#if BUILDFLAG(ARKWEB_MENU)
    if (AsTouchSelectionControllerExt()->SelectOverImg())
      return;
#endif
    HideHandles();
#if BUILDFLAG(ARKWEB_MENU)
    LOG(INFO) << "OnSelectionBoundsChanged show_touch_handles_: " << show_touch_handles_
              << " start.visible: " << start.visible();
    if (show_touch_handles_ && start.visible()) {
      AsTouchSelectionControllerExt()->OnInsertionChangedExt(start, end);
    }
#endif
    return;
  }

#if BUILDFLAG(ARKWEB_MENU)
  utils_->SetResetSelectionTemporarily(false);
#endif

  // Swap the Handles when the start and end selection points cross each other.
  if (active_status_ == SELECTION_ACTIVE) {
    // Bounds have the same orientation.
    bool need_swap = (start_selection_handle_->IsActive() &&
                      end_.edge_end() == start.edge_end()) ||
                     (end_selection_handle_->IsActive() &&
                      end.edge_end() == start_.edge_end());

    // Bounds have different orientation.
    // Specifically, for writing-mode: vertical-*, selection bounds are
    // horizontal.
    // When vertical-lr:
    //   - start bound is from right to left,
    //   - end bound is from left to right.
    // When vertical-rl:
    //   - start bound is from left to right,
    //   - end bound is from right to left.
    // So when previous start/end bound become current end/start bound,
    // edge_start() and edge_end() are swapped. Therefore, we are comparing
    // edge_end() with edge_start() here.
    need_swap |= (start_selection_handle_->IsActive() &&
                  end_.edge_end() == start.edge_start()) ||
                 (end_selection_handle_->IsActive() &&
                  end.edge_end() == start_.edge_start());
#if BUILDFLAG(ARKWEB_MENU)
    need_swap = AsTouchSelectionControllerExt()->OnHandleSwap(need_swap, start, end);
#endif
    if (need_swap)
      start_selection_handle_.swap(end_selection_handle_);
  }

  // Update |anchor_drag_to_selection_start_| for long press drag selector.
  // Since selection can be updated with only one end at a time, if one end is
  // equal to the previous value, the updated end is the other.
  if (start_ == start)
    anchor_drag_to_selection_start_ = false;
  else if (end_ == end)
    anchor_drag_to_selection_start_ = true;

  start_ = start;
  end_ = end;
  start_orientation_ = ToTouchHandleOrientation(start_.type());
  end_orientation_ = ToTouchHandleOrientation(end_.type());

  // Ensure that |response_pending_input_event_| is cleared after the method
  // completes, while also making its current value available for the duration
  // of the call.
  InputEventType causal_input_event = response_pending_input_event_;
  response_pending_input_event_ = INPUT_EVENT_TYPE_NONE;
  base::AutoReset<InputEventType> auto_reset_response_pending_input_event(
      &response_pending_input_event_, causal_input_event);

  if ((start_orientation_ == TouchHandleOrientation::LEFT ||
       start_orientation_ == TouchHandleOrientation::RIGHT) &&
      (end_orientation_ == TouchHandleOrientation::RIGHT ||
       end_orientation_ == TouchHandleOrientation::LEFT)) {
    OnSelectionChanged();
    return;
  }

  if (start_orientation_ == TouchHandleOrientation::CENTER) {
    OnInsertionChanged();
    return;
  }

  HideHandles();
}

void TouchSelectionController::OnViewportChanged(
    const gfx::RectF viewport_rect) {
  // Trigger a force update if the viewport is changed, so that
  // it triggers a call to change the mirror values if required.
  if (viewport_rect_ == viewport_rect)
    return;

  viewport_rect_ = viewport_rect;

  if (active_status_ == INACTIVE)
    return;

  if (active_status_ == INSERTION_ACTIVE) {
    DCHECK(insertion_handle_);
    insertion_handle_->SetViewportRect(viewport_rect);
  } else if (active_status_ == SELECTION_ACTIVE) {
    DCHECK(start_selection_handle_);
    DCHECK(end_selection_handle_);
    start_selection_handle_->SetViewportRect(viewport_rect);
    end_selection_handle_->SetViewportRect(viewport_rect);
  }

  // Update handle layout after setting the new Viewport size.
  UpdateHandleLayoutIfNecessary();

#if BUILDFLAG(ARKWEB_EXT_TOPCONTROLS) && BUILDFLAG(ARKWEB_MENU)
  if (client_) {
    client_->OnSelectionEvent(SELECTION_HANDLES_MOVED);
  }
#endif
}

bool TouchSelectionController::WillHandleTouchEvent(const MotionEvent& event) {
  bool handled = WillHandleTouchEventImpl(event);
  // If Action::DOWN is consumed, the rest of touch sequence should be consumed,
  // too, regardless of value of |handled|.
  // TODO(mohsen): This will consume touch events until the next Action::DOWN.
  // Ideally we should consume until the final Action::UP/Action::CANCEL.
  // But, apparently, we can't reliably determine the final Action::CANCEL in a
  // multi-touch scenario. See https://crbug.com/653212.
  if (event.GetAction() == MotionEvent::Action::DOWN) {
    consume_touch_sequence_ = handled;
  }
  return handled || consume_touch_sequence_;
}

void TouchSelectionController::HandleTapEvent(const gfx::PointF& location,
                                                  int tap_count) {
  if (tap_count > 1) {
    response_pending_input_event_ = REPEATED_TAP;
  } else {
    response_pending_input_event_ = TAP;
  }
}

void TouchSelectionController::HandleLongPressEvent(
    base::TimeTicks event_time,
    const gfx::PointF& location) {
#if BUILDFLAG(ARKWEB_VIBRATE)
  AsTouchSelectionControllerExt()->SetLongPressEvent(true);
#endif  // BUILDFLAG(ARKWEB_VIBRATE)
  longpress_drag_selector_.OnLongPressEvent(event_time, location);
  response_pending_input_event_ = LONG_PRESS;
  drag_selector_initiating_gesture_ = DragSelectorInitiatingGesture::kLongPress;
}

void TouchSelectionController::HandleDoublePressEvent(
    base::TimeTicks event_time,
    const gfx::PointF& location) {
  longpress_drag_selector_.OnDoublePressEvent(event_time, location);
  response_pending_input_event_ = LONG_PRESS;
  drag_selector_initiating_gesture_ =
      DragSelectorInitiatingGesture::kDoublePress;
}

void TouchSelectionController::OnScrollBeginEvent() {
  // When there is an active selection, if the user performs a long-press that
  // does not trigger a new selection (e.g. a long-press on an empty area) and
  // then scrolls, the scroll will move the selection. In this case we will
  // think incorrectly that the selection change was due to the long-press and
  // will activate touch selection and start long-press drag gesture (see
  // ActivateInsertionIfNecessary()). To prevent this, we need to reset the
  // state of touch selection controller and long-press drag selector.
  // TODO(mohsen): Remove this workaround when we have enough information about
  // the cause of a selection change (see https://crbug.com/571897).
  longpress_drag_selector_.OnScrollBeginEvent();
  response_pending_input_event_ = INPUT_EVENT_TYPE_NONE;
}

#if BUILDFLAG(IS_ANDROID)
void TouchSelectionController::OnUpdateNativeViewTree(
    gfx::NativeView parent_native_view,
    cc::slim::Layer* parent_layer) {
  if (insertion_handle_) {
    insertion_handle_->OnUpdateNativeViewTree(parent_native_view, parent_layer);
  }
  if (start_selection_handle_) {
    start_selection_handle_->OnUpdateNativeViewTree(parent_native_view,
                                                    parent_layer);
  }
  if (end_selection_handle_) {
    end_selection_handle_->OnUpdateNativeViewTree(parent_native_view,
                                                  parent_layer);
  }
}
#endif  // BUILDFLAG(IS_ANDROID)

void TouchSelectionController::HideHandles() {
  response_pending_input_event_ = INPUT_EVENT_TYPE_NONE;
  DeactivateInsertion();
  DeactivateSelection();
  start_ = gfx::SelectionBound();
  end_ = gfx::SelectionBound();
  start_orientation_ = ToTouchHandleOrientation(start_.type());
  end_orientation_ = ToTouchHandleOrientation(end_.type());
}

void TouchSelectionController::HideAndDisallowShowingAutomatically() {
  HideHandles();
  show_touch_handles_ = false;
}

void TouchSelectionController::SetTemporarilyHidden(bool hidden) {
  if (temporarily_hidden_ == hidden)
    return;
  temporarily_hidden_ = hidden;
  RefreshHandleVisibility();
}

bool TouchSelectionController::Animate(base::TimeTicks frame_time) {
  if (active_status_ == INSERTION_ACTIVE)
    return insertion_handle_->Animate(frame_time);

  if (active_status_ == SELECTION_ACTIVE) {
    bool needs_animate = start_selection_handle_->Animate(frame_time);
    needs_animate |= end_selection_handle_->Animate(frame_time);
    return needs_animate;
  }

  return false;
}

const gfx::SelectionBound& TouchSelectionController::GetFocusBound() const {
  DCHECK_NE(active_status_, INACTIVE);
  return anchor_drag_to_selection_start_ ? start_ : end_;
}

gfx::RectF TouchSelectionController::GetRectBetweenBounds() const {
  // Short-circuit for efficiency.
  if (active_status_ == INACTIVE)
    return gfx::RectF();

  if (start_.visible() && !end_.visible()) {
    // This BoundingRect is actually a line unless the selection is rotated.
    return gfx::BoundingRect(start_.edge_start(), start_.edge_end());
  }

  if (end_.visible() && !start_.visible()) {
    // This BoundingRect is actually a line unless the selection is rotated.
    return gfx::BoundingRect(end_.edge_start(), end_.edge_end());
  }

  // If both handles are visible, or both are invisible, use the entire rect.
  // Specifically, if both handles are on the same horizontal line for
  // writing-mode: vertical-*, or both are on the same vertical line for
  // writing-mode: horizontal, the entire rect is actually a line unless the
  // selection is rotated.
  return RectFBetweenSelectionBounds(start_, end_);
}

gfx::RectF TouchSelectionController::GetVisibleRectBetweenBounds() const {
  // Short-circuit for efficiency.
  if (active_status_ == INACTIVE)
    return gfx::RectF();

  // Returns the rect of the entire visible selection rect.
  return RectFBetweenVisibleSelectionBounds(start_, end_);
}

gfx::RectF TouchSelectionController::GetStartHandleRect() const {
  if (active_status_ == INSERTION_ACTIVE)
    return insertion_handle_->GetVisibleBounds();
  if (active_status_ == SELECTION_ACTIVE)
    return start_selection_handle_->GetVisibleBounds();
  return gfx::RectF();
}

gfx::RectF TouchSelectionController::GetEndHandleRect() const {
  if (active_status_ == INSERTION_ACTIVE)
    return insertion_handle_->GetVisibleBounds();
  if (active_status_ == SELECTION_ACTIVE)
    return end_selection_handle_->GetVisibleBounds();
  return gfx::RectF();
}

float TouchSelectionController::GetTouchHandleHeight() const {
  if (active_status_ == INSERTION_ACTIVE)
    return insertion_handle_->GetVisibleBounds().height();
  if (active_status_ == SELECTION_ACTIVE) {
    if (GetStartVisible())
      return start_selection_handle_->GetVisibleBounds().height();
    if (GetEndVisible())
      return end_selection_handle_->GetVisibleBounds().height();
  }
  return 0.f;
}

float TouchSelectionController::GetActiveHandleMiddleY() const {
  const gfx::SelectionBound* bound = nullptr;
  if (active_status_ == INSERTION_ACTIVE && insertion_handle_->IsActive())
    bound = &start_;
  if (active_status_ == SELECTION_ACTIVE) {
    if (start_selection_handle_->IsActive())
      bound = &start_;
    else if (end_selection_handle_->IsActive())
      bound = &end_;
  }

  if (!bound)
    return 0.f;
  return (bound->edge_start().y() + bound->edge_end().y()) / 2.f;
}

const gfx::PointF& TouchSelectionController::GetStartPosition() const {
  return start_.edge_end();
}

const gfx::PointF& TouchSelectionController::GetEndPosition() const {
  return end_.edge_end();
}

bool TouchSelectionController::WillHandleTouchEventImpl(
    const MotionEvent& event) {
  show_touch_handles_ = true;
  if (config_.enable_longpress_drag_selection &&
      longpress_drag_selector_.WillHandleTouchEvent(event)) {
    return true;
  }
#if BUILDFLAG(ARKWEB_AI)
  AsTouchSelectionControllerExt()->SetTouchNumsForHandle(event);
#endif
  if (active_status_ == INSERTION_ACTIVE) {
    DCHECK(insertion_handle_);
    return insertion_handle_->WillHandleTouchEvent(event);
  }

  if (active_status_ == SELECTION_ACTIVE) {
    DCHECK(start_selection_handle_);
    DCHECK(end_selection_handle_);
    if (start_selection_handle_->IsActive())
      return start_selection_handle_->WillHandleTouchEvent(event);

    if (end_selection_handle_->IsActive())
      return end_selection_handle_->WillHandleTouchEvent(event);

    const gfx::PointF event_pos(event.GetX(), event.GetY());
    if ((event_pos - GetStartPosition()).LengthSquared() <=
        (event_pos - GetEndPosition()).LengthSquared()) {
      return start_selection_handle_->WillHandleTouchEvent(event);
    }
#if BUILDFLAG(ARKWEB_MENU)
    if (AsTouchSelectionControllerExt()->IsEndHandleNotVisible(event)) {
      return start_selection_handle_->WillHandleTouchEvent(event);
    }
#endif

    return end_selection_handle_->WillHandleTouchEvent(event);
  }

  return false;
}

void TouchSelectionController::OnSwipeToMoveCursorBegin() {
  if (config_.hide_active_handle) {
    SetTemporarilyHidden(true);

    // If the user has typed something, the insertion handle might be hidden.
    // Prepare to show touch handles on end.
    show_touch_handles_ = true;
  }
}

void TouchSelectionController::OnSwipeToMoveCursorEnd() {
  if (config_.hide_active_handle) {
    SetTemporarilyHidden(false);
  }
  RecordTouchSelectionDrag(TouchSelectionDragType::kCursorDrag);
}

void TouchSelectionController::OnDragBegin(
    const TouchSelectionDraggable& draggable,
    const gfx::PointF& drag_position) {
  if (&draggable == insertion_handle_.get()) {
    DCHECK_EQ(active_status_, INSERTION_ACTIVE);
    if (config_.hide_active_handle)
      insertion_handle_->SetTransparent();
    is_first_drag_ = true;
    client_->OnSelectionEvent(INSERTION_HANDLE_DRAG_STARTED);
    anchor_drag_to_selection_start_ = true;
    return;
  }

  DCHECK_EQ(active_status_, SELECTION_ACTIVE);

  if (&draggable == start_selection_handle_.get()) {
    anchor_drag_to_selection_start_ = true;
  } else if (&draggable == end_selection_handle_.get()) {
    anchor_drag_to_selection_start_ = false;
  } else {
    DCHECK_EQ(&draggable, &longpress_drag_selector_);
    anchor_drag_to_selection_start_ =
        (drag_position - GetStartPosition()).LengthSquared() <
        (drag_position - GetEndPosition()).LengthSquared();
  }

  if (config_.hide_active_handle) {
    if (&draggable == start_selection_handle_.get()) {
      start_selection_handle_->SetTransparent();
    } else if (&draggable == end_selection_handle_.get()) {
      end_selection_handle_->SetTransparent();
    }
  }

  gfx::PointF base = GetStartPosition() + GetStartLineOffset();
  gfx::PointF extent = GetEndPosition() + GetEndLineOffset();
  if (anchor_drag_to_selection_start_)
    std::swap(base, extent);

  // If this is the first drag, log an action to allow user action sequencing.
  if (!selection_handle_dragged_)
    base::RecordAction(base::UserMetricsAction("SelectionChanged"));
  selection_handle_dragged_ = true;

#if BUILDFLAG(ARKWEB_CLIPBOARD)
  utils_->SetResetSelectionTemporarily(false);
#endif  // BUILDFLAG(ARKWEB_CLIPBOARD)

  // When moving the handle we want to move only the extent point. Before doing
  // so we must make sure that the base point is set correctly.
#if BUILDFLAG(ARKWEB_MENU)
  AsTouchSelectionControllerExt()->ArkSelectBetweenCoordinates(base, extent);
#else
  client_->SelectBetweenCoordinates(base, extent);
#endif
  client_->OnSelectionEvent(SELECTION_HANDLE_DRAG_STARTED);
}

void TouchSelectionController::OnDragUpdate(
    const TouchSelectionDraggable& draggable,
    const gfx::PointF& drag_position) {
#if BUILDFLAG(ARKWEB_MENU)
  gfx::PointF line_position = drag_position;
#else
  // As the position corresponds to the bottom left point of the selection
  // bound, offset it to some reasonable point on the current line of text.
  gfx::Vector2dF line_offset = anchor_drag_to_selection_start_
                                   ? GetStartLineOffset()
                                   : GetEndLineOffset();
  gfx::PointF line_position = drag_position + line_offset;
#endif  // BUILDFLAG(ARKWEB_CLIPBOARD)
  if (&draggable == insertion_handle_.get())
    client_->MoveCaret(line_position);
  else
    client_->MoveRangeSelectionExtent(line_position);

#if BUILDFLAG(ARKWEB_MENU)
  if (is_first_drag_) {
    client_->NotifyShowMagnifier();
    is_first_drag_ = false;
  }
#endif
  // We use the bound middle point to restrict the ability to move up and
  // down, but let user move it more freely in horizontal direction.
  if (&draggable == &longpress_drag_selector_) {
    // Show magnifier at the selection edge.
    const gfx::SelectionBound* bound =
        anchor_drag_to_selection_start_ ? &start_ : &end_;
    const float x = bound->edge_start().x();
    const float y = (bound->edge_start().y() + bound->edge_end().y()) / 2.f;
    client_->OnDragUpdate(TouchSelectionDraggable::Type::kLongpress,
                          gfx::PointF(x, y));
  } else {
    const float y = GetActiveHandleMiddleY();
    client_->OnDragUpdate(TouchSelectionDraggable::Type::kTouchHandle,
                          gfx::PointF(drag_position.x(), y));
  }
}

void TouchSelectionController::OnDragEnd(
    const TouchSelectionDraggable& draggable) {
#if BUILDFLAG(ARKWEB_CLIPBOARD)
  if (utils_->GetResetSelectionTemporarily()) {
    LOG(INFO) << "reset_selection_temporarily HideHandles";
    HideHandles();
  }
#endif  // BUILDFLAG(ARKWEB_CLIPBOARD)
  if (&draggable == insertion_handle_.get()) {
#if BUILDFLAG(ARKWEB_MENU)
    is_first_drag_ = false;
#endif
    client_->OnSelectionEvent(INSERTION_HANDLE_DRAG_STOPPED);
  } else {
#if BUILDFLAG(ARKWEB_CLIPBOARD)
    AsTouchSelectionControllerExt()->ResetPositionAfterDragEnd(draggable);
#endif  // BUILDFLAG(ARKWEB_CLIPBOARD)
    client_->OnSelectionEvent(SELECTION_HANDLE_DRAG_STOPPED);
  }
  LogDragType(draggable);
  drag_selector_initiating_gesture_ = DragSelectorInitiatingGesture::kNone;
}

bool TouchSelectionController::IsWithinTapSlop(
    const gfx::Vector2dF& delta) const {
  return delta.LengthSquared() <
         (static_cast<double>(config_.tap_slop) * config_.tap_slop);
}

void TouchSelectionController::OnHandleTapped(const TouchHandle& handle) {
  if (insertion_handle_ && &handle == insertion_handle_.get())
    client_->OnSelectionEvent(INSERTION_HANDLE_TAPPED);
}

void TouchSelectionController::SetNeedsAnimate() {
  client_->SetNeedsAnimate();
}

std::unique_ptr<TouchHandleDrawable>
TouchSelectionController::CreateDrawable() {
  return client_->CreateDrawable();
}

base::TimeDelta TouchSelectionController::GetMaxTapDuration() const {
  return config_.max_tap_duration;
}

bool TouchSelectionController::IsAdaptiveHandleOrientationEnabled() const {
  return config_.enable_adaptive_handle_orientation;
}

void TouchSelectionController::OnLongPressDragActiveStateChanged() {
  // The handles should remain hidden for the duration of a longpress drag,
  // including the time between a longpress and the start of drag motion.
  RefreshHandleVisibility();
}

gfx::PointF TouchSelectionController::GetSelectionStart() const {
  return GetStartPosition();
}

gfx::PointF TouchSelectionController::GetSelectionEnd() const {
  return GetEndPosition();
}

#if BUILDFLAG(IS_ANDROID)
void TouchSelectionController::HandleSwipeToMoveCursorGestureAck(
    ui::EventType type,
    const gfx::PointF& point,
    const std::optional<bool>& cursor_control,
    bool is_in_root_view) {
  switch (type) {
    case ui::EventType::kGestureScrollBegin: {
      DCHECK(cursor_control.has_value());
      if (!*cursor_control) {
        break;
      }
      swipe_to_move_cursor_activated_ = true;
      OnSwipeToMoveCursorBegin();
      client_->OnSelectionEvent(ui::INSERTION_HANDLE_DRAG_STARTED);
      break;
    }
    case ui::EventType::kGestureScrollUpdate: {
      if (!is_in_root_view) {
        break;
      }
      if (!swipe_to_move_cursor_activated_) {
        break;
      }
      gfx::RectF rect = GetRectBetweenBounds();
      // Suppress this when the input is not focused, in which case rect will be
      // 0x0.
      if (rect.width() != 0.f || rect.height() != 0.f) {
        client_->OnDragUpdate(ui::TouchSelectionDraggable::Type::kNone,
                              gfx::PointF(point.x(), rect.right_center().y()));
      }
      break;
    }
    case ui::EventType::kGestureScrollEnd: {
      if (!swipe_to_move_cursor_activated_) {
        break;
      }
      swipe_to_move_cursor_activated_ = false;
      OnSwipeToMoveCursorEnd();
      client_->OnSelectionEvent(ui::INSERTION_HANDLE_DRAG_STOPPED);
      break;
    }
    default:
      break;
  }
}
#endif  // BUILDFLAG(IS_ANDROID)

void TouchSelectionController::OnInsertionChanged() {
  DeactivateSelection();

  const bool activated = ActivateInsertionIfNecessary();

  const TouchHandle::AnimationStyle animation = GetAnimationStyle(!activated);
#if BUILDFLAG(ARKWEB_MENU)
  insertion_handle_->AsTouchHandleExt()->SetEdge(start_.edge_start(), start_.edge_end());
#endif
  insertion_handle_->SetFocus(start_.edge_start(), start_.edge_end());
  insertion_handle_->SetVisible(GetStartVisible(), animation);
#if BUILDFLAG(ARKWEB_MENU)
  insertion_handle_->AsTouchHandleExt()->SetInsertHandleAlpha(1.f);
#endif

  UpdateHandleLayoutIfNecessary();

  client_->OnSelectionEvent(activated ? INSERTION_HANDLE_SHOWN
                                      : INSERTION_HANDLE_MOVED);
}

void TouchSelectionController::OnSelectionChanged() {
  DeactivateInsertion();

  const bool activated = ActivateSelectionIfNecessary();

  const TouchHandle::AnimationStyle animation = GetAnimationStyle(!activated);
#if BUILDFLAG(ARKWEB_MENU)
  start_selection_handle_->AsTouchHandleExt()->SetEdge(start_.edge_start(), start_.edge_end());
  end_selection_handle_->AsTouchHandleExt()->SetEdge(end_.edge_start(), end_.edge_end());
#endif

  start_selection_handle_->SetFocus(start_.edge_start(), start_.edge_end());
  end_selection_handle_->SetFocus(end_.edge_start(), end_.edge_end());

  start_selection_handle_->SetOrientation(start_orientation_);
  end_selection_handle_->SetOrientation(end_orientation_);

  start_selection_handle_->SetVisible(GetStartVisible(), animation);
  end_selection_handle_->SetVisible(GetEndVisible(), animation);

  UpdateHandleLayoutIfNecessary();

  client_->OnSelectionEvent(activated ? SELECTION_HANDLES_SHOWN
                                      : SELECTION_HANDLES_MOVED);
}

bool TouchSelectionController::ActivateInsertionIfNecessary() {
  DCHECK_NE(SELECTION_ACTIVE, active_status_);

  if (!insertion_handle_) {
#if BUILDFLAG(ARKWEB_MENU)
    insertion_handle_ = std::make_unique<TouchHandleExt>(
#else
    insertion_handle_ = std::make_unique<TouchHandle>(
#endif
        this, TouchHandleOrientation::CENTER, viewport_rect_);
  }

#if BUILDFLAG(ARKWEB_MENU)
  if (active_status_ == INACTIVE || response_pending_input_event_ == LONG_PRESS) {
#else
  if (active_status_ == INACTIVE || response_pending_input_event_ == TAP ||
      response_pending_input_event_ == LONG_PRESS) {
#endif
    active_status_ = INSERTION_ACTIVE;
    insertion_handle_->SetEnabled(true);
    insertion_handle_->SetViewportRect(viewport_rect_);
    response_pending_input_event_ = INPUT_EVENT_TYPE_NONE;
    return true;
  }
  return false;
}

void TouchSelectionController::DeactivateInsertion() {
  if (active_status_ != INSERTION_ACTIVE)
    return;
  DCHECK(insertion_handle_);
  active_status_ = INACTIVE;
  insertion_handle_->SetEnabled(false);
  client_->OnSelectionEvent(INSERTION_HANDLE_CLEARED);
}

bool TouchSelectionController::ActivateSelectionIfNecessary() {
  DCHECK_NE(INSERTION_ACTIVE, active_status_);

  if (!start_selection_handle_) {
    start_selection_handle_ =
#if BUILDFLAG(ARKWEB_MENU)
        std::make_unique<TouchHandleExt>(this, start_orientation_, viewport_rect_);
#else
        std::make_unique<TouchHandle>(this, start_orientation_, viewport_rect_);
#endif
  } else {
    start_selection_handle_->SetEnabled(true);
    start_selection_handle_->SetViewportRect(viewport_rect_);
  }

  if (!end_selection_handle_) {
    end_selection_handle_ =
#if BUILDFLAG(ARKWEB_MENU)
        std::make_unique<TouchHandleExt>(this, end_orientation_, viewport_rect_);
#else
        std::make_unique<TouchHandle>(this, end_orientation_, viewport_rect_);
#endif
  } else {
    end_selection_handle_->SetEnabled(true);
    end_selection_handle_->SetViewportRect(viewport_rect_);
  }

  // As a long press received while a selection is already active may trigger
  // an entirely new selection, notify the client but avoid sending an
  // intervening SELECTION_HANDLES_CLEARED update to avoid unnecessary state
  // changes.
  if (active_status_ == INACTIVE ||
      response_pending_input_event_ == LONG_PRESS ||
      response_pending_input_event_ == REPEATED_TAP) {
    active_status_ = SELECTION_ACTIVE;
    selection_handle_dragged_ = false;
    response_pending_input_event_ = INPUT_EVENT_TYPE_NONE;
    longpress_drag_selector_.OnSelectionActivated();
    return true;
  }
  return false;
}

void TouchSelectionController::DeactivateSelection() {
  if (active_status_ != SELECTION_ACTIVE)
    return;
  DCHECK(start_selection_handle_);
  DCHECK(end_selection_handle_);
  longpress_drag_selector_.OnSelectionDeactivated();
  start_selection_handle_->SetEnabled(false);
  end_selection_handle_->SetEnabled(false);
  active_status_ = INACTIVE;
  client_->OnSelectionEvent(SELECTION_HANDLES_CLEARED);
}

void TouchSelectionController::UpdateHandleLayoutIfNecessary() {
  if (active_status_ == INSERTION_ACTIVE) {
    DCHECK(insertion_handle_);
    insertion_handle_->UpdateHandleLayout();
  } else if (active_status_ == SELECTION_ACTIVE) {
    DCHECK(start_selection_handle_);
    DCHECK(end_selection_handle_);
    start_selection_handle_->UpdateHandleLayout();
    end_selection_handle_->UpdateHandleLayout();
  }
}

void TouchSelectionController::RefreshHandleVisibility() {
  TouchHandle::AnimationStyle animation_style = GetAnimationStyle(true);
  if (active_status_ == SELECTION_ACTIVE) {
    start_selection_handle_->SetVisible(GetStartVisible(), animation_style);
    end_selection_handle_->SetVisible(GetEndVisible(), animation_style);
  }
  if (active_status_ == INSERTION_ACTIVE)
    insertion_handle_->SetVisible(GetStartVisible(), animation_style);

  // Update handle layout if handle visibility is explicitly changed.
  UpdateHandleLayoutIfNecessary();
}

gfx::Vector2dF TouchSelectionController::GetStartLineOffset() const {
  return ComputeLineOffsetFromBottom(start_);
}

gfx::Vector2dF TouchSelectionController::GetEndLineOffset() const {
  return ComputeLineOffsetFromBottom(end_);
}

bool TouchSelectionController::GetStartVisible() const {
  if (!start_.visible())
    return false;

  return !temporarily_hidden_ && !longpress_drag_selector_.IsActive();
}

bool TouchSelectionController::GetEndVisible() const {
  if (!end_.visible())
    return false;

  return !temporarily_hidden_ && !longpress_drag_selector_.IsActive();
}

TouchHandle::AnimationStyle TouchSelectionController::GetAnimationStyle(
    bool was_active) const {
  return was_active && client_->SupportsAnimation()
             ? TouchHandle::ANIMATION_SMOOTH
             : TouchHandle::ANIMATION_NONE;
}

void TouchSelectionController::LogDragType(
    const TouchSelectionDraggable& draggable) {
  if (&draggable == insertion_handle_.get()) {
    RecordTouchSelectionDrag(TouchSelectionDragType::kCursorHandleDrag);
  } else if (&draggable == start_selection_handle_.get() ||
             &draggable == end_selection_handle_.get()) {
    RecordTouchSelectionDrag(TouchSelectionDragType::kSelectionHandleDrag);
  } else if (drag_selector_initiating_gesture_ ==
             DragSelectorInitiatingGesture::kLongPress) {
    RecordTouchSelectionDrag(TouchSelectionDragType::kLongPressDrag);
  } else if (drag_selector_initiating_gesture_ ==
             DragSelectorInitiatingGesture::kDoublePress) {
    RecordTouchSelectionDrag(TouchSelectionDragType::kDoublePressDrag);
  }
}
}  // namespace ui