import 'dart:math' as math;
import 'dart:math';

import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/rendering.dart';

const EdgeInsetsGeometry _kHorizontalItemPadding = EdgeInsets.symmetric(
  vertical: 2,
  horizontal: 3,
);

const Radius _kCornerRadius = Radius.circular(9);

const Radius _kThumbRadius = Radius.circular(8);

const EdgeInsets _kThumbInsets = EdgeInsets.symmetric(horizontal: 1);

const double _kMinSegmentedControlHeight = 28.0;

const EdgeInsets _kSeparatorInset = EdgeInsets.symmetric(vertical: 5);

const double _kSeparatorWidth = 1;

const double _kMinThumbScale = 0.95;

const double _kSegmentMinPadding = 10;

const double _kTouchYDistanceThreshold = 50.0 * 50.0;

const double _kContentPressedMinOpacity = 0.2;

const double _kFontSize = 13.0;

const FontWeight _kFontWeight = FontWeight.w500;

const FontWeight _kHighlightedFontWeight = FontWeight.w600;

const Color _kDisabledContentColor = Color.fromARGB(115, 122, 122, 122);

final SpringSimulation _kThumbSpringAnimationSimulation = SpringSimulation(
  const SpringDescription(mass: 1, stiffness: 503.551, damping: 44.8799),
  0,
  1,
  0,
);

const Duration _kSpringAnimationDuration = Duration(milliseconds: 412);

const Duration _kOpacityAnimationDuration = Duration(milliseconds: 470);

const Duration _kHighlightAnimationDuration = Duration(milliseconds: 200);

class CommonTabBar<T extends Object> extends StatefulWidget {
  CommonTabBar({
    super.key,
    required this.children,
    required this.onValueChanged,
    this.disabledChildren = const <Never>{},
    this.groupValue,
    required this.thumbColor,
    this.padding = _kHorizontalItemPadding,
    this.backgroundColor,
    this.proportionalWidth = false,
  }) : assert(children.length >= 2),
       assert(
         groupValue == null || children.keys.contains(groupValue),
         'The groupValue must be either null or one of the keys in the children map.',
       );
  final Map<T, Widget> children;
  final Set<T> disabledChildren;
  final T? groupValue;
  final ValueChanged<T?> onValueChanged;
  final Color? backgroundColor;
  final Color thumbColor;
  final bool proportionalWidth;
  final EdgeInsetsGeometry padding;

  @override
  State<CommonTabBar<T>> createState() => _CommonTabBarState<T>();
}

