import 'package:flutter/material.dart';

enum ToggleType {
  expandAllAndCollapseAll,
  expandOnlyCurrent,
  expandAlwaysCurrent,
  collapseAll,
  expandAll,
  none,
}

const Duration _kExpand = Duration(milliseconds: 200);

class ExpansionTileGroup extends StatefulWidget {
  const ExpansionTileGroup(
      {Key? key,
      required this.children,
      this.toggleType = ToggleType.none,
      this.onExpansionItemChanged,
      this.spaceBetweenItem = 0.0})
      : assert(spaceBetweenItem >= 0.0,
            'Error: Please set the spaceBetweenItem of ExpansionTileGroup must be >= 0'),
        super(key: key);
  final List<ExpansionTileItem> children;
  final ToggleType toggleType;
  final Function(int, bool)? onExpansionItemChanged;
  final double spaceBetweenItem;

  @override
  State<ExpansionTileGroup> createState() => _ExpansionTileGroupState();
}

class _ExpansionTileGroupState extends State<ExpansionTileGroup> {
  late final List<GlobalKey<ExpansionTileCustomState>> expansionTileKeys;
  late final List<ExpansionTileItem> expansionChildren;

  bool _isGroupTransforming = false;

  @override
  void initState() {
    expansionTileKeys = List.generate(widget.children.length,
        (index) => widget.children[index].expansionKey ?? GlobalKey());
    expansionChildren = List.generate(
        widget.children.length,
        (index) => widget.children[index].copyWith(
            expansionKey: expansionTileKeys[index],
            onExpansionChanged: (isExpanded) {
              if (!_isGroupTransforming) {
                _updateExpandedForAccordions(index, isExpanded, false);
                widget.onExpansionItemChanged?.call(index, isExpanded);
              }
            }));
    super.initState();
  }

  @override
  void didUpdateWidget(ExpansionTileGroup oldWidget) {
    expansionTileKeys.clear();
    expansionTileKeys.addAll(List.generate(widget.children.length,
        (index) => widget.children[index].expansionKey ?? GlobalKey()));
    expansionChildren.clear();
    expansionChildren.addAll(List.generate(
        widget.children.length,
        (index) => widget.children[index].copyWith(
            expansionKey: expansionTileKeys[index],
            onExpansionChanged: (isExpanded) {
              if (!_isGroupTransforming) {
                _updateExpandedForAccordions(index, isExpanded, false);
                widget.onExpansionItemChanged?.call(index, isExpanded);
              }
            })));
    super.didUpdateWidget(oldWidget);
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [...getChildrenToShown()],
    );
  }

  List<Widget> getChildrenToShown() {
    var result = <Widget>[];
    for (var item in expansionChildren) {
      result.add(item);
      if (expansionChildren.last != item) {
        result.add(SizedBox(
          height: widget.spaceBetweenItem,
        ));
      }
    }
    return result;
  }

  void _updateExpandedForAccordions(
      int index, bool isExpanded, bool defaultState) {
    switch (widget.toggleType) {
      case ToggleType.expandOnlyCurrent:
        _onExpandOne(index, isExpanded, defaultState);
        break;
      case ToggleType.expandAlwaysCurrent:
        _onExpandAlwaysCurrent(index, isExpanded, defaultState);
        break;
      case ToggleType.collapseAll:
        _onCollapseAll(index, isExpanded, defaultState);
        break;
      case ToggleType.expandAll:
        _onExpandAll(index, isExpanded, defaultState);
        break;
      case ToggleType.expandAllAndCollapseAll:
        _onExpandAllAndCollapseALl(index, isExpanded, defaultState);
        break;
      case ToggleType.none:
        break;
    }
  }

  //todo create abstract class to handle action
  void _onExpandOne(int index, bool isExpanded, bool defaultState) {
    _isGroupTransforming = true;
    if (isExpanded) {
      expansionTileKeys[index].currentState?.expand();
    } else {
      expansionTileKeys[index].currentState?.collapse();
    }
    for (int i = 0; i < expansionTileKeys.length; i++) {
      if (i != index) {
        if (defaultState) {
          expansionTileKeys[i].currentState?.expand();
        } else {
          expansionTileKeys[i].currentState?.collapse();
        }
      }
    }
    _isGroupTransforming = false;
  }

  void _onExpandAlwaysCurrent(int index, bool isExpanded, bool defaultState) {
    _isGroupTransforming = true;
    expansionTileKeys[index].currentState?.expand();

    for (int i = 0; i < expansionTileKeys.length; i++) {
      if (i != index) {
        if (defaultState) {
          expansionTileKeys[i].currentState?.expand();
        } else {
          expansionTileKeys[i].currentState?.collapse();
        }
      }
    }
    _isGroupTransforming = false;
  }

  //todo create abstract class to handle action
  void _onCollapseAll(int index, bool isExpanded, bool defaultState) {
    _isGroupTransforming = true;
    if (!isExpanded) {
      for (int i = 0; i < expansionTileKeys.length; i++) {
        expansionTileKeys[i].currentState?.collapse();
      }
    }
    _isGroupTransforming = false;
  }

  //todo create abstract class to handle action
  void _onExpandAll(int index, bool isExpanded, bool defaultState) {
    _isGroupTransforming = true;
    if (isExpanded) {
      for (int i = 0; i < expansionTileKeys.length; i++) {
        expansionTileKeys[i].currentState?.expand();
      }
    }
    _isGroupTransforming = false;
  }

  //todo create abstract class to handle action
  void _onExpandAllAndCollapseALl(
      int index, bool isExpanded, bool defaultState) {
    _isGroupTransforming = true;
    if (isExpanded) {
      for (int i = 0; i < expansionTileKeys.length; i++) {
        expansionTileKeys[i].currentState?.expand();
      }
    } else {
      for (int i = 0; i < expansionTileKeys.length; i++) {
        expansionTileKeys[i].currentState?.collapse();
      }
    }
    _isGroupTransforming = false;
  }
}

