/* -*- 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 "APZCCallbackHelper.h"

#include "gfxPlatform.h"  // For gfxPlatform::UseTiling

#include "mozilla/AsyncEventDispatcher.h"
#include "mozilla/EventForwards.h"
#include "mozilla/dom/CustomEvent.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/MouseEventBinding.h"
#include "mozilla/dom/BrowserParent.h"
#include "mozilla/dom/ScriptSettings.h"
#include "mozilla/IntegerPrintfMacros.h"
#include "mozilla/layers/RepaintRequest.h"
#include "mozilla/layers/WebRenderLayerManager.h"
#include "mozilla/layers/WebRenderBridgeChild.h"
#include "mozilla/DisplayPortUtils.h"
#include "mozilla/PresShell.h"
#include "mozilla/ToString.h"
#include "mozilla/ViewportUtils.h"
#include "nsContainerFrame.h"
#include "nsContentUtils.h"
#include "nsIContent.h"
#include "nsIDOMWindowUtils.h"
#include "mozilla/dom/Document.h"
#include "nsIInterfaceRequestorUtils.h"
#include "nsIScrollableFrame.h"
#include "nsLayoutUtils.h"
#include "nsPrintfCString.h"
#include "nsPIDOMWindow.h"
#include "nsRefreshDriver.h"
#include "nsString.h"
#include "nsView.h"

static mozilla::LazyLogModule sApzHlpLog("apz.helper");
#define APZCCH_LOG(...) MOZ_LOG(sApzHlpLog, LogLevel::Debug, (__VA_ARGS__))
static mozilla::LazyLogModule sDisplayportLog("apz.displayport");

namespace mozilla {
namespace layers {

using dom::BrowserParent;

uint64_t APZCCallbackHelper::sLastTargetAPZCNotificationInputBlock =
    uint64_t(-1);

static ScreenMargin RecenterDisplayPort(const ScreenMargin& aDisplayPort) {
  ScreenMargin margins = aDisplayPort;
  margins.right = margins.left = margins.LeftRight() / 2;
  margins.top = margins.bottom = margins.TopBottom() / 2;
  return margins;
}

static PresShell* GetPresShell(const nsIContent* aContent) {
  if (dom::Document* doc = aContent->GetComposedDoc()) {
    return doc->GetPresShell();
  }
  return nullptr;
}

static CSSPoint ScrollFrameTo(nsIScrollableFrame* aFrame,
                              const RepaintRequest& aRequest,
                              bool& aSuccessOut) {
  aSuccessOut = false;
  CSSPoint targetScrollPosition = aRequest.GetLayoutScrollOffset();

  if (!aFrame) {
    return targetScrollPosition;
  }

  CSSPoint geckoScrollPosition =
      CSSPoint::FromAppUnits(aFrame->GetScrollPosition());

  // If the repaint request was triggered due to a previous main-thread scroll
  // offset update sent to the APZ, then we don't need to do another scroll here
  // and we can just return.
  if (!aRequest.GetScrollOffsetUpdated()) {
    return geckoScrollPosition;
  }

  // If this frame is overflow:hidden, then the expectation is that it was
  // sized in a way that respects its scrollable boundaries. For the root
  // frame, this means that it cannot be scrolled in such a way that it moves
  // the layout viewport. For a non-root frame, this means that it cannot be
  // scrolled at all.
  //
  // In either case, |targetScrollPosition| should be the same as
  // |geckoScrollPosition| here.
  //
  // However, this is slightly racy. We query the overflow property of the
  // scroll frame at the time the repaint request arrives at the main thread
  // (i.e., right now), but APZ made the decision of whether or not to allow
  // scrolling based on the information it had at the time it processed the
  // scroll event. The overflow property could have changed at some time
  // between the two events and so APZ may have computed a scrollable region
  // that is larger than what is actually allowed.
  //
  // Currently, we allow the scroll position to change even though the frame is
  // overflow:hidden (that is, we take |targetScrollPosition|). If this turns
  // out to be problematic, an alternative solution would be to ignore the
  // scroll position change (that is, use |geckoScrollPosition|).
  if (aFrame->GetScrollStyles().mVertical == StyleOverflow::Hidden &&
      targetScrollPosition.y != geckoScrollPosition.y) {
    NS_WARNING(
        nsPrintfCString(
            "APZCCH: targetScrollPosition.y (%f) != geckoScrollPosition.y (%f)",
            targetScrollPosition.y.value, geckoScrollPosition.y.value)
            .get());
  }
  if (aFrame->GetScrollStyles().mHorizontal == StyleOverflow::Hidden &&
      targetScrollPosition.x != geckoScrollPosition.x) {
    NS_WARNING(
        nsPrintfCString(
            "APZCCH: targetScrollPosition.x (%f) != geckoScrollPosition.x (%f)",
            targetScrollPosition.x.value, geckoScrollPosition.x.value)
            .get());
  }

  // If the scrollable frame is currently in the middle of an async or smooth
  // scroll then we don't want to interrupt it (see bug 961280).
  // Also if the scrollable frame got a scroll request from a higher priority
  // origin since the last layers update, then we don't want to push our scroll
  // request because we'll clobber that one, which is bad.
  bool scrollInProgress = APZCCallbackHelper::IsScrollInProgress(aFrame);
  if (!scrollInProgress) {
    ScrollSnapTargetIds snapTargetIds = aRequest.GetLastSnapTargetIds();
    aFrame->ScrollToCSSPixelsForApz(targetScrollPosition,
                                    std::move(snapTargetIds));
    geckoScrollPosition = CSSPoint::FromAppUnits(aFrame->GetScrollPosition());
    aSuccessOut = true;
  }
  // Return the final scroll position after setting it so that anything that
  // relies on it can have an accurate value. Note that even if we set it above
  // re-querying it is a good idea because it may have gotten clamped or
  // rounded.
  return geckoScrollPosition;
}

/**
 * Scroll the scroll frame associated with |aContent| to the scroll position
 * requested in |aRequest|.
 *
 * Any difference between the requested and actual scroll positions is used to
 * update the callback-transform stored on the content, and return a new
 * display port.
 */
