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

#include "WheelHandlingHelper.h"

#include <utility>  // for std::swap

#include "mozilla/EventDispatcher.h"
#include "mozilla/EventStateManager.h"
#include "mozilla/MouseEvents.h"
#include "mozilla/Preferences.h"
#include "mozilla/PresShell.h"
#include "mozilla/StaticPrefs_mousewheel.h"
#include "mozilla/StaticPrefs_test.h"
#include "mozilla/TextControlElement.h"
#include "mozilla/dom/WheelEventBinding.h"
#include "nsCOMPtr.h"
#include "nsContentUtils.h"
#include "nsIContent.h"
#include "nsIContentInlines.h"
#include "mozilla/dom/Document.h"
#include "DocumentInlines.h"  // for Document and HTMLBodyElement
#include "nsIScrollableFrame.h"
#include "nsITimer.h"
#include "nsPresContext.h"
#include "prtime.h"
#include "Units.h"
#include "ScrollAnimationPhysics.h"

namespace mozilla {

/******************************************************************/
/* mozilla::DeltaValues                                           */
/******************************************************************/

DeltaValues::DeltaValues(WidgetWheelEvent* aEvent)
    : deltaX(aEvent->mDeltaX), deltaY(aEvent->mDeltaY) {}

/******************************************************************/
/* mozilla::WheelHandlingUtils                                    */
/******************************************************************/

/* static */
bool WheelHandlingUtils::CanScrollInRange(nscoord aMin, nscoord aValue,
                                          nscoord aMax, double aDirection) {
  return aDirection > 0.0 ? aValue < static_cast<double>(aMax)
                          : static_cast<double>(aMin) < aValue;
}

/* static */
bool WheelHandlingUtils::CanScrollOn(nsIFrame* aFrame, double aDirectionX,
                                     double aDirectionY) {
  nsIScrollableFrame* scrollableFrame = do_QueryFrame(aFrame);
  if (!scrollableFrame) {
    return false;
  }
  return CanScrollOn(scrollableFrame, aDirectionX, aDirectionY);
}

/* static */
bool WheelHandlingUtils::CanScrollOn(nsIScrollableFrame* aScrollFrame,
                                     double aDirectionX, double aDirectionY) {
  MOZ_ASSERT(aScrollFrame);
  NS_ASSERTION(aDirectionX || aDirectionY,
               "One of the delta values must be non-zero at least");

  nsPoint scrollPt = aScrollFrame->GetVisualViewportOffset();
  nsRect scrollRange = aScrollFrame->GetScrollRangeForUserInputEvents();
  layers::ScrollDirections directions =
      aScrollFrame->GetAvailableScrollingDirectionsForUserInputEvents();

  return ((aDirectionX != 0.0) &&
          (directions.contains(layers::ScrollDirection::eHorizontal)) &&
          CanScrollInRange(scrollRange.x, scrollPt.x, scrollRange.XMost(),
                           aDirectionX)) ||
         ((aDirectionY != 0.0) &&
          (directions.contains(layers::ScrollDirection::eVertical)) &&
          CanScrollInRange(scrollRange.y, scrollPt.y, scrollRange.YMost(),
                           aDirectionY));
}

/*static*/ Maybe<layers::ScrollDirection>
WheelHandlingUtils::GetDisregardedWheelScrollDirection(const nsIFrame* aFrame) {
  nsIContent* content = aFrame->GetContent();
  if (!content) {
    return Nothing();
  }
  TextControlElement* textControlElement = TextControlElement::FromNodeOrNull(
      content->IsInNativeAnonymousSubtree()
          ? content->GetClosestNativeAnonymousSubtreeRootParentOrHost()
          : content);
  if (!textControlElement || !textControlElement->IsSingleLineTextControl()) {
    return Nothing();
  }
  // Disregard scroll in the block-flow direction by mouse wheel on a
  // single-line text control. For instance, in tranditional Chinese writing
  // system, a single-line text control cannot be scrolled horizontally with
  // mouse wheel even if they overflow at the right and left edges; Whereas in
  // latin-based writing system, a single-line text control cannot be scrolled
  // vertically with mouse wheel even if they overflow at the top and bottom
  // edges
  return Some(aFrame->GetWritingMode().IsVertical()
                  ? layers::ScrollDirection::eHorizontal
                  : layers::ScrollDirection::eVertical);
}

/******************************************************************/
/* mozilla::WheelTransaction                                      */
/******************************************************************/

AutoWeakFrame WheelTransaction::sScrollTargetFrame(nullptr);
AutoWeakFrame WheelTransaction::sEventTargetFrame(nullptr);
bool WheelTransaction::sHandledByApz(false);
uint32_t WheelTransaction::sTime = 0;
uint32_t WheelTransaction::sMouseMoved = 0;
nsITimer* WheelTransaction::sTimer = nullptr;
int32_t WheelTransaction::sScrollSeriesCounter = 0;
bool WheelTransaction::sOwnScrollbars = false;

/* static */
bool WheelTransaction::OutOfTime(uint32_t aBaseTime, uint32_t aThreshold) {
  uint32_t now = PR_IntervalToMilliseconds(PR_IntervalNow());
  return (now - aBaseTime > aThreshold);
}

/* static */
void WheelTransaction::OwnScrollbars(bool aOwn) { sOwnScrollbars = aOwn; }

/* static */
void WheelTransaction::BeginTransaction(nsIFrame* aScrollTargetFrame,
                                        nsIFrame* aEventTargetFrame,
                                        const WidgetWheelEvent* aEvent) {
  NS_ASSERTION(!sScrollTargetFrame && !sEventTargetFrame,
               "previous transaction is not finished!");
  MOZ_ASSERT(aEvent->mMessage == eWheel,
             "Transaction must be started with a wheel event");

  ScrollbarsForWheel::OwnWheelTransaction(false);
  sScrollTargetFrame = aScrollTargetFrame;

  // Only set the static event target if wheel event groups are enabled.
  if (StaticPrefs::dom_event_wheel_event_groups_enabled()) {
    // Set a static event target for the wheel transaction. This will be used
    // to override the event target frame when computing the event target from
    // input coordinates. When this preference is not set or there is no stored
    // event target for the current wheel transaction, the event target will
    // not be overridden by the current wheel transaction, but will be computed
    // from the input coordinates.
    sEventTargetFrame = aEventTargetFrame;
    // If the wheel events will be handled by APZ, set a flag here. We can use
    // this later to determine if we need to scroll snap at the end of the
    // wheel operation.
    sHandledByApz = aEvent->mFlags.mHandledByAPZ;
  }

  sScrollSeriesCounter = 0;
  if (!UpdateTransaction(aEvent)) {
    NS_ERROR("BeginTransaction is called even cannot scroll the frame");
    EndTransaction();
  }
}

/* static */
bool WheelTransaction::UpdateTransaction(const WidgetWheelEvent* aEvent) {
  nsIFrame* scrollToFrame = GetScrollTargetFrame();
  nsIScrollableFrame* scrollableFrame = scrollToFrame->GetScrollTargetFrame();
  if (scrollableFrame) {
    scrollToFrame = do_QueryFrame(scrollableFrame);
  }

  if (!WheelHandlingUtils::CanScrollOn(scrollToFrame, aEvent->mDeltaX,
                                       aEvent->mDeltaY)) {
    OnFailToScrollTarget();
    // We should not modify the transaction state when the view will not be
    // scrolled actually.
    return false;
  }

  SetTimeout();

  if (sScrollSeriesCounter != 0 &&
      OutOfTime(sTime, StaticPrefs::mousewheel_scroll_series_timeout())) {
    sScrollSeriesCounter = 0;
  }
  sScrollSeriesCounter++;

  // We should use current time instead of WidgetEvent.time.
  // 1. Some events doesn't have the correct creation time.
  // 2. If the computer runs slowly by other processes eating the CPU resource,
  //    the event creation time doesn't keep real time.
  sTime = PR_IntervalToMilliseconds(PR_IntervalNow());
  sMouseMoved = 0;
  return true;
}

/* static */
void WheelTransaction::MayEndTransaction() {
  if (!sOwnScrollbars && ScrollbarsForWheel::IsActive()) {
    ScrollbarsForWheel::OwnWheelTransaction(true);
  } else {
    EndTransaction();
  }
}

/* static */
void WheelTransaction::EndTransaction() {
  if (sTimer) {
    sTimer->Cancel();
  }
  sScrollTargetFrame = nullptr;
  sEventTargetFrame = nullptr;
  sScrollSeriesCounter = 0;
  sHandledByApz = false;
  if (sOwnScrollbars) {
    sOwnScrollbars = false;
    ScrollbarsForWheel::OwnWheelTransaction(false);
    ScrollbarsForWheel::Inactivate();
  }
}

/* static */
bool WheelTransaction::WillHandleDefaultAction(
    WidgetWheelEvent* aWheelEvent, AutoWeakFrame& aScrollTargetWeakFrame,
    AutoWeakFrame& aEventTargetWeakFrame) {
  nsIFrame* lastTargetFrame = GetScrollTargetFrame();
  if (!lastTargetFrame) {
    BeginTransaction(aScrollTargetWeakFrame.GetFrame(),
                     aEventTargetWeakFrame.GetFrame(), aWheelEvent);
  } else if (lastTargetFrame != aScrollTargetWeakFrame.GetFrame()) {
    EndTransaction();
    BeginTransaction(aScrollTargetWeakFrame.GetFrame(),
                     aEventTargetWeakFrame.GetFrame(), aWheelEvent);
  } else {
    UpdateTransaction(aWheelEvent);
  }

  // When the wheel event will not be handled with any frames,
  // UpdateTransaction() fires MozMouseScrollFailed event which is for
  // automated testing.  In the event handler, the target frame might be
  // destroyed.  Then, the caller shouldn't try to handle the default action.
  if (!aScrollTargetWeakFrame.IsAlive()) {
    EndTransaction();
    return false;
  }

  return true;
}

/* static */
void WheelTransaction::OnEvent(WidgetEvent* aEvent) {
  if (!sScrollTargetFrame) {
    return;
  }

  if (OutOfTime(sTime, StaticPrefs::mousewheel_transaction_timeout())) {
    // Even if the scroll event which is handled after timeout, but onTimeout
    // was not fired by timer, then the scroll event will scroll old frame,
    // therefore, we should call OnTimeout here and ensure to finish the old
    // transaction.
    OnTimeout(nullptr, nullptr);
    return;
  }

  switch (aEvent->mMessage) {
    case eWheel:
      if (sMouseMoved != 0 &&
          OutOfTime(sMouseMoved,
                    StaticPrefs::mousewheel_transaction_ignoremovedelay())) {
        // Terminate the current mousewheel transaction if the mouse moved more
        // than ignoremovedelay milliseconds ago
        EndTransaction();
      }
      return;
    case eMouseMove:
    case eDragOver: {
      WidgetMouseEvent* mouseEvent = aEvent->AsMouseEvent();
      if (mouseEvent->IsReal()) {
        // If the cursor is moving to be outside the frame,
        // terminate the scrollwheel transaction.
        LayoutDeviceIntPoint pt = GetScreenPoint(mouseEvent);
        auto r = LayoutDeviceIntRect::FromAppUnitsToNearest(
            sScrollTargetFrame->GetScreenRectInAppUnits(),
            sScrollTargetFrame->PresContext()->AppUnitsPerDevPixel());
        if (!r.Contains(pt)) {
          EndTransaction();
          return;
        }

        // For mouse move events where the wheel transaction is still valid, the
        // stored event target should be reset.
        sEventTargetFrame = nullptr;

        // If the cursor is moving inside the frame, and it is less than
        // ignoremovedelay milliseconds since the last scroll operation, ignore
        // the mouse move; otherwise, record the current mouse move time to be
        // checked later
        if (!sMouseMoved &&
            OutOfTime(sTime,
                      StaticPrefs::mousewheel_transaction_ignoremovedelay())) {
          sMouseMoved = PR_IntervalToMilliseconds(PR_IntervalNow());
        }
      }
      return;
    }
    case eKeyPress:
    case eKeyUp:
    case eKeyDown:
    case eMouseUp:
    case eMouseDown:
    case eMouseDoubleClick:
    case eMouseAuxClick:
    case eMouseClick:
    case eContextMenu:
    case eDrop:
      EndTransaction();
      return;
    default:
      break;
  }
}

/* static */
void WheelTransaction::OnRemoveElement(nsIContent* aContent) {
  // If dom.event.wheel-event-groups.enabled is not set or we have no current
  // wheel event transaction there is no internal state to be updated.
  if (!sEventTargetFrame) {
    return;
  }

  if (sEventTargetFrame->GetContent() == aContent) {
    // Only invalidate the wheel transaction event target frame when the
    // remove target is the event target of the wheel event group. The
    // scroll target frame of the wheel event group may still be valid.
    //
    // With the stored event target unset, the target for any following
    // events will be the frame found using the input coordinates.
    sEventTargetFrame = nullptr;
  }
}

/* static */
void WheelTransaction::Shutdown() { NS_IF_RELEASE(sTimer); }

/* static */
void WheelTransaction::OnFailToScrollTarget() {
  MOZ_ASSERT(sScrollTargetFrame, "We don't have mouse scrolling transaction");

  if (StaticPrefs::test_mousescroll()) {
    // This event is used for automated tests, see bug 442774.
    nsContentUtils::DispatchEventOnlyToChrome(
        sScrollTargetFrame->GetContent()->OwnerDoc(),
        sScrollTargetFrame->GetContent(), u"MozMouseScrollFailed"_ns,
        CanBubble::eYes, Cancelable::eYes);
  }
  // The target frame might be destroyed in the event handler, at that time,
  // we need to finish the current transaction
  if (!sScrollTargetFrame) {
    EndTransaction();
  }
}

/* static */
void WheelTransaction::OnTimeout(nsITimer* aTimer, void* aClosure) {
  if (!sScrollTargetFrame) {
    // The transaction target was destroyed already
    EndTransaction();
    return;
  }
  // Store the sScrollTargetFrame, the variable becomes null in EndTransaction.
  nsIFrame* frame = sScrollTargetFrame;
  // We need to finish current transaction before DOM event firing. Because
  // the next DOM event might create strange situation for us.
  MayEndTransaction();

  if (StaticPrefs::test_mousescroll()) {
    // This event is used for automated tests, see bug 442774.
    nsContentUtils::DispatchEventOnlyToChrome(
        frame->GetContent()->OwnerDoc(), frame->GetContent(),
        u"MozMouseScrollTransactionTimeout"_ns, CanBubble::eYes,
        Cancelable::eYes);
  }
}

/* static */
void WheelTransaction::SetTimeout() {
  if (!sTimer) {
    sTimer = NS_NewTimer().take();
    if (!sTimer) {
      return;
    }
  }
  sTimer->Cancel();
  DebugOnly<nsresult> rv = sTimer->InitWithNamedFuncCallback(
      OnTimeout, nullptr, StaticPrefs::mousewheel_transaction_timeout(),
      nsITimer::TYPE_ONE_SHOT, "WheelTransaction::SetTimeout");
  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
                       "nsITimer::InitWithFuncCallback failed");
}

