// Copyright 2014 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.

#import <QuartzCore/QuartzCore.h>

#include "content/browser/web_contents/web_contents_view_overscroll_animator_slider_mac.h"

#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/web_contents_observer.h"

namespace {
// The minimum possible progress of an overscroll animation.
CGFloat kMinProgress = 0;
// The maximum possible progress of an overscroll animation.
CGFloat kMaxProgress = 2.0;
// The maximum duration of the completion or cancellation animations. The
// effective maximum is half of this value, since the longest animation is from
// progress = 1.0 to progress = 2.0;
CGFloat kMaxAnimationDuration = 0.2;
}  // namespace

// OverscrollAnimatorSliderView Private Category -------------------------------

@interface OverscrollAnimatorSliderView ()
// Callback from WebContentsPaintObserver.
- (void)webContentsFinishedNonEmptyPaint;

// Resets overscroll animation state.
- (void)reset;

// Given a |progress| from 0 to 2, the expected frame origin of the -movingView.
- (NSPoint)frameOriginWithProgress:(CGFloat)progress;

// The NSView that is moving during the overscroll animation.
- (NSView*)movingView;

// The expected duration of an animation from progress_ to |progress|
- (CGFloat)animationDurationForProgress:(CGFloat)progress;

// NSView override. During an overscroll animation, the cursor may no longer
// rest on the RenderWidgetHost's NativeView, which prevents wheel events from
// reaching the NativeView. The overscroll animation is driven by wheel events
// so they must be explicitly forwarded to the NativeView.
- (void)scrollWheel:(NSEvent*)event;
@end

// Helper Class (ResizingView) -------------------------------------------------

// This NSView subclass is intended to be the RenderWidgetHost's NativeView's
// parent NSView. It is possible for the RenderWidgetHost's NativeView's size to
// become out of sync with its parent NSView. The override of
// -resizeSubviewsWithOldSize: ensures that the sizes will eventually become
// consistent.
// http://crbug.com/264207
@interface ResizingView : NSView
@end

