// Copyright 2016 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 'package:flutter/animation.dart';
import 'package:flutter/scheduler.dart';
import 'package:meta/meta.dart';

import 'box.dart';
import 'object.dart';
import 'shifted_box.dart';

/// A render object that animates its size to its child's size over a given
/// [duration] and with a given [curve]. If the child's size itself animates
/// (i.e. if it changes size two frames in a row, as opposed to abruptly
/// changing size in one frame then remaining that size in subsequent frames),
/// this render object sizes itself to fit the child instead of animating
/// itself.
///
/// When the child overflows the current animated size of this render object, it
/// is clipped.
class RenderAnimatedSize extends RenderAligningShiftedBox {
  /// Creates a render object that animates its size to match its child.
  /// The [duration] and [curve] arguments define the animation.
  ///
  /// The [alignment] argument is used to align the child when the parent is not
  /// (yet) the same size as the child.
  ///
  /// The [duration] is required.
  ///
  /// The [vsync] should specify a [TickerProvider] for the animation
  /// controller.
  ///
  /// The arguments [duration], [curve], [alignment], and [vsync] must
  /// not be null.
  RenderAnimatedSize({
    @required TickerProvider vsync,
    @required Duration duration,
    Curve curve: Curves.linear,
    FractionalOffset alignment: FractionalOffset.center,
    RenderBox child,
  }) : _vsync = vsync, super(child: child, alignment: alignment) {
    assert(vsync != null);
    assert(duration != null);
    assert(curve != null);
    _controller = new AnimationController(
      vsync: vsync,
      duration: duration,
    )..addListener(() {
      if (_controller.value != _lastValue)
        markNeedsLayout();
    });
    _animation = new CurvedAnimation(
      parent: _controller,
      curve: curve
    );
  }

  AnimationController _controller;
  CurvedAnimation _animation;
  SizeTween _sizeTween = new SizeTween();
  bool _didChangeTargetSizeLastFrame = false;
  bool _hasVisualOverflow;
  double _lastValue;

  /// The duration of the animation.
  Duration get duration => _controller.duration;
  set duration(Duration value) {
    assert(value != null);
    if (value == _controller.duration)
      return;
    _controller.duration = value;
  }

  /// The curve of the animation.
  Curve get curve => _animation.curve;
  set curve(Curve value) {
    assert(value != null);
    if (value == _animation.curve)
      return;
    _animation.curve = value;
  }

  /// The [TickerProvider] for the [AnimationController] that runs the animation.
  TickerProvider get vsync => _vsync;
  TickerProvider _vsync;
  set vsync(TickerProvider value) {
    assert(value != null);
    if (value == _vsync)
      return;
    _vsync = value;
    _controller.resync(vsync);
  }

  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
    if (_animatedSize != _sizeTween.end && !_controller.isAnimating)
      _controller.forward();
  }

  @override
  void detach() {
    _controller.stop();
    super.detach();
  }

  Size get _animatedSize {
    return _sizeTween.evaluate(_animation);
  }

  @override
  void performLayout() {
    _lastValue = _controller.value;
    _hasVisualOverflow = false;

    if (child == null) {
      size = _sizeTween.begin = _sizeTween.end = constraints.smallest;
      return;
    }

    child.layout(constraints, parentUsesSize: true);
    if (_sizeTween.end != child.size) {
      _sizeTween.begin = _animatedSize ?? child.size;
      _sizeTween.end = child.size;

      if (_didChangeTargetSizeLastFrame) {
        size = child.size;
        _controller.stop();
      } else {
        // Don't register first change as a last-frame change.
        if (_sizeTween.end != _sizeTween.begin)
          _didChangeTargetSizeLastFrame = true;

        _lastValue = 0.0;
        _controller.forward(from: 0.0);

        size = constraints.constrain(_animatedSize);
      }
    } else {
      _didChangeTargetSizeLastFrame = false;

      size = constraints.constrain(_animatedSize);
    }

    alignChild();

    if (size.width < _sizeTween.end.width ||
        size.height < _sizeTween.end.height)
      _hasVisualOverflow = true;
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (child != null && _hasVisualOverflow) {
      final Rect rect = Point.origin & size;
      context.pushClipRect(needsCompositing, offset, rect, super.paint);
    } else {
      super.paint(context, offset);
    }
  }
}
