/* 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 "mozilla/layers/APZInputBridge.h"

#include "AsyncPanZoomController.h"
#include "InputData.h"               // for MouseInput, etc
#include "InputBlockState.h"         // for InputBlockState
#include "OverscrollHandoffState.h"  // for OverscrollHandoffState
#include "nsLayoutUtils.h"           // for IsSmoothScrollingEnabled
#include "mozilla/EventForwards.h"
#include "mozilla/dom/WheelEventBinding.h"  // for WheelEvent constants
#include "mozilla/EventStateManager.h"      // for EventStateManager
#include "mozilla/layers/APZThreadUtils.h"  // for AssertOnControllerThread, etc
#include "mozilla/MouseEvents.h"            // for WidgetMouseEvent
#include "mozilla/StaticPrefs_apz.h"
#include "mozilla/StaticPrefs_general.h"
#include "mozilla/StaticPrefs_test.h"
#include "mozilla/TextEvents.h"           // for WidgetKeyboardEvent
#include "mozilla/TouchEvents.h"          // for WidgetTouchEvent
#include "mozilla/WheelHandlingHelper.h"  // for WheelDeltaHorizontalizer,
                                          //     WheelDeltaAdjustmentStrategy

namespace mozilla {
namespace layers {

APZHandledResult::APZHandledResult(APZHandledPlace aPlace,
                                   const AsyncPanZoomController* aTarget,
                                   bool aPopulateDirectionsForUnhandled)
    : mPlace(aPlace) {
  MOZ_ASSERT(aTarget);
  switch (aPlace) {
    case APZHandledPlace::Unhandled:
      if (aTarget && aPopulateDirectionsForUnhandled) {
        mScrollableDirections = aTarget->ScrollableDirections();
        mOverscrollDirections = aTarget->GetAllowedHandoffDirections(
            HandoffConsumer::PullToRefresh);
      }
      break;
    case APZHandledPlace::HandledByContent:
      if (aTarget) {
        mScrollableDirections = aTarget->ScrollableDirections();
        mOverscrollDirections = aTarget->GetAllowedHandoffDirections(
            HandoffConsumer::PullToRefresh);
      }
      break;
    case APZHandledPlace::HandledByRoot: {
      MOZ_ASSERT(aTarget->IsRootContent());
      if (aTarget) {
        mScrollableDirections = aTarget->ScrollableDirections();
        mOverscrollDirections = aTarget->GetAllowedHandoffDirections(
            HandoffConsumer::PullToRefresh);
      }
      break;
    }
    default:
      MOZ_ASSERT_UNREACHABLE("Invalid APZHandledPlace");
      break;
  }
}

/* static */
Maybe<APZHandledResult> APZHandledResult::Initialize(
    const AsyncPanZoomController* aInitialTarget,
    DispatchToContent aDispatchToContent) {
  if (!aInitialTarget->IsRootContent()) {
    // If the initial target is not the root, this will definitely not be
    // handled by the root. (The confirmed target is either the initial
    // target, or a descendant.)
    return Some(
        APZHandledResult{APZHandledPlace::HandledByContent, aInitialTarget});
  }

  if (!bool(aDispatchToContent)) {
    // If the initial target is the root and we don't need to dispatch to
    // content, the event will definitely be handled by the root.
    return Some(
        APZHandledResult{APZHandledPlace::HandledByRoot, aInitialTarget});
  }

  // Otherwise, we're not sure.
  return Nothing();
}

