/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 mozilla_layers_Axis_h
#define mozilla_layers_Axis_h

#include <sys/types.h>  // for int32_t

#include "APZUtils.h"
#include "AxisPhysicsMSDModel.h"
#include "mozilla/DataMutex.h"  // for DataMutex
#include "mozilla/gfx/Types.h"  // for Side
#include "mozilla/TimeStamp.h"  // for TimeDuration
#include "nsTArray.h"           // for nsTArray
#include "Units.h"

namespace mozilla {
namespace layers {

const float EPSILON = 0.0001f;

/**
 * Compare two coordinates for equality, accounting for rounding error.
 * Use both FuzzyEqualsAdditive() with COORDINATE_EPISLON, which accounts for
 * things like the error introduced by rounding during a round-trip to app
 * units, and FuzzyEqualsMultiplicative(), which accounts for accumulated error
 * due to floating-point operations (which can be larger than COORDINATE_EPISLON
 * for sufficiently large coordinate values).
 */
bool FuzzyEqualsCoordinate(CSSCoord aValue1, CSSCoord aValue2);

struct FrameMetrics;
class AsyncPanZoomController;

/**
 * Interface for computing velocities along the axis based on
 * position samples.
 */
class VelocityTracker {
 public:
  virtual ~VelocityTracker() = default;

  /**
   * Start tracking velocity along this axis, starting with the given
   * initial position and corresponding timestamp.
   */
  virtual void StartTracking(ParentLayerCoord aPos, TimeStamp aTimestamp) = 0;
  /**
   * Record a new position along this axis, at the given timestamp.
   * Returns the average velocity between the last sample and this one, or
   * or Nothing() if a reasonable average cannot be computed.
   */
  virtual Maybe<float> AddPosition(ParentLayerCoord aPos,
                                   TimeStamp aTimestamp) = 0;
  /**
   * Compute an estimate of the axis's current velocity, based on recent
   * position samples. It's up to implementation how many samples to consider
   * and how to perform the computation.
   * If the tracker doesn't have enough samples to compute a result, it
   * may return Nothing{}.
   */
  virtual Maybe<float> ComputeVelocity(TimeStamp aTimestamp) = 0;
  /**
   * Clear all state in the velocity tracker.
   */
  virtual void Clear() = 0;
};

/**
 * Helper class to maintain each axis of movement (X,Y) for panning and zooming.
 * Note that everything here is specific to one axis; that is, the X axis knows
 * nothing about the Y axis and vice versa.
 */
class Axis {
 public:
  explicit Axis(AsyncPanZoomController* aAsyncPanZoomController);

  /**
   * Notify this Axis that a new touch has been received, including a timestamp
   * for when the touch was received. This triggers a recalculation of velocity.
   * This can also used for pan gesture events. For those events, |aPos| is
   * an invented position corresponding to the mouse position plus any
   * accumulated displacements over the course of the pan gesture.
   */
  void UpdateWithTouchAtDevicePoint(ParentLayerCoord aPos,
                                    TimeStamp aTimestamp);

 public:
  /**
   * Notify this Axis that a touch has begun, i.e. the user has put their finger
   * on the screen but has not yet tried to pan.
   */
  void StartTouch(ParentLayerCoord aPos, TimeStamp aTimestamp);

  /**
   * Helper enum class for specifying if EndTouch() should clear the axis lock.
   */
  enum class ClearAxisLock { Yes, No };

  /**
   * Notify this Axis that a touch has ended gracefully. This may perform
   * recalculations of the axis velocity.
   */
  void EndTouch(TimeStamp aTimestamp, ClearAxisLock aClearAxisLock);

  /**
   * Notify this Axis that the gesture has ended forcefully. Useful for stopping
   * flings when a user puts their finger down in the middle of one (i.e. to
   * stop a previous touch including its fling so that a new one can take its
   * place).
   */
  void CancelGesture();

  /**
   * Takes a requested displacement to the position of this axis, and adjusts it
   * to account for overscroll (which might decrease the displacement; this is
   * to prevent the viewport from overscrolling the page rect), and axis locking
   * (which might prevent any displacement from happening). If overscroll
   * ocurred, its amount is written to |aOverscrollAmountOut|.
   * The |aDisplacementOut| parameter is set to the adjusted displacement, and
   * the function returns true if and only if internal overscroll amounts were
   * changed.
   */
  bool AdjustDisplacement(ParentLayerCoord aDisplacement,
                          ParentLayerCoord& aDisplacementOut,
                          ParentLayerCoord& aOverscrollAmountOut,
                          bool aForceOverscroll = false);