class ExpansionTileItem extends StatelessWidget {
  const ExpansionTileItem(
      {Key? key,
      required this.title,
      required this.children,
      this.expansionKey,
      this.onExpansionChanged,
      this.tilePadding,
      this.initiallyExpanded = false,
      this.backgroundColor,
      this.textColor,
      this.subtitle,
      this.maintainState = false,
      this.iconColor,
      this.expandedCrossAxisAlignment,
      this.expandedAlignment,
      this.controlAffinity,
      this.collapsedTextColor,
      this.collapsedIconColor,
      this.collapsedBackgroundColor,
      this.childrenPadding,
      this.trailing,
      this.leading,
      this.themeData,
      this.decoration,
      this.borderRadius,
      this.border,
      this.clipBehavior,
      this.boxShadow,
      this.expendedBorderColor,
      this.collapsedBorderColor,
      this.isHasTopBorder,
      this.isHasLeftBorder,
      this.isHasBottomBorder,
      this.isHasRightBorder,
      this.isHasTrailing,
      this.isEnableExpanded,
      this.isDefaultVerticalPadding})
      : super(key: key);

  /// The primary content of the list item.
  ///
  /// Typically a [Text] widget.
  final Widget title;

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

  /// 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;
  final GlobalKey<ExpansionTileCustomState>? expansionKey;

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

  /// Specifies padding for the [ListTile].
  ///
  /// Analogous to [ListTile.contentPadding], this property defines the insets for
  /// the [leading], [title], [subtitle] and [trailing] widgets. It does not inset
  /// the expanded [children] widgets.
  ///
  /// If this property is null then [ExpansionTileThemeData.tilePadding] is used. If that
  /// is also null then the tile's padding is `EdgeInsets.symmetric(horizontal: 16.0)`.
  ///
  /// See also:
  ///
  /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
  ///   [ExpansionTileThemeData].
  final EdgeInsetsGeometry? tilePadding;

  /// Specifies the alignment of [children], which are arranged in a column when
  /// the tile is expanded.
  ///
  /// The internals of the expanded tile make use of a [Column] widget for
  /// [children], and [Align] widget to align the column. The `expandedAlignment`
  /// parameter is passed directly into the [Align].
  ///
  /// Modifying this property controls the alignment of the column within the
  /// expanded tile, not the alignment of [children] widgets within the column.
  /// To align each child within [children], see [expandedCrossAxisAlignment].
  ///
  /// The width of the column is the width of the widest child widget in [children].
  ///
  /// If this property is null then [ExpansionTileThemeData.expandedAlignment]is used. If that
  /// is also null then the value of `expandedAlignment` is [Alignment.center].
  ///
  /// See also:
  ///
  /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
  ///   [ExpansionTileThemeData].
  final Alignment? expandedAlignment;

  /// The icon color of tile's expansion arrow icon when the sublist is collapsed.
  ///
  /// Used to override to the [ListTileThemeData.iconColor].
  final Color? collapsedIconColor;

  /// Specifies the alignment of each child within [children] when the tile is expanded.
  ///
  /// The internals of the expanded tile make use of a [Column] widget for
  /// [children], and the `crossAxisAlignment` parameter is passed directly into the [Column].
  ///
  /// Modifying this property controls the cross axis alignment of each child
  /// within its [Column]. Note that the width of the [Column] that houses
  /// [children] will be the same as the widest child widget in [children]. It is
  /// not necessarily the width of [Column] is equal to the width of expanded tile.
  ///
  /// To align the [Column] along the expanded tile, use the [expandedAlignment] property
  /// instead.
  ///
  /// When the value is null, the value of `expandedCrossAxisAlignment` is [CrossAxisAlignment.center].
  final CrossAxisAlignment? expandedCrossAxisAlignment;

  /// The icon color of tile's expansion arrow icon when the sublist is expanded.
  ///
  /// Used to override to the [ListTileThemeData.iconColor].
  ///
  /// If this property is null then [ExpansionTileThemeData.iconColor] is used. If that
  /// is also null then the value of [ListTileThemeData.iconColor] is used.
  ///
  /// See also:
  ///
  /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
  ///   [ExpansionTileThemeData].
  final Color? iconColor;

  /// The color of the tile's titles when the sublist is expanded.
  ///
  /// Used to override to the [ListTileThemeData.textColor].
  ///
  /// If this property is null then [ExpansionTileThemeData.textColor] is used. If that
  /// is also null then the value of [ListTileThemeData.textColor] is used.
  ///
  /// See also:
  ///
  /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
  ///   [ExpansionTileThemeData].
  final Color? textColor;

