import 'package:flutter/material.dart';

/// A configurable Expansion Tile edited from the flutter material implementation
/// that allows for customization of most of the behaviour. Includes providing colours,
/// replacement widgets on expansion, and  animating preceding/following widgets.
///
/// See:
/// [ExpansionTile]
class ConfigurableExpansionTile extends StatefulWidget {
  /// Creates a a [Widget] with an optional [animatedWidgetPrecedingHeader] and/or
  /// [animatedWidgetFollowingHeader]. Optionally, the header can change on the
  /// expanded state by proving a [Widget] in [headerExpanded]. Colors can also
  /// be specified for the animated transitions/states. [children] are revealed
  /// when the expansion tile is expanded.
  const ConfigurableExpansionTile({
    super.key,
    this.headerBackgroundColorStart = Colors.transparent,
    this.onExpansionChanged,
    this.children = const <Widget>[],
    this.initiallyExpanded = false,
    required this.header,
    this.animatedWidgetFollowingHeader,
    this.animatedWidgetPrecedingHeader,
    this.expandedBackgroundColor,
    this.borderColorStart = Colors.transparent,
    this.borderColorEnd = Colors.transparent,
    this.topBorderOn = true,
    this.bottomBorderOn = true,
    this.kExpand = const Duration(milliseconds: 200),
    this.headerBackgroundColorEnd,
    this.headerExpanded,
    this.headerAnimationTween,
    this.borderAnimationTween,
    this.animatedWidgetTurnTween,
    this.animatedWidgetTween,
  });

  /// Called when the tile expands or collapses.
  ///
  /// When the tile starts expanding, this function is called with the value
  /// true. When the tile starts collapsing, this function is called with
  /// the value false.
  final ValueChanged<bool>? onExpansionChanged;

  /// The widgets that are displayed when the tile expands.
  ///
  /// Typically [ListTile] widgets.
  final List<Widget> children;

  /// The color of the header, useful to set if your animating widgets are
  /// larger than the header widget, or you want an animating color, in which
  /// case your header widget should be transparent
  final Color headerBackgroundColorStart;

  /// The [Color] the header will transition to on expand
  final Color? headerBackgroundColorEnd;

  /// The [Color] of the background of the [children] when expanded
  final Color? expandedBackgroundColor;

  /// Specifies if the list tile is initially expanded (true) or collapsed (false, the default).
  final bool initiallyExpanded;

  /// The header for the expansion tile
  final Widget header;

  /// An optional widget to replace [header] with if the list is expanded
  final Widget? headerExpanded;

  /// A widget to rotate following the [header] (ie an arrow)
  final Widget? animatedWidgetFollowingHeader;

  /// A widget to rotate preceding the [header] (ie an arrow)
  final Widget? animatedWidgetPrecedingHeader;

  /// The duration of the animations
  final Duration kExpand;

  /// The color the border start, before the list is expanded
  final Color borderColorStart;

  /// The color of the border at the end of animation, after the list is expanded
  final Color borderColorEnd;

  /// Turns the top border of the list is on/off
  final bool topBorderOn;

  /// Turns the bottom border of the list on/off
  final bool bottomBorderOn;

  /// Header transition tween
  final Animatable<double>? headerAnimationTween;

  /// Border animation tween
  final Animatable<double>? borderAnimationTween;

  /// Tween for turning [animatedWidgetFollowingHeader] and [animatedWidgetPrecedingHeader]
  final Animatable<double>? animatedWidgetTurnTween;

  ///  [animatedWidgetFollowingHeader] and [animatedWidgetPrecedingHeader] transition tween
  final Animatable<double>? animatedWidgetTween;

  static final Animatable<double> _easeInTween = CurveTween(
    curve: Curves.easeIn,
  );
  static final Animatable<double> _halfTween = Tween<double>(
    begin: 0.0,
    end: 0.5,
  );

  static final Animatable<double> _easeOutTween = CurveTween(
    curve: Curves.easeOut,
  );
  @override
  State<ConfigurableExpansionTile> createState() =>
      _ConfigurableExpansionTileState();
}

