// Copyright 2014 The Flutter 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 'framework.dart';

/// Builder callback used by [DualTransitionBuilder].
///
/// The builder is expected to return a transition powered by the provided
/// `animation` and wrapping the provided `child`.
///
/// The `animation` provided to the builder always runs forward from 0.0 to 1.0.
typedef AnimatedTransitionBuilder = Widget Function(
  BuildContext context,
  Animation<double> animation,
  Widget? child,
);

/// A transition builder that animates its [child] based on the
/// [AnimationStatus] of the provided [animation].
///
/// This widget can be used to specify different enter and exit transitions for
/// a [child].
///
/// While the [animation] runs forward, the [child] is animated according to
/// [forwardBuilder] and while the [animation] is running in reverse, it is
/// animated according to [reverseBuilder].
///
/// Using this builder allows the widget tree to maintain its shape by nesting
/// the enter and exit transitions. This ensures that no state information of
/// any descendant widget is lost when the transition starts or completes.
class DualTransitionBuilder extends StatefulWidget {
  /// Creates a [DualTransitionBuilder].
  ///
  /// The [animation], [forwardBuilder], and [reverseBuilder] arguments are
  /// required and must not be null.
  const DualTransitionBuilder({
    Key? key,
    required this.animation,
    required this.forwardBuilder,
    required this.reverseBuilder,
    this.child,
  }) : assert(animation != null),
       assert(forwardBuilder != null),
       assert(reverseBuilder != null),
       super(key: key);

  /// The animation that drives the [child]'s transition.
  ///
  /// When this animation runs forward, the [child] transitions as specified by
  /// [forwardBuilder]. When it runs in reverse, the child transitions according
  /// to [reverseBuilder].
  final Animation<double> animation;

  /// A builder for the transition that makes [child] appear on screen.
  ///
  /// The [child] should be fully visible when the provided `animation` reaches
  /// 1.0.
  ///
  /// The `animation` provided to this builder is running forward from 0.0 to
  /// 1.0 when [animation] runs _forward_. When [animation] runs in reverse,
  /// the given `animation` is set to [kAlwaysCompleteAnimation].
  ///
  /// See also:
  ///
  ///  * [reverseBuilder], which builds the transition for making the [child]
  ///   disappear from the screen.
  final AnimatedTransitionBuilder forwardBuilder;

  /// A builder for a transition that makes [child] disappear from the screen.
  ///
  /// The [child] should be fully invisible when the provided `animation`
  /// reaches 1.0.
  ///
  /// The `animation` provided to this builder is running forward from 0.0 to
  /// 1.0 when [animation] runs in _reverse_. When [animation] runs forward,
  /// the given `animation` is set to [kAlwaysDismissedAnimation].
  ///
  /// See also:
  ///
  ///  * [forwardBuilder], which builds the transition for making the [child]
  ///    appear on screen.
  final AnimatedTransitionBuilder reverseBuilder;

  /// The widget below this [DualTransitionBuilder] in the tree.
  ///
  /// This child widget will be wrapped by the transitions built by
  /// [forwardBuilder] and [reverseBuilder].
  final Widget? child;

  @override
  State<DualTransitionBuilder> createState() => _DualTransitionBuilderState();
}

class _DualTransitionBuilderState extends State<DualTransitionBuilder> {
  late AnimationStatus _effectiveAnimationStatus;
  final ProxyAnimation _forwardAnimation = ProxyAnimation();
  final ProxyAnimation _reverseAnimation = ProxyAnimation();

  @override
  void initState() {
    super.initState();
    _effectiveAnimationStatus = widget.animation.status;
    widget.animation.addStatusListener(_animationListener);
    _updateAnimations();
  }

  void _animationListener(AnimationStatus animationStatus) {
    final AnimationStatus oldEffective = _effectiveAnimationStatus;
    _effectiveAnimationStatus = _calculateEffectiveAnimationStatus(
      lastEffective: _effectiveAnimationStatus,
      current: animationStatus,
    );
    if (oldEffective != _effectiveAnimationStatus) {
      _updateAnimations();
    }
  }

  @override
  void didUpdateWidget(DualTransitionBuilder oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.animation != widget.animation) {
      oldWidget.animation.removeStatusListener(_animationListener);
      widget.animation.addStatusListener(_animationListener);
      _animationListener(widget.animation.status);
    }
  }

  // When a transition is interrupted midway we just want to play the ongoing
  // animation in reverse. Switching to the actual reverse transition would
  // yield a disjoint experience since the forward and reverse transitions are
  // very different.
  AnimationStatus _calculateEffectiveAnimationStatus({
    required AnimationStatus lastEffective,
    required AnimationStatus current,
  }) {
    assert(current != null);
    assert(lastEffective != null);
    switch (current) {
      case AnimationStatus.dismissed:
      case AnimationStatus.completed:
        return current;
      case AnimationStatus.forward:
        switch (lastEffective) {
          case AnimationStatus.dismissed:
          case AnimationStatus.completed:
          case AnimationStatus.forward:
            return current;
          case AnimationStatus.reverse:
            return lastEffective;
        }
      case AnimationStatus.reverse:
        switch (lastEffective) {
          case AnimationStatus.dismissed:
          case AnimationStatus.completed:
          case AnimationStatus.reverse:
            return current;
          case AnimationStatus.forward:
            return lastEffective;
        }
    }
  }

  void _updateAnimations() {
    switch (_effectiveAnimationStatus) {
      case AnimationStatus.dismissed:
      case AnimationStatus.forward:
        _forwardAnimation.parent = widget.animation;
        _reverseAnimation.parent = kAlwaysDismissedAnimation;
        break;
      case AnimationStatus.reverse:
      case AnimationStatus.completed:
        _forwardAnimation.parent = kAlwaysCompleteAnimation;
        _reverseAnimation.parent = ReverseAnimation(widget.animation);
        break;
    }
  }

  @override
  void dispose() {
    widget.animation.removeStatusListener(_animationListener);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return widget.forwardBuilder(
      context,
      _forwardAnimation,
      widget.reverseBuilder(
        context,
        _reverseAnimation,
        widget.child,
      ),
    );
  }
}
