import 'dart:math' as math;
import 'dart:ui' show lerpDouble;

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

const double _kTabHeight = 46.0;

typedef _LayoutCallback = void Function(
    List<double> xOffsets, TextDirection textDirection, double width);

class _TabLabelBarRenderer extends RenderFlex {
  _TabLabelBarRenderer({
    required Axis direction,
    required MainAxisSize mainAxisSize,
    required MainAxisAlignment mainAxisAlignment,
    required CrossAxisAlignment crossAxisAlignment,
    required TextDirection textDirection,
    required VerticalDirection verticalDirection,
    required this.onPerformLayout,
  }) : super(
          direction: direction,
          mainAxisSize: mainAxisSize,
          mainAxisAlignment: mainAxisAlignment,
          crossAxisAlignment: crossAxisAlignment,
          textDirection: textDirection,
          verticalDirection: verticalDirection,
        );

  _LayoutCallback onPerformLayout;

  @override
  void performLayout() {
    super.performLayout();
    // xOffsets will contain childCount+1 values, giving the offsets of the
    // leading edge of the first tab as the first value, of the leading edge of
    // the each subsequent tab as each subsequent value, and of the trailing
    // edge of the last tab as the last value.
    RenderBox? child = firstChild;
    final List<double> xOffsets = <double>[];
    while (child != null) {
      final FlexParentData childParentData =
          child.parentData! as FlexParentData;
      xOffsets.add(childParentData.offset.dx);
      assert(child.parentData == childParentData);
      child = childParentData.nextSibling;
    }
    assert(textDirection != null);
    switch (textDirection!) {
      case TextDirection.rtl:
        xOffsets.insert(0, size.width);
        break;
      case TextDirection.ltr:
        xOffsets.add(size.width);
        break;
    }
    onPerformLayout(xOffsets, textDirection!, size.width);
  }
}

// This class and its renderer class only exist to report the widths of the tabs
// upon layout. The tab widths are only used at paint time (see _IndicatorPainter)
// or in response to input.
class _TabLabelBar extends Flex {
  _TabLabelBar({
    required List<Widget> children,
    required this.onPerformLayout,
  }) : super(
          children: children,
          direction: Axis.horizontal,
          mainAxisSize: MainAxisSize.max,
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.center,
          verticalDirection: VerticalDirection.down,
        );

  final _LayoutCallback onPerformLayout;

  @override
  RenderFlex createRenderObject(BuildContext context) {
    return _TabLabelBarRenderer(
      direction: direction,
      mainAxisAlignment: mainAxisAlignment,
      mainAxisSize: mainAxisSize,
      crossAxisAlignment: crossAxisAlignment,
      textDirection: getEffectiveTextDirection(context)!,
      verticalDirection: verticalDirection,
      onPerformLayout: onPerformLayout,
    );
  }

  @override
  void updateRenderObject(
      BuildContext context, _TabLabelBarRenderer renderObject) {
    super.updateRenderObject(context, renderObject);
    renderObject.onPerformLayout = onPerformLayout;
  }
}

class _IndicatorPainter extends CustomPainter {
  _IndicatorPainter({
    required this.controller,
    required this.tabKeys,
    required _IndicatorPainter? old,
  }) : super(repaint: controller.animation) {
    if (old != null) {
      saveTabOffsets(old._currentTabOffsets, old._currentTextDirection);
    }
  }

  final TabController controller;

  final List<GlobalKey> tabKeys;

  // _currentTabOffsets and _currentTextDirection are set each time TabBar
  // layout is completed. These values can be null when TabBar contains no
  // tabs, since there are nothing to lay out.
  List<double>? _currentTabOffsets;
  TextDirection? _currentTextDirection;

  BoxPainter? _painter;
  bool _needsPaint = false;
  void markNeedsPaint() {
    _needsPaint = true;
  }

  void dispose() {
    _painter?.dispose();
  }

  void saveTabOffsets(List<double>? tabOffsets, TextDirection? textDirection) {
    _currentTabOffsets = tabOffsets;
    _currentTextDirection = textDirection;
  }