class _CommonTabBarState<T extends Object> extends State<CommonTabBar<T>>
    with TickerProviderStateMixin<CommonTabBar<T>> {
  late final AnimationController thumbController = AnimationController(
    duration: _kSpringAnimationDuration,
    value: 0,
    vsync: this,
  );
  Animatable<Rect?>? thumbAnimatable;

  late final AnimationController thumbScaleController = AnimationController(
    duration: _kSpringAnimationDuration,
    value: 0,
    vsync: this,
  );
  late Animation<double> thumbScaleAnimation = thumbScaleController.drive(
    Tween<double>(begin: 1, end: _kMinThumbScale),
  );

  final TapGestureRecognizer tap = TapGestureRecognizer();
  final HorizontalDragGestureRecognizer drag =
      HorizontalDragGestureRecognizer();
  final LongPressGestureRecognizer longPress = LongPressGestureRecognizer();
  final GlobalKey segmentedControlRenderWidgetKey = GlobalKey();

  @override
  void initState() {
    super.initState();
    final GestureArenaTeam team = GestureArenaTeam();
    longPress.team = team;
    drag.team = team;
    team.captain = drag;

    drag
      ..onDown = onDown
      ..onUpdate = onUpdate
      ..onEnd = onEnd
      ..onCancel = onCancel;

    tap.onTapUp = onTapUp;
    longPress.onLongPress = () {};

    highlighted = widget.groupValue;
  }

  @override
  void didUpdateWidget(CommonTabBar<T> oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (!isThumbDragging && highlighted != widget.groupValue) {
      thumbController.animateWith(_kThumbSpringAnimationSimulation);
      thumbAnimatable = null;
      highlighted = widget.groupValue;
    }
  }

  @override
  void dispose() {
    thumbScaleController.dispose();
    thumbController.dispose();

    drag.dispose();
    tap.dispose();
    longPress.dispose();

    super.dispose();
  }

  bool? _startedOnSelectedSegment;
  bool _startedOnDisabledSegment = false;

  bool get isThumbDragging =>
      (_startedOnSelectedSegment ?? false) && !_startedOnDisabledSegment;

  T segmentForXPosition(double dx) {
    final BuildContext currentContext =
        segmentedControlRenderWidgetKey.currentContext!;
    final _RenderSegmentedControl<T> renderBox =
        currentContext.findRenderObject()! as _RenderSegmentedControl<T>;

    final int numOfChildren = widget.children.length;
    assert(renderBox.hasSize);
    assert(numOfChildren >= 2);

    int segmentIndex = renderBox.getClosestSegmentIndex(dx);

    switch (Directionality.of(context)) {
      case TextDirection.ltr:
        break;
      case TextDirection.rtl:
        segmentIndex = numOfChildren - 1 - segmentIndex;
    }
    return widget.children.keys.elementAt(segmentIndex);
  }

  bool _hasDraggedTooFar(DragUpdateDetails details) {
    final RenderBox renderBox = context.findRenderObject()! as RenderBox;
    assert(renderBox.hasSize);
    final Size size = renderBox.size;
    final Offset offCenter =
        details.localPosition - Offset(size.width / 2, size.height / 2);
    final double l2 =
        math.pow(math.max(0.0, offCenter.dx.abs() - size.width / 2), 2) +
                math.pow(math.max(0.0, offCenter.dy.abs() - size.height / 2), 2)
            as double;
    return l2 > _kTouchYDistanceThreshold;
  }

  void _playThumbScaleAnimation({required bool isExpanding}) {
    thumbScaleAnimation = thumbScaleController.drive(
      Tween<double>(
        begin: thumbScaleAnimation.value,
        end: isExpanding ? 1 : _kMinThumbScale,
      ),
    );
    thumbScaleController.animateWith(_kThumbSpringAnimationSimulation);
  }

  void onHighlightChangedByGesture(T newValue) {
    if (highlighted == newValue) {
      return;
    }

    setState(() {
      highlighted = newValue;
    });
    thumbController.animateWith(_kThumbSpringAnimationSimulation);
    thumbAnimatable = null;
  }

  void onPressedChangedByGesture(T? newValue) {
    if (pressed != newValue) {
      setState(() {
        pressed = newValue;
      });
    }
  }

  void onTapUp(TapUpDetails details) {
    if (isThumbDragging) {
      return;
    }
    final T segment = segmentForXPosition(details.localPosition.dx);
    onPressedChangedByGesture(null);
    if (segment != widget.groupValue &&
        !widget.disabledChildren.contains(segment)) {
      widget.onValueChanged(segment);
    }
  }

  void onDown(DragDownDetails details) {
    final T touchDownSegment = segmentForXPosition(details.localPosition.dx);
    _startedOnSelectedSegment = touchDownSegment == highlighted;
    _startedOnDisabledSegment = widget.disabledChildren.contains(
      touchDownSegment,
    );
    if (widget.disabledChildren.contains(touchDownSegment)) {
      return;
    }
    onPressedChangedByGesture(touchDownSegment);

    if (isThumbDragging) {
      _playThumbScaleAnimation(isExpanding: false);
    }
  }

  void onUpdate(DragUpdateDetails details) {
    if (_startedOnDisabledSegment) {
      return;
    }
    final T touchDownSegment = segmentForXPosition(details.localPosition.dx);
    if (widget.disabledChildren.contains(touchDownSegment)) {
      return;
    }
    if (isThumbDragging) {
      onPressedChangedByGesture(touchDownSegment);
      onHighlightChangedByGesture(touchDownSegment);
    } else {
      final T? segment = _hasDraggedTooFar(details)
          ? null
          : segmentForXPosition(details.localPosition.dx);
      onPressedChangedByGesture(segment);
    }
  }

  void onEnd(DragEndDetails details) {
    final T? pressed = this.pressed;
    if (isThumbDragging) {
      _playThumbScaleAnimation(isExpanding: true);
      if (highlighted != widget.groupValue) {
        widget.onValueChanged(highlighted);
      }
    } else if (pressed != null) {
      onHighlightChangedByGesture(pressed);
      assert(pressed == highlighted);
      if (highlighted != widget.groupValue) {
        widget.onValueChanged(highlighted);
      }
    }

    onPressedChangedByGesture(null);
    _startedOnSelectedSegment = null;
  }

  void onCancel() {
    if (isThumbDragging) {
      _playThumbScaleAnimation(isExpanding: true);
    }
    onPressedChangedByGesture(null);
    _startedOnSelectedSegment = null;
  }

  T? highlighted;

  T? pressed;

  @override
  Widget build(BuildContext context) {
    assert(widget.children.length >= 2);
    List<Widget> children = <Widget>[];
    bool isPreviousSegmentHighlighted = false;

    int index = 0;
    int? highlightedIndex;
    for (final MapEntry<T, Widget> entry in widget.children.entries) {
      final bool isHighlighted = highlighted == entry.key;
      if (isHighlighted) {
        highlightedIndex = index;
      }

      if (index != 0) {
        children.add(
          _SegmentSeparator(
            key: ValueKey<int>(index),
            highlighted: isPreviousSegmentHighlighted || isHighlighted,
          ),
        );
      }

      final TextDirection textDirection = Directionality.of(context);
      final _SegmentLocation segmentLocation = switch (textDirection) {
        TextDirection.ltr when index == 0 => _SegmentLocation.leftmost,
        TextDirection.ltr when index == widget.children.length - 1 =>
          _SegmentLocation.rightmost,
        TextDirection.rtl when index == widget.children.length - 1 =>
          _SegmentLocation.leftmost,
        TextDirection.rtl when index == 0 => _SegmentLocation.rightmost,
        TextDirection.ltr || TextDirection.rtl => _SegmentLocation.inbetween,
      };
      children.add(
        Semantics(
          button: true,
          onTap: () {
            if (widget.disabledChildren.contains(entry.key)) {
              return;
            }
            widget.onValueChanged(entry.key);
          },
          inMutuallyExclusiveGroup: true,
          selected: widget.groupValue == entry.key,
          child: MouseRegion(
            cursor: kIsWeb ? SystemMouseCursors.click : MouseCursor.defer,
            child: _Segment<T>(
              key: ValueKey<T>(entry.key),
              highlighted: isHighlighted,
              pressed: pressed == entry.key,
              isDragging: isThumbDragging,
              enabled: !widget.disabledChildren.contains(entry.key),
              segmentLocation: segmentLocation,
              child: entry.value,
            ),
          ),
        ),
      );

      index += 1;
      isPreviousSegmentHighlighted = isHighlighted;
    }

    assert((highlightedIndex == null) == (highlighted == null));

    switch (Directionality.of(context)) {
      case TextDirection.ltr:
        break;
      case TextDirection.rtl:
        children = children.reversed.toList(growable: false);
        if (highlightedIndex != null) {
          highlightedIndex = index - 1 - highlightedIndex;
        }
    }

    return UnconstrainedBox(
      constrainedAxis: Axis.horizontal,
      child: Container(
        clipBehavior: Clip.antiAlias,
        padding: widget.padding.resolve(Directionality.of(context)),
        decoration: ShapeDecoration(
          shape: RoundedSuperellipseBorder(
            borderRadius: const BorderRadius.all(_kCornerRadius),
          ),
          color: widget.backgroundColor,
        ),
        child: AnimatedBuilder(
          animation: thumbScaleAnimation,
          builder: (BuildContext context, Widget? child) {
            return _CommonTabBarRenderWidget<T>(
              proportionalWidth: widget.proportionalWidth,
              key: segmentedControlRenderWidgetKey,
              highlightedIndex: highlightedIndex,
              thumbColor: widget.thumbColor,
              thumbScale: thumbScaleAnimation.value,
              state: this,
              children: children,
            );
          },
        ),
      ),
    );
  }
}