  /// Additional content displayed below the title.
  ///
  /// Typically a [Text] widget.
  final Widget? subtitle;

  /// Specifies whether the state of the children is maintained when the tile expands and collapses.
  ///
  /// When true, the children are kept in the tree while the tile is collapsed.
  /// When false (default), the children are removed from the tree when the tile is
  /// collapsed and recreated upon expansion.
  final bool maintainState;

  /// Typically used to force the expansion arrow icon to the tile's leading or trailing edge.
  ///
  /// By default, the value of `controlAffinity` is [ListTileControlAffinity.platform],
  /// which means that the expansion arrow icon will appear on the tile's trailing edge.
  final ListTileControlAffinity? controlAffinity;

  /// The color of the tile's titles when the sublist is collapsed.
  ///
  /// Used to override to the [ListTileThemeData.textColor].
  ///
  /// If this property is null then [ExpansionTileThemeData.collapsedTextColor] is used. If that
  /// is also null then the value of [ListTileThemeData.textColor] is used.
  ///
  /// See also:
  ///
  /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
  ///   [ExpansionTileThemeData].
  final Color? collapsedTextColor;

  /// When not null, defines the background color of tile when the sublist is collapsed.
  ///
  /// If this property is null then [ExpansionTileThemeData.collapsedBackgroundColor] is used.
  /// If that is also null then Colors.transparent is used.
  ///
  /// See also:
  ///
  /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
  ///   [ExpansionTileThemeData].
  final Color? collapsedBackgroundColor;

  /// Specifies padding for [children].
  ///
  /// If this property is null then [ExpansionTileThemeData.childrenPadding] is used. If that
  /// is also null then the value of `childrenPadding` is [EdgeInsets.zero].
  ///
  /// See also:
  ///
  /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
  ///   [ExpansionTileThemeData].
  final EdgeInsetsGeometry? childrenPadding;

  /// A widget to display after the title.
  ///
  /// Note that depending on the value of [controlAffinity], the [trailing] widget
  /// may replace the rotating expansion arrow icon.
  final Widget? trailing;

  /// A widget to display before the title.
  ///
  /// Typically a [CircleAvatar] widget.
  ///
  /// Note that depending on the value of [controlAffinity], the [leading] widget
  /// may replace the rotating expansion arrow icon.
  final Widget? leading;

  /// The color to display behind the sublist when expanded.
  ///
  /// If this property is null then [ExpansionTileThemeData.backgroundColor] is used. If that
  /// is also null then Colors.transparent is used.
  ///
  /// See also:
  ///
  /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
  ///   [ExpansionTileThemeData].
  final Color? backgroundColor;

  ///Specifies the color and typography values for descendant widgets.
  final ThemeData? themeData;

  ///The decoration to paint behind the child.
  /// Use the color property to specify a simple solid color.
  /// The child is not clipped to the decoration. To clip a child to the shape of a particular ShapeDecoration, consider using a ClipPath widget.
  final Decoration? decoration;

  /// If non-null, the corners of this box are rounded by this [BorderRadius].
  ///
  /// Applies only to boxes with rectangular shapes; ignored if [shape] is not
  /// [BoxShape.rectangle].
  ///
  /// {@macro flutter.painting.BoxDecoration.clip}
  final BorderRadius? borderRadius;

  /// A border to draw above the background [color], [gradient], or [image].
  ///
  /// Follows the [shape] and [borderRadius].
  ///
  /// Use [Border] objects to describe borders that do not depend on the reading
  /// direction.
  ///
  /// Use [BoxBorder] objects to describe borders that should flip their left
  /// and right edges based on whether the text is being read left-to-right or
  /// right-to-left.
  final BoxBorder? border;

  ///The clip behavior when Container.decoration is not null.
  /// Defaults to Clip.none. Must be Clip.none if decoration is null.
  /// If a clip is to be applied, the Decoration.getClipPath method for the provided decoration must return a clip path.
  /// (This is not supported by all decorations; the default implementation of that method throws an UnsupportedError.)
  final Clip? clipBehavior;

  ///A list of shadows cast by this box behind the box.
  /// The shadow follows the shape of the box.
  /// See also:
  /// kElevationToShadow, for some predefined shadows used in Material Design.
  /// PhysicalModel, a widget for showing shadows.
  final List<BoxShadow>? boxShadow;

  /// The color to display border box when collapsed.
  ///
  /// If this property is null then [ExpansionTileThemeData.backgroundColor] is used. If that
  /// is also null then Colors.transparent is used.
  ///
  /// See also:
  ///
  /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
  ///   [ExpansionTileThemeData].
  final Color? collapsedBorderColor;

  /// The color to display border box when expanded.
  ///
  /// If this property is null then [ThemeData.dividerColor] is used. If that
  /// is also null then Colors.transparent is used.
  ///
  /// See also:
  ///
  /// * [ThemeData.of], which returns the nearest [ThemeData].
  final Color? expendedBorderColor;

  final bool? isHasTopBorder;
  final bool? isHasBottomBorder;
  final bool? isHasLeftBorder;
  final bool? isHasRightBorder;

  final bool? isHasTrailing;

