/* 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/. */

#include "ScrollThumbUtils.h"
#include "AsyncPanZoomController.h"
#include "FrameMetrics.h"
#include "UnitTransforms.h"
#include "Units.h"
#include "gfxPlatform.h"
#include "mozilla/gfx/Matrix.h"
#include "mozilla/StaticPrefs_toolkit.h"

namespace mozilla {
namespace layers {
namespace apz {

struct AsyncScrollThumbTransformer {
  // Inputs
  const LayerToParentLayerMatrix4x4& mCurrentTransform;
  const gfx::Matrix4x4& mScrollableContentTransform;
  AsyncPanZoomController* mApzc;
  const FrameMetrics& mMetrics;
  const ScrollbarData& mScrollbarData;
  bool mScrollbarIsDescendant;

  // Intermediate results
  AsyncTransformComponentMatrix mAsyncTransform;
  AsyncTransformComponentMatrix mScrollbarTransform;

  LayerToParentLayerMatrix4x4 ComputeTransform();

 private:
  // Helper functions for ComputeTransform().

  // If the thumb's orientation is along |aAxis|, add transformations
  // of the thumb into |mScrollbarTransform|.
  void ApplyTransformForAxis(const Axis& aAxis);

  enum class ScrollThumbExtent { Start, End };

  // Scale the thumb by |aScale| along |aAxis|, while keeping constant the
  // position of the top denoted by |aExtent|.
  void ScaleThumbBy(const Axis& aAxis, float aScale, ScrollThumbExtent aExtent);

