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

import 'basic.dart';
import 'transitions.dart';
import 'framework.dart';
import 'gesture_detector.dart';

const Duration _kCardDismissDuration = const Duration(milliseconds: 200);
const Duration _kCardResizeDuration = const Duration(milliseconds: 300);
const Curve _kCardResizeTimeCurve = const Interval(0.4, 1.0, curve: Curves.ease);
const double _kMinFlingVelocity = 700.0;
const double _kMinFlingVelocityDelta = 400.0;
const double _kFlingVelocityScale = 1.0 / 300.0;
const double _kDismissCardThreshold = 0.4;

/// The direction in which a [Dismissable] can be dismissed.
enum DismissDirection {
  /// The [Dismissable] can be dismissed by dragging either up or down.
  vertical,

  /// The [Dismissable] can be dismissed by dragging either left or right.
  horizontal,

  /// The [Dismissable] can be dismissed by dragging left only.
  left,

  /// The [Dismissable] can be dismissed by dragging right only.
  right,

  /// The [Dismissable] can be dismissed by dragging up only.
  up,

  /// The [Dismissable] can be dismissed by dragging down only.
  down
}

/// Can be dismissed by dragging in one or more directions.
///
/// The child is draggable in the indicated direction(s). When released (or
/// flung), the child disappears off the edge and the dismissable widget
/// animates its height (or width, whichever is perpendicular to the dismiss
/// direction) to zero.
class Dismissable extends StatefulComponent {
  Dismissable({
    Key key,
    this.child,
    this.onResized,
    this.onDismissed,
    this.direction: DismissDirection.horizontal
  }) : super(key: key);

  final Widget child;

  /// Called when the widget changes size (i.e., when contracting after being dismissed).
  final VoidCallback onResized;

  /// Called when the widget has been dismissed, after finishing resizing.
  final VoidCallback onDismissed;

  /// The direction in which the widget can be dismissed.
  final DismissDirection direction;

  _DismissableState createState() => new _DismissableState();
}

class _DismissableState extends State<Dismissable> {
  void initState() {
    super.initState();
    _dismissController = new AnimationController(duration: _kCardDismissDuration);
    _dismissController.addStatusListener((AnimationStatus status) {
      if (status == AnimationStatus.completed)
        _handleDismissCompleted();
    });
  }

  AnimationController _dismissController;
  AnimationController _resizeController;

  Size _size;
  double _dragExtent = 0.0;
  bool _dragUnderway = false;

  void dispose() {
    _dismissController?.stop();
    _resizeController?.stop();
    super.dispose();
  }

  bool get _directionIsYAxis {
    return
      config.direction == DismissDirection.vertical ||
      config.direction == DismissDirection.up ||
      config.direction == DismissDirection.down;
  }

  void _handleDismissCompleted() {
    if (!_dragUnderway)
      _startResizeAnimation();
  }

  bool get _isActive {
    return _size != null && (_dragUnderway || _dismissController.isAnimating);
  }

  void _maybeCallOnResized() {
    if (config.onResized != null)
      config.onResized();
  }

  void _maybeCallOnDismissed() {
    if (config.onDismissed != null)
      config.onDismissed();
  }

  void _startResizeAnimation() {
    assert(_size != null);
    assert(_dismissController != null);
    assert(_dismissController.isCompleted);
    assert(_resizeController == null);
    setState(() {
      _resizeController = new AnimationController(duration: _kCardResizeDuration)
        ..addListener(_handleResizeProgressChanged);
      _resizeController.forward();
    });
  }

  void _handleResizeProgressChanged() {
    if (_resizeController.isCompleted)
      _maybeCallOnDismissed();
    else
      _maybeCallOnResized();
  }

  void _handleDragStart(_) {
    setState(() {
      _dragUnderway = true;
      if (_dismissController.isAnimating) {
        _dragExtent = _dismissController.value * _size.width * _dragExtent.sign;
        _dismissController.stop();
      } else {
        _dragExtent = 0.0;
        _dismissController.value = 0.0;
      }
    });
  }

