// 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/rendering.dart';

import 'basic.dart';
import 'framework.dart';

/// Defines the horizontal alignment of [OverflowBar] children
/// when they're laid out in an overflow column.
///
/// This value must be interpreted relative to the ambient
/// [TextDirection].
enum OverflowBarAlignment {
  /// Each child is left-aligned for [TextDirection.ltr],
  /// right-aligned for [TextDirection.rtl].
  start,

  /// Each child is right-aligned for [TextDirection.ltr],
  /// left-aligned for [TextDirection.rtl].
  end,

  /// Each child is horizontally centered.
  center,
}

/// A widget that lays out its [children] in a row unless they
/// "overflow" the available horizontal space, in which case it lays
/// them out in a column instead.
///
/// This widget's width will expand to contain its children and the
/// specified [spacing] until it overflows. The overflow column will
/// consume all of the available width.  The [overflowAlignment]
/// defines how each child will be aligned within the overflow column
/// and the [overflowSpacing] defines the gap between each child.
///
/// The order that the children appear in the horizontal layout
/// is defined by the [textDirection], just like the [Row] widget.
/// If the layout overflows, then children's order within their
/// column is specified by [overflowDirection] instead.
///
/// {@tool dartpad --template=stateless_widget_scaffold_center}
///
/// This example defines a simple approximation of a dialog
/// layout, where the layout of the dialog's action buttons are
/// defined by an [OverflowBar]. The content is wrapped in a
/// [SingleChildScrollView], so that if overflow occurs, the
/// action buttons will still be accessible by scrolling,
/// no matter how much vertical space is available.
///
/// ```dart
/// Widget build(BuildContext context) {
///   return Container(
///     alignment: Alignment.center,
///     padding: const EdgeInsets.all(16),
///     color: Colors.black.withOpacity(0.15),
///     child: Material(
///       color: Colors.white,
///       elevation: 24,
///       shape: const RoundedRectangleBorder(
///         borderRadius: BorderRadius.all(Radius.circular(4))
///       ),
///       child: Padding(
///         padding: const EdgeInsets.all(8),
///         child: SingleChildScrollView(
///           child: Column(
///             mainAxisSize: MainAxisSize.min,
///             crossAxisAlignment: CrossAxisAlignment.stretch,
///             children: <Widget>[
///               const SizedBox(height: 128, child: Placeholder()),
///               Align(
///                 alignment: AlignmentDirectional.centerEnd,
///                 child: OverflowBar(
///                   spacing: 8,
///                   overflowAlignment: OverflowBarAlignment.end,
///                   children: <Widget>[
///                     TextButton(child: const Text('Cancel'), onPressed: () { }),
///                     TextButton(child: const Text('Really Really Cancel'), onPressed: () { }),
///                     OutlinedButton(child: const Text('OK'), onPressed: () { }),
///                   ],
///                 ),
///               ),
///             ],
///           ),
///         ),
///       ),
///     ),
///   );
/// }
/// ```
/// {@end-tool}
class OverflowBar extends MultiChildRenderObjectWidget {
  /// Constructs an OverflowBar.
  ///
  /// The [spacing], [overflowSpacing], [overflowAlignment],
  /// [overflowDirection], and [clipBehavior] parameters must not be
  /// null. The [children] argument must not be null and must not contain
  /// any null objects.
  OverflowBar({
    Key? key,
    this.spacing = 0.0,
    this.alignment,
    this.overflowSpacing = 0.0,
    this.overflowAlignment = OverflowBarAlignment.start,
    this.overflowDirection = VerticalDirection.down,
    this.textDirection,
    this.clipBehavior = Clip.none,
    List<Widget> children = const <Widget>[],
  }) : assert(spacing != null),
       assert(overflowSpacing != null),
       assert(overflowAlignment != null),
       assert(overflowDirection != null),
       assert(clipBehavior != null),
       super(key: key, children: children);

  /// The width of the gap between [children] for the default
  /// horizontal layout.
  ///
  /// If the horizontal layout overflows, then [overflowSpacing] is
  /// used instead.
  ///
  /// Defaults to 0.0.
  final double spacing;