class _Segment<T> extends StatefulWidget {
  const _Segment({
    required ValueKey<T> key,
    required this.child,
    required this.pressed,
    required this.highlighted,
    required this.isDragging,
    required this.enabled,
    required this.segmentLocation,
  }) : super(key: key);

  final Widget child;

  final bool pressed;
  final bool highlighted;
  final bool enabled;
  final _SegmentLocation segmentLocation;
  final bool isDragging;

  bool get shouldFadeoutContent => pressed && !highlighted && enabled;

  bool get shouldScaleContent =>
      pressed && highlighted && isDragging && enabled;

  @override
  _SegmentState<T> createState() => _SegmentState<T>();
}

class _SegmentState<T> extends State<_Segment<T>>
    with TickerProviderStateMixin<_Segment<T>> {
  late final AnimationController highlightPressScaleController;
  late Animation<double> highlightPressScaleAnimation;

  @override
  void initState() {
    super.initState();
    highlightPressScaleController = AnimationController(
      duration: _kOpacityAnimationDuration,
      value: widget.shouldScaleContent ? 1 : 0,
      vsync: this,
    );

    highlightPressScaleAnimation = highlightPressScaleController.drive(
      Tween<double>(begin: 1.0, end: _kMinThumbScale),
    );
  }

  @override
  void didUpdateWidget(_Segment<T> oldWidget) {
    super.didUpdateWidget(oldWidget);
    assert(oldWidget.key == widget.key);

    if (oldWidget.shouldScaleContent != widget.shouldScaleContent) {
      highlightPressScaleAnimation = highlightPressScaleController.drive(
        Tween<double>(
          begin: highlightPressScaleAnimation.value,
          end: widget.shouldScaleContent ? _kMinThumbScale : 1.0,
        ),
      );
      highlightPressScaleController.animateWith(
        _kThumbSpringAnimationSimulation,
      );
    }
  }

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

  @override
  Widget build(BuildContext context) {
    final Alignment scaleAlignment = switch (widget.segmentLocation) {
      _SegmentLocation.leftmost => Alignment.centerLeft,
      _SegmentLocation.rightmost => Alignment.centerRight,
      _SegmentLocation.inbetween => Alignment.center,
    };

    return MetaData(
      behavior: HitTestBehavior.opaque,
      child: IndexedStack(
        alignment: Alignment.center,
        children: <Widget>[
          AnimatedOpacity(
            opacity: widget.shouldFadeoutContent
                ? _kContentPressedMinOpacity
                : 1,
            duration: _kOpacityAnimationDuration,
            curve: Curves.ease,
            child: AnimatedDefaultTextStyle(
              style: DefaultTextStyle.of(context).style.merge(
                TextStyle(
                  fontWeight: widget.highlighted
                      ? _kHighlightedFontWeight
                      : _kFontWeight,
                  fontSize: _kFontSize,
                  color: widget.enabled ? null : _kDisabledContentColor,
                ),
              ),
              duration: _kHighlightAnimationDuration,
              curve: Curves.ease,
              child: ScaleTransition(
                alignment: scaleAlignment,
                scale: highlightPressScaleAnimation,
                child: widget.child,
              ),
            ),
          ),
          DefaultTextStyle.merge(
            style: const TextStyle(
              fontWeight: _kHighlightedFontWeight,
              fontSize: _kFontSize,
            ),
            child: widget.child,
          ),
        ],
      ),
    );
  }
}