  ///The widget can expand or NOT.
  ///It helps when you want to prohibit expansion until the API requests success.
  ///More detail: https://github.com/congthanhng/Expansion-Tile-Group/issues/22
  final bool? isEnableExpanded;

  final bool? isDefaultVerticalPadding;

  ExpansionTileItem copyWith({
    Key? key,
    Widget? title,
    List<Widget>? children,
    ValueChanged<bool>? onExpansionChanged,
    GlobalKey<ExpansionTileCustomState>? expansionKey,
    bool? initiallyExpanded,
    EdgeInsetsGeometry? tilePadding,
    Alignment? expandedAlignment,
    Color? collapsedIconColor,
    CrossAxisAlignment? expandedCrossAxisAlignment,
    Color? iconColor,
    Color? textColor,
    Widget? subtitle,
    bool? maintainState,
    ListTileControlAffinity? controlAffinity,
    Color? collapsedTextColor,
    Color? collapsedBackgroundColor,
    EdgeInsetsGeometry? childrenPadding,
    Widget? trailing,
    Widget? leading,
    Color? backgroundColor,
    Decoration? decoration,
    BorderRadius? borderRadius,
    BoxBorder? border,
    Clip? clipBehavior,
    ThemeData? themeData,
    List<BoxShadow>? boxShadow,
    Color? expendedBorderColor,
    Color? collapsedBorderColor,
    bool? isHasTopBorder,
    bool? isHasBottomBorder,
    bool? isHasLeftBorder,
    bool? isHasRightBorder,
    bool? isHasTrailing,
    bool? isEnableExpanded,
    bool? isDefaultVerticalPadding,
  }) {
    return ExpansionTileItem(
        key: key ?? this.key,
        title: title ?? this.title,
        expansionKey: expansionKey ?? this.expansionKey,
        onExpansionChanged: (value) {
          onExpansionChanged?.call(value);
          this.onExpansionChanged?.call(value);
        },
        backgroundColor: backgroundColor ?? this.backgroundColor,
        leading: leading ?? this.leading,
        trailing: trailing ?? this.trailing,
        childrenPadding: childrenPadding ?? this.childrenPadding,
        collapsedBackgroundColor:
            collapsedBackgroundColor ?? this.collapsedBackgroundColor,
        collapsedTextColor: collapsedTextColor ?? this.collapsedTextColor,
        controlAffinity: controlAffinity ?? this.controlAffinity,
        maintainState: maintainState ?? this.maintainState,
        subtitle: subtitle ?? this.subtitle,
        textColor: textColor ?? this.textColor,
        initiallyExpanded: initiallyExpanded ?? this.initiallyExpanded,
        tilePadding: tilePadding ?? this.tilePadding,
        iconColor: iconColor ?? this.iconColor,
        expandedCrossAxisAlignment:
            expandedCrossAxisAlignment ?? this.expandedCrossAxisAlignment,
        expandedAlignment: expandedAlignment ?? this.expandedAlignment,
        collapsedIconColor: collapsedIconColor ?? this.collapsedIconColor,
        decoration: decoration ?? this.decoration,
        borderRadius: borderRadius ?? this.borderRadius,
        border: border ?? this.border,
        themeData: themeData ?? this.themeData,
        clipBehavior: clipBehavior ?? this.clipBehavior,
        boxShadow: boxShadow ?? this.boxShadow,
        collapsedBorderColor: collapsedBorderColor ?? this.collapsedBorderColor,
        expendedBorderColor: expendedBorderColor ?? this.expendedBorderColor,
        isHasTopBorder: isHasTopBorder ?? this.isHasTopBorder,
        isHasRightBorder: isHasRightBorder ?? this.isHasRightBorder,
        isHasLeftBorder: isHasLeftBorder ?? this.isHasLeftBorder,
        isHasBottomBorder: isHasBottomBorder ?? this.isHasBottomBorder,
        isHasTrailing: isHasTrailing ?? this.isHasTrailing,
        isEnableExpanded: isEnableExpanded ?? this.isEnableExpanded,
        isDefaultVerticalPadding:
            isDefaultVerticalPadding ?? this.isDefaultVerticalPadding,
        children: children ?? this.children);
  }

  @override
  Widget build(BuildContext context) {
    return Theme(
      data: themeData ?? Theme.of(context).copyWith(),
      child: ExpansionTileCustom(
        key: expansionKey,
        onExpansionChanged: onExpansionChanged,
        initiallyExpanded: initiallyExpanded,
        tilePadding: tilePadding,
        expandedAlignment: expandedAlignment ?? Alignment.topLeft,
        collapsedIconColor: collapsedIconColor,
        expandedCrossAxisAlignment:
            expandedCrossAxisAlignment ?? CrossAxisAlignment.start,
        iconColor: iconColor,
        title: title,
        textColor: textColor,
        subtitle: subtitle,
        maintainState: maintainState,
        controlAffinity: controlAffinity,
        collapsedTextColor: collapsedTextColor,
        collapsedBackgroundColor: collapsedBackgroundColor,
        childrenPadding: childrenPadding ??
            const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
        trailing: trailing,
        leading: leading,
        backgroundColor: backgroundColor,
        decoration: decoration,
        borderRadius: borderRadius,
        clipBehavior: clipBehavior,
        border: border,
        boxShadow: boxShadow,
        expendedBorderColor: expendedBorderColor,
        collapsedBorderColor: collapsedBorderColor,
        isHasBottomBorder: isHasBottomBorder ?? true,
        isHasLeftBorder: isHasLeftBorder ?? false,
        isHasRightBorder: isHasRightBorder ?? false,
        isHasTopBorder: isHasTopBorder ?? true,
        isHasTrailing: isHasTrailing ?? true,
        isEnableExpanded: isEnableExpanded ?? true,
        isDefaultVerticalPadding: isDefaultVerticalPadding ?? true,
        children: children ?? [],
      ),
    );
  }
}