  // Translate the thumb along |aAxis| by |aTranslation| in "scrollbar space"
  // (CSS pixels along the scrollbar track, similar to e.g.
  // |mScrollbarData.mThumbStart|).
  void TranslateThumb(const Axis& aAxis, OuterCSSCoord aTranslation);
};

void AsyncScrollThumbTransformer::TranslateThumb(const Axis& aAxis,
                                                 OuterCSSCoord aTranslation) {
  aAxis.PostTranslate(
      mScrollbarTransform,
      ViewAs<CSSPixel>(aTranslation,
                       PixelCastJustification::CSSPixelsOfSurroundingContent) *
          mMetrics.GetDevPixelsPerCSSPixel() *
          LayoutDeviceToParentLayerScale(1.0));
}

void AsyncScrollThumbTransformer::ScaleThumbBy(const Axis& aAxis, float aScale,
                                               ScrollThumbExtent aExtent) {
  // To keep the position of the top of the thumb constant, the thumb needs to
  // translated to compensate for the scale applied. The origin with respect to
  // which the scale is applied is the origin of the layer tree, rather than
  // the origin of the scroll thumb. This means that the space between the
  // origin and the top of thumb (including the part of the scrollbar track
  // above the thumb, the part of the scrollbar above the track (i.e. a
  // scrollbar button if present), plus whatever content is above the scroll
  // frame) is scaled too, effectively translating the thumb. We undo that
  // translation here. (One can think of the adjustment being done to the
  // translation here as a change of basis. We have a method to help with that,
  // Matrix4x4::ChangeBasis(), but it wouldn't necessarily make the code cleaner
  // in this case).
  const OuterCSSCoord scrollTrackOrigin =
      aAxis.GetPointOffset(
          mMetrics.CalculateCompositionBoundsInOuterCssPixels().TopLeft()) +
      mScrollbarData.mScrollTrackStart;
  OuterCSSCoord thumbExtent = scrollTrackOrigin + mScrollbarData.mThumbStart;
  if (aExtent == ScrollThumbExtent::End) {
    thumbExtent += mScrollbarData.mThumbLength;
  }
  const OuterCSSCoord thumbExtentScaled = thumbExtent * aScale;
  const OuterCSSCoord thumbExtentDelta = thumbExtentScaled - thumbExtent;

  aAxis.PostScale(mScrollbarTransform, aScale);
  TranslateThumb(aAxis, -thumbExtentDelta);
}

void AsyncScrollThumbTransformer::ApplyTransformForAxis(const Axis& aAxis) {
  ParentLayerCoord asyncScroll = aAxis.GetTransformTranslation(mAsyncTransform);
  const float asyncZoom = aAxis.GetTransformScale(mAsyncTransform);
  const ParentLayerCoord overscroll =
      aAxis.GetPointOffset(mApzc->GetOverscrollAmount());

  bool haveAsyncZoom = !FuzzyEqualsAdditive(asyncZoom, 1.f);
  if (!haveAsyncZoom && mApzc->IsZero(asyncScroll) &&
      mApzc->IsZero(overscroll)) {
    return;
  }

  OuterCSSCoord translation;
  float scale = 1.0;

  bool recalcMode = StaticPrefs::apz_scrollthumb_recalc();
  if (recalcMode) {
    // In this branch (taken when apz.scrollthumb.recalc=true), |translation|
    // and |scale| are computed using the approach implemented in bug 1554795
    // of fully recalculating the desired position and size using the logic
    // that attempts to closely match the main-thread calculation.

    const CSSRect visualViewportRect = mApzc->GetCurrentAsyncVisualViewport(
        AsyncPanZoomController::eForCompositing);
    const CSSCoord visualViewportLength =
        aAxis.GetRectLength(visualViewportRect);

    const CSSCoord maxMinPosDifference =
        CSSCoord(
            aAxis.GetRectLength(mMetrics.GetScrollableRect()).Truncated()) -
        visualViewportLength;

    OuterCSSCoord effectiveThumbLength = mScrollbarData.mThumbLength;

    if (haveAsyncZoom) {
      // The calculations here closely follow the main thread calculations at
      // https://searchfox.org/mozilla-central/rev/0bf957f909ae1f3d19b43fd4edfc277342554836/layout/generic/nsGfxScrollFrame.cpp#6902-6927
      // and
      // https://searchfox.org/mozilla-central/rev/0bf957f909ae1f3d19b43fd4edfc277342554836/layout/xul/nsSliderFrame.cpp#587-614
      // Any modifications there should be reflected here as well.
      const CSSCoord pageIncrementMin =
          static_cast<int>(visualViewportLength * 0.8);
      CSSCoord pageIncrement;

      CSSToLayoutDeviceScale deviceScale = mMetrics.GetDevPixelsPerCSSPixel();
      if (*mScrollbarData.mDirection == ScrollDirection::eVertical) {
        const CSSCoord lineScrollAmount =
            (mApzc->GetScrollMetadata().GetLineScrollAmount() / deviceScale)
                .height;
        const double kScrollMultiplier =
            StaticPrefs::toolkit_scrollbox_verticalScrollDistance();
        CSSCoord increment = lineScrollAmount * kScrollMultiplier;

        pageIncrement =
            std::max(visualViewportLength - increment, pageIncrementMin);
      } else {
        pageIncrement = pageIncrementMin;
      }

      float ratio = pageIncrement / (maxMinPosDifference + pageIncrement);

      OuterCSSCoord desiredThumbLength{
          std::max(mScrollbarData.mThumbMinLength,
                   mScrollbarData.mScrollTrackLength * ratio)};

      // Round the thumb length to an integer number of LayoutDevice pixels, to
      // match the main-thread behaviour.
      auto outerDeviceScale = ViewAs<OuterCSSToLayoutDeviceScale>(
          deviceScale, PixelCastJustification::CSSPixelsOfSurroundingContent);
      desiredThumbLength =
          LayoutDeviceCoord((desiredThumbLength * outerDeviceScale).Rounded()) /
          outerDeviceScale;

      effectiveThumbLength = desiredThumbLength;

      scale = desiredThumbLength / mScrollbarData.mThumbLength;
    }

    // Subtracting the offset of the scrollable rect is needed for right-to-left
    // pages.
    const CSSCoord curPos = aAxis.GetRectOffset(visualViewportRect) -
                            aAxis.GetRectOffset(mMetrics.GetScrollableRect());

    const CSSToOuterCSSScale thumbPosRatio(
        (maxMinPosDifference != 0)
            ? float((mScrollbarData.mScrollTrackLength - effectiveThumbLength) /
                    maxMinPosDifference)
            : 1.f);

    const OuterCSSCoord desiredThumbPos = curPos * thumbPosRatio;

    translation = desiredThumbPos - mScrollbarData.mThumbStart;
  } else {
    // In this branch (taken when apz.scrollthumb.recalc=false), |translation|
    // and |scale| are computed using the pre-bug1554795 approach of turning
    // the async scroll and zoom deltas into transforms to apply to the
    // main-thread thumb position and size.

    // The scroll thumb needs to be scaled in the direction of scrolling by the
    // inverse of the async zoom. This is because zooming in decreases the
    // fraction of the whole srollable rect that is in view.
    scale = 1.f / asyncZoom;

    // Note: |metrics.GetZoom()| doesn't yet include the async zoom.
    CSSToParentLayerScale effectiveZoom =
        CSSToParentLayerScale(mMetrics.GetZoom().scale * asyncZoom);

    if (gfxPlatform::UseDesktopZoomingScrollbars()) {
      // As computed by GetCurrentAsyncTransform, asyncScrollY is
      //   asyncScrollY = -(GetEffectiveScrollOffset -
      //   mLastContentPaintMetrics.GetLayoutScrollOffset()) *
      //   effectiveZoom
      // where GetEffectiveScrollOffset includes the visual viewport offset that
      // the main thread knows about plus any async scrolling to the visual
      // viewport offset that the main thread does not (yet) know about. We want
      // asyncScrollY to be
      //   asyncScrollY = -(GetEffectiveScrollOffset -
      //   mLastContentPaintMetrics.GetVisualScrollOffset()) * effectiveZoom
      // because the main thread positions the scrollbars at the visual viewport
      // offset that it knows about. (aMetrics is mLastContentPaintMetrics)

      asyncScroll -= aAxis.GetPointOffset((mMetrics.GetLayoutScrollOffset() -
                                           mMetrics.GetVisualScrollOffset()) *
                                          effectiveZoom);
    }

    // Here we convert the scrollbar thumb ratio into a true unitless ratio by
    // dividing out the conversion factor from the scrollframe's parent's space
    // to the scrollframe's space.
    float unitlessThumbRatio = mScrollbarData.mThumbRatio /
                               (mMetrics.GetPresShellResolution() * asyncZoom);

    // The scroll thumb needs to be translated in opposite direction of the
    // async scroll. This is because scrolling down, which translates the layer
    // content up, should result in moving the scroll thumb down.
    ParentLayerCoord translationPL = -asyncScroll * unitlessThumbRatio;

    // The translation we computed is in the scroll frame's ParentLayer space.
    // This includes the full cumulative resolution, even if we are a subframe.
    // However, the resulting transform is used in a context where the scrollbar
    // is already subject to the resolutions of enclosing scroll frames. To
    // avoid double application of these enclosing resolutions, divide them out,
    // leaving only the local resolution if any.
    translationPL /= (mMetrics.GetCumulativeResolution().scale /
                      mMetrics.GetPresShellResolution());

    // Convert translation to CSS pixels as this is what TranslateThumb expects.
    translation = ViewAs<OuterCSSPixel>(
        translationPL / (mMetrics.GetDevPixelsPerCSSPixel() *
                         LayoutDeviceToParentLayerScale(1.0)),
        PixelCastJustification::CSSPixelsOfSurroundingContent);
  }

  // When scaling the thumb to account for the async zoom, keep the position
  // of the start of the thumb (which corresponds to the scroll offset)
  // constant.
  if (haveAsyncZoom) {
    ScaleThumbBy(aAxis, scale, ScrollThumbExtent::Start);
  }

  // If the page is overscrolled, additionally squish the thumb in accordance
  // with the overscroll amount.
  if (overscroll != 0) {
    ParentLayerCoord compBoundsLength =
        aAxis.GetRectLength(mMetrics.GetCompositionBounds());

    // It's possible for the overscroll amount to be larger than the length
    // of the composition bounds, if an overscroll animation was started with
    // a very large velocity.
    float overscrollProportion =
        std::min(std::abs(overscroll.value), compBoundsLength.value) /
        compBoundsLength.value;

    float overscrollScale = 1.0f - overscrollProportion;
    MOZ_ASSERT(overscrollScale >= 0.0f && overscrollScale <= 1.0f);
    // If we're overscrolled at the top, keep the top of the thumb in place
    // as we squish it. If we're overscrolled at the bottom, keep the bottom of
    // the thumb in place.
    ScaleThumbBy(
        aAxis, overscrollScale,
        overscroll < 0 ? ScrollThumbExtent::Start : ScrollThumbExtent::End);
  }

  TranslateThumb(aAxis, translation);
}

LayerToParentLayerMatrix4x4 AsyncScrollThumbTransformer::ComputeTransform() {
  // We only apply the transform if the scroll-target layer has non-container
  // children (i.e. when it has some possibly-visible content). This is to
  // avoid moving scroll-bars in the situation that only a scroll information
  // layer has been built for a scroll frame, as this would result in a
  // disparity between scrollbars and visible content.
  if (mMetrics.IsScrollInfoLayer()) {
    return LayerToParentLayerMatrix4x4{};
  }

  MOZ_RELEASE_ASSERT(mApzc);

  mAsyncTransform =
      mApzc->GetCurrentAsyncTransform(AsyncPanZoomController::eForCompositing);

  // |mAsyncTransform| represents the amount by which we have scrolled and
  // zoomed since the last paint. Because the scrollbar was sized and positioned
  // based on the painted content, we need to adjust it based on asyncTransform
  // so that it reflects what the user is actually seeing now.
  if (*mScrollbarData.mDirection == ScrollDirection::eVertical) {
    ApplyTransformForAxis(mApzc->mY);
  }
  if (*mScrollbarData.mDirection == ScrollDirection::eHorizontal) {
    ApplyTransformForAxis(mApzc->mX);
  }

  LayerToParentLayerMatrix4x4 transform =
      mCurrentTransform * mScrollbarTransform;

  AsyncTransformComponentMatrix compensation;
  // If the scrollbar layer is a child of the content it is a scrollbar for,
  // then we need to adjust for any async transform (including an overscroll
  // transform) on the content. This needs to be cancelled out because layout
  // positions and sizes the scrollbar on the assumption that there is no async
  // transform, and without this adjustment the scrollbar will end up in the
  // wrong place.
  //
  // Note that since the async transform is applied on top of the content's
  // regular transform, we need to make sure to unapply the async transform in
  // the same coordinate space. This requires applying the content transform
  // and then unapplying it after unapplying the async transform.
  if (mScrollbarIsDescendant) {
    AsyncTransformComponentMatrix overscroll =
        mApzc->GetOverscrollTransform(AsyncPanZoomController::eForCompositing);
    gfx::Matrix4x4 asyncUntransform =
        (mAsyncTransform * overscroll).Inverse().ToUnknownMatrix();
    const gfx::Matrix4x4& contentTransform = mScrollableContentTransform;
    gfx::Matrix4x4 contentUntransform = contentTransform.Inverse();

    compensation *= ViewAs<AsyncTransformComponentMatrix>(
        contentTransform * asyncUntransform * contentUntransform);
  }
  transform = transform * compensation;

  return transform;
}

LayerToParentLayerMatrix4x4 ComputeTransformForScrollThumb(
    const LayerToParentLayerMatrix4x4& aCurrentTransform,
    const gfx::Matrix4x4& aScrollableContentTransform,
    AsyncPanZoomController* aApzc, const FrameMetrics& aMetrics,
    const ScrollbarData& aScrollbarData, bool aScrollbarIsDescendant) {
  return AsyncScrollThumbTransformer{
      aCurrentTransform, aScrollableContentTransform, aApzc, aMetrics,
      aScrollbarData,    aScrollbarIsDescendant}
      .ComputeTransform();
}

}  // namespace apz
}  // namespace layers
}  // namespace mozilla
