/* 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 "HitTestingTreeNode.h"
#include <stack>

#include "AsyncPanZoomController.h"  // for AsyncPanZoomController
#include "mozilla/StaticPrefs_layout.h"
#include "mozilla/gfx/Point.h"        // for Point4D
#include "mozilla/layers/APZUtils.h"  // for AsyncTransform, CompleteAsyncTransform
#include "mozilla/layers/AsyncDragMetrics.h"  // for AsyncDragMetrics
#include "mozilla/ToString.h"                 // for ToString
#include "nsPrintfCString.h"                  // for nsPrintfCString
#include "UnitTransforms.h"                   // for ViewAs

static mozilla::LazyLogModule sApzMgrLog("apz.manager");

namespace mozilla {
namespace layers {

using gfx::CompositorHitTestInfo;

HitTestingTreeNode::HitTestingTreeNode(AsyncPanZoomController* aApzc,
                                       bool aIsPrimaryHolder,
                                       LayersId aLayersId)
    : mApzc(aApzc),
      mIsPrimaryApzcHolder(aIsPrimaryHolder),
      mLockCount(0),
      mLayersId(aLayersId),
      mFixedPosTarget(ScrollableLayerGuid::NULL_SCROLL_ID),
      mStickyPosTarget(ScrollableLayerGuid::NULL_SCROLL_ID),
      mOverride(EventRegionsOverride::NoOverride) {
  if (mIsPrimaryApzcHolder) {
    MOZ_ASSERT(mApzc);
  }
  MOZ_ASSERT(!mApzc || mApzc->GetLayersId() == mLayersId);
}

void HitTestingTreeNode::RecycleWith(
    const RecursiveMutexAutoLock& aProofOfTreeLock,
    AsyncPanZoomController* aApzc, LayersId aLayersId) {
  MOZ_ASSERT(IsRecyclable(aProofOfTreeLock));
  Destroy();  // clear out tree pointers
  mApzc = aApzc;
  mLayersId = aLayersId;
  MOZ_ASSERT(!mApzc || mApzc->GetLayersId() == mLayersId);
  // The caller is expected to call appropriate setters (SetHitTestData,
  // SetScrollbarData, SetFixedPosData, SetStickyPosData, etc.) to repopulate
  // all the data fields before using this node for "real work". Otherwise
  // those data fields may contain stale information from the previous use
  // of this node object.
}

HitTestingTreeNode::~HitTestingTreeNode() = default;

void HitTestingTreeNode::Destroy() {
  // This runs on the updater thread, it's not worth passing around extra raw
  // pointers just to assert it.

  mPrevSibling = nullptr;
  mLastChild = nullptr;
  mParent = nullptr;

  if (mApzc) {
    if (mIsPrimaryApzcHolder) {
      mApzc->Destroy();
    }
    mApzc = nullptr;
  }
}

bool HitTestingTreeNode::IsRecyclable(
    const RecursiveMutexAutoLock& aProofOfTreeLock) {
  return !(IsPrimaryHolder() || (mLockCount > 0));
}

void HitTestingTreeNode::SetLastChild(HitTestingTreeNode* aChild) {
  mLastChild = aChild;
  if (aChild) {
    aChild->mParent = this;

    if (aChild->GetApzc()) {
      AsyncPanZoomController* parent = GetNearestContainingApzc();
      // We assume that HitTestingTreeNodes with an ancestor/descendant
      // relationship cannot both point to the same APZC instance. This
      // assertion only covers a subset of cases in which that might occur,
      // but it's better than nothing.
      MOZ_ASSERT(aChild->GetApzc() != parent);
      aChild->SetApzcParent(parent);
    }
  }
}

void HitTestingTreeNode::SetScrollbarData(
    const Maybe<uint64_t>& aScrollbarAnimationId,
    const ScrollbarData& aScrollbarData) {
  mScrollbarAnimationId = aScrollbarAnimationId;
  mScrollbarData = aScrollbarData;
}

bool HitTestingTreeNode::MatchesScrollDragMetrics(
    const AsyncDragMetrics& aDragMetrics, LayersId aLayersId) const {
  return IsScrollThumbNode() && mLayersId == aLayersId &&
         mScrollbarData.mDirection == aDragMetrics.mDirection &&
         mScrollbarData.mTargetViewId == aDragMetrics.mViewId;
}

bool HitTestingTreeNode::IsScrollThumbNode() const {
  return mScrollbarData.mScrollbarLayerType ==
         layers::ScrollbarLayerType::Thumb;
}

bool HitTestingTreeNode::IsScrollbarNode() const {
  return mScrollbarData.mScrollbarLayerType != layers::ScrollbarLayerType::None;
}

bool HitTestingTreeNode::IsScrollbarContainerNode() const {
  return mScrollbarData.mScrollbarLayerType ==
         layers::ScrollbarLayerType::Container;
}

ScrollDirection HitTestingTreeNode::GetScrollbarDirection() const {
  MOZ_ASSERT(IsScrollbarNode());
  MOZ_ASSERT(mScrollbarData.mDirection.isSome());
  return *mScrollbarData.mDirection;
}

ScrollableLayerGuid::ViewID HitTestingTreeNode::GetScrollTargetId() const {
  return mScrollbarData.mTargetViewId;
}

Maybe<uint64_t> HitTestingTreeNode::GetScrollbarAnimationId() const {
  return mScrollbarAnimationId;
}

const ScrollbarData& HitTestingTreeNode::GetScrollbarData() const {
  return mScrollbarData;
}

void HitTestingTreeNode::SetFixedPosData(
    ScrollableLayerGuid::ViewID aFixedPosTarget, SideBits aFixedPosSides,
    const Maybe<uint64_t>& aFixedPositionAnimationId) {
  mFixedPosTarget = aFixedPosTarget;
  mFixedPosSides = aFixedPosSides;
  mFixedPositionAnimationId = aFixedPositionAnimationId;
}

ScrollableLayerGuid::ViewID HitTestingTreeNode::GetFixedPosTarget() const {
  return mFixedPosTarget;
}

SideBits HitTestingTreeNode::GetFixedPosSides() const { return mFixedPosSides; }

Maybe<uint64_t> HitTestingTreeNode::GetFixedPositionAnimationId() const {
  return mFixedPositionAnimationId;
}

void HitTestingTreeNode::SetPrevSibling(HitTestingTreeNode* aSibling) {
  mPrevSibling = aSibling;
  if (aSibling) {
    aSibling->mParent = mParent;

    if (aSibling->GetApzc()) {
      AsyncPanZoomController* parent =
          mParent ? mParent->GetNearestContainingApzc() : nullptr;
      aSibling->SetApzcParent(parent);
    }
  }
}

void HitTestingTreeNode::SetStickyPosData(
    ScrollableLayerGuid::ViewID aStickyPosTarget,
    const LayerRectAbsolute& aScrollRangeOuter,
    const LayerRectAbsolute& aScrollRangeInner,
    const Maybe<uint64_t>& aStickyPositionAnimationId) {
  mStickyPosTarget = aStickyPosTarget;
  mStickyScrollRangeOuter = aScrollRangeOuter;
  mStickyScrollRangeInner = aScrollRangeInner;
  mStickyPositionAnimationId = aStickyPositionAnimationId;
}

ScrollableLayerGuid::ViewID HitTestingTreeNode::GetStickyPosTarget() const {
  return mStickyPosTarget;
}

const LayerRectAbsolute& HitTestingTreeNode::GetStickyScrollRangeOuter() const {
  return mStickyScrollRangeOuter;
}

const LayerRectAbsolute& HitTestingTreeNode::GetStickyScrollRangeInner() const {
  return mStickyScrollRangeInner;
}

Maybe<uint64_t> HitTestingTreeNode::GetStickyPositionAnimationId() const {
  return mStickyPositionAnimationId;
}

void HitTestingTreeNode::MakeRoot() {
  mParent = nullptr;

  if (GetApzc()) {
    SetApzcParent(nullptr);
  }
}

HitTestingTreeNode* HitTestingTreeNode::GetFirstChild() const {
  HitTestingTreeNode* child = GetLastChild();
  while (child && child->GetPrevSibling()) {
    child = child->GetPrevSibling();
  }
  return child;
}

HitTestingTreeNode* HitTestingTreeNode::GetLastChild() const {
  return mLastChild;
}

HitTestingTreeNode* HitTestingTreeNode::GetPrevSibling() const {
  return mPrevSibling;
}

HitTestingTreeNode* HitTestingTreeNode::GetParent() const { return mParent; }

bool HitTestingTreeNode::IsAncestorOf(const HitTestingTreeNode* aOther) const {
  for (const HitTestingTreeNode* cur = aOther; cur; cur = cur->GetParent()) {
    if (cur == this) {
      return true;
    }
  }
  return false;
}

AsyncPanZoomController* HitTestingTreeNode::GetApzc() const { return mApzc; }

AsyncPanZoomController* HitTestingTreeNode::GetNearestContainingApzc() const {
  for (const HitTestingTreeNode* n = this; n; n = n->GetParent()) {
    if (n->GetApzc()) {
      return n->GetApzc();
    }
  }
  return nullptr;
}

bool HitTestingTreeNode::IsPrimaryHolder() const {
  return mIsPrimaryApzcHolder;
}

LayersId HitTestingTreeNode::GetLayersId() const { return mLayersId; }

void HitTestingTreeNode::SetHitTestData(
    const LayerIntRect& aVisibleRect, const LayerIntSize& aRemoteDocumentSize,
    const CSSTransformMatrix& aTransform, const EventRegionsOverride& aOverride,
    const Maybe<ScrollableLayerGuid::ViewID>& aAsyncZoomContainerId) {
  mVisibleRect = aVisibleRect;
  mRemoteDocumentSize = aRemoteDocumentSize;
  mTransform = aTransform;
  mOverride = aOverride;
  mAsyncZoomContainerId = aAsyncZoomContainerId;
}

EventRegionsOverride HitTestingTreeNode::GetEventRegionsOverride() const {
  return mOverride;
}

const CSSTransformMatrix& HitTestingTreeNode::GetTransform() const {
  return mTransform;
}

LayerToScreenMatrix4x4 HitTestingTreeNode::GetTransformToGecko(
    LayersId aRemoteLayersId) const {
  if (mParent) {
    LayerToParentLayerMatrix4x4 thisToParent =
        mTransform * AsyncTransformMatrix();
    if (mApzc) {
      thisToParent =
          thisToParent * ViewAs<AsyncTransformComponentMatrix>(
                             mApzc->GetTransformToLastDispatchedPaint(
                                 LayoutAndVisual, aRemoteLayersId));
    }
    ParentLayerToScreenMatrix4x4 parentToRoot =
        ViewAs<ParentLayerToScreenMatrix4x4>(
            mParent->GetTransformToGecko(aRemoteLayersId),
            PixelCastJustification::MovingDownToChildren);
    return thisToParent * parentToRoot;
  }

  return ViewAs<LayerToScreenMatrix4x4>(
      mTransform * AsyncTransformMatrix(),
      PixelCastJustification::ScreenIsParentLayerForRoot);
}

const LayerIntRect& HitTestingTreeNode::GetVisibleRect() const {
  return mVisibleRect;
}

ScreenRect HitTestingTreeNode::GetRemoteDocumentScreenRect(
    LayersId aRemoteDocumentLayersId) const {
  ScreenRect result = TransformBy(
      GetTransformToGecko(aRemoteDocumentLayersId),
      IntRectToRect(LayerIntRect(LayerIntPoint(), mRemoteDocumentSize)));

  for (const HitTestingTreeNode* node = this; node; node = node->GetParent()) {
    if (!node->GetApzc()) {
      continue;
    }

    ParentLayerRect compositionBounds = node->GetApzc()->GetCompositionBounds();
    if (compositionBounds.IsEmpty()) {
      return ScreenRect();
    }

    ScreenRect scrollPortOnScreenCoordinate = TransformBy(
        node->GetParent()
            ? node->GetParent()->GetTransformToGecko(node->GetLayersId())
            : LayerToScreenMatrix4x4(),
        ViewAs<LayerPixel>(compositionBounds,
                           PixelCastJustification::MovingDownToChildren));
    if (scrollPortOnScreenCoordinate.IsEmpty()) {
      return ScreenRect();
    }

    result = result.Intersect(scrollPortOnScreenCoordinate);
    if (result.IsEmpty()) {
      return ScreenRect();
    }
  }
  return result;
}

Maybe<ScrollableLayerGuid::ViewID> HitTestingTreeNode::GetAsyncZoomContainerId()
    const {
  return mAsyncZoomContainerId;
}

void HitTestingTreeNode::Dump(const char* aPrefix) const {
  MOZ_LOG(
      sApzMgrLog, LogLevel::Debug,
      ("%sHitTestingTreeNode (%p) APZC (%p) g=(%s) %s%s%s t=(%s) "
       "%s%s\n",
       aPrefix, this, mApzc.get(),
       mApzc ? ToString(mApzc->GetGuid()).c_str()
             : nsPrintfCString("l=0x%" PRIx64, uint64_t(mLayersId)).get(),
       (mOverride & EventRegionsOverride::ForceDispatchToContent) ? "fdtc "
                                                                  : "",
       (mOverride & EventRegionsOverride::ForceEmptyHitRegion) ? "fehr " : "",
       (mFixedPosTarget != ScrollableLayerGuid::NULL_SCROLL_ID)
           ? nsPrintfCString("fixed=%" PRIu64 " ", mFixedPosTarget).get()
           : "",
       ToString(mTransform).c_str(),
       mScrollbarData.mDirection.isSome() ? " scrollbar" : "",
       IsScrollThumbNode() ? " scrollthumb" : ""));

  if (!mLastChild) {
    return;
  }

  // Dump the children in order from first child to last child
  std::stack<HitTestingTreeNode*> children;
  for (HitTestingTreeNode* child = mLastChild.get(); child;
       child = child->mPrevSibling) {
    children.push(child);
  }
  nsPrintfCString childPrefix("%s  ", aPrefix);
  while (!children.empty()) {
    children.top()->Dump(childPrefix.get());
    children.pop();
  }
}

void HitTestingTreeNode::SetApzcParent(AsyncPanZoomController* aParent) {
  // precondition: GetApzc() is non-null
  MOZ_ASSERT(GetApzc() != nullptr);
  if (IsPrimaryHolder()) {
    GetApzc()->SetParent(aParent);
  } else {
    MOZ_ASSERT(GetApzc()->GetParent() == aParent);
  }
}

void HitTestingTreeNode::Lock(const RecursiveMutexAutoLock& aProofOfTreeLock) {
  mLockCount++;
}

void HitTestingTreeNode::Unlock(
    const RecursiveMutexAutoLock& aProofOfTreeLock) {
  MOZ_ASSERT(mLockCount > 0);
  mLockCount--;
}

HitTestingTreeNodeAutoLock::HitTestingTreeNodeAutoLock()
    : mTreeMutex(nullptr) {}

HitTestingTreeNodeAutoLock::~HitTestingTreeNodeAutoLock() { Clear(); }

void HitTestingTreeNodeAutoLock::Initialize(
    const RecursiveMutexAutoLock& aProofOfTreeLock,
    already_AddRefed<HitTestingTreeNode> aNode, RecursiveMutex& aTreeMutex) {
  MOZ_ASSERT(!mNode);

  mNode = aNode;
  mTreeMutex = &aTreeMutex;

  mNode->Lock(aProofOfTreeLock);
}

void HitTestingTreeNodeAutoLock::Clear() {
  if (!mNode) {
    return;
  }
  MOZ_ASSERT(mTreeMutex);

  {  // scope lock
    RecursiveMutexAutoLock lock(*mTreeMutex);
    mNode->Unlock(lock);
  }
  mNode = nullptr;
  mTreeMutex = nullptr;
}

}  // namespace layers
}  // namespace mozilla