class ExpansionTileCustom extends StatefulWidget {
  const ExpansionTileCustom(
      {Key? key,
      this.leading,
      required this.title,
      this.subtitle,
      this.onExpansionChanged,
      this.children = const <Widget>[],
      this.trailing,
      this.initiallyExpanded = false,
      this.maintainState = false,
      this.tilePadding,
      this.expandedCrossAxisAlignment,
      this.expandedAlignment,
      this.childrenPadding,
      this.backgroundColor,
      this.collapsedBackgroundColor,
      this.textColor,
      this.collapsedTextColor,
      this.iconColor,
      this.collapsedIconColor,
      this.controlAffinity,
      this.clipBehavior,
      this.decoration,
      this.borderRadius,
      this.border,
      this.boxShadow,
      this.collapsedBorderColor,
      this.expendedBorderColor,
      this.isHasTopBorder = true,
      this.isHasBottomBorder = true,
      this.isHasLeftBorder = false,
      this.isHasRightBorder = false,
      this.isHasTrailing = true,
      this.isEnableExpanded = true,
      this.isDefaultVerticalPadding = true})
      : assert(
          expandedCrossAxisAlignment != CrossAxisAlignment.baseline,
          'CrossAxisAlignment.baseline is not supported since the expanded children '
          'are aligned in a column, not a row. Try to use another constant.',
        ),
        assert(isHasTrailing == true || trailing == null,
            'Not necessary `trailing` when `isHasTrailing` is false'),
        assert(
            (boxShadow != null &&
                    backgroundColor != null &&
                    collapsedBackgroundColor != null) ||
                boxShadow == null,
            'By default the `backgroundColor` and `collapseBackgroundColor` is transparent, so you need add value when define `boxShadow`'),
        assert(
            (borderRadius != null &&
                    ((isHasTopBorder &&
                            isHasBottomBorder &&
                            isHasLeftBorder &&
                            isHasRightBorder) ||
                        (!isHasTopBorder &&
                            !isHasBottomBorder &&
                            !isHasLeftBorder &&
                            !isHasRightBorder))) ||
                borderRadius == null,
            'A borderRadius can only be given for a uniform Border, \n more info here: https://stackoverflow.com/questions/58812778/a-borderradius-can-only-be-given-for-uniform-borders'),
        super(key: key);

  /// A widget to display before the title.
  ///
  /// Typically a [CircleAvatar] widget.
  ///
  /// Note that depending on the value of [controlAffinity], the [leading] widget
  /// may replace the rotating expansion arrow icon.
  final Widget? leading;

  /// The primary content of the list item.
  ///
  /// Typically a [Text] widget.
  final Widget title;

  /// Additional content displayed below the title.
  ///
  /// Typically a [Text] widget.
  final Widget? subtitle;

  /// 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 to display behind the sublist when expanded.
  ///
  /// If this property is null then [ExpansionTileThemeData.backgroundColor] is used. If that
  /// is also null then Colors.transparent is used.
  ///
  /// See also:
  ///
  /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
  ///   [ExpansionTileThemeData].
  final Color? backgroundColor;

  /// When not null, defines the background color of tile when the sublist is collapsed.
  ///
  /// If this property is null then [ExpansionTileThemeData.collapsedBackgroundColor] is used.
  /// If that is also null then Colors.transparent is used.
  ///
  /// See also:
  ///
  /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
  ///   [ExpansionTileThemeData].
  final Color? collapsedBackgroundColor;

  /// A widget to display after the title.
  ///
  /// Note that depending on the value of [controlAffinity], the [trailing] widget
  /// may replace the rotating expansion arrow icon.
  final Widget? trailing;

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

  /// Specifies whether the state of the children is maintained when the tile expands and collapses.
  ///
  /// When true, the children are kept in the tree while the tile is collapsed.
  /// When false (default), the children are removed from the tree when the tile is
  /// collapsed and recreated upon expansion.
  final bool maintainState;

  /// Specifies padding for the [ListTile].
  ///
  /// Analogous to [ListTile.contentPadding], this property defines the insets for
  /// the [leading], [title], [subtitle] and [trailing] widgets. It does not inset
  /// the expanded [children] widgets.
  ///
  /// If this property is null then [ExpansionTileThemeData.tilePadding] is used. If that
  /// is also null then the tile's padding is `EdgeInsets.symmetric(horizontal: 16.0)`.
  ///
  /// See also:
  ///
  /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
  ///   [ExpansionTileThemeData].
  final EdgeInsetsGeometry? tilePadding;