static DisplayPortMargins ScrollFrame(nsIContent* aContent,
                                      const RepaintRequest& aRequest) {
  // Scroll the window to the desired spot
  nsIScrollableFrame* sf =
      nsLayoutUtils::FindScrollableFrameFor(aRequest.GetScrollId());
  if (sf) {
    sf->ResetScrollInfoIfNeeded(aRequest.GetScrollGeneration(),
                                aRequest.GetScrollGenerationOnApz(),
                                aRequest.GetScrollAnimationType(),
                                nsIScrollableFrame::InScrollingGesture(
                                    aRequest.IsInScrollingGesture()));
    sf->SetScrollableByAPZ(!aRequest.IsScrollInfoLayer());
    if (sf->IsRootScrollFrameOfDocument()) {
      if (!APZCCallbackHelper::IsScrollInProgress(sf)) {
        APZCCH_LOG("Setting VV offset to %s\n",
                   ToString(aRequest.GetVisualScrollOffset()).c_str());
        if (sf->SetVisualViewportOffset(
                CSSPoint::ToAppUnits(aRequest.GetVisualScrollOffset()),
                /* aRepaint = */ false)) {
          // sf can't be destroyed if SetVisualViewportOffset returned true.
          sf->MarkEverScrolled();
        }
      }
    }
  }
  // sf might have been destroyed by the call to SetVisualViewportOffset, so
  // re-get it.
  sf = nsLayoutUtils::FindScrollableFrameFor(aRequest.GetScrollId());
  bool scrollUpdated = false;
  auto displayPortMargins =
      DisplayPortMargins::ForScrollFrame(sf, aRequest.GetDisplayPortMargins());
  CSSPoint apzScrollOffset = aRequest.GetVisualScrollOffset();
  CSSPoint actualScrollOffset = ScrollFrameTo(sf, aRequest, scrollUpdated);
  CSSPoint scrollDelta = apzScrollOffset - actualScrollOffset;

  if (scrollUpdated) {
    if (aRequest.IsScrollInfoLayer()) {
      // In cases where the APZ scroll offset is different from the content
      // scroll offset, we want to interpret the margins as relative to the APZ
      // scroll offset except when the frame is not scrollable by APZ.
      // Therefore, if the layer is a scroll info layer, we leave the margins
      // as-is and they will be interpreted as relative to the content scroll
      // offset.
      if (nsIFrame* frame = aContent->GetPrimaryFrame()) {
        frame->SchedulePaint();
      }
    } else {
      // Correct the display port due to the difference between the requested
      // and actual scroll offsets.
      displayPortMargins =
          DisplayPortMargins::FromAPZ(aRequest.GetDisplayPortMargins(),
                                      apzScrollOffset, actualScrollOffset);
    }
  } else if (aRequest.IsRootContent() &&
             apzScrollOffset != aRequest.GetLayoutScrollOffset()) {
    // APZ uses the visual viewport's offset to calculate where to place the
    // display port, so the display port is misplaced when a pinch zoom occurs.
    //
    // We need to force a display port adjustment in the following paint to
    // account for a difference between the requested and actual scroll
    // offsets in repaints requested by
    // AsyncPanZoomController::NotifyLayersUpdated.
    displayPortMargins = DisplayPortMargins::FromAPZ(
        aRequest.GetDisplayPortMargins(), apzScrollOffset, actualScrollOffset);
  } else {
    // For whatever reason we couldn't update the scroll offset on the scroll
    // frame, which means the data APZ used for its displayport calculation is
    // stale. Fall back to a sane default behaviour. Note that we don't
    // tile-align the recentered displayport because tile-alignment depends on
    // the scroll position, and the scroll position here is out of our control.
    // See bug 966507 comment 21 for a more detailed explanation.
    displayPortMargins = DisplayPortMargins::ForScrollFrame(
        sf, RecenterDisplayPort(aRequest.GetDisplayPortMargins()));
  }

  // APZ transforms inputs assuming we applied the exact scroll offset it
  // requested (|apzScrollOffset|). Since we may not have, record the difference
  // between what APZ asked for and what we actually applied, and apply it to
  // input events to compensate.
  // Note that if the main-thread had a change in its scroll position, we don't
  // want to record that difference here, because it can be large and throw off
  // input events by a large amount. It is also going to be transient, because
  // any main-thread scroll position change will be synced to APZ and we will
  // get another repaint request when APZ confirms. In the interval while this
  // is happening we can just leave the callback transform as it was.
  bool mainThreadScrollChanged =
      sf && sf->CurrentScrollGeneration() != aRequest.GetScrollGeneration() &&
      nsLayoutUtils::CanScrollOriginClobberApz(sf->LastScrollOrigin());
  if (aContent && !mainThreadScrollChanged) {
    aContent->SetProperty(nsGkAtoms::apzCallbackTransform,
                          new CSSPoint(scrollDelta),
                          nsINode::DeleteProperty<CSSPoint>);
  }

  return displayPortMargins;
}