/* static */
LayoutDeviceIntPoint WheelTransaction::GetScreenPoint(WidgetGUIEvent* aEvent) {
  NS_ASSERTION(aEvent, "aEvent is null");
  NS_ASSERTION(aEvent->mWidget, "aEvent-mWidget is null");
  return aEvent->mRefPoint + aEvent->mWidget->WidgetToScreenOffset();
}

/* static */
DeltaValues WheelTransaction::AccelerateWheelDelta(WidgetWheelEvent* aEvent) {
  DeltaValues result = OverrideSystemScrollSpeed(aEvent);

  // Don't accelerate the delta values if the event isn't line scrolling.
  if (aEvent->mDeltaMode != dom::WheelEvent_Binding::DOM_DELTA_LINE) {
    return result;
  }

  // Accelerate by the sScrollSeriesCounter
  int32_t start = StaticPrefs::mousewheel_acceleration_start();
  if (start >= 0 && sScrollSeriesCounter >= start) {
    int32_t factor = StaticPrefs::mousewheel_acceleration_factor();
    if (factor > 0) {
      result.deltaX = ComputeAcceleratedWheelDelta(result.deltaX, factor);
      result.deltaY = ComputeAcceleratedWheelDelta(result.deltaY, factor);
    }
  }

  return result;
}

