// Copyright 2016 The Chromium 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:meta/meta.dart';
import 'package:vector_math/vector_math_64.dart';

import 'binding.dart';
import 'box.dart';
import 'object.dart';
import 'sliver.dart';
import 'viewport_offset.dart';

/// An interface for render objects that are bigger on the inside.
///
/// Some render objects, such as [RenderViewport], present a portion of their
/// content, which can be controlled by a [ViewportOffset]. This interface lets
/// the framework recognize such render objects and interact with them without
/// having specific knowledge of all the various types of viewports.
abstract class RenderAbstractViewport implements RenderObject {
  /// Returns the [RenderAbstractViewport] that most closely encloses the given
  /// render object.
  ///
  /// If the object does not have a [RenderAbstractViewport] as an ancestor,
  /// this function returns null.
  static RenderAbstractViewport of(RenderObject object) {
    while (object != null) {
      if (object is RenderAbstractViewport)
        return object;
      object = object.parent;
    }
    return null;
  }

  /// Returns the offset that would be needed to reveal the target render object.
  ///
  /// The `alignment` argument describes where the target should be positioned
  /// after applying the returned offset. If `alignment` is 0.0, the child must
  /// be positioned as close to the leading edge of the viewport as possible. If
  /// `alignment` is 1.0, the child must be positioned as close to the trailing
  /// edge of the viewport as possible. If `alignment` is 0.5, the child must be
  /// positioned as close to the center of the viewport as possible.
  ///
  /// The target might not be a direct child of this viewport but it must be a
  /// descendant of the viewport and there must not be any other
  /// [RenderAbstractViewport] objects between the target and this object.
  double getOffsetToReveal(RenderObject target, double alignment);
}

typedef RenderSliver _Advancer(RenderSliver child);