static void SetDisplayPortMargins(PresShell* aPresShell, nsIContent* aContent,
                                  const DisplayPortMargins& aDisplayPortMargins,
                                  CSSSize aDisplayPortBase) {
  if (!aContent) {
    return;
  }

  bool hadDisplayPort = DisplayPortUtils::HasDisplayPort(aContent);
  if (MOZ_LOG_TEST(sDisplayportLog, LogLevel::Debug)) {
    if (!hadDisplayPort) {
      mozilla::layers::ScrollableLayerGuid::ViewID viewID =
          mozilla::layers::ScrollableLayerGuid::NULL_SCROLL_ID;
      nsLayoutUtils::FindIDFor(aContent, &viewID);
      MOZ_LOG(
          sDisplayportLog, LogLevel::Debug,
          ("APZCCH installing displayport margins %s on scrollId=%" PRIu64 "\n",
           ToString(aDisplayPortMargins).c_str(), viewID));
    }
  }
  DisplayPortUtils::SetDisplayPortMargins(
      aContent, aPresShell, aDisplayPortMargins,
      hadDisplayPort ? DisplayPortUtils::ClearMinimalDisplayPortProperty::No
                     : DisplayPortUtils::ClearMinimalDisplayPortProperty::Yes,
      0);
  if (!hadDisplayPort) {
    DisplayPortUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors(
        aContent->GetPrimaryFrame());
  }

  nsRect base(0, 0, aDisplayPortBase.width * AppUnitsPerCSSPixel(),
              aDisplayPortBase.height * AppUnitsPerCSSPixel());
  DisplayPortUtils::SetDisplayPortBaseIfNotSet(aContent, base);
}

static void SetPaintRequestTime(nsIContent* aContent,
                                const TimeStamp& aPaintRequestTime) {
  aContent->SetProperty(nsGkAtoms::paintRequestTime,
                        new TimeStamp(aPaintRequestTime),
                        nsINode::DeleteProperty<TimeStamp>);
}

void APZCCallbackHelper::NotifyLayerTransforms(
    const nsTArray<MatrixMessage>& aTransforms) {
  MOZ_ASSERT(NS_IsMainThread());
  for (const MatrixMessage& msg : aTransforms) {
    BrowserParent* parent =
        BrowserParent::GetBrowserParentFromLayersId(msg.GetLayersId());
    if (parent) {
      parent->SetChildToParentConversionMatrix(
          ViewAs<LayoutDeviceToLayoutDeviceMatrix4x4>(
              msg.GetMatrix(),
              PixelCastJustification::ContentProcessIsLayerInUiProcess),
          msg.GetTopLevelViewportVisibleRectInBrowserCoords());
    }
  }
}