/* static */
double WheelTransaction::ComputeAcceleratedWheelDelta(double aDelta,
                                                      int32_t aFactor) {
  return mozilla::ComputeAcceleratedWheelDelta(aDelta, sScrollSeriesCounter,
                                               aFactor);
}

/* static */
DeltaValues WheelTransaction::OverrideSystemScrollSpeed(
    WidgetWheelEvent* aEvent) {
  MOZ_ASSERT(sScrollTargetFrame, "We don't have mouse scrolling transaction");

  // If the event doesn't scroll to both X and Y, we don't need to do anything
  // here.
  if (!aEvent->mDeltaX && !aEvent->mDeltaY) {
    return DeltaValues(aEvent);
  }

  return DeltaValues(aEvent->OverriddenDeltaX(), aEvent->OverriddenDeltaY());
}

/******************************************************************/
/* mozilla::ScrollbarsForWheel                                    */
/******************************************************************/

const DeltaValues ScrollbarsForWheel::directions[kNumberOfTargets] = {
    DeltaValues(-1, 0), DeltaValues(+1, 0), DeltaValues(0, -1),
    DeltaValues(0, +1)};

AutoWeakFrame ScrollbarsForWheel::sActiveOwner = nullptr;
AutoWeakFrame ScrollbarsForWheel::sActivatedScrollTargets[kNumberOfTargets] = {
    nullptr, nullptr, nullptr, nullptr};