  /// Defines the [children]'s horizontal layout according to the same
  /// rules as for [Row.mainAxisAlignment].
  ///
  /// If this property is non-null, and the [children], separated by
  /// [spacing], fit within the available width, then the overflow
  /// bar will be as wide as possible. If the children do not fit
  /// within the available width, then this property is ignored and
  /// [overflowAlignment] applies instead.
  ///
  /// If this property is null (the default) then the overflow bar
  /// will be no wider than needed to layout the [children] separated
  /// by [spacing], modulo the incoming constraints.
  ///
  /// If [alignment] is one of [MainAxisAlignment.spaceAround],
  /// [MainAxisAlignment.spaceBetween], or
  /// [MainAxisAlignment.spaceEvenly], then the [spacing] parameter is
  /// only used to see if the horizontal layout will overflow.
  ///
  /// Defaults to null.
  ///
  /// See also:
  ///
  ///  * [overflowAlignment], the horizontal alignment of the [children] within
  ///    the vertical "overflow" layout.
  ///
  final MainAxisAlignment? alignment;

  /// The height of the gap between [children] in the vertical
  /// "overflow" layout.
  ///
  /// This parameter is only used if the horizontal layout overflows, i.e.
  /// if there isn't enough horizontal room for the [children] and [spacing].
  ///
  /// Defaults to 0.0.
  ///
  /// See also:
  ///
  ///  * [spacing], The width of the gap between each pair of children
  ///    for the default horizontal layout.
  final double overflowSpacing;

  /// The horizontal alignment of the [children] within the vertical
  /// "overflow" layout.
  ///
  /// This parameter is only used if the horizontal layout overflows, i.e.
  /// if there isn't enough horizontal room for the [children] and [spacing].
  /// In that case the overflow bar will expand to fill the available
  /// width and it will layout its [children] in a column. The
  /// horizontal alignment of each child within that column is
  /// defined by this parameter and the [textDirection]. If the
  /// [textDirection] is [TextDirection.ltr] then each child will be
  /// aligned with the left edge of the available space for
  /// [OverflowBarAlignment.start], with the right edge of the
  /// available space for [OverflowBarAlignment.end]. Similarly, if the
  /// [textDirection] is [TextDirection.rtl] then each child will
  /// be aligned with the right edge of the available space for
  /// [OverflowBarAlignment.start], and with the left edge of the
  /// available space for [OverflowBarAlignment.end]. For
  /// [OverflowBarAlignment.center] each child is horizontally
  /// centered within the available space.
  ///
  /// Defaults to [OverflowBarAlignment.start].
  ///
  /// See also:
  ///
  ///  * [alignment], which defines the [children]'s horizontal layout
  ///    (according to the same rules as for [Row.mainAxisAlignment]) when
  ///    the children, separated by [spacing], fit within the available space.
  ///  * [overflowDirection], which defines the order that the
  ///    [OverflowBar]'s children appear in, if the horizontal layout
  ///    overflows.
  final OverflowBarAlignment overflowAlignment;

  /// Defines the order that the [children] appear in, if
  /// the horizontal layout overflows.
  ///
  /// This parameter is only used if the horizontal layout overflows, i.e.
  /// if there isn't enough horizontal room for the [children] and [spacing].
  ///
  /// If the children do not fit into a single row, then they
  /// are arranged in a column. The first child is at the top of the
  /// column if this property is set to [VerticalDirection.down], since it
  /// "starts" at the top and "ends" at the bottom. On the other hand,
  /// the first child will be at the bottom of the column if this
  /// property is set to [VerticalDirection.up], since it "starts" at the
  /// bottom and "ends" at the top.
  ///
  /// Defaults to [VerticalDirection.down].
  ///
  /// See also:
  ///
  ///  * [overflowAlignment], which defines the horizontal alignment
  ///    of the children within the vertical "overflow" layout.
  final VerticalDirection overflowDirection;

  /// Determines the order that the [children] appear in for the default
  /// horizontal layout, and the interpretation of
  /// [OverflowBarAlignment.start] and [OverflowBarAlignment.end] for
  /// the vertical overflow layout.
  ///
  /// For the default horizontal layout, if [textDirection] is
  /// [TextDirection.rtl] then the last child is laid out first. If
  /// [textDirection] is [TextDirection.ltr] then the first child is
  /// laid out first.
  ///
  /// If this parameter is null, then the value of
  /// `Directionality.of(context)` is used.
  ///
  /// See also:
  ///
  ///  * [overflowDirection], which defines the order that the
  ///    [OverflowBar]'s children appear in, if the horizontal layout
  ///    overflows.
  ///  * [Directionality], which defines the ambient directionality of
  ///    text and text-direction-sensitive render objects.
  final TextDirection? textDirection;

