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

#ifndef UI_TOUCH_SELECTION_TOUCH_SELECTION_CONTROLLER_H_
#define UI_TOUCH_SELECTION_TOUCH_SELECTION_CONTROLLER_H_

#include "base/macros.h"
#include "base/time/time.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/gfx/geometry/rect_f.h"
#include "ui/gfx/geometry/vector2d_f.h"
#include "ui/gfx/selection_bound.h"
#include "ui/touch_selection/longpress_drag_selector.h"
#include "ui/touch_selection/selection_event_type.h"
#include "ui/touch_selection/touch_handle.h"
#include "ui/touch_selection/touch_handle_orientation.h"
#include "ui/touch_selection/ui_touch_selection_export.h"

namespace ui {
class MotionEvent;

// Interface through which |TouchSelectionController| issues selection-related
// commands, notifications and requests.
class UI_TOUCH_SELECTION_EXPORT TouchSelectionControllerClient {
 public:
  virtual ~TouchSelectionControllerClient() {}

  virtual bool SupportsAnimation() const = 0;
  virtual void SetNeedsAnimate() = 0;
  virtual void MoveCaret(const gfx::PointF& position) = 0;
  virtual void MoveRangeSelectionExtent(const gfx::PointF& extent) = 0;
  virtual void SelectBetweenCoordinates(const gfx::PointF& base,
                                        const gfx::PointF& extent) = 0;
  virtual void OnSelectionEvent(SelectionEventType event) = 0;
  virtual std::unique_ptr<TouchHandleDrawable> CreateDrawable() = 0;
  virtual void DidScroll() = 0;
};

// Controller for manipulating text selection via touch input.
class UI_TOUCH_SELECTION_EXPORT TouchSelectionController
    : public TouchHandleClient,
      public LongPressDragSelectorClient {
 public:
  enum ActiveStatus {
    INACTIVE,
    INSERTION_ACTIVE,
    SELECTION_ACTIVE,
  };

  struct UI_TOUCH_SELECTION_EXPORT Config {
    Config();
    ~Config();

    // Maximum allowed time for handle tap detection. Defaults to 300 ms.
    base::TimeDelta max_tap_duration;

    // Defaults to 8 DIPs.
    float tap_slop;

    // Controls whether adaptive orientation for selection handles is enabled.
    // Defaults to false.
    bool enable_adaptive_handle_orientation;

    // Controls whether drag selection after a longpress is enabled.
    // Defaults to false.
    bool enable_longpress_drag_selection;
  };

  TouchSelectionController(TouchSelectionControllerClient* client,
                           const Config& config);
  ~TouchSelectionController() override;

  // To be called when the selection bounds have changed.
  // Note that such updates will trigger handle updates only if preceded
  // by an appropriate call to allow automatic showing.
  void OnSelectionBoundsChanged(const gfx::SelectionBound& start,
                                const gfx::SelectionBound& end);

  // To be called when the viewport rect has been changed. This is used for
  // setting the state of the handles.
  void OnViewportChanged(const gfx::RectF viewport_rect);

  // Allows touch-dragging of the handle.
  // Returns true iff the event was consumed, in which case the caller should
  // cease further handling of the event.
  bool WillHandleTouchEvent(const MotionEvent& event);

  // To be called before forwarding a tap event.
  // |tap_count| is tap index in a repeated sequence, i.e., 1 for the first
  // tap, 2 for the second tap, etc...
  void HandleTapEvent(const gfx::PointF& location, int tap_count);

  // To be called before forwarding a longpress event.
  void HandleLongPressEvent(base::TimeTicks event_time,
                                const gfx::PointF& location);

  // To be called before forwarding a gesture scroll begin event to prevent
  // long-press drag.
  void OnScrollBeginEvent();

  // Hide the handles and suppress bounds updates until the next explicit
  // showing allowance.
  void HideAndDisallowShowingAutomatically();

  // Override the handle visibility according to |hidden|.
  void SetTemporarilyHidden(bool hidden);

  // Ticks an active animation, as requested to the client by |SetNeedsAnimate|.
  // Returns true if an animation is active and requires further ticking.
  bool Animate(base::TimeTicks animate_time);

  // Returns the rect between the two active selection bounds. If just one of
  // the bounds is visible, the rect is simply the (one-dimensional) rect of
  // that bound. If no selection is active, an empty rect will be returned.
  gfx::RectF GetRectBetweenBounds() const;

  // Returns the visible rect of specified touch handle. For an active insertion
  // these values will be identical.
  gfx::RectF GetStartHandleRect() const;
  gfx::RectF GetEndHandleRect() const;

  // Returns the middle point of selection bound corresponding to the active
  // selection or insertion handle. If there is no active handle, returns (0,0).
  gfx::PointF GetActiveHandleBoundPoint() const;

  // Returns the focal point of the start and end bounds, as defined by
  // their bottom coordinate.
  const gfx::PointF& GetStartPosition() const;
  const gfx::PointF& GetEndPosition() const;

  const gfx::SelectionBound& start() const { return start_; }
  const gfx::SelectionBound& end() const { return end_; }

  ActiveStatus active_status() const { return active_status_; }

 private:
  friend class TouchSelectionControllerTestApi;

  enum InputEventType { TAP, REPEATED_TAP, LONG_PRESS, INPUT_EVENT_TYPE_NONE };

  bool WillHandleTouchEventImpl(const MotionEvent& event);

  // TouchHandleClient implementation.
  void OnDragBegin(const TouchSelectionDraggable& draggable,
                   const gfx::PointF& drag_position) override;
  void OnDragUpdate(const TouchSelectionDraggable& draggable,
                    const gfx::PointF& drag_position) override;
  void OnDragEnd(const TouchSelectionDraggable& draggable) override;
  bool IsWithinTapSlop(const gfx::Vector2dF& delta) const override;
  void OnHandleTapped(const TouchHandle& handle) override;
  void SetNeedsAnimate() override;
  std::unique_ptr<TouchHandleDrawable> CreateDrawable() override;
  base::TimeDelta GetMaxTapDuration() const override;
  bool IsAdaptiveHandleOrientationEnabled() const override;

  // LongPressDragSelectorClient implementation.
  void OnLongPressDragActiveStateChanged() override;
  gfx::PointF GetSelectionStart() const override;
  gfx::PointF GetSelectionEnd() const override;

  void OnInsertionChanged();
  void OnSelectionChanged();

  // Returns true if insertion mode was newly (re)activated.
  bool ActivateInsertionIfNecessary();
  void DeactivateInsertion();
  // Returns true if selection mode was newly (re)activated.
  bool ActivateSelectionIfNecessary();
  void DeactivateSelection();
  void UpdateHandleLayoutIfNecessary();

  bool WillHandleTouchEventForLongPressDrag(const MotionEvent& event);
  void SetTemporarilyHiddenForLongPressDrag(bool hidden);
  void RefreshHandleVisibility();

  void HideHandles();

  gfx::Vector2dF GetStartLineOffset() const;
  gfx::Vector2dF GetEndLineOffset() const;
  bool GetStartVisible() const;
  bool GetEndVisible() const;
  TouchHandle::AnimationStyle GetAnimationStyle(bool was_active) const;

  void LogSelectionEnd();

  TouchSelectionControllerClient* const client_;
  const Config config_;

  InputEventType response_pending_input_event_;

  gfx::SelectionBound start_;
  gfx::SelectionBound end_;
  TouchHandleOrientation start_orientation_;
  TouchHandleOrientation end_orientation_;

  ActiveStatus active_status_;

  std::unique_ptr<TouchHandle> insertion_handle_;

  std::unique_ptr<TouchHandle> start_selection_handle_;
  std::unique_ptr<TouchHandle> end_selection_handle_;

  bool temporarily_hidden_;

  // Whether to use the start bound (if false, the end bound) for computing the
  // appropriate text line offset when performing a selection drag. This helps
  // ensure that the initial selection induced by the drag doesn't "jump"
  // between lines.
  bool anchor_drag_to_selection_start_;

  // Longpress drag allows direct manipulation of longpress-initiated selection.
  LongPressDragSelector longpress_drag_selector_;

  gfx::RectF viewport_rect_;

  base::TimeTicks selection_start_time_;
  // Whether a selection handle was dragged during the current 'selection
  // session' - i.e. since the current selection has been activated.
  bool selection_handle_dragged_;

  // Determines whether the entire touch sequence should be consumed or not.
  bool consume_touch_sequence_;

  bool show_touch_handles_;

  DISALLOW_COPY_AND_ASSIGN(TouchSelectionController);
};

}  // namespace ui

#endif  // UI_TOUCH_SELECTION_TOUCH_SELECTION_CONTROLLER_H_
