// Copyright (c) 2013 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 "content/browser/web_contents/aura/window_slider.h"

#include <algorithm>

#include "base/bind.h"
#include "base/callback.h"
#include "content/browser/web_contents/aura/shadow_layer_delegate.h"
#include "content/public/browser/overscroll_configuration.h"
#include "ui/aura/window.h"
#include "ui/compositor/layer_animation_observer.h"
#include "ui/compositor/scoped_layer_animation_settings.h"
#include "ui/events/event.h"

namespace content {

namespace {

void DeleteLayerAndShadow(ui::Layer* layer,
                          ShadowLayerDelegate* shadow) {
  delete shadow;
  delete layer;
}

// An animation observer that runs a callback at the end of the animation, and
// destroys itself.
class CallbackAnimationObserver : public ui::ImplicitAnimationObserver {
 public:
  CallbackAnimationObserver(const base::Closure& closure)
      : closure_(closure) {
  }

  virtual ~CallbackAnimationObserver() {}

 private:
  // Overridden from ui::ImplicitAnimationObserver:
  virtual void OnImplicitAnimationsCompleted() OVERRIDE {
    if (!closure_.is_null())
      closure_.Run();
    base::MessageLoop::current()->DeleteSoon(FROM_HERE, this);
  }

  const base::Closure closure_;

