import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

import 'shop_scroll_coordinator.dart';
import 'shop_scroll_position.dart';

/// 滑动控制器
class ShopScrollController extends ScrollController {
  final ShopScrollCoordinator coordinator;

  /// 为可滚动小部件创建一个控制器。
  /// `initialScrollOffset`和`keepScrollOffset`的值不能为null。
  ShopScrollController(
    this.coordinator, {
    double initialScrollOffset = 0.0,
    this.keepScrollOffset = true,
    this.debugLabel,
  })  : assert(initialScrollOffset != null),
        assert(keepScrollOffset != null),
        _initialScrollOffset = initialScrollOffset;

  /// 用于[offset]的初始值。
  /// 如果[keepScrollOffset]为false或尚未保存滚动偏移量，
  /// 则创建并附加到此控制器的新[ScrollPosition]对象的偏移量将初始化为该值。
  /// 默认为0.0。
  double get initialScrollOffset => _initialScrollOffset;
  final double _initialScrollOffset;

  /// 每次滚动完成时，请使用[PageStorage]保存当前滚动[offset]，如果重新创建了此控制器的可滚动内容，则将其还原。
  ///
  /// 如果将此属性设置为false，则永远不会保存滚动偏移量，并且始终使用[initialScrollOffset]
  /// 来初始化滚动偏移量。 如果为true（默认值），则第一次创建控制器的可滚动对象时将使用初始
  /// 滚动偏移量，因为尚无要还原的滚动偏移量。 随后，将恢复保存的偏移，并且忽略[initialScrollOffset]。
  ///
  /// 也可以看看：
  ///  * [PageStorageKey]，当同一路径中出现多个滚动条时，应使用[PageStorageKey]
  ///  来区分用于保存滚动偏移量的[PageStorage]位置。
  final bool keepScrollOffset;

  /// [toString]输出中使用的标签。 旨在帮助在调试输出中标识滚动控制器实例。
  final String? debugLabel;

  /// The currently attached positions. 当前附加的 [positions]。
  /// 这不应直接突变。 可以使用[attach]和[detach]添加和删除[ScrollPosition]对象。
  @protected
  Iterable<ScrollPosition> get positions => _positions;
  final List<ScrollPosition> _positions = <ScrollPosition>[];

  /// 是否有任何[ScrollPosition]对象已使用[attach]方法将自身附加到[ScrollController]。
  ///
  /// 如果为false，则不得调用与[ScrollPosition]交互的成员，
  /// 例如[position]，[offset]，[animateTo]和[jumpTo]。
  bool get hasClients => _positions.isNotEmpty;

  /// 返回附加的[ScrollPosition]，可以从中获取[ScrollView]的实际滚动偏移量。
  ///
  /// Calling this is only valid when only a single position is attached.
  /// 仅在仅连接一个[position]时调用此选项才有效。
  ShopScrollPosition get position {
    assert(_positions.isNotEmpty,
        'ScrollController not attached to any scroll views.');
    assert(_positions.length == 1,
        'ScrollController attached to multiple scroll views.');
    return _positions.single as ShopScrollPosition;
  }

  /// 可滚动小部件的当前滚动偏移量
  /// 可滚动小部件的当前滚动偏移量。要求控制器仅控制一个可滚动小部件。
  double get offset => position.pixels;

  /// 从当前位置到给定值的位置动画。
  /// 任何活动的动画都将被取消。 如果用户当前正在滚动，则该操作将被取消。
  /// 返回的[Future]将在动画结束时完成，无论它是否成功完成或是否被过早中断。
  ///
  /// 每当用户尝试手动滚动或启动其他活动，或者动画到达视口边缘并尝试过度滚动时，动画都会中断。
  /// （如果[ScrollPosition]不会过度滚动，而是允许滚动超出范围，那么超出范围不会中断动画。）
  ///
  /// 动画对视口或内容尺寸的更改无动于衷。
  ///
  /// 一旦动画完成，如果滚动位置的值不稳定，则滚动位置将尝试开始弹道活动。
  /// （例如，如果滚动超出范围，并且在这种情况下滚动位置通常会弹回）
  ///
  /// 持续时间不能为零。 要在没有动画的情况下跳至特定值，请使用[jumpTo]。
  Future<void> animateTo(
    double offset, {
    required Duration duration,
    required Curve curve,
  }) {
    assert(_positions.isNotEmpty,
        'ScrollController not attached to any scroll views.');
    final List<Future<void>> animations = List<Future<void>>.filled(_positions.length, Future<void>.value());
    for (int i = 0; i < _positions.length; i += 1)
      animations[i] =
          _positions[i].animateTo(offset, duration: duration, curve: curve);
    return Future.wait<void>(animations).then<void>((List<void> _) => null);
  }

