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

// For OneChildLayoutDelegate and RenderCustomOneChildLayoutBox, see shifted_box.dart

class MultiChildLayoutParentData extends ContainerBoxParentDataMixin<RenderBox> {
  /// An object representing the identity of this child.
  Object id;

  String toString() => '${super.toString()}; id=$id';
}

/// A delegate that controls the layout of multiple children.
abstract class MultiChildLayoutDelegate {
  Map<Object, RenderBox> _idToChild;
  Set<RenderBox> _debugChildrenNeedingLayout;

  /// Returns the size of this object given the incoming constraints.
  /// The size cannot reflect the instrinsic sizes of the children.
  /// If this layout has a fixed width or height the returned size
  /// can reflect that.
  Size getSize(BoxConstraints constraints) => constraints.biggest;

  /// True if a non-null LayoutChild was provided for the specified id.
  bool isChild(Object childId) => _idToChild[childId] != null;

  /// Ask the child to update its layout within the limits specified by
  /// the constraints parameter. The child's size is returned.
  Size layoutChild(Object childId, BoxConstraints constraints) {
    final RenderBox child = _idToChild[childId];
    assert(() {
      if (child == null) {
        throw new RenderingError(
          'The $this custom multichild layout delegate tried to lay out a non-existent child:\n'
          'There is no child with the id "$childId".'
        );
      }
      if (!_debugChildrenNeedingLayout.remove(child)) {
        throw new RenderingError(
          'The $this custom multichild layout delegate tried to lay out the child with id "$childId" more than once.\n'
          'Each child must be laid out exactly once.'
        );
      }
      try {
        assert(constraints.debugAssertIsNormalized);
      } on AssertionError catch (exception) {
        throw new RenderingError(
          'The $this custom multichild layout delegate provided invalid box constraints for the child with id "$childId":\n'
          '$exception\n'
          'The minimum width and height must be greater than or equal to zero.\n'
          'The maximum width must be greater than or equal to the minimum width.\n'
          'The maximum height must be greater than or equal to the minimum height.'
        );
      }
      return true;
    });
    child.layout(constraints, parentUsesSize: true);
    return child.size;
  }

  /// Specify the child's origin relative to this origin.
  void positionChild(Object childId, Offset offset) {
    final RenderBox child = _idToChild[childId];
    assert(() {
      if (child == null) {
        throw new RenderingError(
          'The $this custom multichild layout delegate tried to position out a non-existent child:\n'
          'There is no child with the id "$childId".'
        );
      }
      if (offset == null) {
        throw new RenderingError(
          'The $this custom multichild layout delegate provided a null position for the child with id "$childId".'
        );
      }
      return true;
    });
    final MultiChildLayoutParentData childParentData = child.parentData;
    childParentData.offset = offset;
  }

  String _debugDescribeChild(RenderBox child) {
    final MultiChildLayoutParentData childParentData = child.parentData;
    return '${childParentData.id}: $child';
  }

  void _callPerformLayout(Size size, BoxConstraints constraints, RenderBox firstChild) {
    final Map<Object, RenderBox> previousIdToChild = _idToChild;

    Set<RenderBox> debugPreviousChildrenNeedingLayout;
    assert(() {
      debugPreviousChildrenNeedingLayout = _debugChildrenNeedingLayout;
      _debugChildrenNeedingLayout = new Set<RenderBox>();
      return true;
    });

    try {
      _idToChild = new Map<Object, RenderBox>();
      RenderBox child = firstChild;
      while (child != null) {
        final MultiChildLayoutParentData childParentData = child.parentData;
        assert(() {
          if (childParentData.id == null) {
            throw new RenderingError(
              'The following child has no ID:\n'
              '  $child\n'
              'Every child of a RenderCustomMultiChildLayoutBox must have an ID in its parent data.'
            );
          }
          return true;
        });
        _idToChild[childParentData.id] = child;
        assert(() {
          _debugChildrenNeedingLayout.add(child);
          return true;
        });
        child = childParentData.nextSibling;
      }
      performLayout(size, constraints);
      assert(() {
        if (_debugChildrenNeedingLayout.isNotEmpty) {
          if (_debugChildrenNeedingLayout.length > 1) {
            throw new RenderingError(
              'The $this custom multichild layout delegate forgot to lay out the following children:\n'
              '  ${_debugChildrenNeedingLayout.map(_debugDescribeChild).join("\n  ")}\n'
              'Each child must be laid out exactly once.'
            );
          } else {
            throw new RenderingError(
              'The $this custom multichild layout delegate forgot to lay out the following child:\n'
              '  ${_debugDescribeChild(_debugChildrenNeedingLayout.single)}\n'
              'Each child must be laid out exactly once.'
            );
          }
        }
        return true;
      });
    } finally {
      _idToChild = previousIdToChild;
      assert(() {
        _debugChildrenNeedingLayout = debugPreviousChildrenNeedingLayout;
        return true;
      });
    }
  }

  /// Override this method to return true when the children need to be laid out.
  bool shouldRelayout(MultiChildLayoutDelegate oldDelegate);

  /// Layout and position all children given this widget's size and the specified
  /// constraints. This method must apply layoutChild() to each child. It should
  /// specify the final position of each child with positionChild().
  void performLayout(Size size, BoxConstraints constraints);

  String toString() => '$runtimeType';
}

/// Defers the layout of multiple children to a delegate.
///
/// The delegate can determine the layout constraints for each child and can
/// decide where to position each child. The delegate can also determine the
/// size of the parent, but the size of the parent cannot depend on the sizes of
/// the children.
class RenderCustomMultiChildLayoutBox extends RenderBox
  with ContainerRenderObjectMixin<RenderBox, MultiChildLayoutParentData>,
       RenderBoxContainerDefaultsMixin<RenderBox, MultiChildLayoutParentData> {
  RenderCustomMultiChildLayoutBox({
    List<RenderBox> children,
    MultiChildLayoutDelegate delegate
  }) : _delegate = delegate {
    assert(delegate != null);
    addAll(children);
  }

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

  /// The delegate that controls the layout of the children.
  MultiChildLayoutDelegate get delegate => _delegate;
  MultiChildLayoutDelegate _delegate;
  void set delegate (MultiChildLayoutDelegate newDelegate) {
    assert(newDelegate != null);
    if (_delegate == newDelegate)
      return;
    if (newDelegate.runtimeType != _delegate.runtimeType || newDelegate.shouldRelayout(_delegate))
      markNeedsLayout();
    _delegate = newDelegate;
  }

  Size _getSize(BoxConstraints constraints) {
    assert(constraints.debugAssertIsNormalized);
    return constraints.constrain(_delegate.getSize(constraints));
  }

  double getMinIntrinsicWidth(BoxConstraints constraints) {
    return _getSize(constraints).width;
  }

  double getMaxIntrinsicWidth(BoxConstraints constraints) {
    return _getSize(constraints).width;
  }

  double getMinIntrinsicHeight(BoxConstraints constraints) {
    return _getSize(constraints).height;
  }

  double getMaxIntrinsicHeight(BoxConstraints constraints) {
    return _getSize(constraints).height;
  }

  bool get sizedByParent => true;

  void performResize() {
    size = _getSize(constraints);
  }

  void performLayout() {
    delegate._callPerformLayout(size, constraints, firstChild);
  }

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

  bool hitTestChildren(HitTestResult result, { Point position }) {
    return defaultHitTestChildren(result, position: position);
  }
}