void APZCCallbackHelper::UpdateRootFrame(const RepaintRequest& aRequest) {
  if (aRequest.GetScrollId() == ScrollableLayerGuid::NULL_SCROLL_ID) {
    return;
  }
  RefPtr<nsIContent> content =
      nsLayoutUtils::FindContentFor(aRequest.GetScrollId());
  if (!content) {
    return;
  }

  RefPtr<PresShell> presShell = GetPresShell(content);
  if (!presShell || aRequest.GetPresShellId() != presShell->GetPresShellId()) {
    return;
  }

  APZCCH_LOG("Handling request %s\n", ToString(aRequest).c_str());
  if (nsLayoutUtils::AllowZoomingForDocument(presShell->GetDocument()) &&
      aRequest.GetAsyncZoom().scale != 1.0) {
    // If zooming is disabled then we don't really want to let APZ fiddle
    // with these things. In theory setting the resolution here should be a
    // no-op, but setting the visual viewport size is bad because it can cause a
    // stale value to be returned by window.innerWidth/innerHeight (see bug
    // 1187792).

    float presShellResolution = presShell->GetResolution();

    // If the pres shell resolution has changed on the content side side
    // the time this repaint request was fired, consider this request out of
    // date and drop it; setting a zoom based on the out-of-date resolution can
    // have the effect of getting us stuck with the stale resolution.
    // One might think that if the last ResolutionChangeOrigin was apz then the
    // pres shell resolutions should match but
    // that is not the case. We can get multiple repaint requests that has the
    // same pres shell resolution (because apz didn't receive a content layers
    // update inbetween) if the first has async zoom we apply that and chance
    // the content pres shell resolution and thus when handling the second
    // repaint request the pres shell resolution won't match. So that's why we
    // also check if the last resolution change origin was apz (aka 'us').
    if (!FuzzyEqualsMultiplicative(presShellResolution,
                                   aRequest.GetPresShellResolution()) &&
        presShell->GetLastResolutionChangeOrigin() !=
            ResolutionChangeOrigin::Apz) {
      return;
    }

    // The pres shell resolution is updated by the the async zoom since the
    // last paint.
    // We want to calculate the new presshell resolution as
    // |aRequest.GetPresShellResolution() * aRequest.GetAsyncZoom()| but that
    // calculation can lead to small inaccuracies due to limited floating point
    // precision. Specifically,
    // clang-format off
    // asyncZoom = zoom / layerPixelsPerCSSPixel
    //           = zoom / (devPixelsPerCSSPixel * cumulativeResolution)
    // clang-format on
    // Since this is a root frame we generally do not allow css transforms to
    // scale it, so it is very likely that cumulativeResolution ==
    // presShellResoluion. So
    // clang-format off
    // newPresShellResoluion = presShellResoluion * asyncZoom
    // = presShellResoluion * zoom / (devPixelsPerCSSPixel * presShellResoluion)
    // = zoom / devPixelsPerCSSPixel
    // clang-format on
    // However, we want to keep the calculation general and so we do not assume
    // presShellResoluion == cumulativeResolution, but rather factor those
    // values out so they cancel and the floating point division has a very high
    // probability of being exactly 1.
    presShellResolution =
        (aRequest.GetPresShellResolution() /
         aRequest.GetCumulativeResolution().scale) *
        (aRequest.GetZoom() / aRequest.GetDevPixelsPerCSSPixel()).scale;
    presShell->SetResolutionAndScaleTo(presShellResolution,
                                       ResolutionChangeOrigin::Apz);

    // Changing the resolution will trigger a reflow which will cause the
    // main-thread scroll position to be realigned in layer pixels. This
    // (subpixel) scroll mutation can trigger a scroll update to APZ which
    // is undesirable. Instead of having that happen as part of the post-reflow
    // code, we force it to happen here with ScrollOrigin::Apz so that it
    // doesn't trigger a scroll update to APZ.
    nsIScrollableFrame* sf =
        nsLayoutUtils::FindScrollableFrameFor(aRequest.GetScrollId());
    CSSPoint currentScrollPosition =
        CSSPoint::FromAppUnits(sf->GetScrollPosition());
    ScrollSnapTargetIds snapTargetIds = aRequest.GetLastSnapTargetIds();
    sf->ScrollToCSSPixelsForApz(currentScrollPosition,
                                std::move(snapTargetIds));
  }

  // Do this as late as possible since scrolling can flush layout. It also
  // adjusts the display port margins, so do it before we set those.
  DisplayPortMargins displayPortMargins = ScrollFrame(content, aRequest);

  SetDisplayPortMargins(presShell, content, displayPortMargins,
                        aRequest.CalculateCompositedSizeInCssPixels());
  SetPaintRequestTime(content, aRequest.GetPaintRequestTime());
}

void APZCCallbackHelper::UpdateSubFrame(const RepaintRequest& aRequest) {
  if (aRequest.GetScrollId() == ScrollableLayerGuid::NULL_SCROLL_ID) {
    return;
  }
  RefPtr<nsIContent> content =
      nsLayoutUtils::FindContentFor(aRequest.GetScrollId());
  if (!content) {
    return;
  }

  // We don't currently support zooming for subframes, so nothing extra
  // needs to be done beyond the tasks common to this and UpdateRootFrame.
  DisplayPortMargins displayPortMargins = ScrollFrame(content, aRequest);
  if (RefPtr<PresShell> presShell = GetPresShell(content)) {
    SetDisplayPortMargins(presShell, content, displayPortMargins,
                          aRequest.CalculateCompositedSizeInCssPixels());
  }
  SetPaintRequestTime(content, aRequest.GetPaintRequestTime());
}

bool APZCCallbackHelper::GetOrCreateScrollIdentifiers(
    nsIContent* aContent, uint32_t* aPresShellIdOut,
    ScrollableLayerGuid::ViewID* aViewIdOut) {
  if (!aContent) {
    return false;
  }
  *aViewIdOut = nsLayoutUtils::FindOrCreateIDFor(aContent);
  if (PresShell* presShell = GetPresShell(aContent)) {
    *aPresShellIdOut = presShell->GetPresShellId();
    return true;
  }
  return false;
}

void APZCCallbackHelper::InitializeRootDisplayport(PresShell* aPresShell) {
  // Create a view-id and set a zero-margin displayport for the root element
  // of the root document in the chrome process. This ensures that the scroll
  // frame for this element gets an APZC, which in turn ensures that all content
  // in the chrome processes is covered by an APZC.
  // The displayport is zero-margin because this element is generally not
  // actually scrollable (if it is, APZC will set proper margins when it's
  // scrolled).
  if (!aPresShell) {
    return;
  }

  MOZ_ASSERT(aPresShell->GetDocument());
  nsIContent* content = aPresShell->GetDocument()->GetDocumentElement();
  if (!content) {
    return;
  }

  uint32_t presShellId;
  ScrollableLayerGuid::ViewID viewId;
  if (APZCCallbackHelper::GetOrCreateScrollIdentifiers(content, &presShellId,
                                                       &viewId)) {
    MOZ_LOG(
        sDisplayportLog, LogLevel::Debug,
        ("Initializing root displayport on scrollId=%" PRIu64 "\n", viewId));
    Maybe<nsRect> baseRect =
        DisplayPortUtils::GetRootDisplayportBase(aPresShell);
    if (baseRect) {
      DisplayPortUtils::SetDisplayPortBaseIfNotSet(content, *baseRect);
    }

    DisplayPortUtils::SetDisplayPortMargins(
        content, aPresShell, DisplayPortMargins::Empty(content),
        DisplayPortUtils::ClearMinimalDisplayPortProperty::Yes, 0);
    DisplayPortUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors(
        content->GetPrimaryFrame());
  }
}

