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

#include <ostream>

#include "gfxUtils.h"
#include "nsStyleConsts.h"
#include "nsStyleStruct.h"
#include "mozilla/WritingModes.h"
#include "mozilla/gfx/Types.h"

namespace mozilla {
namespace layers {

const ScrollableLayerGuid::ViewID ScrollableLayerGuid::NULL_SCROLL_ID = 0;

std::ostream& operator<<(std::ostream& aStream, const FrameMetrics& aMetrics) {
  aStream << "{ [cb=" << aMetrics.GetCompositionBounds()
          << "] [sr=" << aMetrics.GetScrollableRect()
          << "] [s=" << aMetrics.GetVisualScrollOffset();
  if (aMetrics.GetVisualScrollUpdateType() != FrameMetrics::eNone) {
    aStream << "] [vd=" << aMetrics.GetVisualDestination();
  }
  if (aMetrics.IsScrollInfoLayer()) {
    aStream << "] [scrollinfo";
  }
  aStream << "] [dp=" << aMetrics.GetDisplayPort()
          << "] [rcs=" << aMetrics.GetBoundingCompositionSize()
          << "] [v=" << aMetrics.GetLayoutViewport()
          << nsPrintfCString("] [z=(ld=%.3f r=%.3f",
                             aMetrics.GetDevPixelsPerCSSPixel().scale,
                             aMetrics.GetPresShellResolution())
                 .get()
          << " cr=" << aMetrics.GetCumulativeResolution()
          << " z=" << aMetrics.GetZoom()
          << " t=" << aMetrics.GetTransformToAncestorScale() << " )] [u=("
          << (int)aMetrics.GetVisualScrollUpdateType() << " "
          << aMetrics.GetScrollGeneration()
          << ")] scrollId=" << aMetrics.GetScrollId();
  if (aMetrics.IsRootContent()) {
    aStream << " [rcd]";
  }
  aStream << " }";
  return aStream;
}

void FrameMetrics::RecalculateLayoutViewportOffset() {
  // For subframes, the visual and layout viewports coincide, so just
  // keep the layout viewport offset in sync with the visual one.
  if (!mIsRootContent) {
    mLayoutViewport.MoveTo(GetVisualScrollOffset());
    return;
  }
  // For the root, the two viewports can diverge, but the layout
  // viewport needs to keep enclosing the visual viewport.
  KeepLayoutViewportEnclosingVisualViewport(GetVisualViewport(),
                                            mScrollableRect, mLayoutViewport);
}

/* static */
void FrameMetrics::KeepLayoutViewportEnclosingVisualViewport(
    const CSSRect& aVisualViewport, const CSSRect& aScrollableRect,
    CSSRect& aLayoutViewport) {
  // If the visual viewport is contained within the layout viewport, we don't
  // need to make any adjustments, so we can exit early.
  //
  // Additionally, if the composition bounds changes (due to an orientation
  // change, window resize, etc.), it may take a few frames for aLayoutViewport
  // to update and during that time, the visual viewport may be larger than the
  // layout viewport. In such situations, we take an early exit if the visual
  // viewport contains the layout viewport.
  if (aLayoutViewport.Contains(aVisualViewport) ||
      aVisualViewport.Contains(aLayoutViewport)) {
    return;
  }

  // If visual viewport size is greater than the layout viewport, move the
  // layout viewport such that it remains inside the visual viewport. Otherwise,
  // move the layout viewport such that the visual viewport is contained
  // inside the layout viewport.
  if ((aLayoutViewport.Width() < aVisualViewport.Width() &&
       !FuzzyEqualsMultiplicative(aLayoutViewport.Width(),
                                  aVisualViewport.Width())) ||
      (aLayoutViewport.Height() < aVisualViewport.Height() &&
       !FuzzyEqualsMultiplicative(aLayoutViewport.Height(),
                                  aVisualViewport.Height()))) {
    if (aLayoutViewport.X() < aVisualViewport.X()) {
      // layout viewport moves right
      aLayoutViewport.MoveToX(aVisualViewport.X());
    } else if (aVisualViewport.XMost() < aLayoutViewport.XMost()) {
      // layout viewport moves left
      aLayoutViewport.MoveByX(aVisualViewport.XMost() -
                              aLayoutViewport.XMost());
    }
    if (aLayoutViewport.Y() < aVisualViewport.Y()) {
      // layout viewport moves down
      aLayoutViewport.MoveToY(aVisualViewport.Y());
    } else if (aVisualViewport.YMost() < aLayoutViewport.YMost()) {
      // layout viewport moves up
      aLayoutViewport.MoveByY(aVisualViewport.YMost() -
                              aLayoutViewport.YMost());
    }
  } else {
    if (aVisualViewport.X() < aLayoutViewport.X()) {
      aLayoutViewport.MoveToX(aVisualViewport.X());
    } else if (aLayoutViewport.XMost() < aVisualViewport.XMost()) {
      aLayoutViewport.MoveByX(aVisualViewport.XMost() -
                              aLayoutViewport.XMost());
    }
    if (aVisualViewport.Y() < aLayoutViewport.Y()) {
      aLayoutViewport.MoveToY(aVisualViewport.Y());
    } else if (aLayoutViewport.YMost() < aVisualViewport.YMost()) {
      aLayoutViewport.MoveByY(aVisualViewport.YMost() -
                              aLayoutViewport.YMost());
    }
  }

  // Regardless of any adjustment above, the layout viewport is not allowed
  // to go outside the scrollable rect.
  aLayoutViewport = aLayoutViewport.MoveInsideAndClamp(aScrollableRect);
}

/* static */
CSSRect FrameMetrics::CalculateScrollRange(
    const CSSRect& aScrollableRect, const ParentLayerRect& aCompositionBounds,
    const CSSToParentLayerScale& aZoom) {
  CSSSize scrollPortSize =
      CalculateCompositedSizeInCssPixels(aCompositionBounds, aZoom);
  CSSRect scrollRange = aScrollableRect;
  scrollRange.SetWidth(
      std::max(scrollRange.Width() - scrollPortSize.width, 0.0f));
  scrollRange.SetHeight(
      std::max(scrollRange.Height() - scrollPortSize.height, 0.0f));
  return scrollRange;
}

/* static */
CSSSize FrameMetrics::CalculateCompositedSizeInCssPixels(
    const ParentLayerRect& aCompositionBounds,
    const CSSToParentLayerScale& aZoom) {
  if (aZoom == CSSToParentLayerScale(0)) {
    return CSSSize();  // avoid division by zero
  }
  return aCompositionBounds.Size() / aZoom;
}

bool FrameMetrics::ApplyScrollUpdateFrom(const ScrollPositionUpdate& aUpdate) {
  // In applying a main-thread scroll update, try to preserve the relative
  // offset between the visual and layout viewports.
  CSSPoint relativeOffset = GetVisualScrollOffset() - GetLayoutScrollOffset();
  MOZ_ASSERT(IsRootContent() || relativeOffset == CSSPoint());
  // We need to set the two offsets together, otherwise a subsequent
  // RecalculateLayoutViewportOffset() could see divergent layout and
  // visual offsets.
  bool offsetChanged = SetLayoutScrollOffset(aUpdate.GetDestination());
  offsetChanged |=
      ClampAndSetVisualScrollOffset(aUpdate.GetDestination() + relativeOffset);
  return offsetChanged;
}

CSSPoint FrameMetrics::ApplyRelativeScrollUpdateFrom(
    const ScrollPositionUpdate& aUpdate) {
  MOZ_ASSERT(aUpdate.GetType() == ScrollUpdateType::Relative);
  CSSPoint origin = GetVisualScrollOffset();
  CSSPoint delta = (aUpdate.GetDestination() - aUpdate.GetSource());
  ClampAndSetVisualScrollOffset(origin + delta);
  return GetVisualScrollOffset() - origin;
}

CSSPoint FrameMetrics::ApplyPureRelativeScrollUpdateFrom(
    const ScrollPositionUpdate& aUpdate) {
  MOZ_ASSERT(aUpdate.GetType() == ScrollUpdateType::PureRelative);
  CSSPoint origin = GetVisualScrollOffset();
  ClampAndSetVisualScrollOffset(origin + aUpdate.GetDelta());
  return GetVisualScrollOffset() - origin;
}

void FrameMetrics::UpdatePendingScrollInfo(const ScrollPositionUpdate& aInfo) {
  // We only get this "pending scroll info" for paint-skip transactions,
  // but PureRelative position updates always trigger a full paint, so
  // we should never enter this code with a PureRelative update type. For
  // the other types, the destination field on the ScrollPositionUpdate will
  // tell us the final layout scroll position on the main thread.
  MOZ_ASSERT(aInfo.GetType() != ScrollUpdateType::PureRelative);

  // In applying a main-thread scroll update, try to preserve the relative
  // offset between the visual and layout viewports.
  CSSPoint relativeOffset = GetVisualScrollOffset() - GetLayoutScrollOffset();
  MOZ_ASSERT(IsRootContent() || relativeOffset == CSSPoint());

  SetLayoutScrollOffset(aInfo.GetDestination());
  ClampAndSetVisualScrollOffset(aInfo.GetDestination() + relativeOffset);
  mScrollGeneration = aInfo.GetGeneration();
}

ScrollSnapInfo::ScrollSnapInfo()
    : mScrollSnapStrictnessX(StyleScrollSnapStrictness::None),
      mScrollSnapStrictnessY(StyleScrollSnapStrictness::None) {}

bool ScrollSnapInfo::HasScrollSnapping() const {
  return mScrollSnapStrictnessY != StyleScrollSnapStrictness::None ||
         mScrollSnapStrictnessX != StyleScrollSnapStrictness::None;
}

bool ScrollSnapInfo::HasSnapPositions() const {
  if (!HasScrollSnapping()) {
    return false;
  }

  for (const auto& target : mSnapTargets) {
    if ((target.mSnapPositionX &&
         mScrollSnapStrictnessX != StyleScrollSnapStrictness::None) ||
        (target.mSnapPositionY &&
         mScrollSnapStrictnessY != StyleScrollSnapStrictness::None)) {
      return true;
    }
  }
  return false;
}

void ScrollSnapInfo::InitializeScrollSnapStrictness(
    WritingMode aWritingMode, const nsStyleDisplay* aDisplay) {
  if (aDisplay->mScrollSnapType.strictness == StyleScrollSnapStrictness::None) {
    return;
  }

  mScrollSnapStrictnessX = StyleScrollSnapStrictness::None;
  mScrollSnapStrictnessY = StyleScrollSnapStrictness::None;

  switch (aDisplay->mScrollSnapType.axis) {
    case StyleScrollSnapAxis::X:
      mScrollSnapStrictnessX = aDisplay->mScrollSnapType.strictness;
      break;
    case StyleScrollSnapAxis::Y:
      mScrollSnapStrictnessY = aDisplay->mScrollSnapType.strictness;
      break;
    case StyleScrollSnapAxis::Block:
      if (aWritingMode.IsVertical()) {
        mScrollSnapStrictnessX = aDisplay->mScrollSnapType.strictness;
      } else {
        mScrollSnapStrictnessY = aDisplay->mScrollSnapType.strictness;
      }
      break;
    case StyleScrollSnapAxis::Inline:
      if (aWritingMode.IsVertical()) {
        mScrollSnapStrictnessY = aDisplay->mScrollSnapType.strictness;
      } else {
        mScrollSnapStrictnessX = aDisplay->mScrollSnapType.strictness;
      }
      break;
    case StyleScrollSnapAxis::Both:
      mScrollSnapStrictnessX = aDisplay->mScrollSnapType.strictness;
      mScrollSnapStrictnessY = aDisplay->mScrollSnapType.strictness;
      break;
  }
}

std::ostream& operator<<(std::ostream& aStream,
                         const OverscrollBehavior& aBehavior) {
  switch (aBehavior) {
    case OverscrollBehavior::Auto: {
      aStream << "auto";
      break;
    }
    case OverscrollBehavior::Contain: {
      aStream << "contain";
      break;
    }
    case OverscrollBehavior::None: {
      aStream << "none";
      break;
    }
  }
  return aStream;
}

OverscrollBehaviorInfo::OverscrollBehaviorInfo()
    : mBehaviorX(OverscrollBehavior::Auto),
      mBehaviorY(OverscrollBehavior::Auto) {}

static OverscrollBehavior ToOverscrollBehavior(
    StyleOverscrollBehavior aBehavior) {
  switch (aBehavior) {
    case StyleOverscrollBehavior::Auto:
      return OverscrollBehavior::Auto;
    case StyleOverscrollBehavior::Contain:
      return OverscrollBehavior::Contain;
    case StyleOverscrollBehavior::None:
      return OverscrollBehavior::None;
  }
  MOZ_ASSERT_UNREACHABLE("Invalid overscroll behavior");
  return OverscrollBehavior::Auto;
}

OverscrollBehaviorInfo OverscrollBehaviorInfo::FromStyleConstants(
    StyleOverscrollBehavior aBehaviorX, StyleOverscrollBehavior aBehaviorY) {
  OverscrollBehaviorInfo result;
  result.mBehaviorX = ToOverscrollBehavior(aBehaviorX);
  result.mBehaviorY = ToOverscrollBehavior(aBehaviorY);
  return result;
}

bool OverscrollBehaviorInfo::operator==(
    const OverscrollBehaviorInfo& aOther) const {
  return mBehaviorX == aOther.mBehaviorX && mBehaviorY == aOther.mBehaviorY;
}

std::ostream& operator<<(std::ostream& aStream,
                         const OverscrollBehaviorInfo& aInfo) {
  if (aInfo.mBehaviorX == aInfo.mBehaviorY) {
    aStream << aInfo.mBehaviorX;
  } else {
    aStream << "{ x=" << aInfo.mBehaviorX << ", y=" << aInfo.mBehaviorY << " }";
  }
  return aStream;
}

std::ostream& operator<<(std::ostream& aStream,
                         const ScrollMetadata& aMetadata) {
  aStream << "{ [description=" << aMetadata.GetContentDescription()
          << "] [metrics=" << aMetadata.GetMetrics();
  if (aMetadata.GetScrollParentId() != ScrollableLayerGuid::NULL_SCROLL_ID) {
    aStream << "] [scrollParent=" << aMetadata.GetScrollParentId();
  }
  if (aMetadata.GetHasScrollgrab()) {
    aStream << "] [scrollgrab";
  }
  aStream << "] [overscroll=" << aMetadata.GetOverscrollBehavior() << "] ["
          << aMetadata.GetScrollUpdates().Length() << " scrollupdates"
          << "] }";
  return aStream;
}

StaticAutoPtr<const ScrollMetadata> ScrollMetadata::sNullMetadata;

}  // namespace layers
}  // namespace mozilla