  /// 将滚动位置从其当前值跳转到给定值，而不进行动画处理，也无需检查新值是否在范围内。
  /// 任何活动的动画都将被取消。 如果用户当前正在滚动，则该操作将被取消。
  ///
  /// 如果此方法更改了滚动位置，则将分派开始/更新/结束滚动通知的序列。 此方法不能生成过滚动通知。
  ///
  /// 跳跃之后，如果数值超出范围，则立即开始弹道活动。
  void jumpTo(double value) {
    assert(_positions.isNotEmpty,
        'ScrollController not attached to any scroll views.');
    for (ScrollPosition position in List<ScrollPosition>.from(_positions))
      position.jumpTo(value);
  }

  /// 在此控制器上注册给定位置。
  /// 此函数返回后，此控制器上的[animateTo]和[jumpTo]方法将操纵给定位置。
  void attach(ScrollPosition position) {
    assert(!_positions.contains(position));
    _positions.add(position);
    position.addListener(notifyListeners);
  }

  /// 用此控制器注销给定位置。
  /// 此函数返回后，此控制器上的[animateTo]和[jumpTo]方法将不会操纵给定位置。
  void detach(ScrollPosition position) {
    assert(_positions.contains(position));
    position.removeListener(notifyListeners);
    _positions.remove(position);
  }

  @override
  void dispose() {
    for (ScrollPosition position in _positions)
      position.removeListener(notifyListeners);
    super.dispose();
  }

  /// 创建一个[ScrollPosition]供[Scrollable]小部件使用。
  ///
  /// 子类可以重写此功能，以自定义其控制的可滚动小部件使用的[ScrollPosition]。
  /// 例如，[PageController]重写此函数以返回面向页面的滚动位置子类，
  /// 该子类在可滚动窗口小部件调整大小时保持同一页面可见。
  ///
  /// 默认情况下，返回[ScrollPositionWithSingleContext]。
  /// 参数通常传递给正在创建的[ScrollPosition]：
  ///
  ///  * `physics`：[ScrollPhysics]的一个实例，它确定[ScrollPosition]对用户交互的反应方式，
  ///     释放或甩动时如何模拟滚动等。该值不会为null。 它通常来自[ScrollView]或其他创建
  ///     [Scrollable]的小部件，或者（如果未提供）来自环境[ScrollConfiguration]。
  ///  * `context`：一个[ScrollContext]，用于与拥有[ScrollPosition]的对象进行通信
  ///   （通常是[Scrollable]本身）。
  ///  * `oldPosition`: 如果这不是第一次为此[Scrollable]创建[ScrollPosition]，
  ///     则它将是前一个实例。 当环境已更改并且[Scrollable]需要重新创建[ScrollPosition]
  ///     对象时，将使用此方法。 第一次创建[ScrollPosition]时为null。
  ScrollPosition createScrollPosition(ScrollPhysics physics,
      ScrollContext context, ScrollPosition? oldPosition) {
    return ShopScrollPosition(
      coordinator: coordinator,
      physics: physics,
      context: context,
      initialPixels: initialScrollOffset,
      keepScrollOffset: keepScrollOffset,
      oldPosition: oldPosition,
      debugLabel: debugLabel,
    );
  }

  @override
  String toString() {
    final List<String> description = <String>[];
    debugFillDescription(description);
    return '${describeIdentity(this)}(${description.join(", ")})';
  }

  /// 在给定的描述中添加其他信息，以供[toString]使用。
  /// 此方法使子类更易于协调以提供高质量的[toString]实现。 [ScrollController]
  /// 基类上的[toString]实现调用[debugFillDescription]来从子类中收集有用的信息，以合并到其返回值中。
  /// 如果您重写了此方法，请确保通过调用`super.debugFillDescription（description）`来启动方法。
  @mustCallSuper
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    if (debugLabel != null) description.add(debugLabel!);
    if (initialScrollOffset != 0.0)
      description.add(
          'initialScrollOffset: ${initialScrollOffset.toStringAsFixed(1)}, ');
    if (_positions.isEmpty) {
      description.add('no clients');
    } else if (_positions.length == 1) {
      // 实际上不列出客户端本身，因为它的toString可能引用了我们。
      description.add('one client, offset ${offset?.toStringAsFixed(1)}');
    } else {
      description.add('${_positions.length} clients');
    }
  }
}