nsPresContext* APZCCallbackHelper::GetPresContextForContent(
    nsIContent* aContent) {
  dom::Document* doc = aContent->GetComposedDoc();
  if (!doc) {
    return nullptr;
  }
  PresShell* presShell = doc->GetPresShell();
  if (!presShell) {
    return nullptr;
  }
  return presShell->GetPresContext();
}

PresShell* APZCCallbackHelper::GetRootContentDocumentPresShellForContent(
    nsIContent* aContent) {
  nsPresContext* context = GetPresContextForContent(aContent);
  if (!context) {
    return nullptr;
  }
  context = context->GetInProcessRootContentDocumentPresContext();
  if (!context) {
    return nullptr;
  }
  return context->PresShell();
}

nsEventStatus APZCCallbackHelper::DispatchWidgetEvent(WidgetGUIEvent& aEvent) {
  nsEventStatus status = nsEventStatus_eConsumeNoDefault;
  if (aEvent.mWidget) {
    aEvent.mWidget->DispatchEvent(&aEvent, status);
  }
  return status;
}

nsEventStatus APZCCallbackHelper::DispatchSynthesizedMouseEvent(
    EventMessage aMsg, const LayoutDevicePoint& aRefPoint, Modifiers aModifiers,
    int32_t aClickCount, nsIWidget* aWidget) {
  MOZ_ASSERT(aMsg == eMouseMove || aMsg == eMouseDown || aMsg == eMouseUp ||
             aMsg == eMouseLongTap);

  WidgetMouseEvent event(true, aMsg, aWidget, WidgetMouseEvent::eReal,
                         WidgetMouseEvent::eNormal);
  event.mRefPoint = LayoutDeviceIntPoint::Truncate(aRefPoint.x, aRefPoint.y);
  event.mButton = MouseButton::ePrimary;
  event.mButtons |= MouseButtonsFlag::ePrimaryFlag;
  event.mInputSource = dom::MouseEvent_Binding::MOZ_SOURCE_TOUCH;
  if (aMsg == eMouseLongTap) {
    event.mFlags.mOnlyChromeDispatch = true;
  }
  if (aMsg != eMouseMove) {
    event.mClickCount = aClickCount;
  }
  event.mModifiers = aModifiers;
  // Real touch events will generate corresponding pointer events. We set
  // convertToPointer to false to prevent the synthesized mouse events generate
  // pointer events again.
  event.convertToPointer = false;
  return DispatchWidgetEvent(event);
}

PreventDefaultResult APZCCallbackHelper::DispatchMouseEvent(
    PresShell* aPresShell, const nsString& aType, const CSSPoint& aPoint,
    int32_t aButton, int32_t aClickCount, int32_t aModifiers,
    unsigned short aInputSourceArg, uint32_t aPointerId) {
  NS_ENSURE_TRUE(aPresShell, PreventDefaultResult::ByContent);

  PreventDefaultResult preventDefaultResult;
  nsContentUtils::SendMouseEvent(
      aPresShell, aType, aPoint.x, aPoint.y, aButton,
      nsIDOMWindowUtils::MOUSE_BUTTONS_NOT_SPECIFIED, aClickCount, aModifiers,
      /* aIgnoreRootScrollFrame = */ false, 0, aInputSourceArg, aPointerId,
      false, &preventDefaultResult, false,
      /* aIsWidgetEventSynthesized = */ false);
  return preventDefaultResult;
}

void APZCCallbackHelper::FireSingleTapEvent(const LayoutDevicePoint& aPoint,
                                            Modifiers aModifiers,
                                            int32_t aClickCount,
                                            nsIWidget* aWidget) {
  if (aWidget->Destroyed()) {
    return;
  }
  APZCCH_LOG("Dispatching single-tap component events to %s\n",
             ToString(aPoint).c_str());
  DispatchSynthesizedMouseEvent(eMouseMove, aPoint, aModifiers, aClickCount,
                                aWidget);
  DispatchSynthesizedMouseEvent(eMouseDown, aPoint, aModifiers, aClickCount,
                                aWidget);
  DispatchSynthesizedMouseEvent(eMouseUp, aPoint, aModifiers, aClickCount,
                                aWidget);
}

static dom::Element* GetDisplayportElementFor(
    nsIScrollableFrame* aScrollableFrame) {
  if (!aScrollableFrame) {
    return nullptr;
  }
  nsIFrame* scrolledFrame = aScrollableFrame->GetScrolledFrame();
  if (!scrolledFrame) {
    return nullptr;
  }
  // |scrolledFrame| should at this point be the root content frame of the
  // nearest ancestor scrollable frame. The element corresponding to this
  // frame should be the one with the displayport set on it, so find that
  // element and return it.
  nsIContent* content = scrolledFrame->GetContent();
  MOZ_ASSERT(content->IsElement());  // roc says this must be true
  return content->AsElement();
}

