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

#include <algorithm>  // for std::stable_sort
#include "mozilla/Assertions.h"
#include "mozilla/FloatingPoint.h"
#include "AsyncPanZoomController.h"

namespace mozilla {
namespace layers {

OverscrollHandoffChain::~OverscrollHandoffChain() = default;

void OverscrollHandoffChain::Add(AsyncPanZoomController* aApzc) {
  mChain.push_back(aApzc);
}

struct CompareByScrollPriority {
  bool operator()(const RefPtr<AsyncPanZoomController>& a,
                  const RefPtr<AsyncPanZoomController>& b) const {
    return a->HasScrollgrab() && !b->HasScrollgrab();
  }
};

void OverscrollHandoffChain::SortByScrollPriority() {
  // The sorting being stable ensures that the relative order between
  // non-scrollgrabbing APZCs remains child -> parent.
  // (The relative order between scrollgrabbing APZCs will also remain
  // child -> parent, though that's just an artefact of the implementation
  // and users of 'scrollgrab' should not rely on this.)
  std::stable_sort(mChain.begin(), mChain.end(), CompareByScrollPriority());
}

const RefPtr<AsyncPanZoomController>& OverscrollHandoffChain::GetApzcAtIndex(
    uint32_t aIndex) const {
  MOZ_ASSERT(aIndex < Length());
  return mChain[aIndex];
}

uint32_t OverscrollHandoffChain::IndexOf(
    const AsyncPanZoomController* aApzc) const {
  uint32_t i;
  for (i = 0; i < Length(); ++i) {
    if (mChain[i] == aApzc) {
      break;
    }
  }
  return i;
}

void OverscrollHandoffChain::ForEachApzc(APZCMethod aMethod) const {
  for (uint32_t i = 0; i < Length(); ++i) {
    (mChain[i]->*aMethod)();
  }
}

bool OverscrollHandoffChain::AnyApzc(APZCPredicate aPredicate) const {
  MOZ_ASSERT(Length() > 0);
  for (uint32_t i = 0; i < Length(); ++i) {
    if ((mChain[i]->*aPredicate)()) {
      return true;
    }
  }
  return false;
}

void OverscrollHandoffChain::FlushRepaints() const {
  ForEachApzc(&AsyncPanZoomController::FlushRepaintForOverscrollHandoff);
}

void OverscrollHandoffChain::CancelAnimations(
    CancelAnimationFlags aFlags) const {
  MOZ_ASSERT(Length() > 0);
  for (uint32_t i = 0; i < Length(); ++i) {
    mChain[i]->CancelAnimation(aFlags);
  }
}

void OverscrollHandoffChain::ClearOverscroll() const {
  ForEachApzc(&AsyncPanZoomController::ClearOverscroll);
}

void OverscrollHandoffChain::SnapBackOverscrolledApzc(
    const AsyncPanZoomController* aStart) const {
  uint32_t i = IndexOf(aStart);
  for (; i < Length(); ++i) {
    AsyncPanZoomController* apzc = mChain[i];
    if (!apzc->IsDestroyed()) {
      apzc->SnapBackIfOverscrolled();
    }
  }
}

void OverscrollHandoffChain::SnapBackOverscrolledApzcForMomentum(
    const AsyncPanZoomController* aStart,
    const ParentLayerPoint& aVelocity) const {
  uint32_t i = IndexOf(aStart);
  for (; i < Length(); ++i) {
    AsyncPanZoomController* apzc = mChain[i];
    if (!apzc->IsDestroyed()) {
      apzc->SnapBackIfOverscrolledForMomentum(aVelocity);
    }
  }
}

bool OverscrollHandoffChain::CanBePanned(
    const AsyncPanZoomController* aApzc) const {
  // Find |aApzc| in the handoff chain.
  uint32_t i = IndexOf(aApzc);

  // See whether any APZC in the handoff chain starting from |aApzc|
  // has room to be panned.
  for (uint32_t j = i; j < Length(); ++j) {
    if (mChain[j]->IsPannable()) {
      return true;
    }
  }

  return false;
}

bool OverscrollHandoffChain::CanScrollInDirection(
    const AsyncPanZoomController* aApzc, ScrollDirection aDirection) const {
  // Find |aApzc| in the handoff chain.
  uint32_t i = IndexOf(aApzc);

  // See whether any APZC in the handoff chain starting from |aApzc|
  // has room to scroll in the given direction.
  for (uint32_t j = i; j < Length(); ++j) {
    if (mChain[j]->CanScroll(aDirection)) {
      return true;
    }
  }

  return false;
}

bool OverscrollHandoffChain::HasOverscrolledApzc() const {
  return AnyApzc(&AsyncPanZoomController::IsOverscrolled);
}

bool OverscrollHandoffChain::HasFastFlungApzc() const {
  return AnyApzc(&AsyncPanZoomController::IsFlingingFast);
}

bool OverscrollHandoffChain::HasAutoscrollApzc() const {
  return AnyApzc(&AsyncPanZoomController::IsAutoscroll);
}

RefPtr<AsyncPanZoomController> OverscrollHandoffChain::FindFirstScrollable(
    const InputData& aInput, ScrollDirections* aOutAllowedScrollDirections,
    IncludeOverscroll aIncludeOverscroll) const {
  // Start by allowing scrolling in both directions. As we do handoff
  // overscroll-behavior may restrict one or both of the directions.
  *aOutAllowedScrollDirections += ScrollDirection::eVertical;
  *aOutAllowedScrollDirections += ScrollDirection::eHorizontal;

  for (size_t i = 0; i < Length(); i++) {
    if (mChain[i]->CanScroll(aInput)) {
      return mChain[i];
    }

    // If there is any directions we allow overscroll effects on the root
    // content APZC (i.e. the overscroll-behavior of the root one is not
    // `none`), we consider the APZC can be scrollable in terms of pan gestures
    // because it causes overscrolling even if it's not able to scroll to the
    // direction.
    if (StaticPrefs::apz_overscroll_enabled() && bool(aIncludeOverscroll) &&
        // FIXME: Bug 1707491: Drop this pan gesture input check.
        aInput.mInputType == PANGESTURE_INPUT && mChain[i]->IsRootContent()) {
      // Check whether the root content APZC is also overscrollable governed by
      // overscroll-behavior in the same directions where we allow scrolling
      // handoff and where we are going to scroll, if it matches we do handoff
      // to the root content APZC.
      // In other words, if the root content is not scrollable, we don't
      // handoff.
      ScrollDirections allowedOverscrollDirections =
          mChain[i]->GetOverscrollableDirections();
      ParentLayerPoint delta = mChain[i]->GetDeltaForEvent(aInput);
      if (mChain[i]->IsZero(delta.x)) {
        allowedOverscrollDirections -= ScrollDirection::eHorizontal;
      }
      if (mChain[i]->IsZero(delta.y)) {
        allowedOverscrollDirections -= ScrollDirection::eVertical;
      }

      allowedOverscrollDirections &= *aOutAllowedScrollDirections;
      if (!allowedOverscrollDirections.isEmpty()) {
        *aOutAllowedScrollDirections = allowedOverscrollDirections;
        return mChain[i];
      }
    }

    *aOutAllowedScrollDirections &= mChain[i]->GetAllowedHandoffDirections();
    if (aOutAllowedScrollDirections->isEmpty()) {
      return nullptr;
    }
  }
  return nullptr;
}

std::tuple<bool, const AsyncPanZoomController*>
OverscrollHandoffChain::ScrollingDownWillMoveDynamicToolbar(
    const AsyncPanZoomController* aApzc) const {
  MOZ_ASSERT(aApzc && !aApzc->IsRootContent(),
             "Should be used for non-root APZC");

  for (uint32_t i = IndexOf(aApzc); i < Length(); i++) {
    if (mChain[i]->IsRootContent()) {
      bool scrollable = mChain[i]->CanVerticalScrollWithDynamicToolbar();
      return {scrollable, scrollable ? mChain[i].get() : nullptr};
    }

    if (mChain[i]->CanScrollDownwards()) {
      return {false, nullptr};
    }
  }

  return {false, nullptr};
}

bool OverscrollHandoffChain::ScrollingUpWillTriggerPullToRefresh(
    const AsyncPanZoomController* aApzc) const {
  MOZ_ASSERT(aApzc && !aApzc->IsRootContent(),
             "Should be used for non-root APZC");

  for (uint32_t i = IndexOf(aApzc); i < Length(); i++) {
    if (mChain[i]->IsRootContent()) {
      return mChain[i]->CanOverscrollUpwards();
    }

    if (!mChain[i]->CanOverscrollUpwards()) {
      return false;
    }
  }
  return false;
}

}  // namespace layers
}  // namespace mozilla