  DISALLOW_COPY_AND_ASSIGN(CallbackAnimationObserver);
};

}  // namespace

WindowSlider::WindowSlider(Delegate* delegate,
                           aura::Window* event_window,
                           aura::Window* owner)
    : delegate_(delegate),
      event_window_(event_window),
      owner_(owner),
      delta_x_(0.f),
      weak_factory_(this),
      active_start_threshold_(0.f),
      start_threshold_touchscreen_(content::GetOverscrollConfig(
          content::OVERSCROLL_CONFIG_HORIZ_THRESHOLD_START_TOUCHSCREEN)),
      start_threshold_touchpad_(content::GetOverscrollConfig(
          content::OVERSCROLL_CONFIG_HORIZ_THRESHOLD_START_TOUCHPAD)),
      complete_threshold_(content::GetOverscrollConfig(
          content::OVERSCROLL_CONFIG_HORIZ_THRESHOLD_COMPLETE)) {
  event_window_->AddPreTargetHandler(this);

  event_window_->AddObserver(this);
  owner_->AddObserver(this);
}

WindowSlider::~WindowSlider() {
  if (event_window_) {
    event_window_->RemovePreTargetHandler(this);
    event_window_->RemoveObserver(this);
  }
  if (owner_)
    owner_->RemoveObserver(this);
  delegate_->OnWindowSliderDestroyed();
}

void WindowSlider::ChangeOwner(aura::Window* new_owner) {
  if (owner_)
    owner_->RemoveObserver(this);
  owner_ = new_owner;
  if (owner_) {
    owner_->AddObserver(this);
    UpdateForScroll(0.f, 0.f);
  }
}

bool WindowSlider::IsSlideInProgress() const {
  return fabs(delta_x_) >= active_start_threshold_ || slider_.get() ||
      weak_factory_.HasWeakPtrs();
}

void WindowSlider::SetupSliderLayer() {
  ui::Layer* parent = owner_->layer()->parent();
  parent->Add(slider_.get());
  if (delta_x_ < 0)
    parent->StackAbove(slider_.get(), owner_->layer());
  else
    parent->StackBelow(slider_.get(), owner_->layer());
  slider_->SetBounds(owner_->layer()->bounds());
  slider_->SetVisible(true);
}

void WindowSlider::UpdateForScroll(float x_offset, float y_offset) {
  float old_delta = delta_x_;
  delta_x_ += x_offset;
  if (fabs(delta_x_) < active_start_threshold_ && !slider_.get())
    return;

  if ((old_delta < 0 && delta_x_ > 0) ||
      (old_delta > 0 && delta_x_ < 0)) {
    slider_.reset();
    shadow_.reset();
  }

  float translate = 0.f;
  ui::Layer* translate_layer = NULL;

  if (!slider_.get()) {
    slider_.reset(delta_x_ < 0 ? delegate_->CreateFrontLayer() :
                                 delegate_->CreateBackLayer());
    if (!slider_.get())
      return;
    SetupSliderLayer();
  }

  if (delta_x_ <= -active_start_threshold_) {
    translate = owner_->bounds().width() +
        std::max(delta_x_ + active_start_threshold_,
                 static_cast<float>(-owner_->bounds().width()));
    translate_layer = slider_.get();
  } else if (delta_x_ >= active_start_threshold_) {
    translate = std::min(delta_x_ - active_start_threshold_,
                         static_cast<float>(owner_->bounds().width()));
    translate_layer = owner_->layer();
  } else {
    return;
  }

  if (!shadow_.get())
    shadow_.reset(new ShadowLayerDelegate(translate_layer));

  gfx::Transform transform;
  transform.Translate(translate, 0);
  translate_layer->SetTransform(transform);
}

void WindowSlider::UpdateForFling(float x_velocity, float y_velocity) {
  if (!slider_.get())
    return;

  int width = owner_->bounds().width();
  float ratio = (fabs(delta_x_) - active_start_threshold_) / width;
  if (ratio < complete_threshold_) {
    ResetScroll();
    return;
  }

  ui::Layer* sliding = delta_x_ < 0 ? slider_.get() : owner_->layer();
  ui::ScopedLayerAnimationSettings settings(sliding->GetAnimator());
  settings.SetPreemptionStrategy(
      ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
  settings.SetTweenType(gfx::Tween::EASE_OUT);
  settings.AddObserver(new CallbackAnimationObserver(
      base::Bind(&WindowSlider::CompleteWindowSlideAfterAnimation,
                 weak_factory_.GetWeakPtr())));

  gfx::Transform transform;
  transform.Translate(delta_x_ < 0 ? 0 : width, 0);
  sliding->SetTransform(transform);
}

void WindowSlider::ResetScroll() {
  if (!slider_.get())
    return;

  // Do not trigger any callbacks if this animation replaces any in-progress
  // animation.
  weak_factory_.InvalidateWeakPtrs();

  // Reset the state of the sliding layer.
  if (slider_.get()) {
    ui::Layer* layer = slider_.release();
    ui::ScopedLayerAnimationSettings settings(layer->GetAnimator());
    settings.SetPreemptionStrategy(
        ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
    settings.SetTweenType(gfx::Tween::EASE_OUT);

    // Delete the layer and the shadow at the end of the animation.
    settings.AddObserver(new CallbackAnimationObserver(
        base::Bind(&DeleteLayerAndShadow,
                   base::Unretained(layer),
                   base::Unretained(shadow_.release()))));

    gfx::Transform transform;
    transform.Translate(delta_x_ < 0 ? layer->bounds().width() : 0, 0);
    layer->SetTransform(transform);
  }

  // Reset the state of the main layer.
  {
    ui::ScopedLayerAnimationSettings settings(owner_->layer()->GetAnimator());
    settings.SetPreemptionStrategy(
        ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET);
    settings.SetTweenType(gfx::Tween::EASE_OUT);
    settings.AddObserver(new CallbackAnimationObserver(
        base::Bind(&WindowSlider::AbortWindowSlideAfterAnimation,
                   weak_factory_.GetWeakPtr())));
    owner_->layer()->SetTransform(gfx::Transform());
    owner_->layer()->SetLayerBrightness(0.f);
  }

  delta_x_ = 0.f;
}

void WindowSlider::CancelScroll() {
  ResetScroll();
}

void WindowSlider::CompleteWindowSlideAfterAnimation() {
  weak_factory_.InvalidateWeakPtrs();
  shadow_.reset();
  slider_.reset();
  delta_x_ = 0.f;

  delegate_->OnWindowSlideComplete();
}

void WindowSlider::AbortWindowSlideAfterAnimation() {
  weak_factory_.InvalidateWeakPtrs();

  delegate_->OnWindowSlideAborted();
}

void WindowSlider::OnKeyEvent(ui::KeyEvent* event) {
  CancelScroll();
}

void WindowSlider::OnMouseEvent(ui::MouseEvent* event) {
  if (!(event->flags() & ui::EF_IS_SYNTHESIZED))
    CancelScroll();
}

void WindowSlider::OnScrollEvent(ui::ScrollEvent* event) {
  active_start_threshold_ = start_threshold_touchpad_;
  if (event->type() == ui::ET_SCROLL)
    UpdateForScroll(event->x_offset_ordinal(), event->y_offset_ordinal());
  else if (event->type() == ui::ET_SCROLL_FLING_START)
    UpdateForFling(event->x_offset_ordinal(), event->y_offset_ordinal());
  else
    CancelScroll();
  event->SetHandled();
}

void WindowSlider::OnGestureEvent(ui::GestureEvent* event) {
  active_start_threshold_ = start_threshold_touchscreen_;
  const ui::GestureEventDetails& details = event->details();
  switch (event->type()) {
    case ui::ET_GESTURE_SCROLL_BEGIN:
      ResetScroll();
      break;

    case ui::ET_GESTURE_SCROLL_UPDATE:
      UpdateForScroll(details.scroll_x(), details.scroll_y());
      break;

    case ui::ET_GESTURE_SCROLL_END:
      UpdateForFling(0.f, 0.f);
      break;

    case ui::ET_SCROLL_FLING_START:
      UpdateForFling(details.velocity_x(), details.velocity_y());
      break;

    case ui::ET_GESTURE_PINCH_BEGIN:
    case ui::ET_GESTURE_PINCH_UPDATE:
    case ui::ET_GESTURE_PINCH_END:
      CancelScroll();
      break;

    default:
      break;
  }

  event->SetHandled();
}

void WindowSlider::OnWindowRemovingFromRootWindow(aura::Window* window) {
  if (window == event_window_) {
    window->RemoveObserver(this);
    window->RemovePreTargetHandler(this);
    event_window_ = NULL;
  } else if (window == owner_) {
    window->RemoveObserver(this);
    owner_ = NULL;
    delete this;
  } else {
    NOTREACHED();
  }
}

}  // namespace content