// ///
// /// See also:
// ///
// /// - [RenderSliver], which explains more about the Sliver protocol.
// /// - [RenderBox], which explains more about the Box protocol.
// /// - [RenderSliverToBoxAdapter], which allows a [RenderBox] object to be
// ///   placed inside a [RenderSliver] (the opposite of this class).
abstract class RenderViewportBase<ParentDataClass extends ContainerParentDataMixin<RenderSliver>>
    extends RenderBox with ContainerRenderObjectMixin<RenderSliver, ParentDataClass>
    implements RenderAbstractViewport {
  RenderViewportBase({
    AxisDirection axisDirection: AxisDirection.down,
    @required ViewportOffset offset,
  }) : _axisDirection = axisDirection,
       _offset = offset {
    assert(axisDirection != null);
    assert(offset != null);
  }

  AxisDirection get axisDirection => _axisDirection;
  AxisDirection _axisDirection;
  set axisDirection(AxisDirection value) {
    assert(value != null);
    if (value == _axisDirection)
      return;
    _axisDirection = value;
    markNeedsLayout();
  }

  Axis get axis => axisDirectionToAxis(axisDirection);

  ViewportOffset get offset => _offset;
  ViewportOffset _offset;
  set offset(ViewportOffset value) {
    assert(value != null);
    if (value == _offset)
      return;
    if (attached)
      _offset.removeListener(markNeedsLayout);
    _offset = value;
    if (attached)
      _offset.addListener(markNeedsLayout);
    // We need to go through layout even if the new offset has the same pixels
    // value as the old offset so that we will apply our viewport and content
    // dimensions.
    markNeedsLayout();
  }

  @override
  void attach(PipelineOwner owner) {
    super.attach(owner);
    _offset.addListener(markNeedsLayout);
  }

  @override
  void detach() {
    _offset.removeListener(markNeedsLayout);
    super.detach();
  }

  @override
  bool get isRepaintBoundary => true;

  @protected
  double layoutOneSide(
    RenderSliver child,
    double scrollOffset,
    double layoutOffset,
    double remainingPaintExtent,
    double mainAxisExtent,
    double crossAxisExtent,
    GrowthDirection growthDirection,
    _Advancer advance,
  ) {
    assert(scrollOffset.isFinite);
    assert(scrollOffset >= 0.0);
    final ScrollDirection adjustedUserScrollDirection =
        applyGrowthDirecitonToScrollDirection(offset.userScrollDirection, growthDirection);
    assert(adjustedUserScrollDirection != null);
    double maxPaintOffset = layoutOffset;
    final double initialLayoutOffset = layoutOffset;
    while (child != null) {
      assert(scrollOffset >= 0.0);
      child.layout(new SliverConstraints(
        axisDirection: axisDirection,
        growthDirection: growthDirection,
        userScrollDirection: adjustedUserScrollDirection,
        scrollOffset: scrollOffset,
        overlap: maxPaintOffset - layoutOffset,
        remainingPaintExtent: math.max(0.0, remainingPaintExtent - layoutOffset + initialLayoutOffset),
        crossAxisExtent: crossAxisExtent,
        viewportMainAxisExtent: mainAxisExtent,
      ), parentUsesSize: true);
      // collect the child's objects
      final SliverGeometry childLayoutGeometry = child.geometry;

      assert(childLayoutGeometry.debugAssertIsValid);

      // first check that there isn't a correction to apply. If there is we'll
      // have to start over.
      if (childLayoutGeometry.scrollOffsetCorrection != 0.0)
        return childLayoutGeometry.scrollOffsetCorrection;

      // geometry
      updateChildLayoutOffset(child, layoutOffset, growthDirection);
      maxPaintOffset = math.max(layoutOffset + childLayoutGeometry.paintExtent, maxPaintOffset);
      scrollOffset -= childLayoutGeometry.scrollExtent;
      layoutOffset += childLayoutGeometry.layoutExtent;

      if (scrollOffset <= 0.0)
        scrollOffset = 0.0;

      updateOutOfBoundsData(growthDirection, childLayoutGeometry);

      // move on to the next child
      child = advance(child);
    }

    // we made it without a correction, whee!
    return 0.0;
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    if (firstChild == null)
      return;
    if (hasVisualOverflow) {
      context.pushClipRect(needsCompositing, offset, Point.origin & size, paintContents);
    } else {
      paintContents(context, offset);
    }
  }

  @protected
  void paintContents(PaintingContext context, Offset offset) {
    for (RenderSliver child in childrenInPaintOrder) {
      if (child.geometry.visible)
        context.paintChild(child, offset + paintOffsetOf(child));
    }
  }

  @override
  void debugPaintSize(PaintingContext context, Offset offset) {
    assert(() {
      super.debugPaintSize(context, offset);
      final Paint paint = new Paint()
        ..style = PaintingStyle.stroke
        ..strokeWidth = 1.0
        ..color = const Color(0xFF00FF00);
      final Canvas canvas = context.canvas;
      RenderSliver child = firstChild;
      while (child != null) {
        Size size;
        switch (axis) {
          case Axis.vertical:
            size = new Size(child.constraints.crossAxisExtent, child.geometry.layoutExtent);
            break;
          case Axis.horizontal:
            size = new Size(child.geometry.layoutExtent, child.constraints.crossAxisExtent);
            break;
        }
        assert(size != null);
        canvas.drawRect(((offset + paintOffsetOf(child)) & size).deflate(0.5), paint);
        child = childAfter(child);
      }
      return true;
    });
  }

  @override
  bool hitTestChildren(HitTestResult result, { Point position }) {
    double mainAxisPosition, crossAxisPosition;
    switch (axis) {
      case Axis.vertical:
        mainAxisPosition = position.y;
        crossAxisPosition = position.x;
        break;
      case Axis.horizontal:
        mainAxisPosition = position.x;
        crossAxisPosition = position.y;
        break;
    }
    assert(mainAxisPosition != null);
    assert(crossAxisPosition != null);
    for (RenderSliver child in childrenInHitTestOrder) {
      if (child.geometry.visible && child.hitTest(
        result,
        mainAxisPosition: computeChildMainAxisPosition(child, mainAxisPosition),
        crossAxisPosition: crossAxisPosition
      )) {
        return true;
      }
    }
    return false;
  }

  @override
  double getOffsetToReveal(RenderObject target, double alignment) {
    double leadingScrollOffset;
    double targetMainAxisExtent;
    RenderObject descendant;

    if (target is RenderBox) {
      final RenderBox targetBox = target;

      RenderBox pivot = targetBox;
      while (pivot.parent is RenderBox)
        pivot = pivot.parent;

      assert(pivot.parent != null);
      assert(pivot.parent != this);
      assert(pivot != this);

      final Matrix4 transform = targetBox.getTransformTo(pivot);
      final Rect bounds = MatrixUtils.transformRect(transform, targetBox.paintBounds);

      target = pivot;
      // TODO(abarth): Support other kinds of render objects besides slivers.
      assert(target.parent is RenderSliver);
      final RenderSliver pivotParent = target.parent;

      final GrowthDirection growthDirection = pivotParent.constraints.growthDirection;
      switch (applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) {
        case AxisDirection.up:
          leadingScrollOffset = pivot.size.height - bounds.bottom;
          targetMainAxisExtent = bounds.height;
          break;
        case AxisDirection.right:
          leadingScrollOffset = bounds.left;
          targetMainAxisExtent = bounds.width;
          break;
        case AxisDirection.down:
          leadingScrollOffset = bounds.top;
          targetMainAxisExtent = bounds.height;
          break;
        case AxisDirection.left:
          leadingScrollOffset = pivot.size.width - bounds.right;
          targetMainAxisExtent = bounds.width;
          break;
      }
      descendant = pivot;
    } else if (target is RenderSliver) {
      final RenderSliver targetSliver = target;
      leadingScrollOffset = 0.0;
      targetMainAxisExtent = targetSliver.geometry.scrollExtent;
      descendant = targetSliver;
    } else {
      return offset.pixels;
    }

    // The child will be the topmost object before we get to the viewport.
    RenderObject child = descendant;
    while (child.parent is RenderSliver) {
      final RenderSliver parent = child.parent;
      leadingScrollOffset += parent.childScrollOffset(child);
      child = parent;
    }

    assert(child.parent == this);
    assert(child is RenderSliver);
    final RenderSliver sliver = child;
    leadingScrollOffset = scrollOffsetOf(sliver, leadingScrollOffset);

    double mainAxisExtent;
    switch (axis) {
      case Axis.horizontal:
        mainAxisExtent = size.width;
        break;
      case Axis.vertical:
        mainAxisExtent = size.height;
        break;
    }

    return leadingScrollOffset - (mainAxisExtent - targetMainAxisExtent) * alignment;
  }

  @protected
  Offset computeAbsolutePaintOffset(RenderSliver child, double layoutOffset, GrowthDirection growthDirection) {
    assert(hasSize); // this is only usable once we have a size
    assert(axisDirection != null);
    assert(growthDirection != null);
    assert(child != null);
    assert(child.geometry != null);
    switch (applyGrowthDirectionToAxisDirection(axisDirection, growthDirection)) {
      case AxisDirection.up:
        return new Offset(0.0, size.height - (layoutOffset + child.geometry.paintExtent));
      case AxisDirection.right:
        return new Offset(layoutOffset, 0.0);
      case AxisDirection.down:
        return new Offset(0.0, layoutOffset);
      case AxisDirection.left:
        return new Offset(size.width - (layoutOffset + child.geometry.paintExtent), 0.0);
    }
    return null;
  }

  // TODO(ianh): semantics - shouldn't walk the invisible children

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('$axisDirection');
    description.add('offset: $offset');
  }

  @override
  String debugDescribeChildren(String prefix) {
    if (firstChild == null)
      return '$prefix\n';
    int count = indexOfFirstChild();
    String result = '$prefix \u2502\n';
    RenderSliver child = firstChild;
    while (child != lastChild) {
      result += '${child.toStringDeep("$prefix \u251C\u2500${labelForChild(count)}: ", "$prefix \u2502")}';
      count += 1;
      child = childAfter(child);
    }
    assert(child == lastChild);
    result += '${child.toStringDeep("$prefix \u2514\u2500${labelForChild(count)}: ", "$prefix  ")}';
    return result;
  }

  // API TO BE IMPLEMENTED BY SUBCLASSES

  // setupParentData

  // performLayout (and optionally sizedByParent and performResize)

  @protected
  bool get hasVisualOverflow;

  @protected
  void updateOutOfBoundsData(GrowthDirection growthDirection, SliverGeometry childLayoutGeometry);

  @protected
  void updateChildLayoutOffset(RenderSliver child, double layoutOffset, GrowthDirection growthDirection);

  @protected
  Offset paintOffsetOf(RenderSliver child);

  @protected
  double scrollOffsetOf(RenderSliver child, double scrollOffset);

  // applyPaintTransform

  /// Converts the `parentMainAxisPosition` into the child's coordinate system.
  ///
  /// The `parentMainAxisPosition` is a distance from the top edge (for vertical
  /// viewports) or left edge (for horizontal viewports) of the viewport bounds.
  /// This describes a line, perpendicular to the viewport's main axis, heretofor
  /// known as the target line.
  ///
  /// The child's coordinate system's origin in the main axis is at the leading
  /// edge of the given child, as given by the child's
  /// [SliverConstraints.axisDirection] and [SliverConstraints.growthDirection].
  ///
  /// This method returns the distance from the leading edge of the given child to
  /// the target line described above.
  ///
  /// (The `parentMainAxisPosition` is not from the leading edge of the
  /// viewport, it's always the top or left edge.)
  @protected
  double computeChildMainAxisPosition(RenderSliver child, double parentMainAxisPosition);

  @protected
  int indexOfFirstChild();

  @protected
  String labelForChild(int index);

  /// Provides an iterable that walks the children of the viewport, in the order
  /// that they should be painted.
  ///
  /// This should be the reverse order of [childrenInHitTestOrder].
  @protected
  Iterable<RenderSliver> get childrenInPaintOrder;

  /// Provides an iterable that walks the children of the viewport, in the order
  /// that hit-testing should use.
  ///
  /// This should be the reverse order of [childrenInPaintOrder].
  @protected
  Iterable<RenderSliver> get childrenInHitTestOrder;
}