/* static */
void APZHandledResult::UpdateForTouchEvent(
    Maybe<APZHandledResult>& aHandledResult, const InputBlockState& aBlock,
    PointerEventsConsumableFlags aConsumableFlags,
    const AsyncPanZoomController* aTarget,
    DispatchToContent aDispatchToContent) {
  // If the touch event's effect is disallowed by touch-action, treat it as if
  // a touch event listener had preventDefault()-ed it (i.e. return
  // HandledByContent, except we can do it eagerly rather than having to wait
  // for the listener to run).
  if (!aConsumableFlags.mAllowedByTouchAction) {
    aHandledResult =
        Some(APZHandledResult{APZHandledPlace::HandledByContent, aTarget});
    aHandledResult->mOverscrollDirections = ScrollDirections();
    return;
  }

  if (aHandledResult && !bool(aDispatchToContent) &&
      !aConsumableFlags.mHasRoom) {
    // Set result to Unhandled if we have no room to scroll, unless it
    // was HandledByContent because we're over a dispatch-to-content region,
    // in which case it should remain HandledByContent.
    aHandledResult->mPlace = APZHandledPlace::Unhandled;
  }

  if (aTarget && !aTarget->IsRootContent()) {
    // If the event targets a subframe but the subframe and its ancestors
    // are all scrolled to the top, we want an upward swipe to allow
    // triggering pull-to-refresh.
    bool mayTriggerPullToRefresh =
        aBlock.GetOverscrollHandoffChain()->ScrollingUpWillTriggerPullToRefresh(
            aTarget);
    if (mayTriggerPullToRefresh) {
      // Similar to what is done for the dynamic toolbar, we need to ensure
      // that if the input has the dispatch to content flag, we need to change
      // the handled result to Nothing(), so that GeckoView can wait for the
      // result.
      aHandledResult = bool(aDispatchToContent)
                           ? Nothing()
                           : Some(APZHandledResult{APZHandledPlace::Unhandled,
                                                   aTarget, true});
    }

    auto [mayMoveDynamicToolbar, rootApzc] =
        aBlock.GetOverscrollHandoffChain()->ScrollingDownWillMoveDynamicToolbar(
            aTarget);
    if (mayMoveDynamicToolbar) {
      MOZ_ASSERT(rootApzc && rootApzc->IsRootContent());
      // The event is actually consumed by a non-root APZC but scroll
      // positions in all relevant APZCs are at the bottom edge, so if there's
      // still contents covered by the dynamic toolbar we need to move the
      // dynamic toolbar to make the covered contents visible, thus we need
      // to tell it to GeckoView so we handle it as if it's consumed in the
      // root APZC.
      // IMPORTANT NOTE: If the incoming TargetConfirmationFlags has
      // mDispatchToContent, we need to change it to Nothing() so that
      // GeckoView can properly wait for results from the content on the
      // main-thread.
      aHandledResult =
          bool(aDispatchToContent)
              ? Nothing()
              : Some(APZHandledResult{aConsumableFlags.IsConsumable()
                                          ? APZHandledPlace::HandledByRoot
                                          : APZHandledPlace::Unhandled,
                                      rootApzc});
      if (aHandledResult && aHandledResult->IsHandledByRoot() &&
          !mayTriggerPullToRefresh) {
        MOZ_ASSERT(
            !(aTarget->ScrollableDirections() & SideBits::eBottom),
            "If we allowed moving the dynamic toolbar for the sub scroll "
            "container, the sub scroll container should NOT be scrollable to "
            "bottom");

        // In cases we didn't allow pull-to-refresh (!mayTriggerPullToRefresh),
        // it means the root scroll container is NOT overscrollable at top.
        aHandledResult->mOverscrollDirections -= ScrollDirection::eVertical;
      }
    }
  }
}

APZEventResult::APZEventResult()
    : mStatus(nsEventStatus_eIgnore),
      mInputBlockId(InputBlockState::NO_BLOCK_ID) {}

APZEventResult::APZEventResult(
    const RefPtr<AsyncPanZoomController>& aInitialTarget,
    TargetConfirmationFlags aFlags)
    : APZEventResult() {
  mHandledResult = APZHandledResult::Initialize(aInitialTarget,
                                                aFlags.NeedDispatchToContent());
  aInitialTarget->GetGuid(&mTargetGuid);
}

void APZEventResult::SetStatusAsConsumeDoDefault(
    const InputBlockState& aBlock) {
  SetStatusAsConsumeDoDefault(aBlock.GetTargetApzc());
}

void APZEventResult::SetStatusAsConsumeDoDefault(
    const RefPtr<AsyncPanZoomController>& aTarget) {
  mStatus = nsEventStatus_eConsumeDoDefault;
  mHandledResult =
      Some(aTarget && aTarget->IsRootContent()
               ? APZHandledResult{APZHandledPlace::HandledByRoot, aTarget}
               : APZHandledResult{APZHandledPlace::HandledByContent, aTarget});
}