bool ScrollbarsForWheel::sHadWheelStart = false;
bool ScrollbarsForWheel::sOwnWheelTransaction = false;

/* static */
void ScrollbarsForWheel::PrepareToScrollText(EventStateManager* aESM,
                                             nsIFrame* aTargetFrame,
                                             WidgetWheelEvent* aEvent) {
  if (aEvent->mMessage == eWheelOperationStart) {
    WheelTransaction::OwnScrollbars(false);
    if (!IsActive()) {
      TemporarilyActivateAllPossibleScrollTargets(aESM, aTargetFrame, aEvent);
      sHadWheelStart = true;
    }
  } else {
    DeactivateAllTemporarilyActivatedScrollTargets();
  }
}

/* static */
void ScrollbarsForWheel::SetActiveScrollTarget(
    nsIScrollableFrame* aScrollTarget) {
  if (!sHadWheelStart) {
    return;
  }
  nsIScrollbarMediator* scrollbarMediator = do_QueryFrame(aScrollTarget);
  if (!scrollbarMediator) {
    return;
  }
  sHadWheelStart = false;
  sActiveOwner = do_QueryFrame(aScrollTarget);
  scrollbarMediator->ScrollbarActivityStarted();
}

/* static */
void ScrollbarsForWheel::MayInactivate() {
  if (!sOwnWheelTransaction && WheelTransaction::GetScrollTargetFrame()) {
    WheelTransaction::OwnScrollbars(true);
  } else {
    Inactivate();
  }
}