// ///
// /// See also:
// ///
// /// - [RenderSliver], which explains more about the Sliver protocol.
// /// - [RenderBox], which explains more about the Box protocol.
// /// - [RenderSliverToBoxAdapter], which allows a [RenderBox] object to be
// ///   placed inside a [RenderSliver] (the opposite of this class).
// /// - [RenderShrinkWrappingViewport], a variant of [RenderViewport] that
// ///   shrink-wraps its contents along the main axis.
class RenderViewport extends RenderViewportBase<SliverPhysicalContainerParentData> {
  /// Creates a viewport for [RenderSliver] objects.
  ///
  /// If the [center] is not specified, then the first child in the `children`
  /// list, if any, is used.
  ///
  /// The [offset] must be specified. For testing purposes, consider passing a
  /// [new ViewportOffset.zero] or [new ViewportOffset.fixed].
  RenderViewport({
    AxisDirection axisDirection: AxisDirection.down,
    @required ViewportOffset offset,
    double anchor: 0.0,
    List<RenderSliver> children,
    RenderSliver center,
  }) : _anchor = anchor,
       _center = center,
       super(axisDirection: axisDirection, offset: offset) {
    assert(anchor != null);
    assert(anchor >= 0.0 && anchor <= 1.0);
    addAll(children);
    if (center == null && firstChild != null)
      _center = firstChild;
  }

