// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "cc/layers/viewport.h"

#include "base/logging.h"
#include "base/memory/ptr_util.h"
#include "cc/input/browser_controls_offset_manager.h"
#include "cc/trees/layer_tree_host_impl.h"
#include "cc/trees/layer_tree_impl.h"
#include "cc/trees/scroll_node.h"
#include "ui/gfx/geometry/vector2d_conversions.h"
#include "ui/gfx/geometry/vector2d_f.h"

namespace cc {

// static
std::unique_ptr<Viewport> Viewport::Create(LayerTreeHostImpl* host_impl) {
  return base::WrapUnique(new Viewport(host_impl));
}

Viewport::Viewport(LayerTreeHostImpl* host_impl)
    : host_impl_(host_impl)
    , pinch_zoom_active_(false) {
  DCHECK(host_impl_);
}

void Viewport::Pan(const gfx::Vector2dF& delta) {
  gfx::Vector2dF pending_delta = delta;
  float page_scale = host_impl_->active_tree()->current_page_scale_factor();
  pending_delta.Scale(1 / page_scale);
  scroll_tree().ScrollBy(InnerScrollNode(), pending_delta,
                         host_impl_->active_tree());
}

Viewport::ScrollResult Viewport::ScrollBy(const gfx::Vector2dF& delta,
                                          const gfx::Point& viewport_point,
                                          bool is_direct_manipulation,
                                          bool affect_browser_controls,
                                          bool scroll_outer_viewport) {
  if (!OuterScrollNode())
    return ScrollResult();

  gfx::Vector2dF content_delta = delta;

  if (affect_browser_controls && ShouldBrowserControlsConsumeScroll(delta))
    content_delta -= ScrollBrowserControls(delta);

  gfx::Vector2dF pending_content_delta = content_delta;

  ScrollNode* inner_node = InnerScrollNode();
  pending_content_delta -= host_impl_->ScrollSingleNode(
      inner_node, pending_content_delta, viewport_point, is_direct_manipulation,
      &scroll_tree());

  ScrollResult result;

  if (scroll_outer_viewport) {
    pending_content_delta -= host_impl_->ScrollSingleNode(
        OuterScrollNode(), pending_content_delta, viewport_point,
        is_direct_manipulation, &scroll_tree());
  }

  result.consumed_delta = delta - AdjustOverscroll(pending_content_delta);

  result.content_scrolled_delta = content_delta - pending_content_delta;
  return result;
}

bool Viewport::CanScroll(const ScrollState& scroll_state) const {
  auto* outer_node = OuterScrollNode();

  if (!outer_node)
    return false;

  bool result = false;
  if (auto* inner_node = InnerScrollNode())
    result |= host_impl_->CanConsumeDelta(*inner_node, scroll_state);

  result |= host_impl_->CanConsumeDelta(*outer_node, scroll_state);

  return result;
}

void Viewport::ScrollByInnerFirst(const gfx::Vector2dF& delta) {
  gfx::Vector2dF unused_delta = scroll_tree().ScrollBy(
      InnerScrollNode(), delta, host_impl_->active_tree());

  auto* outer_node = OuterScrollNode();
  if (!unused_delta.IsZero() && outer_node) {
    scroll_tree().ScrollBy(outer_node, unused_delta, host_impl_->active_tree());
  }
}

bool Viewport::ShouldAnimateViewport(const gfx::Vector2dF& viewport_delta,
                                     const gfx::Vector2dF& pending_delta) {
  float max_dim_viewport_delta =
      std::max(std::abs(viewport_delta.x()), std::abs(viewport_delta.y()));
  float max_dim_pending_delta =
      std::max(std::abs(pending_delta.x()), std::abs(pending_delta.y()));
  return max_dim_viewport_delta > max_dim_pending_delta;
}

gfx::Vector2dF Viewport::ScrollAnimated(const gfx::Vector2dF& delta,
                                        base::TimeDelta delayed_by) {
  auto* outer_node = OuterScrollNode();
  if (!outer_node)
    return gfx::Vector2dF(0, 0);

  float scale_factor = host_impl_->active_tree()->current_page_scale_factor();
  gfx::Vector2dF scaled_delta = delta;
  scaled_delta.Scale(1.f / scale_factor);

  ScrollNode* inner_node = InnerScrollNode();
  gfx::Vector2dF inner_delta =
      host_impl_->ComputeScrollDelta(*inner_node, delta);

  gfx::Vector2dF pending_delta = scaled_delta - inner_delta;
  pending_delta.Scale(scale_factor);

  gfx::Vector2dF outer_delta =
      host_impl_->ComputeScrollDelta(*outer_node, pending_delta);

  if (inner_delta.IsZero() && outer_delta.IsZero())
    return gfx::Vector2dF(0, 0);

  // Animate the viewport to which the majority of scroll delta will be applied.
  // The animation system only supports running one scroll offset animation.
  // TODO(ymalik): Fix the visible jump seen by instant scrolling one of the
  // viewports.
  bool will_animate = false;
  if (ShouldAnimateViewport(inner_delta, outer_delta)) {
    scroll_tree().ScrollBy(outer_node, outer_delta, host_impl_->active_tree());
    will_animate =
        host_impl_->ScrollAnimationCreate(inner_node, inner_delta, delayed_by);
  } else {
    scroll_tree().ScrollBy(inner_node, inner_delta, host_impl_->active_tree());
    will_animate =
        host_impl_->ScrollAnimationCreate(outer_node, outer_delta, delayed_by);
  }
  if (will_animate) {
    // Consume entire scroll delta as long as we are starting an animation.
    return delta;
  }

  pending_delta = scaled_delta - inner_delta - outer_delta;
  pending_delta.Scale(scale_factor);
  return pending_delta;
}

void Viewport::SnapPinchAnchorIfWithinMargin(const gfx::Point& anchor) {
  gfx::SizeF viewport_size = gfx::SizeF(InnerScrollNode()->container_bounds);

  if (anchor.x() < kPinchZoomSnapMarginDips)
    pinch_anchor_adjustment_.set_x(-anchor.x());
  else if (anchor.x() > viewport_size.width() - kPinchZoomSnapMarginDips)
    pinch_anchor_adjustment_.set_x(viewport_size.width() - anchor.x());

  if (anchor.y() < kPinchZoomSnapMarginDips)
    pinch_anchor_adjustment_.set_y(-anchor.y());
  else if (anchor.y() > viewport_size.height() - kPinchZoomSnapMarginDips)
    pinch_anchor_adjustment_.set_y(viewport_size.height() - anchor.y());
}

void Viewport::PinchUpdate(float magnify_delta, const gfx::Point& anchor) {
  if (!pinch_zoom_active_) {
    // If this is the first pinch update and the pinch is within a margin-
    // length of the screen edge, offset all updates by the amount so that we
    // effectively snap the pinch zoom to the edge of the screen. This makes it
    // easy to zoom in on position: fixed elements.
    SnapPinchAnchorIfWithinMargin(anchor);

    pinch_zoom_active_ = true;
  }

  LayerTreeImpl* active_tree = host_impl_->active_tree();

  // Keep the center-of-pinch anchor specified by (x, y) in a stable
  // position over the course of the magnify.
  gfx::Point adjusted_anchor = anchor + pinch_anchor_adjustment_;
  float page_scale = active_tree->current_page_scale_factor();
  gfx::PointF previous_scale_anchor =
      gfx::ScalePoint(gfx::PointF(adjusted_anchor), 1.f / page_scale);
  active_tree->SetPageScaleOnActiveTree(page_scale * magnify_delta);
  page_scale = active_tree->current_page_scale_factor();
  gfx::PointF new_scale_anchor =
      gfx::ScalePoint(gfx::PointF(adjusted_anchor), 1.f / page_scale);
  gfx::Vector2dF move = previous_scale_anchor - new_scale_anchor;

  // Scale back to viewport space since that's the coordinate space ScrollBy
  // uses.
  move.Scale(page_scale);

  // If clamping the inner viewport scroll offset causes a change, it should
  // be accounted for from the intended move.
  move -= scroll_tree().ClampScrollToMaxScrollOffset(InnerScrollNode(),
                                                     host_impl_->active_tree());

  Pan(move);
}

void Viewport::PinchEnd(const gfx::Point& anchor, bool snap_to_min) {
  if (snap_to_min) {
    LayerTreeImpl* active_tree = host_impl_->active_tree();
    DCHECK(active_tree->InnerViewportScrollNode());
    const float kMaxZoomForSnapToMin = 1.05f;
    const base::TimeDelta kSnapToMinZoomAnimationDuration =
        base::TimeDelta::FromMilliseconds(200);
    float page_scale = active_tree->current_page_scale_factor();
    float min_scale = active_tree->min_page_scale_factor();

    // If the page is close to minimum scale at pinch end, snap to minimum.
    if (page_scale < min_scale * kMaxZoomForSnapToMin &&
        page_scale != min_scale) {
      gfx::PointF adjusted_anchor =
          gfx::PointF(anchor + pinch_anchor_adjustment_);
      adjusted_anchor =
          gfx::ScalePoint(adjusted_anchor, min_scale / page_scale);
      adjusted_anchor += ScrollOffsetToVector2dF(TotalScrollOffset());
      host_impl_->StartPageScaleAnimation(
          ToRoundedVector2d(adjusted_anchor.OffsetFromOrigin()), true,
          min_scale, kSnapToMinZoomAnimationDuration);
    }
  }

  pinch_anchor_adjustment_ = gfx::Vector2d();
  pinch_zoom_active_ = false;
}

LayerImpl* Viewport::MainScrollLayer() const {
  return host_impl_->OuterViewportScrollLayer();
}

gfx::Vector2dF Viewport::ScrollBrowserControls(const gfx::Vector2dF& delta) {
  gfx::Vector2dF excess_delta =
      host_impl_->browser_controls_manager()->ScrollBy(delta);

  return delta - excess_delta;
}

bool Viewport::ShouldBrowserControlsConsumeScroll(
    const gfx::Vector2dF& scroll_delta) const {
  // Always consume if it's in the direction to show the browser controls.
  if (scroll_delta.y() < 0)
    return true;

  if (TotalScrollOffset().y() < MaxTotalScrollOffset().y())
    return true;

  return false;
}

gfx::Vector2dF Viewport::AdjustOverscroll(const gfx::Vector2dF& delta) const {
  // TODO(tdresser): Use a more rational epsilon. See crbug.com/510550 for
  // details.
  const float kEpsilon = 0.1f;
  gfx::Vector2dF adjusted = delta;

  if (std::abs(adjusted.x()) < kEpsilon)
    adjusted.set_x(0.0f);
  if (std::abs(adjusted.y()) < kEpsilon)
    adjusted.set_y(0.0f);

  return adjusted;
}

gfx::ScrollOffset Viewport::MaxTotalScrollOffset() const {
  gfx::ScrollOffset offset;

  offset += scroll_tree().MaxScrollOffset(InnerScrollNode()->id);

  if (auto* outer_node = OuterScrollNode())
    offset += scroll_tree().MaxScrollOffset(outer_node->id);

  return offset;
}

gfx::ScrollOffset Viewport::TotalScrollOffset() const {
  gfx::ScrollOffset offset;

  offset += scroll_tree().current_scroll_offset(InnerScrollNode()->element_id);

  if (auto* outer_node = OuterScrollNode())
    offset += scroll_tree().current_scroll_offset(outer_node->element_id);

  return offset;
}

ScrollNode* Viewport::InnerScrollNode() const {
  return host_impl_->InnerViewportScrollNode();
}

ScrollNode* Viewport::OuterScrollNode() const {
  return host_impl_->OuterViewportScrollNode();
}

ScrollTree& Viewport::scroll_tree() const {
  return host_impl_->active_tree()->property_trees()->scroll_tree;
}

}  // namespace cc