  /// Specifies the alignment of [children], which are arranged in a column when
  /// the tile is expanded.
  ///
  /// The internals of the expanded tile make use of a [Column] widget for
  /// [children], and [Align] widget to align the column. The `expandedAlignment`
  /// parameter is passed directly into the [Align].
  ///
  /// Modifying this property controls the alignment of the column within the
  /// expanded tile, not the alignment of [children] widgets within the column.
  /// To align each child within [children], see [expandedCrossAxisAlignment].
  ///
  /// The width of the column is the width of the widest child widget in [children].
  ///
  /// If this property is null then [ExpansionTileThemeData.expandedAlignment]is used. If that
  /// is also null then the value of `expandedAlignment` is [Alignment.center].
  ///
  /// See also:
  ///
  /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
  ///   [ExpansionTileThemeData].
  final Alignment? expandedAlignment;

  /// Specifies the alignment of each child within [children] when the tile is expanded.
  ///
  /// The internals of the expanded tile make use of a [Column] widget for
  /// [children], and the `crossAxisAlignment` parameter is passed directly into the [Column].
  ///
  /// Modifying this property controls the cross axis alignment of each child
  /// within its [Column]. Note that the width of the [Column] that houses
  /// [children] will be the same as the widest child widget in [children]. It is
  /// not necessarily the width of [Column] is equal to the width of expanded tile.
  ///
  /// To align the [Column] along the expanded tile, use the [expandedAlignment] property
  /// instead.
  ///
  /// When the value is null, the value of `expandedCrossAxisAlignment` is [CrossAxisAlignment.center].
  final CrossAxisAlignment? expandedCrossAxisAlignment;

  /// Specifies padding for [children].
  ///
  /// If this property is null then [ExpansionTileThemeData.childrenPadding] is used. If that
  /// is also null then the value of `childrenPadding` is [EdgeInsets.zero].
  ///
  /// See also:
  ///
  /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
  ///   [ExpansionTileThemeData].
  final EdgeInsetsGeometry? childrenPadding;

  /// The icon color of tile's expansion arrow icon when the sublist is expanded.
  ///
  /// Used to override to the [ListTileThemeData.iconColor].
  ///
  /// If this property is null then [ExpansionTileThemeData.iconColor] is used. If that
  /// is also null then the value of [ListTileThemeData.iconColor] is used.
  ///
  /// See also:
  ///
  /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
  ///   [ExpansionTileThemeData].
  final Color? iconColor;

  /// The icon color of tile's expansion arrow icon when the sublist is collapsed.
  ///
  /// Used to override to the [ListTileThemeData.iconColor].
  final Color? collapsedIconColor;

  /// The color of the tile's titles when the sublist is expanded.
  ///
  /// Used to override to the [ListTileThemeData.textColor].
  ///
  /// If this property is null then [ExpansionTileThemeData.textColor] is used. If that
  /// is also null then the value of [ListTileThemeData.textColor] is used.
  ///
  /// See also:
  ///
  /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
  ///   [ExpansionTileThemeData].
  final Color? textColor;

  /// The color of the tile's titles when the sublist is collapsed.
  ///
  /// Used to override to the [ListTileThemeData.textColor].
  ///
  /// If this property is null then [ExpansionTileThemeData.collapsedTextColor] is used. If that
  /// is also null then the value of [ListTileThemeData.textColor] is used.
  ///
  /// See also:
  ///
  /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
  ///   [ExpansionTileThemeData].
  final Color? collapsedTextColor;

  /// Typically used to force the expansion arrow icon to the tile's leading or trailing edge.
  ///
  /// By default, the value of `controlAffinity` is [ListTileControlAffinity.platform],
  /// which means that the expansion arrow icon will appear on the tile's trailing edge.
  final ListTileControlAffinity? controlAffinity;

  ///The decoration to paint behind the child.
  /// Use the color property to specify a simple solid color.
  /// The child is not clipped to the decoration. To clip a child to the shape of a particular ShapeDecoration, consider using a ClipPath widget.
  final Decoration? decoration;

  /// If non-null, the corners of this box are rounded by this [BorderRadius].
  ///
  /// Applies only to boxes with rectangular shapes; ignored if [shape] is not
  /// [BoxShape.rectangle].
  ///
  /// {@macro flutter.painting.BoxDecoration.clip}
  final BorderRadius? borderRadius;

  /// A border to draw above the background [color], [gradient], or [image].
  ///
  /// Follows the [shape] and [borderRadius].
  ///
  /// Use [Border] objects to describe borders that do not depend on the reading
  /// direction.
  ///
  /// Use [BoxBorder] objects to describe borders that should flip their left
  /// and right edges based on whether the text is being read left-to-right or
  /// right-to-left.
  final BoxBorder? border;

  ///The clip behavior when Container.decoration is not null.
  /// Defaults to Clip.none. Must be Clip.none if decoration is null.
  /// If a clip is to be applied, the Decoration.getClipPath method for the provided decoration must return a clip path.
  /// (This is not supported by all decorations; the default implementation of that method throws an UnsupportedError.)
  final Clip? clipBehavior;

  ///A list of shadows cast by this box behind the box.
  /// The shadow follows the shape of the box.
  /// See also:
  /// kElevationToShadow, for some predefined shadows used in Material Design.
  /// PhysicalModel, a widget for showing shadows.
  final List<BoxShadow>? boxShadow;