  @override
  void setupParentData(RenderObject child) {
    if (child.parentData is! SliverPhysicalContainerParentData)
      child.parentData = new SliverPhysicalContainerParentData();
  }

  double get anchor => _anchor;
  double _anchor;
  set anchor(double value) {
    assert(value != null);
    assert(value >= 0.0 && value <= 1.0);
    if (value == _anchor)
      return;
    _anchor = value;
    markNeedsLayout();
  }

  RenderSliver get center => _center;
  RenderSliver _center;
  set center(RenderSliver value) {
    if (value == _center)
      return;
    _center = value;
    markNeedsLayout();
  }

  @override
  bool get sizedByParent => true;

  @override
  void performResize() {
    assert(() {
      if (!constraints.hasBoundedHeight || !constraints.hasBoundedWidth) {
        switch (axis) {
          case Axis.vertical:
            if (!constraints.hasBoundedHeight) {
              throw new FlutterError(
                'Vertical viewport was given unbounded height.\n'
                'Viewports expand in the scrolling direction to fill their container.'
                'In this case, a vertical viewport was given an unlimited amount of '
                'vertical space in which to expand. This situation typically happens '
                'when a scrollable widget is nested inside another scrollable widget.\n'
                '\n'
                'If this widget is always nested in a scrollable widget there '
                'is no need to use a viewport because there will always be enough '
                'vertical space for the children. In this case, consider using a '
                'Column instead. Otherwise, consider using the "shrinkWrap" propery '
                '(or a ShrinkWrappingViewport) to size the height of the viewport '
                'to the sum of the heights of its children.'
              );
            }
            if (!constraints.hasBoundedWidth) {
              throw new FlutterError(
                'Vertical viewport was given unbounded width.\n'
                'Viewports expand in the cross axis to fill their container and '
                'constrain their children to match their extent in the cross axis. '
                'In this case, a vertical viewport was given an unlimited amount of '
                'horizontal space in which to expand.'
              );
            }
            break;
          case Axis.horizontal:
            if (!constraints.hasBoundedWidth) {
              throw new FlutterError(
                'Horizontal viewport was given unbounded width.\n'
                'Viewports expand in the scrolling direction to fill their container.'
                'In this case, a horizontal viewport was given an unlimited amount of '
                'horizontal space in which to expand. This situation typically happens '
                'when a scrollable widget is nested inside another scrollable widget.\n'
                '\n'
                'If this widget is always nested in a scrollable widget there '
                'is no need to use a viewport because there will always be enough '
                'horizontal space for the children. In this case, consider using a '
                'Row instead. Otherwise, consider using the "shrinkWrap" propery '
                '(or a ShrinkWrappingViewport) to size the width of the viewport '
                'to the sum of the widths of its children.'
              );
            }
            if (!constraints.hasBoundedHeight) {
              throw new FlutterError(
                'Horizontal viewport was given unbounded height.\n'
                'Viewports expand in the cross axis to fill their container and '
                'constrain their children to match their extent in the cross axis. '
                'In this case, a horizontal viewport was given an unlimited amount of '
                'vertical space in which to expand.'
              );
            }
            break;
        }
      }
      return true;
    });
    size = constraints.biggest;
    // We ignore the return value of applyViewportDimension below because we are
    // going to go through performLayout next regardless.
    switch (axis) {
      case Axis.vertical:
        offset.applyViewportDimension(size.height);
        break;
      case Axis.horizontal:
        offset.applyViewportDimension(size.width);
        break;
    }
  }