@implementation ResizingView
- (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize {
  for (NSView* subview in self.subviews)
    [subview setFrame:self.bounds];
}
@end

// Helper Class (WebContentsPaintObserver) -------------------------------------

namespace overscroll_animator {
class WebContentsPaintObserver : public content::WebContentsObserver {
 public:
  WebContentsPaintObserver(content::WebContents* web_contents,
                           OverscrollAnimatorSliderView* slider_view)
      : WebContentsObserver(web_contents), slider_view_(slider_view) {}

  virtual void DidFirstVisuallyNonEmptyPaint() OVERRIDE {
    [slider_view_ webContentsFinishedNonEmptyPaint];
  }

 private:
  OverscrollAnimatorSliderView* slider_view_;  // Weak reference.
};
}  // namespace overscroll_animator

// OverscrollAnimatorSliderView Implementation ---------------------------------

@implementation OverscrollAnimatorSliderView

- (instancetype)initWithFrame:(NSRect)frame {
  self = [super initWithFrame:frame];
  if (self) {
    bottomView_.reset([[NSImageView alloc] initWithFrame:self.bounds]);
    bottomView_.get().imageScaling = NSImageScaleNone;
    bottomView_.get().autoresizingMask =
        NSViewWidthSizable | NSViewHeightSizable;
    bottomView_.get().imageAlignment = NSImageAlignTop;
    [self addSubview:bottomView_];
    middleView_.reset([[ResizingView alloc] initWithFrame:self.bounds]);
    middleView_.get().autoresizingMask =
        NSViewWidthSizable | NSViewHeightSizable;
    [self addSubview:middleView_];
    topView_.reset([[NSImageView alloc] initWithFrame:self.bounds]);
    topView_.get().autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
    topView_.get().imageScaling = NSImageScaleNone;
    topView_.get().imageAlignment = NSImageAlignTop;
    [self addSubview:topView_];

    [self reset];
  }
  return self;
}

- (void)webContentsFinishedNonEmptyPaint {
  observer_.reset();
  [self reset];
}

- (void)reset {
  DCHECK(!animating_);
  inOverscroll_ = NO;
  progress_ = kMinProgress;

  [CATransaction begin];
  [CATransaction setDisableActions:YES];
  bottomView_.get().hidden = YES;
  middleView_.get().hidden = NO;
  topView_.get().hidden = YES;

  [bottomView_ setFrameOrigin:NSMakePoint(0, 0)];
  [middleView_ setFrameOrigin:NSMakePoint(0, 0)];
  [topView_ setFrameOrigin:NSMakePoint(0, 0)];
  [CATransaction commit];
}

- (NSPoint)frameOriginWithProgress:(CGFloat)progress {
  if (direction_ == content::OVERSCROLL_ANIMATOR_DIRECTION_BACKWARDS)
    return NSMakePoint(progress / kMaxProgress * self.bounds.size.width, 0);
  return NSMakePoint((1 - progress / kMaxProgress) * self.bounds.size.width, 0);
}

- (NSView*)movingView {
  if (direction_ == content::OVERSCROLL_ANIMATOR_DIRECTION_BACKWARDS)
    return middleView_;
  return topView_;
}

- (CGFloat)animationDurationForProgress:(CGFloat)progress {
  CGFloat progressPercentage =
      fabs(progress_ - progress) / (kMaxProgress - kMinProgress);
  return progressPercentage * kMaxAnimationDuration;
}

- (void)scrollWheel:(NSEvent*)event {
  NSView* latestRenderWidgetHostView = [[middleView_ subviews] lastObject];
  [latestRenderWidgetHostView scrollWheel:event];
}

// WebContentsOverscrollAnimator Implementation --------------------------------

- (BOOL)needsNavigationSnapshot {
  return YES;
}

- (void)beginOverscrollInDirection:
            (content::OverscrollAnimatorDirection)direction
                navigationSnapshot:(NSImage*)snapshot {
  // TODO(erikchen): If snapshot is nil, need a placeholder.
  if (animating_ || inOverscroll_)
    return;

  inOverscroll_ = YES;
  direction_ = direction;
  if (direction_ == content::OVERSCROLL_ANIMATOR_DIRECTION_BACKWARDS) {
    // The middleView_ will slide to the right, revealing bottomView_.
    bottomView_.get().hidden = NO;
    [bottomView_ setImage:snapshot];
  } else {
    // The topView_ will slide in from the right, concealing middleView_.
    topView_.get().hidden = NO;
    [topView_ setFrameOrigin:NSMakePoint(self.bounds.size.width, 0)];
    [topView_ setImage:snapshot];
  }

  [self updateOverscrollProgress:kMinProgress];
}

- (void)addRenderWidgetHostNativeView:(NSView*)view {
  [middleView_ addSubview:view];
}

- (void)updateOverscrollProgress:(CGFloat)progress {
  if (animating_)
    return;
  DCHECK_LE(progress, kMaxProgress);
  DCHECK_GE(progress, kMinProgress);
  progress_ = progress;
  [[self movingView] setFrameOrigin:[self frameOriginWithProgress:progress]];
}

- (void)completeOverscroll:(content::WebContentsImpl*)webContents {
  if (animating_ || !inOverscroll_)
    return;

  animating_ = YES;

  NSView* view = [self movingView];
  [NSAnimationContext beginGrouping];
  [NSAnimationContext currentContext].duration =
      [self animationDurationForProgress:kMaxProgress];
  [[NSAnimationContext currentContext] setCompletionHandler:^{
      animating_ = NO;

      // Animation is complete. Now perform page load.
      if (direction_ == content::OVERSCROLL_ANIMATOR_DIRECTION_BACKWARDS)
        webContents->GetController().GoBack();
      else
        webContents->GetController().GoForward();

      // Reset the position of the middleView_, but wait for the page to paint
      // before showing it.
      middleView_.get().hidden = YES;
      [middleView_ setFrameOrigin:NSMakePoint(0, 0)];
      observer_.reset(
          new overscroll_animator::WebContentsPaintObserver(webContents, self));
  }];

  // Animate the moving view to its final position.
  [[view animator] setFrameOrigin:[self frameOriginWithProgress:kMaxProgress]];

  [NSAnimationContext endGrouping];
}

- (void)cancelOverscroll {
  if (animating_)
    return;

  if (!inOverscroll_) {
    [self reset];
    return;
  }

  animating_ = YES;

  NSView* view = [self movingView];
  [NSAnimationContext beginGrouping];
  [NSAnimationContext currentContext].duration =
      [self animationDurationForProgress:kMinProgress];
  [[NSAnimationContext currentContext] setCompletionHandler:^{
      // Animation is complete. Reset the state.
      animating_ = NO;
      [self reset];
  }];

  // Animate the moving view to its initial position.
  [[view animator] setFrameOrigin:[self frameOriginWithProgress:kMinProgress]];

  [NSAnimationContext endGrouping];
}

@end