  /**
   * Overscrolls this axis by the requested amount in the requested direction.
   * The axis must be at the end of its scroll range in this direction.
   */
  void OverscrollBy(ParentLayerCoord aOverscroll);

  /**
   * Return the amount of overscroll on this axis, in ParentLayer pixels.
   *
   * If this amount is nonzero, the relevant component of
   * mAsyncPanZoomController->Metrics().mScrollOffset must be at its
   * extreme allowed value in the relevant direction (that is, it must be at
   * its maximum value if we are overscrolled at our composition length, and
   * at its minimum value if we are overscrolled at the origin).
   */
  ParentLayerCoord GetOverscroll() const;

  /**
   * Restore the amount by which this axis is overscrolled to the specified
   * amount. This is for test-related use; overscrolling as a result of user
   * input should happen via OverscrollBy().
   */
  void RestoreOverscroll(ParentLayerCoord aOverscroll);

  /**
   * Start an overscroll animation with the given initial velocity.
   */
  void StartOverscrollAnimation(float aVelocity);

  /**
   * Sample the snap-back animation to relieve overscroll.
   * |aDelta| is the time since the last sample, |aOverscrollSideBits| is
   * the direction where the overscroll happens on this axis.
   */
  bool SampleOverscrollAnimation(const TimeDuration& aDelta,
                                 SideBits aOverscrollSideBits);

  /**
   * Stop an overscroll animation.
   */
  void EndOverscrollAnimation();

  /**
   * Return whether this axis is overscrolled in either direction.
   */
  bool IsOverscrolled() const;

  /**
   * Return true if this axis is overscrolled but its scroll offset
   * has changed in a way that makes the oversrolled state no longer
   * valid (for example, it is overscrolled at the top but the
   * scroll offset is no longer zero).
   */
  bool IsInInvalidOverscroll() const;

  /**
   * Clear any overscroll amount on this axis.
   */
  void ClearOverscroll();

  /**
   * Returns whether the overscroll animation is alive.
   */
  bool IsOverscrollAnimationAlive() const;

  /**
   * Returns whether the overscroll animation is running.
   * Note that unlike the above IsOverscrollAnimationAlive, this function
   * returns false even if the animation is still there but is very close to
   * the destination position and its velocity is quite low, i.e. it's time to
   * finish.
   */
  bool IsOverscrollAnimationRunning() const;

  /**
   * Gets the starting position of the touch supplied in StartTouch().
   */
  ParentLayerCoord PanStart() const;

  /**
   * Gets the distance between the starting position of the touch supplied in
   * StartTouch() and the current touch from the last
   * UpdateWithTouchAtDevicePoint().
   */
  ParentLayerCoord PanDistance() const;

  /**
   * Gets the distance between the starting position of the touch supplied in
   * StartTouch() and the supplied position.
   */
  ParentLayerCoord PanDistance(ParentLayerCoord aPos) const;

  /**
   * Returns true if the page has room to be scrolled along this axis.
   */
  bool CanScroll() const;

  /**
   * Returns whether this axis can scroll any more in a particular direction.
   */
  bool CanScroll(CSSCoord aDelta) const;
  bool CanScroll(ParentLayerCoord aDelta) const;

  /**
   * Returns true if the page has room to be scrolled along this axis
   * and this axis is not scroll-locked.
   */
  bool CanScrollNow() const;

  /**
   * Clamp a point to the page's scrollable bounds. That is, a scroll
   * destination to the returned point will not contain any overscroll.
   */
  CSSCoord ClampOriginToScrollableRect(CSSCoord aOrigin) const;

  void SetAxisLocked(bool aAxisLocked) { mAxisLocked = aAxisLocked; }

  /**
   * Gets the raw velocity of this axis at this moment.
   */
  float GetVelocity() const;

  /**
   * Sets the raw velocity of this axis at this moment.
   * Intended to be called only when the axis "takes over" a velocity from
   * another APZC, in which case there are no touch points available to call
   * UpdateWithTouchAtDevicePoint. In other circumstances,
   * UpdateWithTouchAtDevicePoint should be used and the velocity calculated
   * there.
   */
  void SetVelocity(float aVelocity);

  /**
   * If a displacement will overscroll the axis, this returns the amount and in
   * what direction.
   */
  ParentLayerCoord DisplacementWillOverscrollAmount(
      ParentLayerCoord aDisplacement) const;