  /// The color to display border box when collapsed.
  ///
  /// If this property is null then [ExpansionTileThemeData.backgroundColor] is used. If that
  /// is also null then Colors.transparent is used.
  ///
  /// See also:
  ///
  /// * [ExpansionTileTheme.of], which returns the nearest [ExpansionTileTheme]'s
  ///   [ExpansionTileThemeData].
  final Color? collapsedBorderColor;

  /// The color to display border box when expanded.
  ///
  /// If this property is null then [ThemeData.dividerColor] is used. If that
  /// is also null then Colors.transparent is used.
  ///
  /// See also:
  ///
  /// * [ThemeData.of], which returns the nearest [ThemeData].
  final Color? expendedBorderColor;

  final bool isHasTopBorder;
  final bool isHasBottomBorder;
  final bool isHasLeftBorder;
  final bool isHasRightBorder;

  final bool isHasTrailing;

  ///The widget can expand or NOT.
  ///It helps when you want to prohibit expansion until the API requests success.
  ///More detail: https://github.com/congthanhng/Expansion-Tile-Group/issues/22
  final bool isEnableExpanded;

  ///Remove completely default vertical title padding
  final bool isDefaultVerticalPadding;

  @override
  State<ExpansionTileCustom> createState() => ExpansionTileCustomState();
}