  // _currentTabOffsets[index] is the offset of the start edge of the tab at index, and
  // _currentTabOffsets[_currentTabOffsets.length] is the end edge of the last tab.
  int get maxTabIndex => _currentTabOffsets!.length - 2;

  double centerOf(int tabIndex) {
    assert(_currentTabOffsets != null);
    assert(_currentTabOffsets!.isNotEmpty);
    assert(tabIndex >= 0);
    assert(tabIndex <= maxTabIndex);
    return (_currentTabOffsets![tabIndex] + _currentTabOffsets![tabIndex + 1]) /
        2.0;
  }

  @override
  void paint(Canvas canvas, Size size) {
    _needsPaint = false;
  }

  @override
  bool shouldRepaint(_IndicatorPainter old) {
    return _needsPaint ||
        controller != old.controller ||
        tabKeys.length != old.tabKeys.length ||
        (!listEquals(_currentTabOffsets, old._currentTabOffsets)) ||
        _currentTextDirection != old._currentTextDirection;
  }
}

// This class, and TabBarScrollController, only exist to handle the case
// where a scrollable TabBar has a non-zero initialIndex. In that case we can
// only compute the scroll position's initial scroll offset (the "correct"
// pixels value) after the TabBar viewport width and scroll limits are known.

class _TabBarScrollPosition extends ScrollPositionWithSingleContext {
  _TabBarScrollPosition({
    required ScrollPhysics physics,
    required ScrollContext context,
    required ScrollPosition? oldPosition,
    required this.tabBar,
  }) : super(
          initialPixels: null,
          physics: physics,
          context: context,
          oldPosition: oldPosition,
        );

  final _FlutterButtonTabBarState tabBar;

  bool _viewportDimensionWasNonZero = false;

  // Position should be adjusted at least once.
  bool _needsPixelsCorrection = true;

  @override
  bool applyContentDimensions(double minScrollExtent, double maxScrollExtent) {
    bool result = true;
    if (!_viewportDimensionWasNonZero) {
      _viewportDimensionWasNonZero = viewportDimension != 0.0;
    }
    // If the viewport never had a non-zero dimension, we just want to jump
    // to the initial scroll position to avoid strange scrolling effects in
    // release mode: In release mode, the viewport temporarily may have a
    // dimension of zero before the actual dimension is calculated. In that
    // scenario, setting the actual dimension would cause a strange scroll
    // effect without this guard because the super call below would starts a
    // ballistic scroll activity.
    if (!_viewportDimensionWasNonZero || _needsPixelsCorrection) {
      _needsPixelsCorrection = false;
      correctPixels(tabBar._initialScrollOffset(
          viewportDimension, minScrollExtent, maxScrollExtent));
      result = false;
    }
    return super.applyContentDimensions(minScrollExtent, maxScrollExtent) &&
        result;
  }

  void markNeedsPixelsCorrection() {
    _needsPixelsCorrection = true;
  }
}

// This class, and TabBarScrollPosition, only exist to handle the case
// where a scrollable TabBar has a non-zero initialIndex.
class _TabBarScrollController extends ScrollController {
  _TabBarScrollController(this.tabBar);

  final _FlutterButtonTabBarState tabBar;

  @override
  ScrollPosition createScrollPosition(ScrollPhysics physics,
      ScrollContext context, ScrollPosition? oldPosition) {
    return _TabBarScrollPosition(
      physics: physics,
      context: context,
      oldPosition: oldPosition,
      tabBar: tabBar,
    );
  }
}