/* static */
void ScrollbarsForWheel::Inactivate() {
  nsIScrollbarMediator* scrollbarMediator = do_QueryFrame(sActiveOwner);
  if (scrollbarMediator) {
    scrollbarMediator->ScrollbarActivityStopped();
  }
  sActiveOwner = nullptr;
  DeactivateAllTemporarilyActivatedScrollTargets();
  if (sOwnWheelTransaction) {
    sOwnWheelTransaction = false;
    WheelTransaction::OwnScrollbars(false);
    WheelTransaction::EndTransaction();
  }
}

/* static */
bool ScrollbarsForWheel::IsActive() {
  if (sActiveOwner) {
    return true;
  }
  for (size_t i = 0; i < kNumberOfTargets; ++i) {
    if (sActivatedScrollTargets[i]) {
      return true;
    }
  }
  return false;
}

/* static */
void ScrollbarsForWheel::OwnWheelTransaction(bool aOwn) {
  sOwnWheelTransaction = aOwn;
}

/* static */
void ScrollbarsForWheel::TemporarilyActivateAllPossibleScrollTargets(
    EventStateManager* aESM, nsIFrame* aTargetFrame, WidgetWheelEvent* aEvent) {
  for (size_t i = 0; i < kNumberOfTargets; i++) {
    const DeltaValues* dir = &directions[i];
    AutoWeakFrame* scrollTarget = &sActivatedScrollTargets[i];
    MOZ_ASSERT(!*scrollTarget, "scroll target still temporarily activated!");
    nsIScrollableFrame* target = do_QueryFrame(aESM->ComputeScrollTarget(
        aTargetFrame, dir->deltaX, dir->deltaY, aEvent,
        EventStateManager::COMPUTE_DEFAULT_ACTION_TARGET));
    nsIScrollbarMediator* scrollbarMediator = do_QueryFrame(target);
    if (scrollbarMediator) {
      nsIFrame* targetFrame = do_QueryFrame(target);
      *scrollTarget = targetFrame;
      scrollbarMediator->ScrollbarActivityStarted();
    }
  }
}

/* static */
void ScrollbarsForWheel::DeactivateAllTemporarilyActivatedScrollTargets() {
  for (size_t i = 0; i < kNumberOfTargets; i++) {
    AutoWeakFrame* scrollTarget = &sActivatedScrollTargets[i];
    if (*scrollTarget) {
      nsIScrollbarMediator* scrollbarMediator = do_QueryFrame(*scrollTarget);
      if (scrollbarMediator) {
        scrollbarMediator->ScrollbarActivityStopped();
      }
      *scrollTarget = nullptr;
    }
  }
}

/******************************************************************/
/* mozilla::WheelDeltaHorizontalizer                              */
/******************************************************************/

