// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:math' as math;

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

import 'colors.dart';

// Extracted from https://developer.apple.com/design/resources/.

// Minimum padding from edges of the segmented control to edges of
// encompassing widget.
const EdgeInsetsGeometry _kHorizontalItemPadding = EdgeInsets.symmetric(vertical: 2, horizontal: 3);

// The corner radius of the thumb.
const Radius _kThumbRadius = Radius.circular(6.93);
// The amount of space by which to expand the thumb from the size of the currently
// selected child.
const EdgeInsets _kThumbInsets = EdgeInsets.symmetric(horizontal: 1);

// Minimum height of the segmented control.
const double _kMinSegmentedControlHeight = 28.0;

const Color _kSeparatorColor = Color(0x4D8E8E93);

const CupertinoDynamicColor _kThumbColor = CupertinoDynamicColor.withBrightness(
  color: Color(0xFFFFFFFF),
  darkColor: Color(0xFF636366),
);

// The amount of space by which to inset each separator.
const EdgeInsets _kSeparatorInset = EdgeInsets.symmetric(vertical: 6);
const double _kSeparatorWidth = 1;
const Radius _kSeparatorRadius = Radius.circular(_kSeparatorWidth/2);

// The minimum scale factor of the thumb, when being pressed on for a sufficient
// amount of time.
const double _kMinThumbScale = 0.95;

// The minimum horizontal distance between the edges of the separator and the
// closest child.
const double _kSegmentMinPadding = 9.25;

// The threshold value used in hasDraggedTooFar, for checking against the square
// L2 distance from the location of the current drag pointer, to the closest
// vertex of the CupertinoSlidingSegmentedControl's Rect.
//
// Both the mechanism and the value are speculated.
const double _kTouchYDistanceThreshold = 50.0 * 50.0;

// The corner radius of the segmented control.
//
// Inspected from iOS 13.2 simulator.
const double _kCornerRadius = 8;

// The spring animation used when the thumb changes its rect.
final SpringSimulation _kThumbSpringAnimationSimulation = SpringSimulation(
  const SpringDescription(mass: 1, stiffness: 503.551, damping: 44.8799),
  0,
  1,
  0, // Every time a new spring animation starts the previous animation stops.
);

const Duration _kSpringAnimationDuration = Duration(milliseconds: 412);

const Duration _kOpacityAnimationDuration = Duration(milliseconds: 470);

const Duration _kHighlightAnimationDuration = Duration(milliseconds: 200);

class _FontWeightTween extends Tween<FontWeight> {
  _FontWeightTween({ required FontWeight begin, required FontWeight end }) : super(begin: begin, end: end);

  @override
  FontWeight lerp(double t) => FontWeight.lerp(begin, end, t)!;
}