  /**
   * If a scale will overscroll the axis, this returns the amount and in what
   * direction.
   *
   * |aFocus| is the point at which the scale is focused at. We will offset the
   * scroll offset in such a way that it remains in the same place on the page
   * relative.
   *
   * Note: Unlike most other functions in Axis, this functions operates in
   *       CSS coordinates so there is no confusion as to whether the
   *       ParentLayer coordinates it operates in are before or after the scale
   *       is applied.
   */
  CSSCoord ScaleWillOverscrollAmount(float aScale, CSSCoord aFocus) const;

  /**
   * Checks if an axis will overscroll in both directions by computing the
   * content rect and checking that its height/width (depending on the axis)
   * does not overextend past the viewport.
   *
   * This gets called by ScaleWillOverscroll().
   */
  bool ScaleWillOverscrollBothSides(float aScale) const;

  /**
   * Returns true if movement on this axis is locked.
   */
  bool IsAxisLocked() const;

  ParentLayerCoord GetOrigin() const;
  ParentLayerCoord GetCompositionLength() const;
  ParentLayerCoord GetPageStart() const;
  ParentLayerCoord GetPageLength() const;
  ParentLayerCoord GetCompositionEnd() const;
  ParentLayerCoord GetPageEnd() const;
  ParentLayerCoord GetScrollRangeEnd() const;

  bool IsScrolledToStart() const;
  bool IsScrolledToEnd() const;

  ParentLayerCoord GetPos() const { return mPos; }

  bool OverscrollBehaviorAllowsHandoff() const;
  bool OverscrollBehaviorAllowsOverscrollEffect() const;

  virtual CSSToParentLayerScale GetAxisScale(
      const CSSToParentLayerScale2D& aScale) const = 0;
  virtual CSSCoord GetPointOffset(const CSSPoint& aPoint) const = 0;
  virtual OuterCSSCoord GetPointOffset(const OuterCSSPoint& aPoint) const = 0;
  virtual ParentLayerCoord GetPointOffset(
      const ParentLayerPoint& aPoint) const = 0;
  virtual ParentLayerCoord GetRectLength(
      const ParentLayerRect& aRect) const = 0;
  virtual CSSCoord GetRectLength(const CSSRect& aRect) const = 0;
  virtual ParentLayerCoord GetRectOffset(
      const ParentLayerRect& aRect) const = 0;
  virtual CSSCoord GetRectOffset(const CSSRect& aRect) const = 0;
  virtual float GetTransformScale(
      const AsyncTransformComponentMatrix& aMatrix) const = 0;
  virtual ParentLayerCoord GetTransformTranslation(
      const AsyncTransformComponentMatrix& aMatrix) const = 0;
  virtual void PostScale(AsyncTransformComponentMatrix& aMatrix,
                         float aScale) const = 0;
  virtual void PostTranslate(AsyncTransformComponentMatrix& aMatrix,
                             ParentLayerCoord aTranslation) const = 0;

  virtual ScreenPoint MakePoint(ScreenCoord aCoord) const = 0;

  const void* OpaqueApzcPointer() const { return mAsyncPanZoomController; }

  virtual const char* Name() const = 0;

  // Convert a velocity from global inches/ms into ParentLayerCoords/ms.
  float ToLocalVelocity(float aVelocityInchesPerMs) const;

 protected:
  // A position along the axis, used during input event processing to
  // track velocities (and for touch gestures, to track the length of
  // the gesture). For touch events, this represents the position of
  // the finger (or in the case of two-finger scrolling, the midpoint
  // of the two fingers). For pan gesture events, this represents an
  // invented position corresponding to the mouse position at the start
  // of the pan, plus deltas representing the displacement of the pan.
  ParentLayerCoord mPos;

  ParentLayerCoord mStartPos;
  // The velocity can be accessed from multiple threads (e.g. APZ
  // controller thread and APZ sampler thread), so needs to be
  // protected by a mutex.
  // Units: ParentLayerCoords per millisecond
  mutable DataMutex<float> mVelocity;
  bool mAxisLocked;  // Whether movement on this axis is locked.
  AsyncPanZoomController* mAsyncPanZoomController;

  // The amount by which we are overscrolled; see GetOverscroll().
  ParentLayerCoord mOverscroll;

  // The mass-spring-damper model for overscroll physics.
  AxisPhysicsMSDModel mMSDModel;