void WheelDeltaHorizontalizer::Horizontalize() {
  MOZ_ASSERT(!mWheelEvent.mDeltaValuesHorizontalizedForDefaultHandler,
             "Wheel delta values in one wheel scroll event are being adjusted "
             "a second time");

  // Log the old values.
  mOldDeltaX = mWheelEvent.mDeltaX;
  mOldDeltaZ = mWheelEvent.mDeltaZ;
  mOldOverflowDeltaX = mWheelEvent.mOverflowDeltaX;
  mOldLineOrPageDeltaX = mWheelEvent.mLineOrPageDeltaX;

  // Move deltaY values to deltaX and set both deltaY and deltaZ to 0.
  mWheelEvent.mDeltaX = mWheelEvent.mDeltaY;
  mWheelEvent.mDeltaY = 0.0;
  mWheelEvent.mDeltaZ = 0.0;
  mWheelEvent.mOverflowDeltaX = mWheelEvent.mOverflowDeltaY;
  mWheelEvent.mOverflowDeltaY = 0.0;
  mWheelEvent.mLineOrPageDeltaX = mWheelEvent.mLineOrPageDeltaY;
  mWheelEvent.mLineOrPageDeltaY = 0;

  // Mark it horizontalized in order to restore the delta values when this
  // instance is being destroyed.
  mWheelEvent.mDeltaValuesHorizontalizedForDefaultHandler = true;
  mHorizontalized = true;
}

void WheelDeltaHorizontalizer::CancelHorizontalization() {
  // Restore the horizontalized delta.
  if (mHorizontalized &&
      mWheelEvent.mDeltaValuesHorizontalizedForDefaultHandler) {
    mWheelEvent.mDeltaY = mWheelEvent.mDeltaX;
    mWheelEvent.mDeltaX = mOldDeltaX;
    mWheelEvent.mDeltaZ = mOldDeltaZ;
    mWheelEvent.mOverflowDeltaY = mWheelEvent.mOverflowDeltaX;
    mWheelEvent.mOverflowDeltaX = mOldOverflowDeltaX;
    mWheelEvent.mLineOrPageDeltaY = mWheelEvent.mLineOrPageDeltaX;
    mWheelEvent.mLineOrPageDeltaX = mOldLineOrPageDeltaX;
    mWheelEvent.mDeltaValuesHorizontalizedForDefaultHandler = false;
    mHorizontalized = false;
  }
}

WheelDeltaHorizontalizer::~WheelDeltaHorizontalizer() {
  CancelHorizontalization();
}

/******************************************************************/
/* mozilla::AutoDirWheelDeltaAdjuster                             */
/******************************************************************/

bool AutoDirWheelDeltaAdjuster::ShouldBeAdjusted() {
  // Sometimes, this function can be called more than one time. If we have
  // already checked if the scroll should be adjusted, there's no need to check
  // it again.
  if (mCheckedIfShouldBeAdjusted) {
    return mShouldBeAdjusted;
  }
  mCheckedIfShouldBeAdjusted = true;

  // For an auto-dir wheel scroll, if all the following conditions are met, we
  // should adjust X and Y values:
  // 1. There is only one non-zero value between DeltaX and DeltaY.
  // 2. There is only one direction for the target that overflows and is
  //    scrollable with wheel.
  // 3. The direction described in Condition 1 is orthogonal to the one
  // described in Condition 2.
  if ((mDeltaX && mDeltaY) || (!mDeltaX && !mDeltaY)) {
    return false;
  }
  if (mDeltaX) {
    if (CanScrollAlongXAxis()) {
      return false;
    }
    if (IsHorizontalContentRightToLeft()) {
      mShouldBeAdjusted =
          mDeltaX > 0 ? CanScrollUpwards() : CanScrollDownwards();
    } else {
      mShouldBeAdjusted =
          mDeltaX < 0 ? CanScrollUpwards() : CanScrollDownwards();
    }
    return mShouldBeAdjusted;
  }
  MOZ_ASSERT(0 != mDeltaY);
  if (CanScrollAlongYAxis()) {
    return false;
  }
  if (IsHorizontalContentRightToLeft()) {
    mShouldBeAdjusted =
        mDeltaY > 0 ? CanScrollLeftwards() : CanScrollRightwards();
  } else {
    mShouldBeAdjusted =
        mDeltaY < 0 ? CanScrollLeftwards() : CanScrollRightwards();
  }
  return mShouldBeAdjusted;
}

void AutoDirWheelDeltaAdjuster::Adjust() {
  if (!ShouldBeAdjusted()) {
    return;
  }
  std::swap(mDeltaX, mDeltaY);
  if (IsHorizontalContentRightToLeft()) {
    mDeltaX *= -1;
    mDeltaY *= -1;
  }
  mShouldBeAdjusted = false;
  OnAdjusted();
}