/// An iOS 13 style segmented control.
///
/// Displays the widgets provided in the [Map] of [children] in a horizontal list.
/// Used to select between a number of mutually exclusive options. When one option
/// in the segmented control is selected, the other options in the segmented
/// control cease to be selected.
///
/// A segmented control can feature any [Widget] as one of the values in its
/// [Map] of [children]. The type T is the type of the [Map] keys used to identify
/// each widget and determine which widget is selected. As required by the [Map]
/// class, keys must be of consistent types and must be comparable. The [children]
/// argument must be an ordered [Map] such as a [LinkedHashMap], the ordering of
/// the keys will determine the order of the widgets in the segmented control.
///
/// When the state of the segmented control changes, the widget calls the
/// [onValueChanged] callback. The map key associated with the newly selected
/// widget is returned in the [onValueChanged] callback. Typically, widgets
/// that use a segmented control will listen for the [onValueChanged] callback
/// and rebuild the segmented control with a new [groupValue] to update which
/// option is currently selected.
///
/// The [children] will be displayed in the order of the keys in the [Map].
/// The height of the segmented control is determined by the height of the
/// tallest widget provided as a value in the [Map] of [children].
/// The width of each child in the segmented control will be equal to the width
/// of widest child, unless the combined width of the children is wider than
/// the available horizontal space. In this case, the available horizontal space
/// is divided by the number of provided [children] to determine the width of
/// each widget. The selection area for each of the widgets in the [Map] of
/// [children] will then be expanded to fill the calculated space, so each
/// widget will appear to have the same dimensions.
///
/// A segmented control may optionally be created with custom colors. The
/// [thumbColor], [backgroundColor] arguments can be used to override the segmented
/// control's colors from its defaults.
///
/// See also:
///
///  * [CupertinoSlidingSegmentedControl], a segmented control widget in the
///    style introduced in iOS 13.
///  * <https://developer.apple.com/design/human-interface-guidelines/ios/controls/segmented-controls/>
class CupertinoSlidingSegmentedControl<T> extends StatefulWidget {
  /// Creates an iOS-style segmented control bar.
  ///
  /// The [children] and [onValueChanged] arguments must not be null. The
  /// [children] argument must be an ordered [Map] such as a [LinkedHashMap].
  /// Further, the length of the [children] list must be greater than one.
  ///
  /// Each widget value in the map of [children] must have an associated key
  /// that uniquely identifies this widget. This key is what will be returned
  /// in the [onValueChanged] callback when a new value from the [children] map
  /// is selected.
  ///
  /// The [groupValue] is the currently selected value for the segmented control.
  /// If no [groupValue] is provided, or the [groupValue] is null, no widget will
  /// appear as selected. The [groupValue] must be either null or one of the keys
  /// in the [children] map.
  CupertinoSlidingSegmentedControl({
    Key? key,
    required this.children,
    required this.onValueChanged,
    this.groupValue,
    this.thumbColor = _kThumbColor,
    this.padding = _kHorizontalItemPadding,
    this.backgroundColor = CupertinoColors.tertiarySystemFill,
  }) : assert(children != null),
       assert(children.length >= 2),
       assert(padding != null),
       assert(onValueChanged != null),
       assert(
         groupValue == null || children.keys.contains(groupValue),
         'The groupValue must be either null or one of the keys in the children map.',
       ),
       super(key: key);

  /// The identifying keys and corresponding widget values in the
  /// segmented control.
  ///
  /// The map must have more than one entry.
  /// This attribute must be an ordered [Map] such as a [LinkedHashMap].
  final Map<T, Widget> children;

  /// The identifier of the widget that is currently selected.
  ///
  /// This must be one of the keys in the [Map] of [children].
  /// If this attribute is null, no widget will be initially selected.
  final T? groupValue;

  /// The callback that is called when a new option is tapped.
  ///
  /// This attribute must not be null.
  ///
  /// The segmented control passes the newly selected widget's associated key
  /// to the callback but does not actually change state until the parent
  /// widget rebuilds the segmented control with the new [groupValue].
  ///
  /// The callback provided to [onValueChanged] should update the state of
  /// the parent [StatefulWidget] using the [State.setState] method, so that
  /// the parent gets rebuilt; for example:
  ///
  /// {@tool snippet}
  ///
  /// ```dart
  /// class SegmentedControlExample extends StatefulWidget {
  ///   @override
  ///   State createState() => SegmentedControlExampleState();
  /// }
  ///
  /// class SegmentedControlExampleState extends State<SegmentedControlExample> {
  ///   final Map<int, Widget> children = const {
  ///     0: Text('Child 1'),
  ///     1: Text('Child 2'),
  ///   };
  ///
  ///   int currentValue;
  ///
  ///   @override
  ///   Widget build(BuildContext context) {
  ///     return Container(
  ///       child: CupertinoSlidingSegmentedControl<int>(
  ///         children: children,
  ///         onValueChanged: (int newValue) {
  ///           setState(() {
  ///             currentValue = newValue;
  ///           });
  ///         },
  ///         groupValue: currentValue,
  ///       ),
  ///     );
  ///   }
  /// }
  /// ```
  /// {@end-tool}
  final ValueChanged<T?> onValueChanged;

  /// The color used to paint the rounded rect behind the [children] and the separators.
  ///
  /// The default value is [CupertinoColors.tertiarySystemFill]. The background
  /// will not be painted if null is specified.
  final Color backgroundColor;

  /// The color used to paint the interior of the thumb that appears behind the
  /// currently selected item.
  ///
  /// The default value is a [CupertinoDynamicColor] that appears white in light
  /// mode and becomes a gray color in dark mode.
  final Color thumbColor;