  /// {@macro flutter.material.Material.clipBehavior}
  ///
  /// Defaults to [Clip.none], and must not be null.
  final Clip clipBehavior;

  @override
  RenderObject createRenderObject(BuildContext context) {
    return _RenderOverflowBar(
      spacing: spacing,
      alignment: alignment,
      overflowSpacing: overflowSpacing,
      overflowAlignment: overflowAlignment,
      overflowDirection: overflowDirection,
      textDirection: textDirection ?? Directionality.of(context),
      clipBehavior: clipBehavior,
    );
  }

  @override
  void updateRenderObject(BuildContext context, RenderObject renderObject) {
    (renderObject as _RenderOverflowBar)
      ..spacing = spacing
      ..alignment = alignment
      ..overflowSpacing = overflowSpacing
      ..overflowAlignment = overflowAlignment
      ..overflowDirection = overflowDirection
      ..textDirection = textDirection ?? Directionality.of(context)
      ..clipBehavior = clipBehavior;
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DoubleProperty('spacing', spacing, defaultValue: 0));
    properties.add(EnumProperty<MainAxisAlignment>('alignment', alignment, defaultValue: null));
    properties.add(DoubleProperty('overflowSpacing', overflowSpacing, defaultValue: 0));
    properties.add(EnumProperty<OverflowBarAlignment>('overflowAlignment', overflowAlignment, defaultValue: OverflowBarAlignment.start));
    properties.add(EnumProperty<VerticalDirection>('overflowDirection', overflowDirection, defaultValue: VerticalDirection.down));
    properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
  }
}

class _OverflowBarParentData extends ContainerBoxParentData<RenderBox> { }