/// A flutter Design widget that displays a horizontal row of tabs.
class FlutterButtonTabBar extends StatefulWidget
    implements PreferredSizeWidget {
  /// The [tabs] argument must not be null and its length must match the [controller]'s
  /// [TabController.length].
  ///
  /// If a [TabController] is not provided, then there must be a
  /// [DefaultTabController] ancestor.
  ///
  const FlutterButtonTabBar({
    Key? key,
    required this.tabs,
    this.controller,
    this.isScrollable = false,
    this.useToggleButtonStyle = false,
    this.dragStartBehavior = DragStartBehavior.start,
    this.onTap,
    this.backgroundColor,
    this.unselectedBackgroundColor,
    this.decoration,
    this.unselectedDecoration,
    this.labelStyle,
    this.unselectedLabelStyle,
    this.labelColor,
    this.unselectedLabelColor,
    this.borderWidth = 0,
    this.borderColor = Colors.transparent,
    this.unselectedBorderColor = Colors.transparent,
    this.physics = const BouncingScrollPhysics(),
    this.labelPadding = const EdgeInsets.symmetric(horizontal: 4),
    this.buttonMargin = const EdgeInsets.all(4),
    this.padding = EdgeInsets.zero,
    this.borderRadius = 8.0,
    this.elevation = 0,
  }) : super(key: key);

  /// Typically a list of two or more [Tab] widgets.
  ///
  /// The length of this list must match the [controller]'s [TabController.length]
  /// and the length of the [TabBarView.children] list.
  final List<Widget> tabs;

  /// This widget's selection and animation state.
  ///
  /// If [TabController] is not provided, then the value of [DefaultTabController.of]
  /// will be used.
  final TabController? controller;

  /// Whether this tab bar can be scrolled horizontally.
  ///
  /// If [isScrollable] is true, then each tab is as wide as needed for its label
  /// and the entire [FlutterButtonTabBar] is scrollable. Otherwise each tab gets an equal
  /// share of the available space.
  final bool isScrollable;

  /// Whether the tab buttons should be styled as toggle buttons.
  final bool useToggleButtonStyle;

  /// The background [Color] of the button on its selected state.
  final Color? backgroundColor;

  /// The background [Color] of the button on its unselected state.
  final Color? unselectedBackgroundColor;

  /// The [BoxDecoration] of the button on its selected state.
  ///
  /// If [BoxDecoration] is not provided, [backgroundColor] is used.
  final BoxDecoration? decoration;

  /// The [BoxDecoration] of the button on its unselected state.
  ///
  /// If [BoxDecoration] is not provided, [unselectedBackgroundColor] is used.
  final BoxDecoration? unselectedDecoration;

  /// The [TextStyle] of the button's [Text] on its selected state. The color provided
  /// on the TextStyle will be used for the [Icon]'s color.
  final TextStyle? labelStyle;

  /// The color of selected tab labels.
  final Color? labelColor;

  /// The color of unselected tab labels.
  final Color? unselectedLabelColor;

  /// The [TextStyle] of the button's [Text] on its unselected state. The color provided
  /// on the TextStyle will be used for the [Icon]'s color.
  final TextStyle? unselectedLabelStyle;

  /// The with of solid [Border] for each button. If no value is provided, the border
  /// is not drawn.
  final double borderWidth;

  /// The [Color] of solid [Border] for each button.
  final Color? borderColor;

  /// The [Color] of solid [Border] for each button. If no value is provided, the value of
  /// [this.borderColor] is used.
  final Color? unselectedBorderColor;

  /// The [EdgeInsets] used for the [Padding] of the buttons' content.
  ///
  /// The default value is [EdgeInsets.symmetric(horizontal: 4)].
  final EdgeInsetsGeometry labelPadding;

  /// The [EdgeInsets] used for the [Margin] of the buttons.
  ///
  /// The default value is [EdgeInsets.all(4)].
  final EdgeInsetsGeometry buttonMargin;

  /// The amount of space by which to inset the tab bar.
  final EdgeInsetsGeometry? padding;

  /// The value of the [BorderRadius.circular] applied to each button.
  final double borderRadius;

  /// The value of the [elevation] applied to each button.
  final double elevation;

  final DragStartBehavior dragStartBehavior;

  final ValueChanged<int>? onTap;

  final ScrollPhysics? physics;

  /// A size whose height depends on if the tabs have both icons and text.
  ///
  /// [AppBar] uses this size to compute its own preferred size.
  @override
  Size get preferredSize {
    double maxHeight = _kTabHeight;
    for (final Widget item in tabs) {
      if (item is PreferredSizeWidget) {
        final double itemHeight = item.preferredSize.height;
        maxHeight = math.max(itemHeight, maxHeight);
      }
    }
    return Size.fromHeight(
        maxHeight + labelPadding.vertical + buttonMargin.vertical);
  }

  @override
  State<FlutterButtonTabBar> createState() =>
      _FlutterButtonTabBarState();
}