static dom::Element* GetRootDocumentElementFor(nsIWidget* aWidget) {
  // This returns the root element that ChromeProcessController sets the
  // displayport on during initialization.
  if (nsView* view = nsView::GetViewFor(aWidget)) {
    if (PresShell* presShell = view->GetPresShell()) {
      MOZ_ASSERT(presShell->GetDocument());
      return presShell->GetDocument()->GetDocumentElement();
    }
  }
  return nullptr;
}

namespace {

using FrameForPointOption = nsLayoutUtils::FrameForPointOption;

// Determine the scrollable target frame for the given point and add it to
// the target list. If the frame doesn't have a displayport, set one.
// Return whether or not the frame had a displayport that has already been
// painted (in this case, the caller can send the SetTargetAPZC notification
// right away, rather than waiting for a transaction to propagate the
// displayport to APZ first).
static bool PrepareForSetTargetAPZCNotification(
    nsIWidget* aWidget, const LayersId& aLayersId, nsIFrame* aRootFrame,
    const LayoutDeviceIntPoint& aRefPoint,
    nsTArray<ScrollableLayerGuid>* aTargets) {
  ScrollableLayerGuid guid(aLayersId, 0, ScrollableLayerGuid::NULL_SCROLL_ID);
  RelativeTo relativeTo{aRootFrame, ViewportType::Visual};
  nsPoint point = nsLayoutUtils::GetEventCoordinatesRelativeTo(
      aWidget, aRefPoint, relativeTo);
  nsIFrame* target = nsLayoutUtils::GetFrameForPoint(relativeTo, point);
  nsIScrollableFrame* scrollAncestor =
      target ? nsLayoutUtils::GetAsyncScrollableAncestorFrame(target)
             : aRootFrame->PresShell()->GetRootScrollFrameAsScrollable();

  // Assuming that if there's no scrollAncestor, there's already a displayPort.
  nsCOMPtr<dom::Element> dpElement =
      scrollAncestor ? GetDisplayportElementFor(scrollAncestor)
                     : GetRootDocumentElementFor(aWidget);

  if (MOZ_LOG_TEST(sApzHlpLog, LogLevel::Debug)) {
    nsAutoString dpElementDesc;
    if (dpElement) {
      dpElement->Describe(dpElementDesc);
    }
    APZCCH_LOG("For event at %s found scrollable element %p (%s)\n",
               ToString(aRefPoint).c_str(), dpElement.get(),
               NS_LossyConvertUTF16toASCII(dpElementDesc).get());
  }

  bool guidIsValid = APZCCallbackHelper::GetOrCreateScrollIdentifiers(
      dpElement, &(guid.mPresShellId), &(guid.mScrollId));
  aTargets->AppendElement(guid);

  if (!guidIsValid) {
    return false;
  }
  if (DisplayPortUtils::HasNonMinimalNonZeroDisplayPort(dpElement)) {
    // If the element has a displayport but it hasn't been painted yet,
    // we want the caller to wait for the paint to happen, but we don't
    // need to set the displayport here since it's already been set.
    return !DisplayPortUtils::HasPaintedDisplayPort(dpElement);
  }

  if (!scrollAncestor) {
    // This can happen if the document element gets swapped out after
    // ChromeProcessController runs InitializeRootDisplayport. In this case
    // let's try to set a displayport again and bail out on this operation.
    APZCCH_LOG("Widget %p's document element %p didn't have a displayport\n",
               aWidget, dpElement.get());
    APZCCallbackHelper::InitializeRootDisplayport(aRootFrame->PresShell());
    return false;
  }

  APZCCH_LOG("%p didn't have a displayport, so setting one...\n",
             dpElement.get());
  MOZ_LOG(sDisplayportLog, LogLevel::Debug,
          ("Activating displayport on scrollId=%" PRIu64 " for SetTargetAPZC\n",
           guid.mScrollId));
  bool activated = DisplayPortUtils::CalculateAndSetDisplayPortMargins(
      scrollAncestor, DisplayPortUtils::RepaintMode::Repaint);
  if (!activated) {
    return false;
  }

  nsIFrame* frame = do_QueryFrame(scrollAncestor);
  DisplayPortUtils::SetZeroMarginDisplayPortOnAsyncScrollableAncestors(frame);

  return !DisplayPortUtils::HasPaintedDisplayPort(dpElement);
}

static void SendLayersDependentApzcTargetConfirmation(
    nsIWidget* aWidget, uint64_t aInputBlockId,
    nsTArray<ScrollableLayerGuid>&& aTargets) {
  WindowRenderer* renderer = aWidget->GetWindowRenderer();
  if (!renderer) {
    return;
  }

  if (WebRenderLayerManager* wrlm = renderer->AsWebRender()) {
    if (WebRenderBridgeChild* wrbc = wrlm->WrBridge()) {
      wrbc->SendSetConfirmedTargetAPZC(aInputBlockId, aTargets);
    }
    return;
  }
}

}  // namespace