class _ConfigurableExpansionTileState extends State<ConfigurableExpansionTile>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _iconTurns;
  late Animation<double> _heightFactor;

  late Animation<Color?> _borderColor;
  Animation<Color?>? _headerColor;

  final ColorTween _borderColorTween = ColorTween();
  final ColorTween _headerColorTween = ColorTween();

  bool _isExpanded = false;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(duration: widget.kExpand, vsync: this);
    _heightFactor = _controller.drive(ConfigurableExpansionTile._easeInTween);
    _iconTurns = _controller.drive(
      (widget.animatedWidgetTurnTween ?? ConfigurableExpansionTile._halfTween)
          .chain(
            widget.animatedWidgetTween ??
                ConfigurableExpansionTile._easeInTween,
          ),
    );

    _borderColor = _controller.drive(
      _borderColorTween.chain(
        widget.borderAnimationTween ?? ConfigurableExpansionTile._easeOutTween,
      ),
    );
    _borderColorTween.end = widget.borderColorEnd;

    _headerColor = _controller.drive(
      _headerColorTween.chain(
        widget.headerAnimationTween ?? ConfigurableExpansionTile._easeInTween,
      ),
    );
    _headerColorTween.end =
        widget.headerBackgroundColorEnd ?? widget.headerBackgroundColorStart;
    _isExpanded =
        PageStorage.of(context).readState(context) ?? widget.initiallyExpanded;
    if (_isExpanded) _controller.value = 1.0;
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  void _handleTap() {
    setState(() {
      _isExpanded = !_isExpanded;
      if (_isExpanded) {
        _controller.forward();
      } else {
        _controller.reverse().then<void>((void value) {
          if (!mounted) return;
          setState(() {
            // Rebuild without widget.children.
          });
        });
      }
      PageStorage.of(context).writeState(context, _isExpanded);
    });
    if (widget.onExpansionChanged != null) {
      widget.onExpansionChanged!(_isExpanded);
    }
  }

  Widget _buildChildren(BuildContext context, Widget? child) {
    var borderSideColor = _borderColor.value ?? widget.borderColorStart;
    var headerColor = _headerColor?.value ?? widget.headerBackgroundColorStart;
    return Container(
      decoration: BoxDecoration(
        border: Border(
          top: BorderSide(
            color: widget.topBorderOn ? borderSideColor : Colors.transparent,
          ),
          bottom: BorderSide(
            color: widget.bottomBorderOn ? borderSideColor : Colors.transparent,
          ),
        ),
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          GestureDetector(
            onTap: _handleTap,
            child: Container(
              color: headerColor,
              child: Row(
                mainAxisSize: MainAxisSize.max,
                mainAxisAlignment: MainAxisAlignment.center,
                children: <Widget>[
                  RotationTransition(
                    turns: _iconTurns,
                    child:
                        widget.animatedWidgetPrecedingHeader ??
                        const SizedBox(),
                  ),
                  _getHeader(),
                  RotationTransition(
                    turns: _iconTurns,
                    child:
                        widget.animatedWidgetFollowingHeader ??
                        const SizedBox(),
                  ),
                ],
              ),
            ),
          ),
          ClipRect(
            child: Align(heightFactor: _heightFactor.value, child: child),
          ),
        ],
      ),
    );
  }

  /// Retrieves the header to display for the tile, derived from [_isExpanded] state
  Widget _getHeader() {
    if (!_isExpanded) {
      return widget.header;
    } else {
      return widget.headerExpanded ?? widget.header;
    }
  }

  @override
  Widget build(BuildContext context) {
    var closed = !_isExpanded && _controller.isDismissed;
    return AnimatedBuilder(
      animation: _controller.view,
      builder: _buildChildren,
      child: closed
          ? null
          : Container(
              color: widget.expandedBackgroundColor ?? Colors.transparent,
              child: Column(children: widget.children),
            ),
    );
  }
}