/******************************************************************/
/* mozilla::ESMAutoDirWheelDeltaAdjuster                          */
/******************************************************************/

ESMAutoDirWheelDeltaAdjuster::ESMAutoDirWheelDeltaAdjuster(
    WidgetWheelEvent& aEvent, nsIFrame& aScrollFrame, bool aHonoursRoot)
    : AutoDirWheelDeltaAdjuster(aEvent.mDeltaX, aEvent.mDeltaY),
      mLineOrPageDeltaX(aEvent.mLineOrPageDeltaX),
      mLineOrPageDeltaY(aEvent.mLineOrPageDeltaY),
      mOverflowDeltaX(aEvent.mOverflowDeltaX),
      mOverflowDeltaY(aEvent.mOverflowDeltaY) {
  mScrollTargetFrame = aScrollFrame.GetScrollTargetFrame();
  MOZ_ASSERT(mScrollTargetFrame);

  nsIFrame* honouredFrame = nullptr;
  if (aHonoursRoot) {
    // If we are going to honour root, first try to get the frame for <body> as
    // the honoured root, because <body> is in preference to <html> if the
    // current document is an HTML document.
    dom::Document* document = aScrollFrame.PresShell()->GetDocument();
    if (document) {
      dom::Element* bodyElement = document->GetBodyElement();
      if (bodyElement) {
        honouredFrame = bodyElement->GetPrimaryFrame();
      }
    }

    if (!honouredFrame) {
      // If there is no <body> frame, fall back to the real root frame.
      honouredFrame = aScrollFrame.PresShell()->GetRootScrollFrame();
    }

    if (!honouredFrame) {
      // If there is no root scroll frame, fall back to the current scrolling
      // frame.
      honouredFrame = &aScrollFrame;
    }
  } else {
    honouredFrame = &aScrollFrame;
  }

  WritingMode writingMode = honouredFrame->GetWritingMode();
  WritingMode::BlockDir blockDir = writingMode.GetBlockDir();
  WritingMode::InlineDir inlineDir = writingMode.GetInlineDir();
  // Get whether the honoured frame's content in the horizontal direction starts
  // from right to left(E.g. it's true either if "writing-mode: vertical-rl", or
  // if "writing-mode: horizontal-tb; direction: rtl;" in CSS).
  mIsHorizontalContentRightToLeft =
      (blockDir == WritingMode::BlockDir::eBlockRL ||
       (blockDir == WritingMode::BlockDir::eBlockTB &&
        inlineDir == WritingMode::InlineDir::eInlineRTL));
}

void ESMAutoDirWheelDeltaAdjuster::OnAdjusted() {
  // Adjust() only adjusted basic deltaX and deltaY, which are not enough for
  // ESM, we should continue to adjust line-or-page and overflow values.
  if (mDeltaX) {
    // A vertical scroll was adjusted to be horizontal.
    MOZ_ASSERT(0 == mDeltaY);

    mLineOrPageDeltaX = mLineOrPageDeltaY;
    mLineOrPageDeltaY = 0;
    mOverflowDeltaX = mOverflowDeltaY;
    mOverflowDeltaY = 0;
  } else {
    // A horizontal scroll was adjusted to be vertical.
    MOZ_ASSERT(0 != mDeltaY);

    mLineOrPageDeltaY = mLineOrPageDeltaX;
    mLineOrPageDeltaX = 0;
    mOverflowDeltaY = mOverflowDeltaX;
    mOverflowDeltaX = 0;
  }
  if (mIsHorizontalContentRightToLeft) {
    // If in RTL writing mode, reverse the side the scroll will go towards.
    mLineOrPageDeltaX *= -1;
    mLineOrPageDeltaY *= -1;
    mOverflowDeltaX *= -1;
    mOverflowDeltaY *= -1;
  }
}

bool ESMAutoDirWheelDeltaAdjuster::CanScrollAlongXAxis() const {
  return mScrollTargetFrame->GetAvailableScrollingDirections().contains(
      layers::ScrollDirection::eHorizontal);
}

bool ESMAutoDirWheelDeltaAdjuster::CanScrollAlongYAxis() const {
  return mScrollTargetFrame->GetAvailableScrollingDirections().contains(
      layers::ScrollDirection::eVertical);
}

bool ESMAutoDirWheelDeltaAdjuster::CanScrollUpwards() const {
  nsPoint scrollPt = mScrollTargetFrame->GetScrollPosition();
  nsRect scrollRange = mScrollTargetFrame->GetScrollRange();
  return static_cast<double>(scrollRange.y) < scrollPt.y;
}

