/* 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_Overscroll_h
#define mozilla_layers_Overscroll_h

#include "AsyncPanZoomAnimation.h"
#include "AsyncPanZoomController.h"
#include "mozilla/TimeStamp.h"
#include "nsThreadUtils.h"

namespace mozilla {
namespace layers {

// Animation used by GenericOverscrollEffect.
class OverscrollAnimation : public AsyncPanZoomAnimation {
 public:
  OverscrollAnimation(AsyncPanZoomController& aApzc,
                      const ParentLayerPoint& aVelocity,
                      SideBits aOverscrollSideBits)
      : mApzc(aApzc), mOverscrollSideBits(aOverscrollSideBits) {
    MOZ_ASSERT(
        (mOverscrollSideBits & SideBits::eTopBottom) != SideBits::eTopBottom &&
            (mOverscrollSideBits & SideBits::eLeftRight) !=
                SideBits::eLeftRight,
        "Don't allow overscrolling on both sides at the same time");
    if ((aOverscrollSideBits & SideBits::eLeftRight) != SideBits::eNone) {
      mApzc.mX.StartOverscrollAnimation(aVelocity.x);
    }
    if ((aOverscrollSideBits & SideBits::eTopBottom) != SideBits::eNone) {
      mApzc.mY.StartOverscrollAnimation(aVelocity.y);
    }
  }
  virtual ~OverscrollAnimation() {
    mApzc.mX.EndOverscrollAnimation();
    mApzc.mY.EndOverscrollAnimation();
  }

  virtual bool DoSample(FrameMetrics& aFrameMetrics,
                        const TimeDuration& aDelta) override {
    // Can't inline these variables due to short-circuit evaluation.
    bool continueX = mApzc.mX.IsOverscrollAnimationAlive() &&
                     mApzc.mX.SampleOverscrollAnimation(
                         aDelta, mOverscrollSideBits & SideBits::eLeftRight);
    bool continueY = mApzc.mY.IsOverscrollAnimationAlive() &&
                     mApzc.mY.SampleOverscrollAnimation(
                         aDelta, mOverscrollSideBits & SideBits::eTopBottom);
    if (!continueX && !continueY) {
      // If we got into overscroll from a fling, that fling did not request a
      // fling snap to avoid a resulting scrollTo from cancelling the overscroll
      // animation too early. We do still want to request a fling snap, though,
      // in case the end of the axis at which we're overscrolled is not a valid
      // snap point, so we request one now. If there are no snap points, this
      // will do nothing. If there are snap points, we'll get a scrollTo that
      // snaps us back to the nearest valid snap point. The scroll snapping is
      // done in a deferred task, otherwise the state change to NOTHING caused
      // by the overscroll animation ending would clobber a possible state
      // change to SMOOTH_SCROLL in ScrollSnap().
      mDeferredTasks.AppendElement(NewRunnableMethod<ScrollSnapFlags>(
          "layers::AsyncPanZoomController::ScrollSnap", &mApzc,
          &AsyncPanZoomController::ScrollSnap,
          ScrollSnapFlags::IntendedEndPosition));
      return false;
    }
    return true;
  }

  virtual bool WantsRepaints() override { return false; }

  // Tell the overscroll animation about the pan momentum event. For each axis,
  // the overscroll animation may start, stop, or continue managing that axis in
  // response to the pan momentum event
  void HandlePanMomentum(const ParentLayerPoint& aDisplacement) {
    float xOverscroll = mApzc.mX.GetOverscroll();
    if ((xOverscroll > 0 && aDisplacement.x > 0) ||
        (xOverscroll < 0 && aDisplacement.x < 0)) {
      if (!mApzc.mX.IsOverscrollAnimationRunning()) {
        // Start a new overscroll animation on this axis, if there is no
        // overscroll animation running and if the pan momentum displacement
        // the pan momentum displacement is the same direction of the current
        // overscroll.
        mApzc.mX.StartOverscrollAnimation(mApzc.mX.GetVelocity());
        mOverscrollSideBits |=
            xOverscroll > 0 ? SideBits::eRight : SideBits::eLeft;
      }
    } else if ((xOverscroll > 0 && aDisplacement.x < 0) ||
               (xOverscroll < 0 && aDisplacement.x > 0)) {
      // Otherwise, stop the animation in the direction so that it won't clobber
      // subsequent pan momentum scrolling.
      mApzc.mX.EndOverscrollAnimation();
    }

    // Same as above but for Y axis.
    float yOverscroll = mApzc.mY.GetOverscroll();
    if ((yOverscroll > 0 && aDisplacement.y > 0) ||
        (yOverscroll < 0 && aDisplacement.y < 0)) {
      if (!mApzc.mY.IsOverscrollAnimationRunning()) {
        mApzc.mY.StartOverscrollAnimation(mApzc.mY.GetVelocity());
        mOverscrollSideBits |=
            yOverscroll > 0 ? SideBits::eBottom : SideBits::eTop;
      }
    } else if ((yOverscroll > 0 && aDisplacement.y < 0) ||
               (yOverscroll < 0 && aDisplacement.y > 0)) {
      mApzc.mY.EndOverscrollAnimation();
    }
  }

  ScrollDirections GetDirections() const {
    ScrollDirections directions;
    if (mApzc.mX.IsOverscrollAnimationRunning()) {
      directions += ScrollDirection::eHorizontal;
    }
    if (mApzc.mY.IsOverscrollAnimationRunning()) {
      directions += ScrollDirection::eVertical;
    }
    return directions;
  };

  OverscrollAnimation* AsOverscrollAnimation() override { return this; }

  bool IsManagingXAxis() const {
    return mApzc.mX.IsOverscrollAnimationRunning();
  }
  bool IsManagingYAxis() const {
    return mApzc.mY.IsOverscrollAnimationRunning();
  }

 private:
  AsyncPanZoomController& mApzc;
  SideBits mOverscrollSideBits;
};

// Base class for different overscroll effects;
class OverscrollEffectBase {
 public:
  virtual ~OverscrollEffectBase() = default;

  // Try to increase the amount of overscroll by |aOverscroll|. Limited to
  // directions contained in |aOverscrollableDirections|. Components of
  // |aOverscroll| in directions that are successfully consumed are dropped.
  virtual void ConsumeOverscroll(
      ParentLayerPoint& aOverscroll,
      ScrollDirections aOverscrollableDirections) = 0;

  // Relieve overscroll. Depending on the implementation, the relief may
  // be immediate, or gradual (e.g. after an animation) but this starts
  // the process. |aVelocity| is the current velocity of the APZC, and
  // |aOverscrollSideBits| contains the side(s) at which the APZC is
  // overscrolled.
  virtual void RelieveOverscroll(const ParentLayerPoint& aVelocity,
                                 SideBits aOverscrollSideBits) = 0;

  virtual bool IsOverscrolled() const = 0;

  // Similarly to RelieveOverscroll(), but has immediate effect
  // (no animation).
  virtual void ClearOverscroll() = 0;
};

// A generic overscroll effect, implemented by AsyncPanZoomController itself.
class GenericOverscrollEffect : public OverscrollEffectBase {
 public:
  explicit GenericOverscrollEffect(AsyncPanZoomController& aApzc)
      : mApzc(aApzc) {}

  void ConsumeOverscroll(ParentLayerPoint& aOverscroll,
                         ScrollDirections aOverscrollableDirections) override {
    if (aOverscrollableDirections.contains(ScrollDirection::eHorizontal)) {
      mApzc.mX.OverscrollBy(aOverscroll.x);
      aOverscroll.x = 0;
    }

    if (aOverscrollableDirections.contains(ScrollDirection::eVertical)) {
      mApzc.mY.OverscrollBy(aOverscroll.y);
      aOverscroll.y = 0;
    }

    if (!aOverscrollableDirections.isEmpty()) {
      mApzc.ScheduleComposite();
    }
  }

  void RelieveOverscroll(const ParentLayerPoint& aVelocity,
                         SideBits aOverscrollSideBits) override {
    mApzc.StartOverscrollAnimation(aVelocity, aOverscrollSideBits);
  }

  bool IsOverscrolled() const override {
    return mApzc.IsPhysicallyOverscrolled();
  }

  void ClearOverscroll() override { mApzc.ClearPhysicalOverscroll(); }

 private:
  AsyncPanZoomController& mApzc;
};

// A widget-specific overscroll effect, implemented by the widget via
// GeckoContentController.
class WidgetOverscrollEffect : public OverscrollEffectBase {
 public:
  explicit WidgetOverscrollEffect(AsyncPanZoomController& aApzc)
      : mApzc(aApzc), mIsOverscrolled(false) {}

  void ConsumeOverscroll(ParentLayerPoint& aOverscroll,
                         ScrollDirections aOverscrollableDirections) override {
    RefPtr<GeckoContentController> controller =
        mApzc.GetGeckoContentController();
    if (!aOverscrollableDirections.contains(ScrollDirection::eHorizontal)) {
      aOverscroll.x = 0;
    }

    if (!aOverscrollableDirections.contains(ScrollDirection::eVertical)) {
      aOverscroll.y = 0;
    }

    if (controller && !aOverscrollableDirections.isEmpty()) {
      mIsOverscrolled = true;
      controller->UpdateOverscrollOffset(mApzc.GetGuid(), aOverscroll.x,
                                         aOverscroll.y, mApzc.IsRootContent());
      aOverscroll = ParentLayerPoint();
    }
  }

  void RelieveOverscroll(const ParentLayerPoint& aVelocity,
                         SideBits aOverscrollSideBits) override {
    if (!mIsOverscrolled) {
      return;
    }
    RefPtr<GeckoContentController> controller =
        mApzc.GetGeckoContentController();
    // From APZC's point of view, consider it to no longer be overscrolled
    // as soon as RelieveOverscroll() is called. The widget may use a
    // delay or animation until the relieving of the overscroll is complete,
    // but we don't have any insight into that.
    mIsOverscrolled = false;
    if (controller) {
      controller->UpdateOverscrollVelocity(mApzc.GetGuid(), aVelocity.x,
                                           aVelocity.y, mApzc.IsRootContent());
    }
  }

  bool IsOverscrolled() const override { return mIsOverscrolled; }

  void ClearOverscroll() override {
    RelieveOverscroll(ParentLayerPoint(), SideBits() /* ignored */);
  }

 private:
  AsyncPanZoomController& mApzc;
  bool mIsOverscrolled;
};

}  // namespace layers
}  // namespace mozilla

#endif  // mozilla_layers_Overscroll_h