  // Used to track velocity over a series of input events and compute
  // a resulting velocity to use for e.g. starting a fling animation.
  // This member can only be accessed on the controller/UI thread.
  UniquePtr<VelocityTracker> mVelocityTracker;

  float DoGetVelocity() const;
  void DoSetVelocity(float aVelocity);

  const FrameMetrics& GetFrameMetrics() const;
  const ScrollMetadata& GetScrollMetadata() const;

  // Do not use this function directly, use
  // AsyncPanZoomController::GetAllowedHandoffDirections instead.
  virtual OverscrollBehavior GetOverscrollBehavior() const = 0;

  // Adjust a requested overscroll amount for resistance, yielding a smaller
  // actual overscroll amount.
  ParentLayerCoord ApplyResistance(ParentLayerCoord aOverscroll) const;

  // Helper function for SampleOverscrollAnimation().
  void StepOverscrollAnimation(double aStepDurationMilliseconds);
};

class AxisX : public Axis {
 public:
  explicit AxisX(AsyncPanZoomController* mAsyncPanZoomController);
  CSSToParentLayerScale GetAxisScale(
      const CSSToParentLayerScale2D& aScale) const override;
  CSSCoord GetPointOffset(const CSSPoint& aPoint) const override;
  OuterCSSCoord GetPointOffset(const OuterCSSPoint& aPoint) const override;
  ParentLayerCoord GetPointOffset(
      const ParentLayerPoint& aPoint) const override;
  ParentLayerCoord GetRectLength(const ParentLayerRect& aRect) const override;
  CSSCoord GetRectLength(const CSSRect& aRect) const override;
  ParentLayerCoord GetRectOffset(const ParentLayerRect& aRect) const override;
  CSSCoord GetRectOffset(const CSSRect& aRect) const override;
  float GetTransformScale(
      const AsyncTransformComponentMatrix& aMatrix) const override;
  ParentLayerCoord GetTransformTranslation(
      const AsyncTransformComponentMatrix& aMatrix) const override;
  void PostScale(AsyncTransformComponentMatrix& aMatrix,
                 float aScale) const override;
  void PostTranslate(AsyncTransformComponentMatrix& aMatrix,
                     ParentLayerCoord aTranslation) const override;
  ScreenPoint MakePoint(ScreenCoord aCoord) const override;
  const char* Name() const override;
  bool CanScrollTo(Side aSide) const;
  SideBits ScrollableDirections() const;

 private:
  OverscrollBehavior GetOverscrollBehavior() const override;
};

class AxisY : public Axis {
 public:
  explicit AxisY(AsyncPanZoomController* mAsyncPanZoomController);
  CSSCoord GetPointOffset(const CSSPoint& aPoint) const override;
  OuterCSSCoord GetPointOffset(const OuterCSSPoint& aPoint) const override;
  ParentLayerCoord GetPointOffset(
      const ParentLayerPoint& aPoint) const override;
  CSSToParentLayerScale GetAxisScale(
      const CSSToParentLayerScale2D& aScale) const override;
  ParentLayerCoord GetRectLength(const ParentLayerRect& aRect) const override;
  CSSCoord GetRectLength(const CSSRect& aRect) const override;
  ParentLayerCoord GetRectOffset(const ParentLayerRect& aRect) const override;
  CSSCoord GetRectOffset(const CSSRect& aRect) const override;
  float GetTransformScale(
      const AsyncTransformComponentMatrix& aMatrix) const override;
  ParentLayerCoord GetTransformTranslation(
      const AsyncTransformComponentMatrix& aMatrix) const override;
  void PostScale(AsyncTransformComponentMatrix& aMatrix,
                 float aScale) const override;
  void PostTranslate(AsyncTransformComponentMatrix& aMatrix,
                     ParentLayerCoord aTranslation) const override;
  ScreenPoint MakePoint(ScreenCoord aCoord) const override;
  const char* Name() const override;
  bool CanScrollTo(Side aSide) const;
  bool CanVerticalScrollWithDynamicToolbar() const;
  SideBits ScrollableDirections() const;
  SideBits ScrollableDirectionsWithDynamicToolbar(
      const ScreenMargin& aFixedLayerMargins) const;

 private:
  OverscrollBehavior GetOverscrollBehavior() const override;
  ParentLayerCoord GetCompositionLengthWithoutDynamicToolbar() const;
  bool HasDynamicToolbar() const;
};

}  // namespace layers
}  // namespace mozilla

#endif