  /// The amount of space by which to inset the [children].
  ///
  /// Must not be null. Defaults to EdgeInsets.symmetric(vertical: 2, horizontal: 3).
  final EdgeInsetsGeometry padding;

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

class _SegmentedControlState<T> extends State<CupertinoSlidingSegmentedControl<T>>
    with TickerProviderStateMixin<CupertinoSlidingSegmentedControl<T>> {

  final Map<T, AnimationController> _highlightControllers = <T, AnimationController>{};
  final Tween<FontWeight> _highlightTween = _FontWeightTween(begin: FontWeight.normal, end: FontWeight.w500);

  final Map<T, AnimationController> _pressControllers = <T, AnimationController>{};
  final Tween<double> _pressTween = Tween<double>(begin: 1, end: 0.2);

  late List<T> keys;

  late AnimationController thumbController;
  late AnimationController separatorOpacityController;
  late AnimationController thumbScaleController;

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

  AnimationController _createHighlightAnimationController({ bool isCompleted = false }) {
    return AnimationController(
      duration: _kHighlightAnimationDuration,
      value: isCompleted ? 1 : 0,
      vsync: this,
    );
  }

  AnimationController _createFadeoutAnimationController() {
    return AnimationController(
      duration: _kOpacityAnimationDuration,
      vsync: this,
    );
  }

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

    final GestureArenaTeam team = GestureArenaTeam();
    // If the long press or horizontal drag recognizer gets accepted, we know for
    // sure the gesture is meant for the segmented control. Hand everything to
    // the drag gesture recognizer.
    longPress.team = team;
    drag.team = team;
    team.captain = drag;

    _highlighted = widget.groupValue;

    thumbController = AnimationController(
      duration: _kSpringAnimationDuration,
      value: 0,
      vsync: this,
    );

    thumbScaleController = AnimationController(
      duration: _kSpringAnimationDuration,
      value: 1,
      vsync: this,
    );

    separatorOpacityController = AnimationController(
      duration: _kSpringAnimationDuration,
      value: 0,
      vsync: this,
    );

    for (final T currentKey in widget.children.keys) {
      _highlightControllers[currentKey] = _createHighlightAnimationController(
        isCompleted: currentKey == widget.groupValue,  // Highlight the current selection.
      );
      _pressControllers[currentKey] = _createFadeoutAnimationController();
    }
  }

  @override
  void didUpdateWidget(CupertinoSlidingSegmentedControl<T> oldWidget) {
    super.didUpdateWidget(oldWidget);

    // Update animation controllers.
    for (final T oldKey in oldWidget.children.keys) {
      if (!widget.children.containsKey(oldKey)) {
        _highlightControllers[oldKey]!.dispose();
        _pressControllers[oldKey]!.dispose();

        _highlightControllers.remove(oldKey);
        _pressControllers.remove(oldKey);
      }
    }

    for (final T newKey in widget.children.keys) {
      if (!_highlightControllers.keys.contains(newKey)) {
        _highlightControllers[newKey] = _createHighlightAnimationController();
        _pressControllers[newKey] = _createFadeoutAnimationController();
      }
    }

    highlighted = widget.groupValue;
  }

  @override
  void dispose() {
    for (final AnimationController animationController in _highlightControllers.values) {
      animationController.dispose();
    }

    for (final AnimationController animationController in _pressControllers.values) {
      animationController.dispose();
    }

    thumbScaleController.dispose();
    thumbController.dispose();
    separatorOpacityController.dispose();

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

    super.dispose();
  }

  // Play highlight animation for the child located at _highlightControllers[at].
  void _animateHighlightController({ T? at, required bool forward }) {
    if (at == null)
      return;
    final AnimationController? controller = _highlightControllers[at];
    assert(!forward || controller != null);
    controller?.animateTo(forward ? 1 : 0, duration: _kHighlightAnimationDuration, curve: Curves.ease);
  }

  T? _highlighted;
  set highlighted(T? newValue) {
    if (_highlighted == newValue)
      return;
    _animateHighlightController(at: newValue, forward: true);
    _animateHighlightController(at: _highlighted, forward: false);
    _highlighted = newValue;
  }