void APZEventResult::SetStatusForTouchEvent(
    const InputBlockState& aBlock, TargetConfirmationFlags aFlags,
    PointerEventsConsumableFlags aConsumableFlags,
    const AsyncPanZoomController* aTarget) {
  // Note, we need to continue setting mStatus to eIgnore in the {mHasRoom=true,
  // mAllowedByTouchAction=false} case because this is the behaviour expected by
  // APZEventState::ProcessTouchEvent() when it determines when to send a
  // `pointercancel` event. TODO: Use something more descriptive than
  // nsEventStatus for this purpose.
  mStatus = aConsumableFlags.IsConsumable() ? nsEventStatus_eConsumeDoDefault
                                            : nsEventStatus_eIgnore;

  APZHandledResult::UpdateForTouchEvent(mHandledResult, aBlock,
                                        aConsumableFlags, aTarget,
                                        aFlags.NeedDispatchToContent());
}

void APZEventResult::SetStatusForFastFling(
    const TouchBlockState& aBlock, TargetConfirmationFlags aFlags,
    PointerEventsConsumableFlags aConsumableFlags,
    const AsyncPanZoomController* aTarget) {
  MOZ_ASSERT(aBlock.IsDuringFastFling());

  // Set eConsumeNoDefault for fast fling since we don't want to send the event
  // to content at all.
  mStatus = nsEventStatus_eConsumeNoDefault;

  // Re-initialize with DispatchToContent::No, since being in a fast fling
  // means the event will definitely not be dispatched to content.
  mHandledResult = APZHandledResult::Initialize(aTarget, DispatchToContent::No);

  // In the case of fast fling, the event will never be sent to content, so we
  // want a result where `aDispatchToContent` is false whatever the original
  // `aFlags.mDispatchToContent` is.
  APZHandledResult::UpdateForTouchEvent(
      mHandledResult, aBlock, aConsumableFlags, aTarget, DispatchToContent::No);
}

static bool WillHandleMouseEvent(const WidgetMouseEventBase& aEvent) {
  return aEvent.mMessage == eMouseMove || aEvent.mMessage == eMouseDown ||
         aEvent.mMessage == eMouseUp || aEvent.mMessage == eDragEnd ||
         (StaticPrefs::test_events_async_enabled() &&
          aEvent.mMessage == eMouseHitTest);
}

/* static */
Maybe<APZWheelAction> APZInputBridge::ActionForWheelEvent(
    WidgetWheelEvent* aEvent) {
  if (!(aEvent->mDeltaMode == dom::WheelEvent_Binding::DOM_DELTA_LINE ||
        aEvent->mDeltaMode == dom::WheelEvent_Binding::DOM_DELTA_PIXEL ||
        aEvent->mDeltaMode == dom::WheelEvent_Binding::DOM_DELTA_PAGE)) {
    return Nothing();
  }
  return EventStateManager::APZWheelActionFor(aEvent);
}