class _SegmentSeparator extends StatefulWidget {
  const _SegmentSeparator({
    required ValueKey<int> key,
    required this.highlighted,
  }) : super(key: key);

  final bool highlighted;

  @override
  _SegmentSeparatorState createState() => _SegmentSeparatorState();
}

class _SegmentSeparatorState extends State<_SegmentSeparator>
    with TickerProviderStateMixin<_SegmentSeparator> {
  late final AnimationController separatorOpacityController;

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

    separatorOpacityController = AnimationController(
      duration: _kSpringAnimationDuration,
      value: widget.highlighted ? 0 : 1,
      vsync: this,
    );
  }

  @override
  void didUpdateWidget(_SegmentSeparator oldWidget) {
    super.didUpdateWidget(oldWidget);
    assert(oldWidget.key == widget.key);

    if (oldWidget.highlighted != widget.highlighted) {
      separatorOpacityController.animateTo(
        widget.highlighted ? 0 : 1,
        duration: _kSpringAnimationDuration,
        curve: Curves.ease,
      );
    }
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: separatorOpacityController,
      child: const SizedBox(width: _kSeparatorWidth),
      builder: (BuildContext context, Widget? child) {
        return Padding(
          padding: _kSeparatorInset,
          child: DecoratedBox(
            decoration: BoxDecoration(color: Colors.transparent),
            child: child,
          ),
        );
      },
    );
  }
}