bool ESMAutoDirWheelDeltaAdjuster::CanScrollDownwards() const {
  nsPoint scrollPt = mScrollTargetFrame->GetScrollPosition();
  nsRect scrollRange = mScrollTargetFrame->GetScrollRange();
  return static_cast<double>(scrollRange.YMost()) > scrollPt.y;
}

bool ESMAutoDirWheelDeltaAdjuster::CanScrollLeftwards() const {
  nsPoint scrollPt = mScrollTargetFrame->GetScrollPosition();
  nsRect scrollRange = mScrollTargetFrame->GetScrollRange();
  return static_cast<double>(scrollRange.x) < scrollPt.x;
}

bool ESMAutoDirWheelDeltaAdjuster::CanScrollRightwards() const {
  nsPoint scrollPt = mScrollTargetFrame->GetScrollPosition();
  nsRect scrollRange = mScrollTargetFrame->GetScrollRange();
  return static_cast<double>(scrollRange.XMost()) > scrollPt.x;
}

bool ESMAutoDirWheelDeltaAdjuster::IsHorizontalContentRightToLeft() const {
  return mIsHorizontalContentRightToLeft;
}

/******************************************************************/
/* mozilla::ESMAutoDirWheelDeltaRestorer                          */
/******************************************************************/

/*explicit*/
ESMAutoDirWheelDeltaRestorer::ESMAutoDirWheelDeltaRestorer(
    WidgetWheelEvent& aEvent)
    : mEvent(aEvent),
      mOldDeltaX(aEvent.mDeltaX),
      mOldDeltaY(aEvent.mDeltaY),
      mOldLineOrPageDeltaX(aEvent.mLineOrPageDeltaX),
      mOldLineOrPageDeltaY(aEvent.mLineOrPageDeltaY),
      mOldOverflowDeltaX(aEvent.mOverflowDeltaX),
      mOldOverflowDeltaY(aEvent.mOverflowDeltaY) {}

ESMAutoDirWheelDeltaRestorer::~ESMAutoDirWheelDeltaRestorer() {
  if (mOldDeltaX == mEvent.mDeltaX || mOldDeltaY == mEvent.mDeltaY) {
    // The delta of the event wasn't adjusted during the lifetime of this
    // |ESMAutoDirWheelDeltaRestorer| instance. No need to restore it.
    return;
  }

  bool forRTL = false;

  // First, restore the basic deltaX and deltaY.
  std::swap(mEvent.mDeltaX, mEvent.mDeltaY);
  if (mOldDeltaX != mEvent.mDeltaX || mOldDeltaY != mEvent.mDeltaY) {
    // If X and Y still don't equal to their original values after being
    // swapped, then it must be because they were adjusted for RTL.
    forRTL = true;
    mEvent.mDeltaX *= -1;
    mEvent.mDeltaY *= -1;
    MOZ_ASSERT(mOldDeltaX == mEvent.mDeltaX && mOldDeltaY == mEvent.mDeltaY);
  }

  if (mEvent.mDeltaX) {
    // A horizontal scroll was adjusted to be vertical during the lifetime of
    // this instance.
    MOZ_ASSERT(0 == mEvent.mDeltaY);

    // Restore the line-or-page and overflow values to be horizontal.
    mEvent.mOverflowDeltaX = mEvent.mOverflowDeltaY;
    mEvent.mLineOrPageDeltaX = mEvent.mLineOrPageDeltaY;
    if (forRTL) {
      mEvent.mOverflowDeltaX *= -1;
      mEvent.mLineOrPageDeltaX *= -1;
    }
    mEvent.mOverflowDeltaY = mOldOverflowDeltaY;
    mEvent.mLineOrPageDeltaY = mOldLineOrPageDeltaY;
  } else {
    // A vertical scroll was adjusted to be horizontal during the lifetime of
    // this instance.
    MOZ_ASSERT(0 != mEvent.mDeltaY);

    // Restore the line-or-page and overflow values to be vertical.
    mEvent.mOverflowDeltaY = mEvent.mOverflowDeltaX;
    mEvent.mLineOrPageDeltaY = mEvent.mLineOrPageDeltaX;
    if (forRTL) {
      mEvent.mOverflowDeltaY *= -1;
      mEvent.mLineOrPageDeltaY *= -1;
    }
    mEvent.mOverflowDeltaX = mOldOverflowDeltaX;
    mEvent.mLineOrPageDeltaX = mOldLineOrPageDeltaX;
  }
}

}  // namespace mozilla