APZEventResult APZInputBridge::ReceiveInputEvent(
    WidgetInputEvent& aEvent, InputBlockCallback&& aCallback) {
  APZThreadUtils::AssertOnControllerThread();

  APZEventResult result;

  switch (aEvent.mClass) {
    case eMouseEventClass:
    case eDragEventClass: {
      WidgetMouseEvent& mouseEvent = *aEvent.AsMouseEvent();
      if (WillHandleMouseEvent(mouseEvent)) {
        MouseInput input(mouseEvent);
        input.mOrigin =
            ScreenPoint(mouseEvent.mRefPoint.x, mouseEvent.mRefPoint.y);

        result = ReceiveInputEvent(input, std::move(aCallback));

        mouseEvent.mRefPoint = TruncatedToInt(ViewAs<LayoutDevicePixel>(
            input.mOrigin,
            PixelCastJustification::LayoutDeviceIsScreenForUntransformedEvent));
        mouseEvent.mFlags.mHandledByAPZ = input.mHandledByAPZ;
        mouseEvent.mFocusSequenceNumber = input.mFocusSequenceNumber;
#ifdef XP_MACOSX
        // It's not assumed that the click event has already been prevented,
        // except mousedown event with ctrl key is pressed where we prevent
        // click event from widget on Mac platform.
        MOZ_ASSERT_IF(!mouseEvent.IsControl() ||
                          mouseEvent.mMessage != eMouseDown ||
                          mouseEvent.mButton != MouseButton::ePrimary,
                      !mouseEvent.mClickEventPrevented);
#else
        MOZ_ASSERT(
            !mouseEvent.mClickEventPrevented,
            "It's not assumed that the click event has already been prevented");
#endif
        mouseEvent.mClickEventPrevented |= input.mPreventClickEvent;
        MOZ_ASSERT_IF(mouseEvent.mClickEventPrevented,
                      mouseEvent.mMessage == eMouseDown ||
                          mouseEvent.mMessage == eMouseUp);
        aEvent.mLayersId = input.mLayersId;

        if (mouseEvent.IsReal()) {
          UpdateWheelTransaction(mouseEvent.mRefPoint, mouseEvent.mMessage,
                                 Some(result.mTargetGuid));
        }

        return result;
      }

      if (mouseEvent.IsReal()) {
        UpdateWheelTransaction(mouseEvent.mRefPoint, mouseEvent.mMessage,
                               Nothing());
      }

      ProcessUnhandledEvent(&mouseEvent.mRefPoint, &result.mTargetGuid,
                            &aEvent.mFocusSequenceNumber, &aEvent.mLayersId);
      return result;
    }
    case eTouchEventClass: {
      WidgetTouchEvent& touchEvent = *aEvent.AsTouchEvent();
      MultiTouchInput touchInput(touchEvent);
      result = ReceiveInputEvent(touchInput, std::move(aCallback));
      // touchInput was modified in-place to possibly remove some
      // touch points (if we are overscrolled), and the coordinates were
      // modified using the APZ untransform. We need to copy these changes
      // back into the WidgetInputEvent.
      touchEvent.mTouches.Clear();
      touchEvent.mTouches.SetCapacity(touchInput.mTouches.Length());
      for (size_t i = 0; i < touchInput.mTouches.Length(); i++) {
        *touchEvent.mTouches.AppendElement() =
            touchInput.mTouches[i].ToNewDOMTouch();
      }
      touchEvent.mFlags.mHandledByAPZ = touchInput.mHandledByAPZ;
      touchEvent.mFocusSequenceNumber = touchInput.mFocusSequenceNumber;
      aEvent.mLayersId = touchInput.mLayersId;
      return result;
    }
    case eWheelEventClass: {
      WidgetWheelEvent& wheelEvent = *aEvent.AsWheelEvent();

      if (Maybe<APZWheelAction> action = ActionForWheelEvent(&wheelEvent)) {
        ScrollWheelInput::ScrollMode scrollMode =
            ScrollWheelInput::SCROLLMODE_INSTANT;
        if (nsLayoutUtils::IsSmoothScrollingEnabled() &&
            ((wheelEvent.mDeltaMode ==
                  dom::WheelEvent_Binding::DOM_DELTA_LINE &&
              StaticPrefs::general_smoothScroll_mouseWheel()) ||
             (wheelEvent.mDeltaMode ==
                  dom::WheelEvent_Binding::DOM_DELTA_PAGE &&
              StaticPrefs::general_smoothScroll_pages()))) {
          scrollMode = ScrollWheelInput::SCROLLMODE_SMOOTH;
        }

        WheelDeltaAdjustmentStrategy strategy =
            EventStateManager::GetWheelDeltaAdjustmentStrategy(wheelEvent);
        // Adjust the delta values of the wheel event if the current default
        // action is to horizontalize scrolling. I.e., deltaY values are set to
        // deltaX and deltaY and deltaZ values are set to 0.
        // If horizontalized, the delta values will be restored and its overflow
        // deltaX will become 0 when the WheelDeltaHorizontalizer instance is
        // being destroyed.
        WheelDeltaHorizontalizer horizontalizer(wheelEvent);
        if (WheelDeltaAdjustmentStrategy::eHorizontalize == strategy) {
          horizontalizer.Horizontalize();
        }

        // If the wheel event becomes no-op event, don't handle it as scroll.
        if (wheelEvent.mDeltaX || wheelEvent.mDeltaY) {
          ScreenPoint origin(wheelEvent.mRefPoint.x, wheelEvent.mRefPoint.y);
          ScrollWheelInput input(
              wheelEvent.mTimeStamp, 0, scrollMode,
              ScrollWheelInput::DeltaTypeForDeltaMode(wheelEvent.mDeltaMode),
              origin, wheelEvent.mDeltaX, wheelEvent.mDeltaY,
              wheelEvent.mAllowToOverrideSystemScrollSpeed, strategy);
          input.mAPZAction = action.value();

          // We add the user multiplier as a separate field, rather than
          // premultiplying it, because if the input is converted back to a
          // WidgetWheelEvent, then EventStateManager would apply the delta a
          // second time. We could in theory work around this by asking ESM to
          // customize the event much sooner, and then save the
          // "mCustomizedByUserPrefs" bit on ScrollWheelInput - but for now,
          // this seems easier.
          EventStateManager::GetUserPrefsForWheelEvent(
              &wheelEvent, &input.mUserDeltaMultiplierX,
              &input.mUserDeltaMultiplierY);

          result = ReceiveInputEvent(input, std::move(aCallback));
          wheelEvent.mRefPoint = TruncatedToInt(ViewAs<LayoutDevicePixel>(
              input.mOrigin, PixelCastJustification::
                                 LayoutDeviceIsScreenForUntransformedEvent));
          wheelEvent.mFlags.mHandledByAPZ = input.mHandledByAPZ;
          wheelEvent.mFocusSequenceNumber = input.mFocusSequenceNumber;
          aEvent.mLayersId = input.mLayersId;

          return result;
        }
      }

      UpdateWheelTransaction(aEvent.mRefPoint, aEvent.mMessage, Nothing());
      ProcessUnhandledEvent(&aEvent.mRefPoint, &result.mTargetGuid,
                            &aEvent.mFocusSequenceNumber, &aEvent.mLayersId);
      MOZ_ASSERT(result.GetStatus() == nsEventStatus_eIgnore);
      return result;
    }
    case eKeyboardEventClass: {
      WidgetKeyboardEvent& keyboardEvent = *aEvent.AsKeyboardEvent();

      KeyboardInput input(keyboardEvent);

      result = ReceiveInputEvent(input, std::move(aCallback));

      keyboardEvent.mFlags.mHandledByAPZ = input.mHandledByAPZ;
      keyboardEvent.mFocusSequenceNumber = input.mFocusSequenceNumber;
      return result;
    }
    default: {
      UpdateWheelTransaction(aEvent.mRefPoint, aEvent.mMessage, Nothing());
      ProcessUnhandledEvent(&aEvent.mRefPoint, &result.mTargetGuid,
                            &aEvent.mFocusSequenceNumber, &aEvent.mLayersId);
      return result;
    }
  }

  MOZ_ASSERT_UNREACHABLE("Invalid WidgetInputEvent type.");
  result.SetStatusAsConsumeNoDefault();
  return result;
}

