// Copyright 2015 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 'box.dart';
import 'object.dart';

/// Parent data for use with [RenderListBody].
class ListBodyParentData extends ContainerBoxParentData<RenderBox> { }

typedef double _ChildSizingFunction(RenderBox child);

/// Displays its children sequentially along a given axis, forcing them to the
/// dimensions of the parent in the other axis.
///
/// This layout algorithm arranges its children linearly along the main axis
/// (either horizontally or vertically). In the cross axis, children are
/// stretched to match the box's cross-axis extent. In the main axis, children
/// are given unlimited space and the box expands its main axis to contain all
/// its children. Because [RenderListBody] boxes expand in the main axis, they
/// must be given unlimited space in the main axis, typically by being contained
/// in a viewport with a scrolling direction that matches the box's main axis.
class RenderListBody extends RenderBox
    with ContainerRenderObjectMixin<RenderBox, ListBodyParentData>,
         RenderBoxContainerDefaultsMixin<RenderBox, ListBodyParentData> {
  /// Creates a render object that arranges its children sequentially along a
  /// given axis.
  ///
  /// By default, children are arranged along the vertical axis.
  RenderListBody({
    List<RenderBox> children,
    AxisDirection axisDirection = AxisDirection.down,
  }) : assert(axisDirection != null),
       _axisDirection = axisDirection {
    addAll(children);
  }

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

  /// The direction in which the children are laid out.
  ///
  /// For example, if the [axisDirection] is [AxisDirection.down], each child
  /// will be laid out below the next, vertically.
  AxisDirection get axisDirection => _axisDirection;
  AxisDirection _axisDirection;
  set axisDirection(AxisDirection value) {
    assert(value != null);
    if (_axisDirection == value)
      return;
    _axisDirection = value;
    markNeedsLayout();
  }

  /// The axis (horizontal or vertical) corresponding to the current
  /// [axisDirection].
  Axis get mainAxis => axisDirectionToAxis(axisDirection);

  @override
  void performLayout() {
    assert(() {
      switch (mainAxis) {
        case Axis.horizontal:
          if (!constraints.hasBoundedWidth)
            return true;
          break;
        case Axis.vertical:
          if (!constraints.hasBoundedHeight)
            return true;
          break;
      }
      throw new FlutterError(
        'RenderListBody must have unlimited space along its main axis.\n'
        'RenderListBody does not clip or resize its children, so it must be '
        'placed in a parent that does not constrain the main '
        'axis. You probably want to put the RenderListBody inside a '
        'RenderViewport with a matching main axis.'
      );
    }());
    assert(() {
      switch (mainAxis) {
        case Axis.horizontal:
          if (constraints.hasBoundedHeight)
            return true;
          break;
        case Axis.vertical:
          if (constraints.hasBoundedWidth)
            return true;
          break;
      }
      // TODO(ianh): Detect if we're actually nested blocks and say something
      // more specific to the exact situation in that case, and don't mention
      // nesting blocks in the negative case.
      throw new FlutterError(
        'RenderListBody must have a bounded constraint for its cross axis.\n'
        'RenderListBody forces its children to expand to fit the RenderListBody\'s container, '
        'so it must be placed in a parent that constrains the cross '
        'axis to a finite dimension. If you are attempting to nest a RenderListBody with '
        'one direction inside one of another direction, you will want to '
        'wrap the inner one inside a box that fixes the dimension in that direction, '
        'for example, a RenderIntrinsicWidth or RenderIntrinsicHeight object. '
        'This is relatively expensive, however.' // (that's why we don't do it automatically)
      );
    }());
    double mainAxisExtent = 0.0;
    RenderBox child = firstChild;
    switch (axisDirection) {
    case AxisDirection.right:
      final BoxConstraints innerConstraints = new BoxConstraints.tightFor(height: constraints.maxHeight);
      while (child != null) {
        child.layout(innerConstraints, parentUsesSize: true);
        final ListBodyParentData childParentData = child.parentData;
        childParentData.offset = new Offset(mainAxisExtent, 0.0);
        mainAxisExtent += child.size.width;
        assert(child.parentData == childParentData);
        child = childParentData.nextSibling;
      }
      size = constraints.constrain(new Size(mainAxisExtent, constraints.maxHeight));
      break;
    case AxisDirection.left:
      final BoxConstraints innerConstraints = new BoxConstraints.tightFor(height: constraints.maxHeight);
      while (child != null) {
        child.layout(innerConstraints, parentUsesSize: true);
        final ListBodyParentData childParentData = child.parentData;
        mainAxisExtent += child.size.width;
        assert(child.parentData == childParentData);
        child = childParentData.nextSibling;
      }
      double position = 0.0;
      child = firstChild;
      while (child != null) {
        final ListBodyParentData childParentData = child.parentData;
        position += child.size.width;
        childParentData.offset = new Offset(mainAxisExtent - position, 0.0);
        assert(child.parentData == childParentData);
        child = childParentData.nextSibling;
      }
      size = constraints.constrain(new Size(mainAxisExtent, constraints.maxHeight));
      break;
    case AxisDirection.down:
      final BoxConstraints innerConstraints = new BoxConstraints.tightFor(width: constraints.maxWidth);
      while (child != null) {
        child.layout(innerConstraints, parentUsesSize: true);
        final ListBodyParentData childParentData = child.parentData;
        childParentData.offset = new Offset(0.0, mainAxisExtent);
          mainAxisExtent += child.size.height;
        assert(child.parentData == childParentData);
        child = childParentData.nextSibling;
      }
      size = constraints.constrain(new Size(constraints.maxWidth, mainAxisExtent));
      break;
    case AxisDirection.up:
      final BoxConstraints innerConstraints = new BoxConstraints.tightFor(width: constraints.maxWidth);
      while (child != null) {
        child.layout(innerConstraints, parentUsesSize: true);
        final ListBodyParentData childParentData = child.parentData;
        mainAxisExtent += child.size.height;
        assert(child.parentData == childParentData);
        child = childParentData.nextSibling;
      }
      double position = 0.0;
      child = firstChild;
      while (child != null) {
        final ListBodyParentData childParentData = child.parentData;
        position += child.size.height;
        childParentData.offset = new Offset(0.0, mainAxisExtent - position);
        assert(child.parentData == childParentData);
        child = childParentData.nextSibling;
      }
      size = constraints.constrain(new Size(constraints.maxWidth, mainAxisExtent));
      break;
    }
    assert(size.isFinite);
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(new EnumProperty<AxisDirection>('axisDirection', axisDirection));
  }

  double _getIntrinsicCrossAxis(_ChildSizingFunction childSize) {
    double extent = 0.0;
    RenderBox child = firstChild;
    while (child != null) {
      extent = math.max(extent, childSize(child));
      final ListBodyParentData childParentData = child.parentData;
      child = childParentData.nextSibling;
    }
    return extent;
  }

  double _getIntrinsicMainAxis(_ChildSizingFunction childSize) {
    double extent = 0.0;
    RenderBox child = firstChild;
    while (child != null) {
      extent += childSize(child);
      final ListBodyParentData childParentData = child.parentData;
      child = childParentData.nextSibling;
    }
    return extent;
  }

  @override
  double computeMinIntrinsicWidth(double height) {
    assert(mainAxis != null);
    switch (mainAxis) {
      case Axis.horizontal:
        return _getIntrinsicMainAxis((RenderBox child) => child.getMinIntrinsicWidth(height));
      case Axis.vertical:
        return _getIntrinsicCrossAxis((RenderBox child) => child.getMinIntrinsicWidth(height));
    }
    return null;
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    assert(mainAxis != null);
    switch (mainAxis) {
      case Axis.horizontal:
        return _getIntrinsicMainAxis((RenderBox child) => child.getMaxIntrinsicWidth(height));
      case Axis.vertical:
        return _getIntrinsicCrossAxis((RenderBox child) => child.getMaxIntrinsicWidth(height));
    }
    return null;
  }

  @override
  double computeMinIntrinsicHeight(double width) {
    assert(mainAxis != null);
    switch (mainAxis) {
      case Axis.horizontal:
        return _getIntrinsicMainAxis((RenderBox child) => child.getMinIntrinsicHeight(width));
      case Axis.vertical:
        return _getIntrinsicCrossAxis((RenderBox child) => child.getMinIntrinsicHeight(width));
    }
    return null;
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    assert(mainAxis != null);
    switch (mainAxis) {
      case Axis.horizontal:
        return _getIntrinsicMainAxis((RenderBox child) => child.getMaxIntrinsicHeight(width));
      case Axis.vertical:
        return _getIntrinsicCrossAxis((RenderBox child) => child.getMaxIntrinsicHeight(width));
    }
    return null;
  }

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

  @override
  void paint(PaintingContext context, Offset offset) {
    defaultPaint(context, offset);
  }

  @override
  bool hitTestChildren(HitTestResult result, { Offset position }) {
    return defaultHitTestChildren(result, position: position);
  }

}