  static const int _kMaxLayoutCycles = 10;

  // Out-of-band data computed during layout.
  double _minScrollExtent;
  double _maxScrollExtent;
  bool _hasVisualOverflow = false;

  @override
  void performLayout() {
    if (center == null) {
      assert(firstChild == null);
      _minScrollExtent = 0.0;
      _maxScrollExtent = 0.0;
      _hasVisualOverflow = false;
      offset.applyContentDimensions(0.0, 0.0);
      return;
    }
    assert(center.parent == this);

    double mainAxisExtent;
    double crossAxisExtent;
    switch (axis) {
      case Axis.vertical:
        mainAxisExtent = size.height;
        crossAxisExtent = size.width;
        break;
      case Axis.horizontal:
        mainAxisExtent = size.width;
        crossAxisExtent = size.height;
        break;
    }

    final double centerOffsetAdjustment = center.centerOffsetAdjustment;

    double correction;
    int count = 0;
    do {
      assert(offset.pixels != null);
      correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels + centerOffsetAdjustment);
      if (correction != 0.0) {
        offset.correctBy(correction);
      } else {
        if (offset.applyContentDimensions(
              math.min(0.0, _minScrollExtent + mainAxisExtent * anchor),
              math.max(0.0, _maxScrollExtent - mainAxisExtent * (1.0 - anchor)),
           ))
          break;
      }
      count += 1;
    } while (count < _kMaxLayoutCycles);
    assert(() {
      if (count >= _kMaxLayoutCycles) {
        assert(count != 1);
        throw new FlutterError(
          'A RenderViewport exceeded its maximum number of layout cycles.\n'
          'RenderViewport render objects, during layout, can retry if either their '
          'slivers or their ViewportOffset decide that the offset should be corrected '
          'to take into account information collected during that layout.\n'
          'In the case of this RenderViewport object, however, this happened $count '
          'times and still there was no consensus on the scroll offset. This usually '
          'indicates a bug. Specifically, it means that one of the following three '
          'problems is being experienced by the RenderViewport object:\n'
          ' * One of the RenderSliver children or the ViewportOffset have a bug such'
          ' that they always think that they need to correct the offset regardless.\n'
          ' * Some combination of the RenderSliver children and the ViewportOffset'
          ' have a bad interaction such that one applies a correction then another'
          ' applies a reverse correction, leading to an infinite loop of corrections.\n'
          ' * There is a pathological case that would eventually resolve, but it is'
          ' so complicated that it cannot be resolved in any reasonable number of'
          ' layout passes.'
        );
      }
      return true;
    });
  }

  double _attemptLayout(double mainAxisExtent, double crossAxisExtent, double correctedOffset) {
    assert(!mainAxisExtent.isNaN);
    assert(mainAxisExtent >= 0.0);
    assert(crossAxisExtent.isFinite);
    assert(crossAxisExtent >= 0.0);
    assert(correctedOffset.isFinite);
    _minScrollExtent = 0.0;
    _maxScrollExtent = 0.0;
    _hasVisualOverflow = false;

    // centerOffset is the offset from the leading edge of the RenderViewport
    // to the zero scroll offset (the line between the forward slivers and the
    // reverse slivers). The other two are that, but clamped to the visible
    // region of the viewport.
    final double centerOffset = mainAxisExtent * anchor - correctedOffset;
    final double clampedForwardCenter = math.max(0.0, math.min(mainAxisExtent, centerOffset));
    final double clampedReverseCenter = math.max(0.0, math.min(mainAxisExtent, mainAxisExtent - centerOffset));

    // negative scroll offsets
    double result = layoutOneSide(
      childBefore(center),
      math.max(mainAxisExtent, mainAxisExtent * anchor - correctedOffset) - mainAxisExtent,
      clampedReverseCenter,
      clampedForwardCenter,
      mainAxisExtent,
      crossAxisExtent,
      GrowthDirection.reverse,
      childBefore,
    );
    if (result != 0.0)
      return -result;

    // positive scroll offsets
    return layoutOneSide(
      center,
      math.max(0.0, correctedOffset - mainAxisExtent * anchor),
      clampedForwardCenter,
      clampedReverseCenter,
      mainAxisExtent,
      crossAxisExtent,
      GrowthDirection.forward,
      childAfter,
    );
  }

  @override
  bool get hasVisualOverflow => _hasVisualOverflow;

  @override
  void updateOutOfBoundsData(GrowthDirection growthDirection, SliverGeometry childLayoutGeometry) {
    switch (growthDirection) {
      case GrowthDirection.forward:
        _maxScrollExtent += childLayoutGeometry.scrollExtent;
        break;
      case GrowthDirection.reverse:
        _minScrollExtent -= childLayoutGeometry.scrollExtent;
        break;
    }
    if (childLayoutGeometry.hasVisualOverflow)
      _hasVisualOverflow = true;
  }

  @override
  void updateChildLayoutOffset(RenderSliver child, double layoutOffset, GrowthDirection growthDirection) {
    final SliverPhysicalParentData childParentData = child.parentData;
    childParentData.paintOffset = computeAbsolutePaintOffset(child, layoutOffset, growthDirection);
  }

  @override
  Offset paintOffsetOf(RenderSliver child) {
    final SliverPhysicalParentData childParentData = child.parentData;
    return childParentData.paintOffset;
  }

  @override
  double scrollOffsetOf(RenderSliver child, double scrollOffsetWithinChild) {
    assert(child.parent == this);
    final GrowthDirection growthDirection = child.constraints.growthDirection;
    assert(growthDirection != null);
    switch (growthDirection) {
      case GrowthDirection.forward:
        double scrollOffsetToChild = 0.0;
        RenderSliver current = center;
        while (current != child) {
          scrollOffsetToChild += current.geometry.scrollExtent;
          current = childAfter(current);
        }
        return scrollOffsetToChild + scrollOffsetWithinChild;
      case GrowthDirection.reverse:
        double scrollOffsetToChild = 0.0;
        RenderSliver current = childBefore(center);
        while (current != child) {
          scrollOffsetToChild -= current.geometry.scrollExtent;
          current = childBefore(current);
        }
        return scrollOffsetToChild - scrollOffsetWithinChild;
    }
    return null;
  }

  @override
  void applyPaintTransform(RenderObject child, Matrix4 transform) {
    assert(child != null);
    final SliverPhysicalParentData childParentData = child.parentData;
    childParentData.applyPaintTransform(transform);
  }

  @override
  double computeChildMainAxisPosition(RenderSliver child, double parentMainAxisPosition) {
    assert(child != null);
    assert(child.constraints != null);
    final SliverPhysicalParentData childParentData = child.parentData;
    switch (applyGrowthDirectionToAxisDirection(child.constraints.axisDirection, child.constraints.growthDirection)) {
      case AxisDirection.down:
        return parentMainAxisPosition - childParentData.paintOffset.dy;
      case AxisDirection.right:
        return parentMainAxisPosition - childParentData.paintOffset.dx;
      case AxisDirection.up:
        return child.geometry.paintExtent - (parentMainAxisPosition - childParentData.paintOffset.dy);
      case AxisDirection.left:
        return child.geometry.paintExtent - (parentMainAxisPosition - childParentData.paintOffset.dx);
    }
    return 0.0;
  }

  @override
  int indexOfFirstChild() {
    assert(center != null);
    assert(center.parent == this);
    assert(firstChild != null);
    int count = 0;
    RenderSliver child = center;
    while (child != firstChild) {
      count -= 1;
      child = childBefore(child);
    }
    return count;
  }

  @override
  String labelForChild(int index) {
    if (index == 0)
      return 'center child';
    return 'child $index';
  }

  @override
  Iterable<RenderSliver> get childrenInPaintOrder sync* {
    if (firstChild == null)
      return;
    RenderSliver child = firstChild;
    while (child != center) {
      yield child;
      child = childAfter(child);
    }
    child = lastChild;
    while (true) {
      yield child;
      if (child == center)
        return;
      child = childBefore(child);
    }
  }

  @override
  Iterable<RenderSliver> get childrenInHitTestOrder sync* {
    if (firstChild == null)
      return;
    RenderSliver child = center;
    while (child != null) {
      yield child;
      child = childAfter(child);
    }
    child = childBefore(center);
    while (child != null) {
      yield child;
      child = childBefore(child);
    }
  }

  @override
  void debugFillDescription(List<String> description) {
    super.debugFillDescription(description);
    description.add('anchor: $anchor');
  }
}