  T? _pressed;
  set pressed(T? newValue) {
    if (_pressed == newValue)
      return;

    if (_pressed != null) {
      _pressControllers[_pressed]?.animateTo(0, duration: _kOpacityAnimationDuration, curve: Curves.ease);
    }
    if (newValue != _highlighted && newValue != null) {
      _pressControllers[newValue]!.animateTo(1, duration: _kOpacityAnimationDuration, curve: Curves.ease);
    }
    _pressed = newValue;
  }

  void didChangeSelectedViaGesture() {
    widget.onValueChanged(_highlighted);
  }

  T? indexToKey(int? index) => index == null ? null : keys[index];

  @override
  Widget build(BuildContext context) {
    debugCheckHasDirectionality(context);

    switch (Directionality.of(context)!) {
      case TextDirection.ltr:
        keys = widget.children.keys.toList(growable: false);
        break;
      case TextDirection.rtl:
        keys = widget.children.keys.toList().reversed.toList(growable: false);
        break;
    }

    return AnimatedBuilder(
      animation: Listenable.merge(<Listenable>[
        ..._highlightControllers.values,
        ..._pressControllers.values,
      ]),
      builder: (BuildContext context, Widget? child) {
        final List<Widget> children = <Widget>[];
        for (final T currentKey in keys) {
          final TextStyle textStyle = DefaultTextStyle.of(context).style.copyWith(
            fontWeight: _highlightTween.evaluate(_highlightControllers[currentKey]!),
          );

          final Widget child = DefaultTextStyle(
            style: textStyle,
            child: Semantics(
              button: true,
              onTap: () { widget.onValueChanged(currentKey); },
              inMutuallyExclusiveGroup: true,
              selected: widget.groupValue == currentKey,
              child: Opacity(
                opacity: _pressTween.evaluate(_pressControllers[currentKey]!),
                // Expand the hitTest area to be as large as the Opacity widget.
                child: MetaData(
                  behavior: HitTestBehavior.opaque,
                  child: Center(child: widget.children[currentKey]),
                ),
              ),
            ),
          );

          children.add(child);
        }

        final int? selectedIndex = widget.groupValue == null ? null : keys.indexOf(widget.groupValue as T);

        final Widget box = _SegmentedControlRenderWidget<T>(
          children: children,
          selectedIndex: selectedIndex,
          thumbColor: CupertinoDynamicColor.resolve(widget.thumbColor, context),
          state: this,
        );

        return UnconstrainedBox(
          constrainedAxis: Axis.horizontal,
          child: Container(
            padding: widget.padding.resolve(Directionality.of(context)),
            decoration: BoxDecoration(
              borderRadius: const BorderRadius.all(Radius.circular(_kCornerRadius)),
              color: CupertinoDynamicColor.resolve(widget.backgroundColor, context),
            ),
            child: box,
          ),
        );
      },
    );
  }
}

class _SegmentedControlRenderWidget<T> extends MultiChildRenderObjectWidget {
  _SegmentedControlRenderWidget({
    Key? key,
    List<Widget> children = const <Widget>[],
    required this.selectedIndex,
    required this.thumbColor,
    required this.state,
  }) : super(key: key, children: children);

  final int? selectedIndex;
  final Color? thumbColor;
  final _SegmentedControlState<T> state;

  @override
  RenderObject createRenderObject(BuildContext context) {
    return _RenderSegmentedControl<T>(
      selectedIndex: selectedIndex,
      thumbColor: CupertinoDynamicColor.resolve(thumbColor, context),
      state: state,
    );
  }

  @override
  void updateRenderObject(BuildContext context, _RenderSegmentedControl<T> renderObject) {
    renderObject
      ..thumbColor = CupertinoDynamicColor.resolve(thumbColor, context)
      ..guardedSetHighlightedIndex(selectedIndex);
  }
}

class _ChildAnimationManifest {
  _ChildAnimationManifest({
    this.opacity = 1,
    required this.separatorOpacity,
  }) : assert(separatorOpacity != null),
       assert(opacity != null),
       separatorTween = Tween<double>(begin: separatorOpacity, end: separatorOpacity),
       opacityTween = Tween<double>(begin: opacity, end: opacity);