class ExpansionTileCustomState extends State<ExpansionTileCustom>
    with SingleTickerProviderStateMixin {
  static final Animatable<double> _easeOutTween =
      CurveTween(curve: Curves.easeOut);
  static final Animatable<double> _easeInTween =
      CurveTween(curve: Curves.easeIn);
  static final Animatable<double> _halfTween =
      Tween<double>(begin: 0.0, end: 0.5);

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

  late AnimationController _controller;
  late Animation<double> _iconTurns;
  late Animation<double> _heightFactor;
  late Animation<Color?> _borderColor;
  late Animation<Color?> _headerColor;
  late Animation<Color?> _iconColor;
  late Animation<Color?> _backgroundColor;

  bool _isExpanded = false;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(duration: _kExpand, vsync: this);
    _heightFactor = _controller.drive(_easeInTween);
    _iconTurns = _controller.drive(_halfTween.chain(_easeInTween));
    _borderColor = _controller.drive(_borderColorTween.chain(_easeOutTween));
    _headerColor = _controller.drive(_headerColorTween.chain(_easeInTween));
    _iconColor = _controller.drive(_iconColorTween.chain(_easeInTween));
    _backgroundColor =
        _controller.drive(_backgroundColorTween.chain(_easeOutTween));

    if (widget.isEnableExpanded) {
      _isExpanded = PageStorage.of(context).readState(context) ??
          widget.initiallyExpanded;
    }

    if (_isExpanded) {
      _controller.value = 1.0;
    }
  }

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

  void expand() {
    _setExpanded(true);
  }

  void collapse() {
    _setExpanded(false);
  }

  void toggle() {
    _setExpanded(!_isExpanded);
  }

  void expandedChanged(bool isExpanded) {
    _setExpanded(isExpanded);
  }

  void _setExpanded(bool isExpanded) {
    if (!isExpanded && !widget.isEnableExpanded) return;
    if (_isExpanded != isExpanded) {
      setState(() {
        _isExpanded = isExpanded;
        if (_isExpanded) {
          _controller.forward();
        } else {
          _controller.reverse().then((_) {
            setState(() {
              // Rebuild without widget.children.
            });
          });
        }
        PageStorage.of(context).writeState(context, _isExpanded);
      });
      if (widget.onExpansionChanged != null) {
        widget.onExpansionChanged!(_isExpanded);
      }
    }
  }

  void _handleTap() {
    if (!_isExpanded && !widget.isEnableExpanded) return;
    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);
    });
    widget.onExpansionChanged?.call(_isExpanded);
  }

  // Platform or null affinity defaults to trailing.
  ListTileControlAffinity _effectiveAffinity(
      ListTileControlAffinity? affinity) {
    switch (affinity ?? ListTileControlAffinity.trailing) {
      case ListTileControlAffinity.leading:
        return ListTileControlAffinity.leading;
      case ListTileControlAffinity.trailing:
      case ListTileControlAffinity.platform:
        return ListTileControlAffinity.trailing;
    }
  }

  Widget? _buildIcon(BuildContext context) {
    return RotationTransition(
      turns: _iconTurns,
      child: const Icon(Icons.expand_more),
    );
  }

  Widget? _buildLeadingIcon(BuildContext context) {
    if (_effectiveAffinity(widget.controlAffinity) !=
        ListTileControlAffinity.leading) {
      return null;
    }
    return _buildIcon(context);
  }

  Widget? _buildTrailingIcon(BuildContext context) {
    if (_effectiveAffinity(widget.controlAffinity) !=
        ListTileControlAffinity.trailing) {
      return null;
    }
    return _buildIcon(context);
  }

  Widget _buildChildren(BuildContext context, Widget? child) {
    final ExpansionTileThemeData expansionTileTheme =
        ExpansionTileTheme.of(context);
    final Color borderSideColor = _borderColor.value ?? Colors.transparent;

    return Container(
      decoration: widget.decoration ??
          BoxDecoration(
            color: _backgroundColor.value ??
                expansionTileTheme.backgroundColor ??
                Colors.transparent,
            border: (widget.border ??
                Border(
                    top: widget.isHasTopBorder
                        ? BorderSide(color: borderSideColor)
                        : const BorderSide(color: Colors.transparent),
                    bottom: widget.isHasBottomBorder
                        ? BorderSide(color: borderSideColor)
                        : const BorderSide(color: Colors.transparent),
                    right: widget.isHasRightBorder
                        ? BorderSide(color: borderSideColor)
                        : const BorderSide(color: Colors.transparent),
                    left: widget.isHasLeftBorder
                        ? BorderSide(color: borderSideColor)
                        : const BorderSide(color: Colors.transparent))),
            borderRadius: widget.borderRadius,
            boxShadow: widget.boxShadow,
          ),
      clipBehavior: widget.clipBehavior ?? Clip.hardEdge,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          widget.isDefaultVerticalPadding == false
              ? IconTheme.merge(
                  data: IconThemeData(
                      color: _iconColor.value ?? expansionTileTheme.iconColor),
                  child: InkWell(
                    borderRadius: widget.borderRadius,
                    onTap: _handleTap,
                    child: Row(
                      children: [
                        widget.leading ??
                            _buildLeadingIcon(context) ??
                            const SizedBox.shrink(),
                        Column(
                          children: [
                            Padding(
                              padding: widget.tilePadding ??
                                  expansionTileTheme.tilePadding ??
                                  const EdgeInsets.symmetric(horizontal: 8),
                              child: DefaultTextStyle(
                                  style: TextStyle(color: _headerColor.value),
                                  child: widget.title),
                            ),
                            widget.subtitle ?? const SizedBox.shrink(),
                          ],
                        ),
                        const Spacer(),
                        widget.isHasTrailing == true
                            ? widget.trailing ??
                                _buildTrailingIcon(context) ??
                                const SizedBox.shrink()
                            : const SizedBox.shrink(),
                      ],
                    ),
                  ))
              : ListTileTheme.merge(
                  iconColor: _iconColor.value ?? expansionTileTheme.iconColor,
                  textColor: _headerColor.value,
                  child: InkWell(
                    borderRadius: widget.borderRadius,
                    onTap: _handleTap,
                    child: ListTile(
                      dense: widget.isDefaultVerticalPadding ? null : true,
                      visualDensity: widget.isDefaultVerticalPadding
                          ? null
                          : const VisualDensity(horizontal: 0, vertical: -4),
                      contentPadding:
                          widget.tilePadding ?? expansionTileTheme.tilePadding,
                      leading: widget.leading ?? _buildLeadingIcon(context),
                      title: widget.title,
                      subtitle: widget.subtitle,
                      trailing: widget.isHasTrailing == true
                          ? widget.trailing ?? _buildTrailingIcon(context)
                          : null,
                    ),
                  ),
                ),
          ClipRect(
            child: Align(
              alignment: widget.expandedAlignment ??
                  expansionTileTheme.expandedAlignment ??
                  Alignment.center,
              heightFactor: _heightFactor.value,
              child: child,
            ),
          ),
        ],
      ),
    );
  }

  @override
  void didChangeDependencies() {
    final ThemeData theme = Theme.of(context);
    final ExpansionTileThemeData expansionTileTheme =
        ExpansionTileTheme.of(context);
    final ColorScheme colorScheme = theme.colorScheme;
    _borderColorTween
      ..begin =
          widget.collapsedBorderColor ?? expansionTileTheme.backgroundColor
      ..end = widget.expendedBorderColor ?? theme.dividerColor;
    _headerColorTween
      ..begin = widget.collapsedTextColor ??
          expansionTileTheme.collapsedTextColor ??
          theme.textTheme.titleMedium!.color
      ..end = widget.textColor ??
          expansionTileTheme.textColor ??
          colorScheme.primary;
    _iconColorTween
      ..begin = widget.collapsedIconColor ??
          expansionTileTheme.collapsedIconColor ??
          theme.unselectedWidgetColor
      ..end = widget.iconColor ??
          expansionTileTheme.iconColor ??
          colorScheme.primary;
    _backgroundColorTween
      ..begin = widget.collapsedBackgroundColor ??
          expansionTileTheme.collapsedBackgroundColor
      ..end = widget.backgroundColor ?? expansionTileTheme.backgroundColor;
    super.didChangeDependencies();
  }

  @override
  Widget build(BuildContext context) {
    final ExpansionTileThemeData expansionTileTheme =
        ExpansionTileTheme.of(context);
    final bool closed = !_isExpanded && _controller.isDismissed;
    final bool shouldRemoveChildren = closed && !widget.maintainState;

    final Widget result = Offstage(
      offstage: closed,
      child: TickerMode(
        enabled: !closed,
        child: Padding(
          padding: widget.childrenPadding ??
              expansionTileTheme.childrenPadding ??
              EdgeInsets.zero,
          child: Column(
            crossAxisAlignment:
                widget.expandedCrossAxisAlignment ?? CrossAxisAlignment.center,
            children: widget.children,
          ),
        ),
      ),
    );

    return AnimatedBuilder(
      animation: _controller.view,
      builder: _buildChildren,
      child: shouldRemoveChildren ? null : result,
    );
  }
}