// ///
// /// See also:
// ///
// /// - [RenderViewport], a viewport that does not shrink-wrap its contents
// /// - [RenderSliver], which explains more about the Sliver protocol.
// /// - [RenderBox], which explains more about the Box protocol.
// /// - [RenderSliverToBoxAdapter], which allows a [RenderBox] object to be
// ///   placed inside a [RenderSliver] (the opposite of this class).
class RenderShrinkWrappingViewport extends RenderViewportBase<SliverLogicalContainerParentData> {
  /// Creates a viewport (for [RenderSliver] objects) that shrink-wraps its
  /// contents.
  ///
  /// The [offset] must be specified. For testing purposes, consider passing a
  /// [new ViewportOffset.zero] or [new ViewportOffset.fixed].
  RenderShrinkWrappingViewport({
    AxisDirection axisDirection: AxisDirection.down,
    @required ViewportOffset offset,
    List<RenderSliver> children,
  }) : super(axisDirection: axisDirection, offset: offset) {
    addAll(children);
  }

  @override
  void setupParentData(RenderObject child) {
    if (child.parentData is! SliverLogicalContainerParentData)
      child.parentData = new SliverLogicalContainerParentData();
  }

  // Out-of-band data computed during layout.
  double _maxScrollExtent;
  double _shrinkWrapExtent;
  bool _hasVisualOverflow = false;