class _CommonTabBarRenderWidget<T extends Object>
    extends MultiChildRenderObjectWidget {
  const _CommonTabBarRenderWidget({
    super.key,
    super.children,
    required this.highlightedIndex,
    required this.thumbColor,
    required this.thumbScale,
    required this.state,
    required this.proportionalWidth,
  });

  final int? highlightedIndex;
  final Color thumbColor;
  final double thumbScale;
  final bool proportionalWidth;
  final _CommonTabBarState<T> state;

  @override
  RenderObject createRenderObject(BuildContext context) {
    return _RenderSegmentedControl<T>(
      highlightedIndex: highlightedIndex,
      thumbColor: thumbColor,
      thumbScale: thumbScale,
      proportionalWidth: proportionalWidth,
      state: state,
    );
  }

  @override
  void updateRenderObject(
    BuildContext context,
    _RenderSegmentedControl<T> renderObject,
  ) {
    assert(renderObject.state == state);
    renderObject
      ..thumbColor = thumbColor
      ..thumbScale = thumbScale
      ..highlightedIndex = highlightedIndex
      ..proportionalWidth = proportionalWidth;
  }
}

class _SegmentedControlContainerBoxParentData
    extends ContainerBoxParentData<RenderBox> {}

enum _SegmentLocation { leftmost, rightmost, inbetween }