DisplayportSetListener::DisplayportSetListener(
    nsIWidget* aWidget, nsPresContext* aPresContext,
    const uint64_t& aInputBlockId, nsTArray<ScrollableLayerGuid>&& aTargets)
    : ManagedPostRefreshObserver(aPresContext),
      mWidget(aWidget),
      mInputBlockId(aInputBlockId),
      mTargets(std::move(aTargets)) {
  MOZ_ASSERT(!mAction, "Setting Action twice");
  mAction = [instance = MOZ_KnownLive(this)](bool aWasCanceled) {
    instance->OnPostRefresh();
    return Unregister::Yes;
  };
}

DisplayportSetListener::~DisplayportSetListener() = default;

void DisplayportSetListener::Register() {
  APZCCH_LOG("DisplayportSetListener::Register\n");
  mPresContext->RegisterManagedPostRefreshObserver(this);
}

void DisplayportSetListener::OnPostRefresh() {
  APZCCH_LOG("Got refresh, sending target APZCs for input block %" PRIu64 "\n",
             mInputBlockId);
  SendLayersDependentApzcTargetConfirmation(mWidget, mInputBlockId,
                                            std::move(mTargets));
}

already_AddRefed<DisplayportSetListener>
APZCCallbackHelper::SendSetTargetAPZCNotification(nsIWidget* aWidget,
                                                  dom::Document* aDocument,
                                                  const WidgetGUIEvent& aEvent,
                                                  const LayersId& aLayersId,
                                                  uint64_t aInputBlockId) {
  if (!aWidget || !aDocument) {
    return nullptr;
  }
  if (aInputBlockId == sLastTargetAPZCNotificationInputBlock) {
    // We have already confirmed the target APZC for a previous event of this
    // input block. If we activated a scroll frame for this input block,
    // sending another target APZC confirmation would be harmful, as it might
    // race the original confirmation (which needs to go through a layers
    // transaction).
    APZCCH_LOG("Not resending target APZC confirmation for input block %" PRIu64
               "\n",
               aInputBlockId);
    return nullptr;
  }
  sLastTargetAPZCNotificationInputBlock = aInputBlockId;
  if (PresShell* presShell = aDocument->GetPresShell()) {
    if (nsIFrame* rootFrame = presShell->GetRootFrame()) {
      bool waitForRefresh = false;
      nsTArray<ScrollableLayerGuid> targets;

      if (const WidgetTouchEvent* touchEvent = aEvent.AsTouchEvent()) {
        for (size_t i = 0; i < touchEvent->mTouches.Length(); i++) {
          waitForRefresh |= PrepareForSetTargetAPZCNotification(
              aWidget, aLayersId, rootFrame, touchEvent->mTouches[i]->mRefPoint,
              &targets);
        }
      } else if (const WidgetWheelEvent* wheelEvent = aEvent.AsWheelEvent()) {
        waitForRefresh = PrepareForSetTargetAPZCNotification(
            aWidget, aLayersId, rootFrame, wheelEvent->mRefPoint, &targets);
      } else if (const WidgetMouseEvent* mouseEvent = aEvent.AsMouseEvent()) {
        waitForRefresh = PrepareForSetTargetAPZCNotification(
            aWidget, aLayersId, rootFrame, mouseEvent->mRefPoint, &targets);
      }
      // TODO: Do other types of events need to be handled?

      if (!targets.IsEmpty()) {
        if (waitForRefresh) {
          APZCCH_LOG(
              "At least one target got a new displayport, need to wait for "
              "refresh\n");
          return MakeAndAddRef<DisplayportSetListener>(
              aWidget, presShell->GetPresContext(), aInputBlockId,
              std::move(targets));
        }
        APZCCH_LOG("Sending target APZCs for input block %" PRIu64 "\n",
                   aInputBlockId);
        aWidget->SetConfirmedTargetAPZC(aInputBlockId, targets);
      }
    }
  }
  return nullptr;
}

void APZCCallbackHelper::NotifyMozMouseScrollEvent(
    const ScrollableLayerGuid::ViewID& aScrollId, const nsString& aEvent) {
  nsCOMPtr<nsIContent> targetContent = nsLayoutUtils::FindContentFor(aScrollId);
  if (!targetContent) {
    return;
  }
  RefPtr<dom::Document> ownerDoc = targetContent->OwnerDoc();
  if (!ownerDoc) {
    return;
  }

  nsContentUtils::DispatchEventOnlyToChrome(ownerDoc, targetContent, aEvent,
                                            CanBubble::eYes, Cancelable::eYes);
}

void APZCCallbackHelper::NotifyFlushComplete(PresShell* aPresShell) {
  MOZ_ASSERT(NS_IsMainThread());
  // In some cases, flushing the APZ state to the main thread doesn't actually
  // trigger a flush and repaint (this is an intentional optimization - the
  // stuff visible to the user is still correct). However, reftests update their
  // snapshot based on invalidation events that are emitted during paints,
  // so we ensure that we kick off a paint when an APZ flush is done. Note that
  // only chrome/testing code can trigger this behaviour.
  if (aPresShell && aPresShell->GetRootFrame()) {
    aPresShell->GetRootFrame()->SchedulePaint(nsIFrame::PAINT_DEFAULT, false);
  }

  nsCOMPtr<nsIObserverService> observerService =
      mozilla::services::GetObserverService();
  MOZ_ASSERT(observerService);
  observerService->NotifyObservers(nullptr, "apz-repaints-flushed", nullptr);
}