  @override
  void performLayout() {
    if (firstChild == null) {
      switch (axis) {
        case Axis.vertical:
          assert(constraints.hasBoundedWidth);
          size = new Size(constraints.maxWidth, constraints.minHeight);
          break;
        case Axis.horizontal:
          assert(constraints.hasBoundedHeight);
          size = new Size(constraints.minWidth, constraints.maxHeight);
          break;
      }
      offset.applyViewportDimension(0.0);
      _maxScrollExtent = 0.0;
      _shrinkWrapExtent = 0.0;
      _hasVisualOverflow = false;
      offset.applyContentDimensions(0.0, 0.0);
      return;
    }

    double mainAxisExtent;
    double crossAxisExtent;
    switch (axis) {
      case Axis.vertical:
        assert(constraints.hasBoundedWidth);
        mainAxisExtent = constraints.maxHeight;
        crossAxisExtent = constraints.maxWidth;
        break;
      case Axis.horizontal:
        assert(constraints.hasBoundedHeight);
        mainAxisExtent = constraints.maxWidth;
        crossAxisExtent = constraints.maxHeight;
        break;
    }

    double correction;
    double effectiveExtent;
    do {
      assert(offset.pixels != null);
      correction = _attemptLayout(mainAxisExtent, crossAxisExtent, offset.pixels);
      if (correction != 0.0) {
        offset.correctBy(correction);
      } else {
        switch (axis) {
          case Axis.vertical:
            effectiveExtent = constraints.constrainHeight(_shrinkWrapExtent);
            break;
          case Axis.horizontal:
            effectiveExtent = constraints.constrainWidth(_shrinkWrapExtent);
            break;
        }
        final bool didAcceptViewportDimension = offset.applyViewportDimension(effectiveExtent);
        final bool didAcceptContentDimension = offset.applyContentDimensions(0.0, math.max(0.0, _maxScrollExtent - effectiveExtent));
        if (didAcceptViewportDimension && didAcceptContentDimension)
          break;
      }
    } while (true);
    switch (axis) {
      case Axis.vertical:
        size = constraints.constrainDimensions(crossAxisExtent, effectiveExtent);
        break;
      case Axis.horizontal:
        size = constraints.constrainDimensions(effectiveExtent, crossAxisExtent);
        break;
    }
  }

