/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 sw=2 et tw=78: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#ifndef SelectionCarets_h__
#define SelectionCarets_h__

#include "nsIReflowObserver.h"
#include "nsIScrollObserver.h"
#include "nsISelectionListener.h"
#include "nsWeakPtr.h"
#include "nsWeakReference.h"
#include "Units.h"
#include "mozilla/dom/SelectionStateChangedEvent.h"
#include "mozilla/EventForwards.h"
#include "mozilla/WeakPtr.h"

class nsCanvasFrame;
class nsDocShell;
class nsFrameSelection;
class nsIContent;
class nsIDocument;
class nsIFrame;
class nsIPresShell;
class nsITimer;
class nsIWidget;
class nsPresContext;

namespace mozilla {

namespace dom {
class Selection;
}

/**
 * The SelectionCarets draw a pair of carets when the selection is not
 * collapsed, one at each end of the selection.
 * SelectionCarets also handle visibility, dragging caret and selecting word
 * when long tap event fired.
 *
 * The DOM structure is 2 div elements for showing start and end caret.
 * Each div element has a child div element. That is, each caret consist of
 * outer div and inner div. Outer div takes responsibility for detecting two
 * carets are overlapping. Inner div is for actual appearance.
 *
 * Here is an explanation of the html class names:
 *   .moz-selectioncaret-left: Indicates start DIV.
 *   .moz-selectioncaret-right: Indicates end DIV.
 *   .hidden: This class name is set by SetVisibility,
 *            SetStartFrameVisibility and SetEndFrameVisibility. Element
 *            with this class name become hidden.
 *   .tilt: This class name is set by SetTilted. According to the
 *          UX spec, when selection carets are overlapping, the image of
 *          caret becomes tilt.
 */
class SelectionCarets final : public nsIReflowObserver,
                                  public nsISelectionListener,
                                  public nsIScrollObserver,
                                  public nsSupportsWeakReference
{
public:
  /**
   * Indicate which part of caret we are dragging at.
   */
  enum DragMode {
    NONE,
    START_FRAME,
    END_FRAME
  };

  explicit SelectionCarets(nsIPresShell *aPresShell);

  NS_DECL_ISUPPORTS
  NS_DECL_NSIREFLOWOBSERVER
  NS_DECL_NSISELECTIONLISTENER

  // Notify selection carets about the blur event to hidden itself
  void NotifyBlur(bool aIsLeavingDocument);

  // nsIScrollObserver
  virtual void ScrollPositionChanged() override;

  // AsyncPanZoom started/stopped callbacks from nsIScrollObserver
  virtual void AsyncPanZoomStarted(const mozilla::CSSIntPoint aScrollPos) override;
  virtual void AsyncPanZoomStopped(const mozilla::CSSIntPoint aScrollPos) override;

  void Init();
  void Terminate();

  nsEventStatus HandleEvent(WidgetEvent* aEvent);

  bool GetVisibility() const
  {
    return mVisible;
  }

  /**
   * Get from pref "selectioncaret.inflatesize.threshold". This will inflate size of
   * caret frame when we checking if user click on caret or not. In app units.
   */
  static int32_t SelectionCaretsInflateSize()
  {
    return sSelectionCaretsInflateSize;
  }

private:
  virtual ~SelectionCarets();

  SelectionCarets() = delete;

  /**
   * Set visibility for selection caret.
   */
  void SetVisibility(bool aVisible);

  /**
   * Update selection caret position base on current selection range.
   */
  void UpdateSelectionCarets();

  /**
   * Select a word base on current position, which activates only if element is
   * selectable. Triggered by long tap event.
   */
  nsresult SelectWord();

  /**
   * Move selection base on current touch/mouse point
   */
  nsEventStatus DragSelection(const nsPoint &movePoint);

  /**
   * Get the vertical center position of selection caret relative to root
   * frame.
   */
  nscoord GetCaretYCenterPosition();

  /**
   * Simulate drag state when we change the selection range.
   * Hence, the selection change event will fire normally.
   */
  void SetSelectionDragState(bool aState);

  void SetSelectionDirection(bool aForward);

  /**
   * Move start frame of selection caret to given position.
   * In app units.
   */
  void SetStartFramePos(const nsPoint& aPosition);

  /**
   * Move end frame of selection caret to given position.
   * In app units.
   */
  void SetEndFramePos(const nsPoint& aPosition);

  /**
   * Check if aPosition is on the start or end frame of the
   * selection caret's inner div element.
   *
   * @param aPosition should be relative to document's root frame
   * in app units
   */
  bool IsOnStartFrameInner(const nsPoint& aPosition);
  bool IsOnEndFrameInner(const nsPoint& aPosition);

  /**
   * Get rect of selection caret's outer div element relative
   * to document's root frame, in app units.
   */
  nsRect GetStartFrameRect();
  nsRect GetEndFrameRect();

  /**
   * Get rect of selection caret's inner div element relative
   * to document's root frame, in app units.
   */
  nsRect GetStartFrameRectInner();
  nsRect GetEndFrameRectInner();

  /**
   * Set visibility for start part of selection caret, this function
   * only affects css property of start frame. So it doesn't change
   * mVisible member. When caret overflows element's box we'll hide
   * it by calling this function.
   */
  void SetStartFrameVisibility(bool aVisible);

  /**
   * Same as above function but for end frame of selection caret.
   */
  void SetEndFrameVisibility(bool aVisible);

  /**
   * Set tilt class name to start and end frame of selection caret.
   */
  void SetTilted(bool aIsTilt);

  // Utility functions
  dom::Selection* GetSelection();
  already_AddRefed<nsFrameSelection> GetFrameSelection();
  nsIContent* GetFocusedContent();
  void DispatchSelectionStateChangedEvent(dom::Selection* aSelection,
                                          dom::SelectionState aState);
  void DispatchSelectionStateChangedEvent(dom::Selection* aSelection,
                                          const dom::Sequence<dom::SelectionState>& aStates);
  void DispatchCustomEvent(const nsAString& aEvent);

  /**
   * Detecting long tap using timer
   */
  void LaunchLongTapDetector();
  void CancelLongTapDetector();
  static void FireLongTap(nsITimer* aTimer, void* aSelectionCarets);

  void LaunchScrollEndDetector();
  void CancelScrollEndDetector();
  static void FireScrollEnd(nsITimer* aTimer, void* aSelectionCarets);

  nsIPresShell* mPresShell;
  WeakPtr<nsDocShell> mDocShell;

  // This timer is used for detecting long tap fire. If content process
  // has APZC, we'll use APZC for long tap detecting. Otherwise, we use this
  // timer to detect long tap.
  nsCOMPtr<nsITimer> mLongTapDetectorTimer;

  // This timer is used for detecting scroll end. We don't have
  // scroll end event now, so we will fire this event with a
  // const time when we scroll. So when timer triggers, we treat it
  // as scroll end event.
  nsCOMPtr<nsITimer> mScrollEndDetectorTimer;

  // When touch or mouse down, we save the position for detecting
  // drag distance
  nsPoint mDownPoint;

  // For filter multitouch event
  int32_t mActiveTouchId;

  nscoord mCaretCenterToDownPointOffsetY;

  // The horizontal boundary is defined by the first selected frame which
  // determines the start-caret position. When users drag the end-caret up,
  // the touch input(pos.y) will be changed to not cross this boundary.
  // Otherwise, the selection range changes to one character only
  // which causes the bad user experience.
  nscoord mDragUpYBoundary;
  // The horizontal boundary is defined by the last selected frame which
  // determines the end-caret position. When users drag the start-caret down,
  // the touch input(pos.y) will be changed to not cross this boundary.
  // Otherwise, the selection range changes to one character only
  // which causes the bad user experience.
  nscoord mDragDownYBoundary;

  DragMode mDragMode;

  // True if AsyncPanZoom is enabled
  bool mAsyncPanZoomEnabled;
  // True if AsyncPanZoom is started
  bool mInAsyncPanZoomGesture;

  bool mEndCaretVisible;
  bool mStartCaretVisible;
  bool mSelectionVisibleInScrollFrames;
  bool mVisible;

  // Preference
  static int32_t sSelectionCaretsInflateSize;
};
} // namespace mozilla

#endif //SelectionCarets_h__