class _RenderSegmentedControl<T extends Object> extends RenderBox
    with
        ContainerRenderObjectMixin<
          RenderBox,
          ContainerBoxParentData<RenderBox>
        >,
        RenderBoxContainerDefaultsMixin<
          RenderBox,
          ContainerBoxParentData<RenderBox>
        > {
  _RenderSegmentedControl({
    required int? highlightedIndex,
    required Color thumbColor,
    required double thumbScale,
    required bool proportionalWidth,
    required this.state,
  }) : _highlightedIndex = highlightedIndex,
       _thumbColor = thumbColor,
       _thumbScale = thumbScale,
       _proportionalWidth = proportionalWidth;

  final _CommonTabBarState<T> state;

  Rect? currentThumbRect;

  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
    state.thumbController.addListener(markNeedsPaint);
  }

  @override
  void detach() {
    state.thumbController.removeListener(markNeedsPaint);
    super.detach();
  }

  double get thumbScale => _thumbScale;
  double _thumbScale;

  set thumbScale(double value) {
    if (_thumbScale == value) {
      return;
    }

    _thumbScale = value;
    if (state.highlighted != null) {
      markNeedsPaint();
    }
  }

  int? get highlightedIndex => _highlightedIndex;
  int? _highlightedIndex;

  set highlightedIndex(int? value) {
    if (_highlightedIndex == value) {
      return;
    }

    _highlightedIndex = value;
    markNeedsPaint();
  }

  Color get thumbColor => _thumbColor;
  Color _thumbColor;

  set thumbColor(Color value) {
    if (_thumbColor == value) {
      return;
    }
    _thumbColor = value;
    markNeedsPaint();
  }

  bool get proportionalWidth => _proportionalWidth;
  bool _proportionalWidth;

  set proportionalWidth(bool value) {
    if (_proportionalWidth == value) {
      return;
    }
    _proportionalWidth = value;
    markNeedsLayout();
  }

  @override
  void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
    assert(debugHandleEvent(event, entry));
    if (event is PointerDownEvent && !state.isThumbDragging) {
      state.tap.addPointer(event);
      state.longPress.addPointer(event);
      state.drag.addPointer(event);
    }
  }

  double get separatorWidth => _kSeparatorInset.horizontal + _kSeparatorWidth;

  double get totalSeparatorWidth => separatorWidth * (childCount ~/ 2);

  int getClosestSegmentIndex(double dx) {
    int index = 0;
    RenderBox? child = firstChild;
    while (child != null) {
      final _SegmentedControlContainerBoxParentData childParentData =
          child.parentData! as _SegmentedControlContainerBoxParentData;
      final double clampX = clampDouble(
        dx,
        childParentData.offset.dx,
        child.size.width + childParentData.offset.dx,
      );

      if (dx <= clampX) {
        break;
      }

      index++;
      child = nonSeparatorChildAfter(child);
    }

    final int segmentCount = childCount ~/ 2 + 1;
    return min(index, segmentCount - 1);
  }

  RenderBox? nonSeparatorChildAfter(RenderBox child) {
    final RenderBox? nextChild = childAfter(child);
    return nextChild == null ? null : childAfter(nextChild);
  }

  @override
  double computeMinIntrinsicWidth(double height) {
    final int childCount = this.childCount ~/ 2 + 1;
    RenderBox? child = firstChild;
    double maxMinChildWidth = 0;
    while (child != null) {
      final double childWidth = child.getMinIntrinsicWidth(height);
      maxMinChildWidth = math.max(maxMinChildWidth, childWidth);
      child = nonSeparatorChildAfter(child);
    }
    return (maxMinChildWidth + 2 * _kSegmentMinPadding) * childCount +
        totalSeparatorWidth;
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    final int childCount = this.childCount ~/ 2 + 1;
    RenderBox? child = firstChild;
    double maxMaxChildWidth = 0;
    while (child != null) {
      final double childWidth = child.getMaxIntrinsicWidth(height);
      maxMaxChildWidth = math.max(maxMaxChildWidth, childWidth);
      child = nonSeparatorChildAfter(child);
    }
    return (maxMaxChildWidth + 2 * _kSegmentMinPadding) * childCount +
        totalSeparatorWidth;
  }

  @override
  double computeMinIntrinsicHeight(double width) {
    RenderBox? child = firstChild;
    double maxMinChildHeight = _kMinSegmentedControlHeight;
    while (child != null) {
      final double childHeight = child.getMinIntrinsicHeight(width);
      maxMinChildHeight = math.max(maxMinChildHeight, childHeight);
      child = nonSeparatorChildAfter(child);
    }
    return maxMinChildHeight;
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    RenderBox? child = firstChild;
    double maxMaxChildHeight = _kMinSegmentedControlHeight;
    while (child != null) {
      final double childHeight = child.getMaxIntrinsicHeight(width);
      maxMaxChildHeight = math.max(maxMaxChildHeight, childHeight);
      child = nonSeparatorChildAfter(child);
    }
    return maxMaxChildHeight;
  }

  @override
  double? computeDistanceToActualBaseline(TextBaseline baseline) {
    return defaultComputeDistanceToHighestActualBaseline(baseline);
  }

  @override
  void setupParentData(RenderBox child) {
    if (child.parentData is! _SegmentedControlContainerBoxParentData) {
      child.parentData = _SegmentedControlContainerBoxParentData();
    }
  }

  double _getMaxChildHeight(BoxConstraints constraints, double childWidth) {
    double maxHeight = _kMinSegmentedControlHeight;
    RenderBox? child = firstChild;
    while (child != null) {
      final double boxHeight = child.getMaxIntrinsicHeight(childWidth);
      maxHeight = math.max(maxHeight, boxHeight);
      child = nonSeparatorChildAfter(child);
    }
    return maxHeight;
  }

  double _getMaxChildWidth(BoxConstraints constraints) {
    final int childCount = this.childCount ~/ 2 + 1;
    double childWidth =
        (constraints.minWidth - totalSeparatorWidth) / childCount;
    RenderBox? child = firstChild;
    while (child != null) {
      childWidth = math.max(
        childWidth,
        child.getMaxIntrinsicWidth(double.infinity) + 2 * _kSegmentMinPadding,
      );
      child = nonSeparatorChildAfter(child);
    }
    return math.min(
      childWidth,
      (constraints.maxWidth - totalSeparatorWidth) / childCount,
    );
  }

  List<double> _getChildWidths(BoxConstraints constraints) {
    if (!proportionalWidth) {
      final double maxChildWidth = _getMaxChildWidth(constraints);
      final int segmentCount = childCount ~/ 2 + 1;
      return List<double>.filled(segmentCount, maxChildWidth);
    }

    final List<double> segmentWidths = <double>[];
    RenderBox? child = firstChild;
    while (child != null) {
      final double childWidth =
          child.getMaxIntrinsicWidth(double.infinity) + 2 * _kSegmentMinPadding;
      child = nonSeparatorChildAfter(child);
      segmentWidths.add(childWidth);
    }

    final double totalWidth = segmentWidths.sum;
    final double allowedMaxWidth = constraints.maxWidth - totalSeparatorWidth;
    final double allowedMinWidth = constraints.minWidth - totalSeparatorWidth;

    final double scale =
        clampDouble(totalWidth, allowedMinWidth, allowedMaxWidth) / totalWidth;
    if (scale != 1) {
      for (int i = 0; i < segmentWidths.length; i++) {
        segmentWidths[i] = segmentWidths[i] * scale;
      }
    }
    return segmentWidths;
  }

  Size _computeOverallSize(BoxConstraints constraints) {
    final double maxChildHeight = _getMaxChildHeight(
      constraints,
      constraints.maxWidth,
    );
    return constraints.constrain(
      Size(
        _getChildWidths(constraints).sum + totalSeparatorWidth,
        maxChildHeight,
      ),
    );
  }

  @override
  double? computeDryBaseline(
    covariant BoxConstraints constraints,
    TextBaseline baseline,
  ) {
    final List<double> segmentWidths = _getChildWidths(constraints);
    final double childHeight = _getMaxChildHeight(
      constraints,
      constraints.maxWidth,
    );

    int index = 0;
    BaselineOffset baselineOffset = BaselineOffset.noBaseline;
    RenderBox? child = firstChild;
    while (child != null) {
      final BoxConstraints childConstraints = BoxConstraints.tight(
        Size(segmentWidths[index], childHeight),
      );
      baselineOffset = baselineOffset.minOf(
        BaselineOffset(child.getDryBaseline(childConstraints, baseline)),
      );

      child = nonSeparatorChildAfter(child);
      index++;
    }

    return baselineOffset.offset;
  }

  @override
  Size computeDryLayout(BoxConstraints constraints) {
    return _computeOverallSize(constraints);
  }

  @override
  void performLayout() {
    final BoxConstraints constraints = this.constraints;
    final List<double> segmentWidths = _getChildWidths(constraints);

    final double childHeight = _getMaxChildHeight(constraints, double.infinity);
    final BoxConstraints separatorConstraints = BoxConstraints(
      minHeight: childHeight,
      maxHeight: childHeight,
    );
    RenderBox? child = firstChild;
    int index = 0;
    double start = 0;
    while (child != null) {
      final BoxConstraints childConstraints = BoxConstraints.tight(
        Size(segmentWidths[index ~/ 2], childHeight),
      );
      child.layout(
        index.isEven ? childConstraints : separatorConstraints,
        parentUsesSize: true,
      );
      final _SegmentedControlContainerBoxParentData childParentData =
          child.parentData! as _SegmentedControlContainerBoxParentData;
      final Offset childOffset = Offset(start, 0);
      childParentData.offset = childOffset;
      start += child.size.width;
      assert(
        index.isEven ||
            child.size.width == _kSeparatorWidth + _kSeparatorInset.horizontal,
        '${child.size.width} != ${_kSeparatorWidth + _kSeparatorInset.horizontal}',
      );
      child = childAfter(child);
      index += 1;
    }
    size = _computeOverallSize(constraints);
  }

  Rect? moveThumbRectInBound(Rect? thumbRect, List<RenderBox> children) {
    assert(hasSize);
    assert(children.length >= 2);
    if (thumbRect == null) {
      return null;
    }

    final Offset firstChildOffset =
        (children.first.parentData! as _SegmentedControlContainerBoxParentData)
            .offset;
    final double leftMost = firstChildOffset.dx;
    final double rightMost =
        (children.last.parentData! as _SegmentedControlContainerBoxParentData)
            .offset
            .dx +
        children.last.size.width;
    assert(rightMost > leftMost);
    return Rect.fromLTRB(
      math.max(thumbRect.left, leftMost - _kThumbInsets.left),
      firstChildOffset.dy - _kThumbInsets.top,
      math.min(thumbRect.right, rightMost + _kThumbInsets.right),
      firstChildOffset.dy + children.first.size.height + _kThumbInsets.bottom,
    );
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    final List<RenderBox> children = getChildrenAsList();
    for (int index = 1; index < childCount; index += 2) {
      _paintSeparator(context, offset, children[index]);
    }

    final int? highlightedChildIndex = highlightedIndex;
    if (highlightedChildIndex != null) {
      final RenderBox selectedChild = children[highlightedChildIndex * 2];

      final _SegmentedControlContainerBoxParentData childParentData =
          selectedChild.parentData! as _SegmentedControlContainerBoxParentData;
      final Rect newThumbRect = _kThumbInsets.inflateRect(
        childParentData.offset & selectedChild.size,
      );
      if (state.thumbController.isAnimating) {
        final Animatable<Rect?>? thumbTween = state.thumbAnimatable;
        if (thumbTween == null) {
          final Rect startingRect =
              moveThumbRectInBound(currentThumbRect, children) ?? newThumbRect;
          state.thumbAnimatable = RectTween(
            begin: startingRect,
            end: newThumbRect,
          );
        } else if (newThumbRect != thumbTween.transform(1)) {
          final Rect startingRect =
              moveThumbRectInBound(currentThumbRect, children) ?? newThumbRect;
          state.thumbAnimatable = RectTween(
            begin: startingRect,
            end: newThumbRect,
          ).chain(CurveTween(curve: Interval(state.thumbController.value, 1)));
        }
      } else {
        state.thumbAnimatable = null;
      }

      final Rect unscaledThumbRect =
          state.thumbAnimatable?.evaluate(state.thumbController) ??
          newThumbRect;
      currentThumbRect = unscaledThumbRect;

      final _SegmentLocation childLocation;
      if (highlightedChildIndex == 0) {
        childLocation = _SegmentLocation.leftmost;
      } else if (highlightedChildIndex == children.length ~/ 2) {
        childLocation = _SegmentLocation.rightmost;
      } else {
        childLocation = _SegmentLocation.inbetween;
      }
      final double delta = switch (childLocation) {
        _SegmentLocation.leftmost =>
          unscaledThumbRect.width - unscaledThumbRect.width * thumbScale,
        _SegmentLocation.rightmost =>
          unscaledThumbRect.width * thumbScale - unscaledThumbRect.width,
        _SegmentLocation.inbetween => 0,
      };
      final Rect thumbRect = Rect.fromCenter(
        center: unscaledThumbRect.center - Offset(delta / 2, 0),
        width: unscaledThumbRect.width * thumbScale,
        height: unscaledThumbRect.height * thumbScale,
      );

      _paintThumb(context, offset, thumbRect);
    } else {
      currentThumbRect = null;
    }

    for (int index = 0; index < children.length; index += 2) {
      _paintChild(context, offset, children[index]);
    }
  }

  final Paint separatorPaint = Paint();

  void _paintSeparator(
    PaintingContext context,
    Offset offset,
    RenderBox child,
  ) {
    final _SegmentedControlContainerBoxParentData childParentData =
        child.parentData! as _SegmentedControlContainerBoxParentData;
    context.paintChild(child, offset + childParentData.offset);
  }

  void _paintChild(PaintingContext context, Offset offset, RenderBox child) {
    final _SegmentedControlContainerBoxParentData childParentData =
        child.parentData! as _SegmentedControlContainerBoxParentData;
    context.paintChild(child, childParentData.offset + offset);
  }

  void _paintThumb(PaintingContext context, Offset offset, Rect thumbRect) {
    final RSuperellipse thumbRSuperellipse = RSuperellipse.fromRectAndRadius(
      thumbRect.shift(offset),
      _kThumbRadius,
    );

    context.canvas.drawRSuperellipse(
      thumbRSuperellipse.inflate(0.5),
      Paint()..color = const Color(0x0A000000),
    );

    context.canvas.drawRSuperellipse(
      thumbRSuperellipse,
      Paint()..color = thumbColor,
    );
  }

  @override
  bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
    RenderBox? child = lastChild;
    while (child != null) {
      final _SegmentedControlContainerBoxParentData childParentData =
          child.parentData! as _SegmentedControlContainerBoxParentData;
      if ((childParentData.offset & child.size).contains(position)) {
        return result.addWithPaintOffset(
          offset: childParentData.offset,
          position: position,
          hitTest: (BoxHitTestResult result, Offset localOffset) {
            assert(localOffset == position - childParentData.offset);
            return child!.hitTest(result, position: localOffset);
          },
        );
      }
      child = childParentData.previousSibling;
    }
    return false;
  }
}