class _RenderOverflowBar extends RenderBox
    with ContainerRenderObjectMixin<RenderBox, _OverflowBarParentData>,
         RenderBoxContainerDefaultsMixin<RenderBox, _OverflowBarParentData> {
  _RenderOverflowBar({
    List<RenderBox>? children,
    double spacing = 0.0,
    MainAxisAlignment? alignment,
    double overflowSpacing = 0.0,
    OverflowBarAlignment overflowAlignment = OverflowBarAlignment.start,
    VerticalDirection overflowDirection = VerticalDirection.down,
    required TextDirection textDirection,
    Clip clipBehavior = Clip.none,
  }) : assert(spacing != null),
       assert(overflowSpacing != null),
       assert(overflowAlignment != null),
       assert(textDirection != null),
       assert(clipBehavior != null),
       _spacing = spacing,
       _alignment = alignment,
       _overflowSpacing = overflowSpacing,
       _overflowAlignment = overflowAlignment,
       _overflowDirection = overflowDirection,
       _textDirection = textDirection,
       _clipBehavior = clipBehavior {
    addAll(children);
  }

  double get spacing => _spacing;
  double _spacing;
  set spacing (double value) {
    assert(value != null);
    if (_spacing == value)
      return;
    _spacing = value;
    markNeedsLayout();
  }

  MainAxisAlignment? get alignment => _alignment;
  MainAxisAlignment? _alignment;
  set alignment (MainAxisAlignment? value) {
    if (_alignment == value)
      return;
    _alignment = value;
    markNeedsLayout();
  }

  double get overflowSpacing => _overflowSpacing;
  double _overflowSpacing;
  set overflowSpacing (double value) {
    assert(value != null);
    if (_overflowSpacing == value)
      return;
    _overflowSpacing = value;
    markNeedsLayout();
  }

  OverflowBarAlignment get overflowAlignment => _overflowAlignment;
  OverflowBarAlignment _overflowAlignment;
  set overflowAlignment (OverflowBarAlignment value) {
    assert(value != null);
    if (_overflowAlignment == value)
      return;
    _overflowAlignment = value;
    markNeedsLayout();
  }

  VerticalDirection get overflowDirection => _overflowDirection;
  VerticalDirection _overflowDirection;
  set overflowDirection (VerticalDirection value) {
    assert(value != null);
    if (_overflowDirection == value)
      return;
    _overflowDirection = value;
    markNeedsLayout();
  }

  TextDirection get textDirection => _textDirection;
  TextDirection _textDirection;
  set textDirection(TextDirection value) {
    if (_textDirection == value)
      return;
    _textDirection = value;
    markNeedsLayout();
  }

  Clip get clipBehavior => _clipBehavior;
  Clip _clipBehavior = Clip.none;
  set clipBehavior(Clip value) {
    assert(value != null);
    if (value == _clipBehavior)
      return;
    _clipBehavior = value;
    markNeedsPaint();
    markNeedsSemanticsUpdate();
  }

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

  @override
  double computeMinIntrinsicHeight(double width) {
    RenderBox? child = firstChild;
    if (child == null)
      return 0;
    double barWidth = 0.0;
    while (child != null) {
      barWidth += child.getMinIntrinsicWidth(double.infinity);
      child = childAfter(child);
    }
    barWidth += spacing * (childCount - 1);

    double height = 0.0;
    if (barWidth > width) {
      child = firstChild;
      while (child != null) {
        height += child.getMinIntrinsicHeight(width);
        child = childAfter(child);
      }
      return height + overflowSpacing * (childCount - 1);
    } else {
      child = firstChild;
      while (child != null) {
        height = math.max(height, child.getMinIntrinsicHeight(width));
        child = childAfter(child);
      }
      return height;
    }
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    RenderBox? child = firstChild;
    if (child == null)
      return 0;
    double barWidth = 0.0;
    while (child != null) {
      barWidth += child.getMinIntrinsicWidth(double.infinity);
      child = childAfter(child);
    }
    barWidth += spacing * (childCount - 1);

    double height = 0.0;
    if (barWidth > width) {
      child = firstChild;
      while (child != null) {
        height += child.getMaxIntrinsicHeight(width);
        child = childAfter(child);
      }
      return height + overflowSpacing * (childCount - 1);
    } else {
      child = firstChild;
      while (child != null) {
        height = math.max(height, child.getMaxIntrinsicHeight(width));
        child = childAfter(child);
      }
      return height;
    }
  }

  @override
  double computeMinIntrinsicWidth(double height) {
    RenderBox? child = firstChild;
    if (child == null)
      return 0;
    double width = 0.0;
    while (child != null) {
      width += child.getMinIntrinsicWidth(double.infinity);
      child = childAfter(child);
    }
    return width + spacing * (childCount - 1);
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    RenderBox? child = firstChild;
    if (child == null)
      return 0;
    double width = 0.0;
    while (child != null) {
      width += child.getMaxIntrinsicWidth(double.infinity);
      child = childAfter(child);
    }
    return width + spacing * (childCount - 1);
  }

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

  @override
  Size computeDryLayout(BoxConstraints constraints) {
    RenderBox? child = firstChild;
    if (child == null) {
      return constraints.smallest;
    }
    final BoxConstraints childConstraints = constraints.loosen();
    double childrenWidth = 0.0;
    double maxChildHeight = 0.0;
    double y = 0.0;
    while (child != null) {
      final Size childSize = child.getDryLayout(childConstraints);
      childrenWidth += childSize.width;
      maxChildHeight = math.max(maxChildHeight, childSize.height);
      y += childSize.height + overflowSpacing;
      child = childAfter(child);
    }
    final double actualWidth = childrenWidth + spacing * (childCount - 1);
    if (actualWidth > constraints.maxWidth) {
      return constraints.constrain(Size(constraints.maxWidth, y - overflowSpacing));
    } else {
      final double overallWidth = alignment == null ? actualWidth : constraints.maxWidth;
      return constraints.constrain(Size(overallWidth, maxChildHeight));
    }
  }

  @override
  void performLayout() {
    RenderBox? child = firstChild;
    if (child == null) {
      size = constraints.smallest;
      return;
    }

    final BoxConstraints childConstraints = constraints.loosen();
    double childrenWidth = 0;
    double maxChildHeight = 0;
    double maxChildWidth = 0;

    while (child != null) {
      child.layout(childConstraints, parentUsesSize: true);
      childrenWidth += child.size.width;
      maxChildHeight = math.max(maxChildHeight, child.size.height);
      maxChildWidth = math.max(maxChildWidth, child.size.width);
      child = childAfter(child);
    }

    final bool rtl = textDirection == TextDirection.rtl;
    final double actualWidth = childrenWidth + spacing * (childCount - 1);

    if (actualWidth > constraints.maxWidth) {
      // Overflow vertical layout
      child = overflowDirection == VerticalDirection.down ? firstChild : lastChild;
      RenderBox? nextChild() => overflowDirection == VerticalDirection.down ? childAfter(child!) : childBefore(child!);
      double y = 0;
      while (child != null) {
        final _OverflowBarParentData childParentData = child.parentData! as _OverflowBarParentData;
        double x = 0;
        switch (overflowAlignment) {
          case OverflowBarAlignment.start:
            x = rtl ? constraints.maxWidth - child.size.width : 0;
            break;
          case OverflowBarAlignment.center:
            x = (constraints.maxWidth - child.size.width) / 2;
            break;
          case OverflowBarAlignment.end:
            x = rtl ? 0 : constraints.maxWidth - child.size.width;
            break;
        }
        assert(x != null);
        childParentData.offset = Offset(x, y);
        y += child.size.height + overflowSpacing;
        child = nextChild();
      }
      size = constraints.constrain(Size(constraints.maxWidth, y - overflowSpacing));
    } else {
      // Default horizontal layout
      child = firstChild;
      final double firstChildWidth = child!.size.width;
      final double overallWidth = alignment == null ? actualWidth : constraints.maxWidth;
      size = constraints.constrain(Size(overallWidth, maxChildHeight));

      late double x; // initial value: origin of the first child
      double layoutSpacing = spacing; // space between children
      switch (alignment) {
        case null:
          x = rtl ? size.width - firstChildWidth : 0;
          break;
        case MainAxisAlignment.start:
          x = rtl ? size.width - firstChildWidth : 0;
          break;
        case MainAxisAlignment.center:
          final double halfRemainingWidth = (size.width - actualWidth) / 2;
          x = rtl ? size.width - halfRemainingWidth - firstChildWidth : halfRemainingWidth;
          break;
        case MainAxisAlignment.end:
          x = rtl ? actualWidth - firstChildWidth : size.width - actualWidth;
          break;
        case MainAxisAlignment.spaceBetween:
          layoutSpacing = (size.width - childrenWidth) / (childCount - 1);
          x = rtl ? size.width - firstChildWidth : 0;
          break;
        case MainAxisAlignment.spaceAround:
          layoutSpacing = childCount > 0 ? (size.width - childrenWidth) / childCount : 0;
          x = rtl ? size.width - layoutSpacing / 2 - firstChildWidth : layoutSpacing / 2;
          break;
        case MainAxisAlignment.spaceEvenly:
          layoutSpacing = (size.width - childrenWidth) / (childCount + 1);
          x = rtl ? size.width - layoutSpacing - firstChildWidth : layoutSpacing;
          break;
      }

      while (child != null) {
        final _OverflowBarParentData childParentData = child.parentData! as _OverflowBarParentData;
        childParentData.offset = Offset(x, (maxChildHeight - child.size.height) / 2);
        // x is the horizontal origin of child. To advance x to the next child's
        // origin for LTR: add the width of the current child. To advance x to
        // the origin of the next child for RTL: subtract the width of the next
        // child (if there is one).
        if (!rtl) {
          x += child.size.width + layoutSpacing;
        }
        child = childAfter(child);
        if (rtl && child != null) {
          x -= child.size.width + layoutSpacing;
        }
      }
    }
  }

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

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

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DoubleProperty('spacing', spacing, defaultValue: 0));
    properties.add(DoubleProperty('overflowSpacing', overflowSpacing, defaultValue: 0));
    properties.add(EnumProperty<OverflowBarAlignment>('overflowAlignment', overflowAlignment, defaultValue: OverflowBarAlignment.start));
    properties.add(EnumProperty<VerticalDirection>('overflowDirection', overflowDirection, defaultValue: VerticalDirection.down));
    properties.add(EnumProperty<TextDirection>('textDirection', textDirection, defaultValue: null));
  }
}