  double opacity;
  Tween<double> opacityTween;
  double separatorOpacity;
  Tween<double> separatorTween;
}

class _SegmentedControlContainerBoxParentData extends ContainerBoxParentData<RenderBox> { }

// The behavior of a UISegmentedControl as observed on iOS 13.1:
//
// 1. Tap up inside events will set the current selected index to the index of the
//    segment at the tap up location instantaneously (there might be animation but
//    the index change seems to happen before animation finishes), unless the tap
//    down event from the same touch event didn't happen within the segmented
//    control, in which case the touch event will be ignored entirely (will be
//    referring to these touch events as invalid touch events below).
//
// 2. A valid tap up event will also trigger the sliding CASpringAnimation (even
//    when it lands on the current segment), starting from the current `frame`
//    of the thumb. The previous sliding animation, if still playing, will be
//    removed and its velocity reset to 0. The sliding animation has a fixed
//    duration, regardless of the distance or transform.
//
// 3. When the sliding animation plays two other animations take place. In one animation
//    the content of the current segment gradually becomes "highlighted", turning the
//    font weight to semibold (CABasicAnimation, timingFunction = default, duration = 0.2).
//    The other is the separator fadein/fadeout animation.
//
// 4. A tap down event on the segment pointed to by the current selected
//    index will trigger a CABasicAnimation that shrinks the thumb to 95% of its
//    original size, even if the sliding animation is still playing. The
///   corresponding tap up event inverts the process (eyeballed).
//
// 5. A tap down event on other segments will trigger a CABasicAnimation
//    (timingFunction = default, duration = 0.47.) that fades out the content,
//    eventually reducing the alpha of that segment to 20% unless interrupted by
//    a tap up event or the pointer moves out of the region (either outside of the
//    segmented control's vicinity or to a different segment). The reverse animation
//    has the same duration and timing function.
class _RenderSegmentedControl<T> extends RenderBox
    with ContainerRenderObjectMixin<RenderBox, ContainerBoxParentData<RenderBox>>,
        RenderBoxContainerDefaultsMixin<RenderBox, ContainerBoxParentData<RenderBox>> {
  _RenderSegmentedControl({
    required int? selectedIndex,
    required Color? thumbColor,
    required this.state,
  }) : _highlightedIndex = selectedIndex,
       _thumbColor = thumbColor,
       assert(state != null) {
         state.drag
          ..onDown = _onDown
          ..onUpdate = _onUpdate
          ..onEnd = _onEnd
          ..onCancel = _onCancel;

         state.tap.onTapUp = _onTapUp;
         // Empty callback to enable the long press recognizer.
         state.longPress.onLongPress = () { };
       }

  final _SegmentedControlState<T> state;

  Map<RenderBox, _ChildAnimationManifest>? _childAnimations = <RenderBox, _ChildAnimationManifest>{};

  // The current **Unscaled** Thumb Rect.
  Rect? currentThumbRect;

  Tween<Rect?>? _currentThumbTween;

  Tween<double> _thumbScaleTween = Tween<double>(begin: _kMinThumbScale, end: 1);
  double currentThumbScale = 1;

  // The current position of the active drag pointer.
  Offset? _localDragOffset;
  // Whether the current drag gesture started on a selected segment.
  bool? _startedOnSelectedSegment;

  @override
  void insert(RenderBox child, { RenderBox? after }) {
    super.insert(child, after: after);
    if (_childAnimations == null)
      return;

    assert(_childAnimations![child] == null);
    _childAnimations![child] = _ChildAnimationManifest(separatorOpacity: 1);
  }

  @override
  void remove(RenderBox child) {
    super.remove(child);
    _childAnimations?.remove(child);
  }

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

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

  // Indicates whether selectedIndex has changed and animations need to be updated.
  // when true some animation tweens will be updated in paint phase.
  bool _needsThumbAnimationUpdate = false;

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

    _needsThumbAnimationUpdate = true;
    _highlightedIndex = value;

    state.thumbController.animateWith(_kThumbSpringAnimationSimulation);

    state.separatorOpacityController.reset();
    state.separatorOpacityController.animateTo(
      1,
      duration: _kSpringAnimationDuration,
      curve: Curves.ease,
    );

    state.highlighted = state.indexToKey(value);
    markNeedsPaint();
    markNeedsSemanticsUpdate();
  }

  void guardedSetHighlightedIndex(int? value) {
    // Ignore set highlightedIndex when the user is dragging the thumb around.
    if (_startedOnSelectedSegment == true)
      return;
    highlightedIndex = value;
  }

  int? get pressedIndex => _pressedIndex;
  int? _pressedIndex;
  set pressedIndex(int? value) {
    if (_pressedIndex == value) {
      return;
    }

    assert(value == null || (value >= 0 && value < childCount));

    _pressedIndex = value;
    state.pressed = state.indexToKey(value);
  }

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

  double get totalSeparatorWidth => (_kSeparatorInset.horizontal + _kSeparatorWidth) * (childCount - 1);

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

  int? indexFromLocation(Offset location) {
    return childCount == 0
      ? null
      // This assumes all children have the same width.
      : (location.dx / (size.width / childCount))
          .floor()
          .clamp(0, childCount - 1);
  }

  void _onTapUp(TapUpDetails details) {
    highlightedIndex = indexFromLocation(details.localPosition);
    state.didChangeSelectedViaGesture();
  }

  void _onDown(DragDownDetails details) {
    assert(size.contains(details.localPosition));
    _localDragOffset = details.localPosition;
    final int? index = indexFromLocation(_localDragOffset!);
    _startedOnSelectedSegment = index == highlightedIndex;
    pressedIndex = index;

    if (_startedOnSelectedSegment!) {
      _playThumbScaleAnimation(isExpanding: false);
    }
  }

  void _onUpdate(DragUpdateDetails details) {
    _localDragOffset = details.localPosition;
    final int? newIndex = indexFromLocation(_localDragOffset!);

    if (_startedOnSelectedSegment!) {
      highlightedIndex = newIndex;
      pressedIndex = newIndex;
    } else {
      pressedIndex = _hasDraggedTooFar(details) ? null : newIndex;
    }
  }

  void _onEnd(DragEndDetails details) {
    if (_startedOnSelectedSegment!) {
      _playThumbScaleAnimation(isExpanding: true);
      state.didChangeSelectedViaGesture();
    }

    if (pressedIndex != null) {
      highlightedIndex = pressedIndex;
      state.didChangeSelectedViaGesture();
    }
    pressedIndex = null;
    _localDragOffset = null;
    _startedOnSelectedSegment = null;
  }

  void _onCancel() {
    if (_startedOnSelectedSegment!) {
      _playThumbScaleAnimation(isExpanding: true);
    }

    _localDragOffset = null;
    pressedIndex = null;
    _startedOnSelectedSegment = null;
  }

  void _playThumbScaleAnimation({ required bool isExpanding }) {
    assert(isExpanding != null);
    _thumbScaleTween = Tween<double>(begin: currentThumbScale, end: isExpanding ? 1 : _kMinThumbScale);
    state.thumbScaleController.animateWith(_kThumbSpringAnimationSimulation);
  }

  bool _hasDraggedTooFar(DragUpdateDetails details) {
    final Offset offCenter = details.localPosition - Offset(size.width/2, size.height/2);
    return math.pow(math.max(0, offCenter.dx.abs() - size.width/2), 2) + math.pow(math.max(0, offCenter.dy.abs() - size.height/2), 2) > _kTouchYDistanceThreshold;
  }

  @override
  double computeMinIntrinsicWidth(double height) {
    RenderBox? child = firstChild;
    double maxMinChildWidth = 0;
    while (child != null) {
      final _SegmentedControlContainerBoxParentData childParentData =
        child.parentData! as _SegmentedControlContainerBoxParentData;
      final double childWidth = child.getMinIntrinsicWidth(height);
      maxMinChildWidth = math.max(maxMinChildWidth, childWidth);
      child = childParentData.nextSibling;
    }
    return (maxMinChildWidth + 2 * _kSegmentMinPadding) * childCount + totalSeparatorWidth;
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    RenderBox? child = firstChild;
    double maxMaxChildWidth = 0;
    while (child != null) {
      final _SegmentedControlContainerBoxParentData childParentData =
        child.parentData! as _SegmentedControlContainerBoxParentData;
      final double childWidth = child.getMaxIntrinsicWidth(height);
      maxMaxChildWidth = math.max(maxMaxChildWidth, childWidth);
      child = childParentData.nextSibling;
    }
    return (maxMaxChildWidth + 2 * _kSegmentMinPadding) * childCount + totalSeparatorWidth;
  }

  @override
  double computeMinIntrinsicHeight(double width) {
    RenderBox? child = firstChild;
    double maxMinChildHeight = 0;
    while (child != null) {
      final _SegmentedControlContainerBoxParentData childParentData =
        child.parentData! as _SegmentedControlContainerBoxParentData;
      final double childHeight = child.getMinIntrinsicHeight(width);
      maxMinChildHeight = math.max(maxMinChildHeight, childHeight);
      child = childParentData.nextSibling;
    }
    return maxMinChildHeight;
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    RenderBox? child = firstChild;
    double maxMaxChildHeight = 0;
    while (child != null) {
      final _SegmentedControlContainerBoxParentData childParentData =
        child.parentData! as _SegmentedControlContainerBoxParentData;
      final double childHeight = child.getMaxIntrinsicHeight(width);
      maxMaxChildHeight = math.max(maxMaxChildHeight, childHeight);
      child = childParentData.nextSibling;
    }
    return maxMaxChildHeight;
  }

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

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

  @override
  void performLayout() {
    final BoxConstraints constraints = this.constraints;
    double childWidth = (constraints.minWidth - totalSeparatorWidth) / childCount;
    double maxHeight = _kMinSegmentedControlHeight;

    for (final RenderBox child in getChildrenAsList()) {
      childWidth = math.max(childWidth, child.getMaxIntrinsicWidth(double.infinity) + 2 * _kSegmentMinPadding);
    }

    childWidth = math.min(
      childWidth,
      (constraints.maxWidth - totalSeparatorWidth) / childCount,
    );

    RenderBox? child = firstChild;
    while (child != null) {
      final double boxHeight = child.getMaxIntrinsicHeight(childWidth);
      maxHeight = math.max(maxHeight, boxHeight);
      child = childAfter(child);
    }

    constraints.constrainHeight(maxHeight);

    final BoxConstraints childConstraints = BoxConstraints.tightFor(
      width: childWidth,
      height: maxHeight,
    );

    // Layout children.
    child = firstChild;
    while (child != null) {
      child.layout(childConstraints, parentUsesSize: true);
      child = childAfter(child);
    }

    double start = 0;
    child = firstChild;

    while (child != null) {
      final _SegmentedControlContainerBoxParentData childParentData =
        child.parentData! as _SegmentedControlContainerBoxParentData;
      final Offset childOffset = Offset(start, 0);
      childParentData.offset = childOffset;
      start += child.size.width + _kSeparatorWidth + _kSeparatorInset.horizontal;
      child = childAfter(child);
    }

    size = constraints.constrain(Size(childWidth * childCount + totalSeparatorWidth, maxHeight));
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    final List<RenderBox> children = getChildrenAsList();

    // Paint thumb if highlightedIndex is not null.
    if (highlightedIndex != null) {
      if (_childAnimations == null) {
        _childAnimations = <RenderBox, _ChildAnimationManifest> { };
        for (int i = 0; i < childCount - 1; i += 1) {
          // The separator associated with the last child will not be painted (unless
          // a new trailing segment is added), and its opacity will always be 1.
          final bool shouldFadeOut = i == highlightedIndex || i == highlightedIndex! - 1;
          final RenderBox child = children[i];
          _childAnimations![child] = _ChildAnimationManifest(separatorOpacity: shouldFadeOut ? 0 : 1);
        }
      }

      final RenderBox selectedChild = children[highlightedIndex!];

      final _SegmentedControlContainerBoxParentData childParentData =
        selectedChild.parentData! as _SegmentedControlContainerBoxParentData;
      final Rect unscaledThumbTargetRect = _kThumbInsets.inflateRect(childParentData.offset & selectedChild.size);

      // Update related Tweens before animation update phase.
      if (_needsThumbAnimationUpdate) {
        // Needs to ensure _currentThumbRect is valid.
        _currentThumbTween = RectTween(begin: currentThumbRect ?? unscaledThumbTargetRect, end: unscaledThumbTargetRect);

        for (int i = 0; i < childCount - 1; i += 1) {
          // The separator associated with the last child will not be painted (unless
          // a new segment is appended to the child list), and its opacity will always be 1.
          final bool shouldFadeOut = i == highlightedIndex || i == highlightedIndex! - 1;
          final RenderBox child = children[i];
          final _ChildAnimationManifest manifest = _childAnimations![child]!;
          assert(manifest != null);
          manifest.separatorTween = Tween<double>(
            begin: manifest.separatorOpacity,
            end: shouldFadeOut ? 0 : 1,
          );
        }

        _needsThumbAnimationUpdate = false;
      } else if (_currentThumbTween != null && unscaledThumbTargetRect != _currentThumbTween!.begin) {
        _currentThumbTween = RectTween(begin: _currentThumbTween!.begin, end: unscaledThumbTargetRect);
      }

      for (int index = 0; index < childCount - 1; index += 1) {
        _paintSeparator(context, offset, children[index]);
      }

      currentThumbRect = _currentThumbTween?.evaluate(state.thumbController)
                        ?? unscaledThumbTargetRect;

      currentThumbScale = _thumbScaleTween.evaluate(state.thumbScaleController);

      final Rect thumbRect = Rect.fromCenter(
        center: currentThumbRect!.center,
        width: currentThumbRect!.width * currentThumbScale,
        height: currentThumbRect!.height * currentThumbScale,
      );

      _paintThumb(context, offset, thumbRect);
    } else {
      // Reset all animations when there's no thumb.
      currentThumbRect = null;
      _childAnimations = null;

      for (int index = 0; index < childCount - 1; index += 1) {
        _paintSeparator(context, offset, children[index]);
      }
    }

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

  // Paint the separator to the right of the given child.
  void _paintSeparator(PaintingContext context, Offset offset, RenderBox child) {
    assert(child != null);
    final _SegmentedControlContainerBoxParentData childParentData =
      child.parentData! as _SegmentedControlContainerBoxParentData;

    final Paint paint = Paint();

    final _ChildAnimationManifest? manifest = _childAnimations == null ? null : _childAnimations![child];
    final double opacity = manifest?.separatorTween.evaluate(state.separatorOpacityController) ?? 1;
    manifest?.separatorOpacity = opacity;
    paint.color = _kSeparatorColor.withOpacity(_kSeparatorColor.opacity * opacity);

    final Rect childRect = (childParentData.offset + offset) & child.size;
    final Rect separatorRect = _kSeparatorInset.deflateRect(
      childRect.topRight & Size(_kSeparatorInset.horizontal + _kSeparatorWidth, child.size.height),
    );

    context.canvas.drawRRect(
      RRect.fromRectAndRadius(separatorRect, _kSeparatorRadius),
      paint,
    );
  }

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

  void _paintThumb(PaintingContext context, Offset offset, Rect thumbRect) {
    // Colors extracted from https://developer.apple.com/design/resources/.
    const List<BoxShadow> thumbShadow = <BoxShadow> [
      BoxShadow(
        color: Color(0x1F000000),
        offset: Offset(0, 3),
        blurRadius: 8,
      ),
      BoxShadow(
        color: Color(0x0A000000),
        offset: Offset(0, 3),
        blurRadius: 1,
      ),
    ];

    final RRect thumbRRect = RRect.fromRectAndRadius(thumbRect.shift(offset), _kThumbRadius);

    for (final BoxShadow shadow in thumbShadow) {
      context.canvas.drawRRect(thumbRRect.shift(shadow.offset), shadow.toPaint());
    }

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

    context.canvas.drawRRect(
      thumbRRect,
      Paint()..color = thumbColor!,
    );
  }

  @override
  bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
    assert(position != null);
    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;
  }
}