class _FlutterButtonTabBarState extends State<FlutterButtonTabBar>
    with TickerProviderStateMixin {
  ScrollController? _scrollController;
  TabController? _controller;
  _IndicatorPainter? _indicatorPainter;
  late AnimationController _animationController;
  int _currentIndex = 0;
  int _prevIndex = -1;

  late double _tabStripWidth;
  late List<GlobalKey> _tabKeys;

  final GlobalKey _tabsParentKey = GlobalKey();

  bool _debugHasScheduledValidTabsCountCheck = false;

  @override
  void initState() {
    super.initState();
    // If indicatorSize is TabIndicatorSize.label, _tabKeys[i] is used to find
    // the width of tab widget i. See _IndicatorPainter.indicatorRect().
    _tabKeys = widget.tabs.map((tab) => GlobalKey()).toList();

    /// The animation duration is 2/3 of the tab scroll animation duration in
    /// Material design (kTabScrollDuration).
    _animationController = AnimationController(
        vsync: this, duration: const Duration(milliseconds: 200));

    // so the buttons start in their "final" state (color)
    _animationController
      ..value = 1.0
      ..addListener(() {
        if (mounted) {
          setState(() {});
        }
      });
  }

  // If the TabBar is rebuilt with a new tab controller, the caller should
  // dispose the old one. In that case the old controller's animation will be
  // null and should not be accessed.
  bool get _controllerIsValid => _controller?.animation != null;

  void _updateTabController() {
    final TabController? newController =
        widget.controller ?? DefaultTabController.maybeOf(context);
    assert(() {
      if (newController == null) {
        throw FlutterError(
          'No TabController for ${widget.runtimeType}.\n'
          'When creating a ${widget.runtimeType}, you must either provide an explicit '
          'TabController using the "controller" property, or you must ensure that there '
          'is a DefaultTabController above the ${widget.runtimeType}.\n'
          'In this case, there was neither an explicit controller nor a default controller.',
        );
      }
      return true;
    }());

    if (newController == _controller) {
      return;
    }

    if (_controllerIsValid) {
      _controller!.animation!.removeListener(_handleTabControllerAnimationTick);
      _controller!.removeListener(_handleTabControllerTick);
    }
    _controller = newController;
    if (_controller != null) {
      _controller!.animation!.addListener(_handleTabControllerAnimationTick);
      _controller!.addListener(_handleTabControllerTick);
      _currentIndex = _controller!.index;
    }
  }

  void _initIndicatorPainter() {
    _indicatorPainter = !_controllerIsValid
        ? null
        : _IndicatorPainter(
            controller: _controller!,
            tabKeys: _tabKeys,
            old: _indicatorPainter,
          );
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    assert(debugCheckHasMaterial(context));
    _updateTabController();
    _initIndicatorPainter();
  }

  @override
  void didUpdateWidget(FlutterButtonTabBar oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (widget.controller != oldWidget.controller) {
      _updateTabController();
      _initIndicatorPainter();
      // Adjust scroll position.
      if (_scrollController != null) {
        final ScrollPosition position = _scrollController!.position;
        if (position is _TabBarScrollPosition) {
          position.markNeedsPixelsCorrection();
        }
      }
    }

    if (widget.tabs.length > _tabKeys.length) {
      final int delta = widget.tabs.length - _tabKeys.length;
      _tabKeys.addAll(List<GlobalKey>.generate(delta, (int n) => GlobalKey()));
    } else if (widget.tabs.length < _tabKeys.length) {
      _tabKeys.removeRange(widget.tabs.length, _tabKeys.length);
    }
  }

  @override
  void dispose() {
    _indicatorPainter!.dispose();
    if (_controllerIsValid) {
      _controller!.animation!.removeListener(_handleTabControllerAnimationTick);
      _controller!.removeListener(_handleTabControllerTick);
    }
    _controller = null;
    // We don't own the _controller Animation, so it's not disposed here.
    super.dispose();
  }

  int get maxTabIndex => _indicatorPainter!.maxTabIndex;

  double _tabScrollOffset(
      int index, double viewportWidth, double minExtent, double maxExtent) {
    if (!widget.isScrollable) {
      return 0.0;
    }
    double tabCenter = _indicatorPainter!.centerOf(index);
    double paddingStart;
    switch (Directionality.of(context)) {
      case TextDirection.rtl:
        paddingStart = widget.padding?.resolve(TextDirection.rtl).right ?? 0;
        tabCenter = _tabStripWidth - tabCenter;
        break;
      case TextDirection.ltr:
        paddingStart = widget.padding?.resolve(TextDirection.ltr).left ?? 0;
        break;
    }

    return clampDouble(
        tabCenter + paddingStart - viewportWidth / 2.0, minExtent, maxExtent);
  }

  double _tabCenteredScrollOffset(int index) {
    final ScrollPosition position = _scrollController!.position;
    return _tabScrollOffset(index, position.viewportDimension,
        position.minScrollExtent, position.maxScrollExtent);
  }

  double _initialScrollOffset(
      double viewportWidth, double minExtent, double maxExtent) {
    return _tabScrollOffset(_currentIndex, viewportWidth, minExtent, maxExtent);
  }

  void _scrollToCurrentIndex() {
    final double offset = _tabCenteredScrollOffset(_currentIndex);
    _scrollController!
        .animateTo(offset, duration: kTabScrollDuration, curve: Curves.ease);
  }

  void _scrollToControllerValue() {
    final double? leadingPosition =
        _currentIndex > 0 ? _tabCenteredScrollOffset(_currentIndex - 1) : null;
    final double middlePosition = _tabCenteredScrollOffset(_currentIndex);
    final double? trailingPosition = _currentIndex < maxTabIndex
        ? _tabCenteredScrollOffset(_currentIndex + 1)
        : null;

    final double index = _controller!.index.toDouble();
    final double value = _controller!.animation!.value;
    final double offset;
    if (value == index - 1.0) {
      offset = leadingPosition ?? middlePosition;
    } else if (value == index + 1.0) {
      offset = trailingPosition ?? middlePosition;
    } else if (value == index) {
      offset = middlePosition;
    } else if (value < index) {
      offset = leadingPosition == null
          ? middlePosition
          : lerpDouble(middlePosition, leadingPosition, index - value)!;
    } else {
      offset = trailingPosition == null
          ? middlePosition
          : lerpDouble(middlePosition, trailingPosition, value - index)!;
    }

    _scrollController!.jumpTo(offset);
  }

  void _handleTabControllerAnimationTick() {
    assert(mounted);
    if (!_controller!.indexIsChanging && widget.isScrollable) {
      // Sync the TabBar's scroll position with the TabBarView's PageView.
      _currentIndex = _controller!.index;
      _scrollToControllerValue();
    }
  }

  void _handleTabControllerTick() {
    if (_controller!.index != _currentIndex) {
      _prevIndex = _currentIndex;
      _currentIndex = _controller!.index;
      _triggerAnimation();
      if (widget.isScrollable) {
        _scrollToCurrentIndex();
      }
    }
    setState(() {
      // Rebuild the tabs after a (potentially animated) index change
      // has completed.
    });
  }

  void _triggerAnimation() {
    // reset the animation so it's ready to go
    _animationController
      ..reset()
      ..forward();
  }

  // Called each time layout completes.
  void _saveTabOffsets(
      List<double> tabOffsets, TextDirection textDirection, double width) {
    _tabStripWidth = width;
    _indicatorPainter?.saveTabOffsets(tabOffsets, textDirection);
  }

  void _handleTap(int index) {
    assert(index >= 0 && index < widget.tabs.length);
    _controller?.animateTo(index);
    widget.onTap?.call(index);
  }

  Widget _buildStyledTab(Widget child, int index) {
    final TabBarTheme tabBarTheme = TabBarTheme.of(context);

    final double animationValue;
    if (index == _currentIndex) {
      animationValue = _animationController.value;
    } else if (index == _prevIndex) {
      animationValue = 1 - _animationController.value;
    } else {
      animationValue = 0;
    }

    final TextStyle? textStyle = TextStyle.lerp(
        (widget.unselectedLabelStyle ??
                tabBarTheme.labelStyle ??
                DefaultTextStyle.of(context).style)
            .copyWith(
          color: widget.unselectedLabelColor,
        ),
        (widget.labelStyle ??
                tabBarTheme.labelStyle ??
                DefaultTextStyle.of(context).style)
            .copyWith(
          color: widget.labelColor,
        ),
        animationValue);

    final Color? textColor = Color.lerp(
        widget.unselectedLabelColor, widget.labelColor, animationValue);

    final Color? borderColor = Color.lerp(
        widget.unselectedBorderColor, widget.borderColor, animationValue);

    BoxDecoration? boxDecoration = BoxDecoration.lerp(
        BoxDecoration(
          color: widget.unselectedDecoration?.color ??
              widget.unselectedBackgroundColor ??
              Colors.transparent,
          boxShadow: widget.unselectedDecoration?.boxShadow,
          gradient: widget.unselectedDecoration?.gradient,
          borderRadius: widget.useToggleButtonStyle
              ? null
              : BorderRadius.circular(widget.borderRadius),
        ),
        BoxDecoration(
          color: widget.decoration?.color ??
              widget.backgroundColor ??
              Colors.transparent,
          boxShadow: widget.decoration?.boxShadow,
          gradient: widget.decoration?.gradient,
          borderRadius: widget.useToggleButtonStyle
              ? null
              : BorderRadius.circular(widget.borderRadius),
        ),
        animationValue);

    if (widget.useToggleButtonStyle &&
        widget.borderWidth > 0 &&
        boxDecoration != null) {
      if (index == 0) {
        boxDecoration = boxDecoration.copyWith(
          border: Border(
            right: BorderSide(
              color: widget.unselectedBorderColor ?? Colors.transparent,
              width: widget.borderWidth / 2,
            ),
          ),
        );
      } else if (index == widget.tabs.length - 1) {
        boxDecoration = boxDecoration.copyWith(
          border: Border(
            left: BorderSide(
              color: widget.unselectedBorderColor ?? Colors.transparent,
              width: widget.borderWidth / 2,
            ),
          ),
        );
      } else {
        boxDecoration = boxDecoration.copyWith(
          border: Border.symmetric(
            vertical: BorderSide(
              color: widget.unselectedBorderColor ?? Colors.transparent,
              width: widget.borderWidth / 2,
            ),
          ),
        );
      }
    }

    return Padding(
      key: _tabKeys[index],
      // padding for the buttons
      padding:
          widget.useToggleButtonStyle ? EdgeInsets.zero : widget.buttonMargin,
      child: TextButton(
        onPressed: () => _handleTap(index),
        style: ButtonStyle(
          elevation: MaterialStateProperty.all(
              widget.useToggleButtonStyle ? 0 : widget.elevation),

          /// give a pretty small minimum size
          minimumSize: MaterialStateProperty.all(const Size(10, 10)),
          padding: MaterialStateProperty.all(EdgeInsets.zero),
          textStyle: MaterialStateProperty.all(textStyle),
          foregroundColor: MaterialStateProperty.all(textColor),
          tapTargetSize: MaterialTapTargetSize.shrinkWrap,
          shape: MaterialStateProperty.all(
            widget.useToggleButtonStyle
                ? const RoundedRectangleBorder(
                    side: BorderSide.none,
                    borderRadius: BorderRadius.zero,
                  )
                : RoundedRectangleBorder(
                    side: (widget.borderWidth == 0)
                        ? BorderSide.none
                        : BorderSide(
                            color: borderColor ?? Colors.transparent,
                            width: widget.borderWidth,
                          ),
                    borderRadius: BorderRadius.circular(widget.borderRadius),
                  ),
          ),
        ),
        child: Ink(
          decoration: boxDecoration,
          child: Container(
            padding: widget.labelPadding,
            alignment: Alignment.center,
            child: child,
          ),
        ),
      ),
    );
  }

  bool _debugScheduleCheckHasValidTabsCount() {
    if (_debugHasScheduledValidTabsCountCheck) {
      return true;
    }
    WidgetsBinding.instance.addPostFrameCallback((Duration duration) {
      _debugHasScheduledValidTabsCountCheck = false;
      if (!mounted) {
        return;
      }
      assert(() {
        if (_controller!.length != widget.tabs.length) {
          throw FlutterError(
            "Controller's length property (${_controller!.length}) does not match the "
            "number of tabs (${widget.tabs.length}) present in TabBar's tabs property.",
          );
        }
        return true;
      }());
    });
    _debugHasScheduledValidTabsCountCheck = true;
    return true;
  }

  @override
  Widget build(BuildContext context) {
    assert(_debugScheduleCheckHasValidTabsCount());

    if (_controller!.length == 0) {
      return Container(
        height: _kTabHeight +
            widget.labelPadding.vertical +
            widget.buttonMargin.vertical,
      );
    }

    final List<Widget> wrappedTabs =
        List<Widget>.generate(widget.tabs.length, (int index) {
      return _buildStyledTab(widget.tabs[index], index);
    });

    final int tabCount = widget.tabs.length;
    // Add the tap handler to each tab. If the tab bar is not scrollable,
    // then give all of the tabs equal flexibility so that they each occupy
    // the same share of the tab bar's overall width.

    for (int index = 0; index < tabCount; index += 1) {
      if (!widget.isScrollable) {
        wrappedTabs[index] = Expanded(child: wrappedTabs[index]);
      }
    }

    Widget tabBar = AnimatedBuilder(
      animation: _animationController,
      key: _tabsParentKey,
      builder: (context, child) {
        Widget tabBarTemp = _TabLabelBar(
          onPerformLayout: _saveTabOffsets,
          children: wrappedTabs,
        );

        if (widget.useToggleButtonStyle) {
          tabBarTemp = Material(
            shape: widget.useToggleButtonStyle
                ? RoundedRectangleBorder(
                    side: (widget.borderWidth == 0)
                        ? BorderSide.none
                        : BorderSide(
                            color: widget.borderColor ?? Colors.transparent,
                            width: widget.borderWidth,
                            style: BorderStyle.solid,
                          ),
                    borderRadius: BorderRadius.circular(widget.borderRadius),
                  )
                : null,
            elevation: widget.useToggleButtonStyle ? widget.elevation : 0,
            clipBehavior: Clip.antiAliasWithSaveLayer,
            child: tabBarTemp,
          );
        }
        return CustomPaint(
          painter: _indicatorPainter,
          child: tabBarTemp,
        );
      },
    );

    if (widget.isScrollable) {
      _scrollController ??= _TabBarScrollController(this);
      tabBar = SingleChildScrollView(
        dragStartBehavior: widget.dragStartBehavior,
        scrollDirection: Axis.horizontal,
        controller: _scrollController,
        padding: widget.padding,
        physics: widget.physics,
        child: tabBar,
      );
    } else if (widget.padding != null) {
      tabBar = Padding(
        padding: widget.padding!,
        child: tabBar,
      );
    }

    return tabBar;
  }
}
