import 'dart:collection';
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';

/// Helps [child] stay visible by resizing it to avoid the given [areaToAvoid].
///
/// Wraps the [child] in a [AnimatedContainer] that adjusts its bottom [padding] to accommodate the given area.
///
/// If [autoScroll] is true and the [child] contains a focused widget such as a [TextField],
/// automatically scrolls so that it is just visible above the keyboard, plus any additional [overscroll].
class BottomAreaAvoider extends StatefulWidget {
  static const Duration defaultDuration = Duration(milliseconds: 100);
  static const Curve defaultCurve = Curves.easeIn;
  static const double defaultOverscroll = 12.0;
  static const bool defaultAutoScroll = false;

  /// The child to embed.
  ///
  /// If the [child] is not a [ScrollView], it is automatically embedded in a [SingleChildScrollView].
  /// If the [child] is a [ScrollView], it must have a [ScrollController].
  final Widget child;

  /// Amount of bottom area to avoid. For example, the height of the currently-showing system keyboard, or
  /// any custom bottom overlays.
  final double areaToAvoid;

  /// Whether to auto-scroll to the focused widget after the keyboard appears. Defaults to false.
  /// Could be expensive because it searches all the child objects in this widget's render tree.
  final bool autoScroll;

  /// Extra amount to scroll past the focused widget. Defaults to [defaultOverscroll].
  /// Useful in case the focused widget is inside a parent widget that you also want to be visible.
  final double overscroll;

  /// Duration of the resize animation. Defaults to [defaultDuration]. To disable, set to [Duration.zero].
  final Duration duration;

  /// Animation curve. Defaults to [defaultCurve]
  final Curve curve;

  final ScrollController scrollTo;

  BottomAreaAvoider({
    Key key,
    @required this.child,
    @required this.areaToAvoid,
    this.autoScroll = false,
    this.duration = defaultDuration,
    this.curve = defaultCurve,
    this.scrollTo,
    this.overscroll = defaultOverscroll,
  })  : //assert(child is ScrollView ? child.controller != null : true),
        assert(areaToAvoid >= 0, 'Cannot avoid a negative area'),
        super(key: key);

  BottomAreaAvoiderState createState() => BottomAreaAvoiderState();
}

class BottomAreaAvoiderState extends State<BottomAreaAvoider> {
  final _animationKey = new GlobalKey<ImplicitlyAnimatedWidgetState>();
  Function(AnimationStatus) _animationListener;
  ScrollController _scrollController;
  double _previousAreaToAvoid;

  scroll() {
    _scrollController.position.moveTo(
      200,
      duration: Duration(milliseconds: 100),
      curve: widget.curve,
    );
  }

  @override
  void initState() {
    super.initState();
  }

  @override
  void didUpdateWidget(BottomAreaAvoider oldWidget) {
    _previousAreaToAvoid = oldWidget.areaToAvoid;
    super.didUpdateWidget(oldWidget);
  }

  @override
  void dispose() {
    _animationKey.currentState?.animation
        ?.removeStatusListener(_animationListener);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // Add a status listener to the animation after the initial build.
    // Wait a frame so that _animationKey.currentState is not null.
    if (_animationListener == null) {
      WidgetsBinding.instance.addPostFrameCallback((_) {
        _animationListener = _paddingAnimationStatusChanged;
        _animationKey.currentState.animation
            .addStatusListener(_animationListener);
      });
    }

    // If [child] is a [ScrollView], get its [ScrollController]
    // and embed the [child] directly in an [AnimatedContainer].
    if (widget.child is ScrollView) {
      var scrollView = widget.child as ScrollView;
      _scrollController =
          scrollView.controller ?? PrimaryScrollController.of(context);

      return _buildAnimatedContainer(widget.child);
    }
    // If [child] is not a [ScrollView], and [autoScroll] is true,
    // embed the [child] in a [SingleChildScrollView] to make
    // it possible to scroll to the focused widget.
    if (widget.autoScroll) {
      _scrollController = widget.scrollTo;
//      widget.scrollTo =_scrollController;
      return _buildAnimatedContainer(
        LayoutBuilder(
          builder: (context, constraints) {
            return SingleChildScrollView(
              controller: _scrollController,
              child: ConstrainedBox(
                constraints: BoxConstraints(
                  minHeight: constraints.maxHeight,
                ),
                child: widget.child,
              ),
            );
          },
        ),
      );
    }
    // Just embed the [child] directly in an [AnimatedContainer].
    return _buildAnimatedContainer(widget.child);
  }

  Widget _buildAnimatedContainer(Widget child) {
    return AnimatedContainer(
      key: _animationKey,
      padding: EdgeInsets.only(bottom: widget.areaToAvoid),
      duration: widget.duration,
      curve: widget.curve,
      child: child,
    );
  }

  /// Called whenever the status of our padding animation changes.
  ///
  /// If the animation has completed, we added overlap, and scroll is on, scroll to that.
  void _paddingAnimationStatusChanged(AnimationStatus status) {
    if (status != AnimationStatus.completed) {
      return; // Only check when the animation is finishing
    }
    if (!widget.autoScroll) {
      return; // auto scroll is not enabled, do nothing
    }
    if (widget.areaToAvoid <= _previousAreaToAvoid) {
      return; // decreased-- do nothing. We only scroll when area to avoid is added (keyboard shown).
    }

    // Need to wait a frame to get the new size (todo: is this still needed? we dont use mediaquery anymore)
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (context == null || !mounted) {
        return; // context is no longer valid
      }
      final focused = findFocusedObject(context.findRenderObject());
      if (focused == null) {
        return; // no focused object found
      }
      scrollToObject(focused, _scrollController, widget.duration, widget.curve,
          widget.overscroll);
    });
  }
}

/// Utility helper methods

/// Finds the first focused focused child of [root] using a breadth-first search.
RenderObject findFocusedObject(RenderObject root) {
  final q = Queue<RenderObject>();
  q.add(root);
  while (q.isNotEmpty) {
    final RenderObject node = q.removeFirst();
    final config = SemanticsConfiguration();
    node.describeSemanticsConfiguration(config);
    if (config.isFocused) {
      return node;
    }
    node.visitChildrenForSemantics((child) {
      q.add(child);
    });
  }
  return null;
}

/// Scroll to the given [object], which must be inside [scrollController]s viewport.
scrollToObject(RenderObject object, ScrollController scrollController,
    Duration duration, Curve curve, double overscroll) {
  // Calculate the offset needed to show the object in the [ScrollView]
  // so that its bottom touches the top of the keyboard.
  final viewport = RenderAbstractViewport.of(object);
  final offset = viewport.getOffsetToReveal(object, 1.0).offset + overscroll;

  // If the object is covered by the keyboard, scroll to reveal it,
  // and add [focusPadding] between it and top of the keyboard.
  if (offset > scrollController.position.pixels) {
    scrollController.position.moveTo(
      offset,
      duration: duration,
      curve: curve,
    );
  }
}