  double _attemptLayout(double mainAxisExtent, double crossAxisExtent, double correctedOffset) {
    assert(!mainAxisExtent.isNaN);
    assert(mainAxisExtent >= 0.0);
    assert(crossAxisExtent.isFinite);
    assert(crossAxisExtent >= 0.0);
    assert(correctedOffset.isFinite);
    _maxScrollExtent = 0.0;
    _shrinkWrapExtent = 0.0;
    _hasVisualOverflow = false;
    return layoutOneSide(
      firstChild,
      math.max(0.0, correctedOffset),
      0.0,
      mainAxisExtent,
      mainAxisExtent,
      crossAxisExtent,
      GrowthDirection.forward,
      childAfter,
    );
  }

  @override
  bool get hasVisualOverflow => _hasVisualOverflow;

  @override
  void updateOutOfBoundsData(GrowthDirection growthDirection, SliverGeometry childLayoutGeometry) {
    assert(growthDirection == GrowthDirection.forward);
    _maxScrollExtent += childLayoutGeometry.scrollExtent;
    if (childLayoutGeometry.hasVisualOverflow)
      _hasVisualOverflow = true;
    _shrinkWrapExtent += childLayoutGeometry.maxPaintExtent;
  }

  @override
  void updateChildLayoutOffset(RenderSliver child, double layoutOffset, GrowthDirection growthDirection) {
    assert(growthDirection == GrowthDirection.forward);
    final SliverLogicalParentData childParentData = child.parentData;
    childParentData.layoutOffset = layoutOffset;
  }

  @override
  Offset paintOffsetOf(RenderSliver child) {
    final SliverLogicalParentData childParentData = child.parentData;
    return computeAbsolutePaintOffset(child, childParentData.layoutOffset, GrowthDirection.forward);
  }

  @override
  double scrollOffsetOf(RenderSliver child, double scrollOffsetWithinChild) {
    assert(child.parent == this);
    assert(child.constraints.growthDirection == GrowthDirection.forward);
    double scrollOffsetToChild = 0.0;
    RenderSliver current = firstChild;
    while (current != child) {
      scrollOffsetToChild += current.geometry.scrollExtent;
      current = childAfter(current);
    }
    return scrollOffsetToChild + scrollOffsetWithinChild;
  }

  @override
  void applyPaintTransform(RenderObject child, Matrix4 transform) {
    assert(child != null);
    final Offset offset = paintOffsetOf(child);
    transform.translate(offset.dx, offset.dy);
  }

  @override
  double computeChildMainAxisPosition(RenderSliver child, double parentMainAxisPosition) {
    assert(child != null);
    assert(child.constraints != null);
    assert(hasSize);
    final SliverLogicalParentData childParentData = child.parentData;
    switch (applyGrowthDirectionToAxisDirection(child.constraints.axisDirection, child.constraints.growthDirection)) {
      case AxisDirection.down:
      case AxisDirection.right:
        return parentMainAxisPosition - childParentData.layoutOffset;
      case AxisDirection.up:
        return (size.height - parentMainAxisPosition) - childParentData.layoutOffset;
      case AxisDirection.left:
        return (size.width - parentMainAxisPosition) - childParentData.layoutOffset;
    }
    return 0.0;
  }

  @override
  int indexOfFirstChild() => 0;

  @override
  String labelForChild(int index) => 'child $index';

  @override
  Iterable<RenderSliver> get childrenInPaintOrder sync* {
    RenderSliver child = firstChild;
    while (child != null) {
      yield child;
      child = childAfter(child);
    }
  }

  @override
  Iterable<RenderSliver> get childrenInHitTestOrder sync* {
    RenderSliver child = lastChild;
    while (child != null) {
      yield child;
      child = childBefore(child);
    }
  }
}