std::ostream& operator<<(std::ostream& aOut, const SideBits& aSideBits) {
  if ((aSideBits & SideBits::eAll) == SideBits::eAll) {
    aOut << "all";
  } else {
    AutoTArray<nsCString, 4> strings;
    if (aSideBits & SideBits::eTop) {
      strings.AppendElement("top"_ns);
    }
    if (aSideBits & SideBits::eRight) {
      strings.AppendElement("right"_ns);
    }
    if (aSideBits & SideBits::eBottom) {
      strings.AppendElement("bottom"_ns);
    }
    if (aSideBits & SideBits::eLeft) {
      strings.AppendElement("left"_ns);
    }
    aOut << strings;
  }
  return aOut;
}

std::ostream& operator<<(std::ostream& aOut,
                         const ScrollDirections& aScrollDirections) {
  if (aScrollDirections.contains(EitherScrollDirection)) {
    aOut << "either";
  } else if (aScrollDirections.contains(HorizontalScrollDirection)) {
    aOut << "horizontal";
  } else if (aScrollDirections.contains(VerticalScrollDirection)) {
    aOut << "vertical";
  } else {
    aOut << "none";
  }
  return aOut;
}

std::ostream& operator<<(std::ostream& aOut,
                         const APZHandledPlace& aHandledPlace) {
  switch (aHandledPlace) {
    case APZHandledPlace::Unhandled:
      aOut << "unhandled";
      break;
    case APZHandledPlace::HandledByRoot: {
      aOut << "handled-by-root";
      break;
    }
    case APZHandledPlace::HandledByContent: {
      aOut << "handled-by-content";
      break;
    }
    case APZHandledPlace::Invalid: {
      aOut << "INVALID";
      break;
    }
  }
  return aOut;
}

std::ostream& operator<<(std::ostream& aOut,
                         const APZHandledResult& aHandledResult) {
  aOut << "handled: " << aHandledResult.mPlace << ", ";
  aOut << "scrollable: " << aHandledResult.mScrollableDirections << ", ";
  aOut << "overscroll: " << aHandledResult.mOverscrollDirections << std::endl;
  return aOut;
}

}  // namespace layers
}  // namespace mozilla