/* static */
bool APZCCallbackHelper::IsScrollInProgress(nsIScrollableFrame* aFrame) {
  using AnimationState = nsIScrollableFrame::AnimationState;

  return aFrame->ScrollAnimationState().contains(AnimationState::MainThread) ||
         nsLayoutUtils::CanScrollOriginClobberApz(aFrame->LastScrollOrigin());
}

/* static */
void APZCCallbackHelper::NotifyAsyncScrollbarDragInitiated(
    uint64_t aDragBlockId, const ScrollableLayerGuid::ViewID& aScrollId,
    ScrollDirection aDirection) {
  MOZ_ASSERT(NS_IsMainThread());
  if (nsIScrollableFrame* scrollFrame =
          nsLayoutUtils::FindScrollableFrameFor(aScrollId)) {
    scrollFrame->AsyncScrollbarDragInitiated(aDragBlockId, aDirection);
  }
}

/* static */
void APZCCallbackHelper::NotifyAsyncScrollbarDragRejected(
    const ScrollableLayerGuid::ViewID& aScrollId) {
  MOZ_ASSERT(NS_IsMainThread());
  if (nsIScrollableFrame* scrollFrame =
          nsLayoutUtils::FindScrollableFrameFor(aScrollId)) {
    scrollFrame->AsyncScrollbarDragRejected();
  }
}

/* static */
void APZCCallbackHelper::NotifyAsyncAutoscrollRejected(
    const ScrollableLayerGuid::ViewID& aScrollId) {
  MOZ_ASSERT(NS_IsMainThread());
  nsCOMPtr<nsIObserverService> observerService =
      mozilla::services::GetObserverService();
  MOZ_ASSERT(observerService);

  nsAutoString data;
  data.AppendInt(aScrollId);
  observerService->NotifyObservers(nullptr, "autoscroll-rejected-by-apz",
                                   data.get());
}

/* static */
void APZCCallbackHelper::CancelAutoscroll(
    const ScrollableLayerGuid::ViewID& aScrollId) {
  MOZ_ASSERT(NS_IsMainThread());
  nsCOMPtr<nsIObserverService> observerService =
      mozilla::services::GetObserverService();
  MOZ_ASSERT(observerService);

  nsAutoString data;
  data.AppendInt(aScrollId);
  observerService->NotifyObservers(nullptr, "apz:cancel-autoscroll",
                                   data.get());
}

/* static */
void APZCCallbackHelper::NotifyScaleGestureComplete(
    const nsCOMPtr<nsIWidget>& aWidget, float aScale) {
  MOZ_ASSERT(NS_IsMainThread());

  if (nsView* view = nsView::GetViewFor(aWidget)) {
    if (PresShell* presShell = view->GetPresShell()) {
      dom::Document* doc = presShell->GetDocument();
      MOZ_ASSERT(doc);
      if (nsPIDOMWindowInner* win = doc->GetInnerWindow()) {
        dom::AutoJSAPI jsapi;
        if (!jsapi.Init(win)) {
          return;
        }

        JSContext* cx = jsapi.cx();
        JS::Rooted<JS::Value> detail(cx, JS::Float32Value(aScale));
        RefPtr<dom::CustomEvent> event =
            NS_NewDOMCustomEvent(doc, nullptr, nullptr);
        event->InitCustomEvent(cx, u"MozScaleGestureComplete"_ns,
                               /* CanBubble */ true,
                               /* Cancelable */ false, detail);
        event->SetTrusted(true);
        AsyncEventDispatcher* dispatcher = new AsyncEventDispatcher(doc, event);
        dispatcher->mOnlyChromeDispatch = ChromeOnlyDispatch::eYes;

        dispatcher->PostDOMEvent();
      }
    }
  }
}

/* static */
void APZCCallbackHelper::NotifyPinchGesture(
    PinchGestureInput::PinchGestureType aType,
    const LayoutDevicePoint& aFocusPoint, LayoutDeviceCoord aSpanChange,
    Modifiers aModifiers, const nsCOMPtr<nsIWidget>& aWidget) {
  APZCCH_LOG("APZCCallbackHelper dispatching pinch gesture\n");
  EventMessage msg;
  switch (aType) {
    case PinchGestureInput::PINCHGESTURE_START:
      msg = eMagnifyGestureStart;
      break;
    case PinchGestureInput::PINCHGESTURE_SCALE:
      msg = eMagnifyGestureUpdate;
      break;
    case PinchGestureInput::PINCHGESTURE_FINGERLIFTED:
    case PinchGestureInput::PINCHGESTURE_END:
      msg = eMagnifyGesture;
      break;
  }

  WidgetSimpleGestureEvent event(true, msg, aWidget.get());
  // XXX mDelta for the eMagnifyGesture event is supposed to be the
  // cumulative magnification over the entire gesture (per docs in
  // SimpleGestureEvent.webidl) but currently APZ just sends us a zero
  // aSpanChange for that event, so the mDelta is wrong. Nothing relies
  // on that currently, but we might want to fix it at some point.
  event.mDelta = aSpanChange;
  event.mModifiers = aModifiers;
  event.mRefPoint = RoundedToInt(aFocusPoint);

  DispatchWidgetEvent(event);
}

}  // namespace layers
}  // namespace mozilla