  void _handleDragUpdate(double delta) {
    if (!_isActive || _dismissController.isAnimating)
      return;

    double oldDragExtent = _dragExtent;
    switch (config.direction) {
      case DismissDirection.horizontal:
      case DismissDirection.vertical:
        _dragExtent += delta;
        break;

      case DismissDirection.up:
      case DismissDirection.left:
        if (_dragExtent + delta < 0)
          _dragExtent += delta;
        break;

      case DismissDirection.down:
      case DismissDirection.right:
        if (_dragExtent + delta > 0)
          _dragExtent += delta;
        break;
    }

    if (oldDragExtent.sign != _dragExtent.sign) {
      setState(() {
        // Rebuild to update the new drag endpoint.
        // The sign of _dragExtent is part of our build state;
        // the actual value is not, it's just used to configure
        // the animations.
      });
    }
    if (!_dismissController.isAnimating)
      _dismissController.value = _dragExtent.abs() / _size.width;
  }

  bool _isFlingGesture(Velocity velocity) {
    double vx = velocity.pixelsPerSecond.dx;
    double vy = velocity.pixelsPerSecond.dy;
    if (_directionIsYAxis) {
      if (vy.abs() - vx.abs() < _kMinFlingVelocityDelta)
        return false;
      switch(config.direction) {
        case DismissDirection.vertical:
          return vy.abs() > _kMinFlingVelocity;
        case DismissDirection.up:
          return -vy > _kMinFlingVelocity;
        default:
          return vy > _kMinFlingVelocity;
      }
    } else {
      if (vx.abs() - vy.abs() < _kMinFlingVelocityDelta)
        return false;
      switch(config.direction) {
        case DismissDirection.horizontal:
          return vx.abs() > _kMinFlingVelocity;
        case DismissDirection.left:
          return -vx > _kMinFlingVelocity;
        default:
          return vx > _kMinFlingVelocity;
      }
    }
    return false;
  }

  void _handleDragEnd(Velocity velocity) {
    if (!_isActive || _dismissController.isAnimating)
      return;
    setState(() {
      _dragUnderway = false;
      if (_dismissController.isCompleted) {
        _startResizeAnimation();
      } else if (_isFlingGesture(velocity)) {
        double flingVelocity = _directionIsYAxis ? velocity.pixelsPerSecond.dy : velocity.pixelsPerSecond.dx;
        _dragExtent = flingVelocity.sign;
        _dismissController.fling(velocity: flingVelocity.abs() * _kFlingVelocityScale);
      } else if (_dismissController.value > _kDismissCardThreshold) {
        _dismissController.forward();
      } else {
        _dismissController.reverse();
      }
    });
  }

  void _handleSizeChanged(Size newSize) {
    setState(() {
      _size = new Size.copy(newSize);
    });
  }

  FractionalOffset get _activeCardDragEndPoint {
    if (!_isActive)
      return FractionalOffset.zero;
    if (_directionIsYAxis)
      return new FractionalOffset(0.0, _dragExtent.sign);
    return new FractionalOffset(_dragExtent.sign, 0.0);
  }

  Widget build(BuildContext context) {
    if (_resizeController != null) {
      // make sure you remove this widget once it's been dismissed!
      assert(_resizeController.status == AnimationStatus.forward);

      Animation<double> squashAxisExtent = new Tween<double>(
        begin: _directionIsYAxis ? _size.width : _size.height,
        end: 0.0
      ).animate(new CurvedAnimation(
        parent: _resizeController,
        curve: _kCardResizeTimeCurve
      ));

      return new AnimatedBuilder(
        animation: squashAxisExtent,
        builder: (BuildContext context, Widget child) {
          return new SizedBox(
            width: _directionIsYAxis ? squashAxisExtent.value : null,
            height: !_directionIsYAxis ? squashAxisExtent.value : null
          );
        }
      );
    }

    return new GestureDetector(
      onHorizontalDragStart: _directionIsYAxis ? null : _handleDragStart,
      onHorizontalDragUpdate: _directionIsYAxis ? null : _handleDragUpdate,
      onHorizontalDragEnd: _directionIsYAxis ? null : _handleDragEnd,
      onVerticalDragStart: _directionIsYAxis ? _handleDragStart : null,
      onVerticalDragUpdate: _directionIsYAxis ? _handleDragUpdate : null,
      onVerticalDragEnd: _directionIsYAxis ? _handleDragEnd : null,
      behavior: HitTestBehavior.opaque,
      child: new SizeObserver(
        onSizeChanged: _handleSizeChanged,
        child: new SlideTransition(
          position: new Tween<FractionalOffset>(
            begin: FractionalOffset.zero,
            end: _activeCardDragEndPoint
          ).animate(_dismissController),
          child: config.child
        )
      )
    );
  }
}