// 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.

/// @docImport 'icon_button.dart';
/// @docImport 'navigation_rail.dart';
/// @docImport 'text_button.dart';
/// @docImport 'text_theme.dart';
library;

import 'dart:math' as math;

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

import 'badge_theme.dart';
import 'color_scheme.dart';
import 'theme.dart';

/// A Material Design "badge".
///
/// A badge's [label] conveys a small amount of information about its
/// [child], like a count or status. If the label is null then this is
/// a "small" badge that's displayed as a [smallSize] diameter filled
/// circle. Otherwise this is a [StadiumBorder] shaped "large" badge
/// with height [largeSize].
///
/// Badges are typically used to decorate the icon within a
/// [BottomNavigationBarItem] or a [NavigationRailDestination]
/// or a button's icon, as in [TextButton.icon]. The badge's default
/// configuration is intended to work well with a default sized (24)
/// [Icon].
///
/// {@tool dartpad}
/// This example shows how to create a [Badge] with label and count
/// wrapped on an icon in an [IconButton].
///
/// ** See code in examples/api/lib/material/badge/badge.0.dart **
/// {@end-tool}
class Badge extends StatelessWidget {
  /// Create a Badge that stacks [label] on top of [child].
  ///
  /// If [label] is null then just a filled circle is displayed. Otherwise
  /// the [label] is displayed within a [StadiumBorder] shaped area.
  const Badge({
    super.key,
    this.backgroundColor,
    this.textColor,
    this.smallSize,
    this.largeSize,
    this.textStyle,
    this.padding,
    this.alignment,
    this.offset,
    this.label,
    this.isLabelVisible = true,
    this.child,
  });

  /// Convenience constructor for creating a badge with a numeric label based on [count].
  ///
  /// Initializes [label] with a [Text] widget that shows:
  /// - the [count] value if it is less than or equal to [maxCount],
  /// - otherwise, shows '[maxCount]+'.
  ///
  /// For example, if [count] is 1000 and [maxCount] is 99, the label will display '99+'.
  Badge.count({
    super.key,
    this.backgroundColor,
    this.textColor,
    this.smallSize,
    this.largeSize,
    this.textStyle,
    this.padding,
    this.alignment,
    this.offset,
    required int count,
    int maxCount = 999,
    this.isLabelVisible = true,
    this.child,
  }) : assert(count >= 0, 'count must be non-negative'),
       assert(maxCount > 0, 'maxCount must be positive'),
       label = Text(count > maxCount ? '$maxCount+' : '$count');

  /// The badge's fill color.
  ///
  /// Defaults to the [BadgeTheme]'s background color, or
  /// [ColorScheme.error] if the theme value is null.
  final Color? backgroundColor;

  /// The color of the badge's [label] text.
  ///
  /// This color overrides the color of the label's [textStyle].
  ///
  /// Defaults to the [BadgeTheme]'s foreground color, or
  /// [ColorScheme.onError] if the theme value is null.
  final Color? textColor;

  /// The diameter of the badge if [label] is null.
  ///
  /// Defaults to the [BadgeTheme]'s small size, or 6 if the theme value
  /// is null.
  final double? smallSize;

  /// The badge's height if [label] is non-null.
  ///
  /// Defaults to the [BadgeTheme]'s large size, or 16 if the theme value
  /// is null. If the default value is overridden then it may be useful to
  /// also override [padding] and [alignment].
  final double? largeSize;

  /// The [DefaultTextStyle] for the badge's label.
  ///
  /// The text style's color is overwritten by the [textColor].
  ///
  /// This value is only used if [label] is non-null.
  ///
  /// Defaults to the [BadgeTheme]'s text style, or the overall theme's
  /// [TextTheme.labelSmall] if the badge theme's value is null. If
  /// the default text style is overridden then it may be useful to
  /// also override [largeSize], [padding], and [alignment].
  final TextStyle? textStyle;

  /// The padding added to the badge's label.
  ///
  /// This value is only used if [label] is non-null.
  ///
  /// Defaults to the [BadgeTheme]'s padding, or 4 pixels on the
  /// left and right if the theme's value is null.
  final EdgeInsetsGeometry? padding;

  /// Combined with [offset] to determine the location of the [label]
  /// relative to the [child].
  ///
  /// The alignment positions the label in the same way a child of an
  /// [Align] widget is positioned, except that, the alignment is
  /// resolved as if the label was a [largeSize] square and [offset]
  /// is added to the result.
  ///
  /// This value is only used if [label] is non-null.
  ///
  /// Defaults to the [BadgeTheme]'s alignment, or
  /// [AlignmentDirectional.topEnd] if the theme's value is null.
  final AlignmentGeometry? alignment;

  /// Combined with [alignment] to determine the location of the [label]
  /// relative to the [child].
  ///
  /// This value is only used if [label] is non-null.
  ///
  /// Defaults to the [BadgeTheme]'s offset, or
  /// if the theme's value is null then `Offset(4, -4)` for
  /// [TextDirection.ltr] or `Offset(-4, -4)` for [TextDirection.rtl].
  final Offset? offset;

  /// The badge's content, typically a [Text] widget that contains 1 to 4
  /// characters.
  ///
  /// If the label is null then this is a "small" badge that's
  /// displayed as a [smallSize] diameter filled circle. Otherwise
  /// this is a [StadiumBorder] shaped "large" badge with height [largeSize].
  final Widget? label;

  /// If false, the badge's [label] is not included.
  ///
  /// This flag is true by default. It's intended to make it convenient
  /// to create a badge that's only shown under certain conditions.
  final bool isLabelVisible;

  /// The widget that the badge is stacked on top of.
  ///
  /// Typically this is an default sized [Icon] that's part of a
  /// [BottomNavigationBarItem] or a [NavigationRailDestination].
  final Widget? child;

  @override
  Widget build(BuildContext context) {
    if (!isLabelVisible) {
      return child ?? const SizedBox();
    }

    final BadgeThemeData badgeTheme = BadgeTheme.of(context);
    final BadgeThemeData defaults = _BadgeDefaultsM3(context);
    final Decoration effectiveDecoration = ShapeDecoration(
      color: backgroundColor ?? badgeTheme.backgroundColor ?? defaults.backgroundColor!,
      shape: const StadiumBorder(),
    );
    final double effectiveWidthOffset;
    final Widget badge;
    final hasLabel = label != null;
    if (hasLabel) {
      final double minSize = effectiveWidthOffset =
          largeSize ?? badgeTheme.largeSize ?? defaults.largeSize!;
      badge = DefaultTextStyle(
        style: (textStyle ?? badgeTheme.textStyle ?? defaults.textStyle!).copyWith(
          color: textColor ?? badgeTheme.textColor ?? defaults.textColor!,
        ),
        child: _IntrinsicHorizontalStadium(
          minSize: minSize,
          child: Container(
            clipBehavior: Clip.antiAlias,
            decoration: effectiveDecoration,
            padding: padding ?? badgeTheme.padding ?? defaults.padding!,
            alignment: Alignment.center,
            child: label,
          ),
        ),
      );
    } else {
      final double effectiveSmallSize = effectiveWidthOffset =
          smallSize ?? badgeTheme.smallSize ?? defaults.smallSize!;
      badge = Container(
        width: effectiveSmallSize,
        height: effectiveSmallSize,
        clipBehavior: Clip.antiAlias,
        decoration: effectiveDecoration,
      );
    }

    if (child == null) {
      return badge;
    }

    final AlignmentGeometry effectiveAlignment =
        alignment ?? badgeTheme.alignment ?? defaults.alignment!;
    final TextDirection textDirection = Directionality.of(context);
    final defaultOffset = textDirection == TextDirection.ltr
        ? const Offset(4, -4)
        : const Offset(-4, -4);
    // Adds a offset const Offset(0, 8) to avoiding breaking customers after
    // the offset calculation changes.
    // See https://github.com/flutter/flutter/pull/146853.
    final Offset effectiveOffset =
        (offset ?? badgeTheme.offset ?? defaultOffset) + const Offset(0, 8);

    return Stack(
      clipBehavior: Clip.none,
      children: <Widget>[
        child!,
        Positioned.fill(
          child: _Badge(
            alignment: effectiveAlignment,
            offset: hasLabel ? effectiveOffset : Offset.zero,
            hasLabel: hasLabel,
            widthOffset: effectiveWidthOffset,
            textDirection: textDirection,
            child: badge,
          ),
        ),
      ],
    );
  }
}

class _Badge extends SingleChildRenderObjectWidget {
  const _Badge({
    required this.alignment,
    required this.offset,
    required this.widthOffset,
    required this.textDirection,
    required this.hasLabel,
    super.child, // the badge
  });

  final AlignmentGeometry alignment;
  final Offset offset;
  final double widthOffset;
  final TextDirection textDirection;
  final bool hasLabel;

  @override
  _RenderBadge createRenderObject(BuildContext context) {
    return _RenderBadge(
      alignment: alignment,
      widthOffset: widthOffset,
      hasLabel: hasLabel,
      offset: offset,
      textDirection: Directionality.maybeOf(context),
    );
  }

  @override
  void updateRenderObject(BuildContext context, _RenderBadge renderObject) {
    renderObject
      ..alignment = alignment
      ..offset = offset
      ..widthOffset = widthOffset
      ..hasLabel = hasLabel
      ..textDirection = Directionality.maybeOf(context);
  }

  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties) {
    super.debugFillProperties(properties);
    properties.add(DiagnosticsProperty<AlignmentGeometry>('alignment', alignment));
    properties.add(DiagnosticsProperty<Offset>('offset', offset));
  }
}

class _RenderBadge extends RenderAligningShiftedBox {
  _RenderBadge({
    super.textDirection,
    super.alignment,
    required Offset offset,
    required bool hasLabel,
    required double widthOffset,
  }) : _offset = offset,
       _hasLabel = hasLabel,
       _widthOffset = widthOffset;

  Offset get offset => _offset;
  Offset _offset;
  set offset(Offset value) {
    if (_offset == value) {
      return;
    }
    _offset = value;
    markNeedsLayout();
  }

  bool get hasLabel => _hasLabel;
  bool _hasLabel;
  set hasLabel(bool value) {
    if (_hasLabel == value) {
      return;
    }
    _hasLabel = value;
    markNeedsLayout();
  }

  double get widthOffset => _widthOffset;
  double _widthOffset;
  set widthOffset(double value) {
    if (_widthOffset == value) {
      return;
    }
    _widthOffset = value;
    markNeedsLayout();
  }

  @override
  void performLayout() {
    final BoxConstraints constraints = this.constraints;
    assert(constraints.hasBoundedWidth);
    assert(constraints.hasBoundedHeight);
    size = constraints.biggest;

    child!.layout(const BoxConstraints(), parentUsesSize: true);
    final double badgeSize = child!.size.height;
    final Alignment resolvedAlignment = alignment.resolve(textDirection);
    final childParentData = child!.parentData! as BoxParentData;
    Offset badgeLocation =
        offset + resolvedAlignment.alongOffset(Offset(size.width - widthOffset, size.height));
    if (hasLabel) {
      // Adjust for label height.
      badgeLocation = badgeLocation - Offset(0, badgeSize / 2);
    }
    childParentData.offset = badgeLocation;
  }

  @override
  @protected
  Size computeDryLayout(covariant BoxConstraints constraints) {
    // Mirrors performLayout: size is the tightest allowed (biggest) under bounded constraints.
    // Callers (e.g., Stack) pass in tight constraints for Positioned.fill; otherwise, this
    // is still consistent with performLayout which asserts bounded constraints.
    return constraints.biggest;
  }

  @override
  double? computeDryBaseline(BoxConstraints constraints, TextBaseline baseline) {
    final RenderBox? child = this.child;
    if (child == null) {
      return null;
    }

    // Child is laid out with unconstrained BoxConstraints in performLayout.
    const childConstraints = BoxConstraints();
    final double? childBaseline = child.getDryBaseline(childConstraints, baseline);
    if (childBaseline == null) {
      return null;
    }

    // Mirror the paint offset logic from performLayout using dry sizes only.
    final Size mySize = getDryLayout(constraints);
    final Alignment resolvedAlignment = alignment.resolve(textDirection);
    final Size childSize = child.getDryLayout(childConstraints);

    Offset badgeLocation =
        offset + resolvedAlignment.alongOffset(Offset(mySize.width - widthOffset, mySize.height));
    if (hasLabel) {
      // Subtract half of the badge height when we have a label (as in performLayout).
      badgeLocation -= Offset(0, childSize.height / 2);
    }

    return childBaseline + badgeLocation.dy;
  }
}

/// A widget size itself to the smallest horizontal stadium rect that can still
/// fit the child's intrinsic size.
///
/// A horizontal stadium means a rect that has width >= height.
///
/// Uses [minSize] to set the min size of width and height.
class _IntrinsicHorizontalStadium extends SingleChildRenderObjectWidget {
  const _IntrinsicHorizontalStadium({super.child, required this.minSize});
  final double minSize;

  @override
  _RenderIntrinsicHorizontalStadium createRenderObject(BuildContext context) {
    return _RenderIntrinsicHorizontalStadium(minSize: minSize);
  }
}

class _RenderIntrinsicHorizontalStadium extends RenderProxyBox {
  _RenderIntrinsicHorizontalStadium({RenderBox? child, required double minSize})
    : _minSize = minSize,
      super(child);

  double get minSize => _minSize;
  double _minSize;
  set minSize(double value) {
    if (_minSize == value) {
      return;
    }
    _minSize = value;
    markNeedsLayout();
  }

  @override
  double computeMinIntrinsicWidth(double height) {
    return getMaxIntrinsicWidth(height);
  }

  @override
  double computeMaxIntrinsicWidth(double height) {
    return math.max(getMaxIntrinsicHeight(double.infinity), super.computeMaxIntrinsicWidth(height));
  }

  @override
  double computeMinIntrinsicHeight(double width) {
    return getMaxIntrinsicHeight(width);
  }

  @override
  double computeMaxIntrinsicHeight(double width) {
    return math.max(minSize, super.computeMaxIntrinsicHeight(width));
  }

  BoxConstraints _childConstraints(RenderBox child, BoxConstraints constraints) {
    final double childHeight = math.max(minSize, child.getMaxIntrinsicHeight(constraints.maxWidth));
    final double childWidth = child.getMaxIntrinsicWidth(constraints.maxHeight);
    return constraints.tighten(width: math.max(childWidth, childHeight), height: childHeight);
  }

  Size _computeSize({required ChildLayouter layoutChild, required BoxConstraints constraints}) {
    final RenderBox child = this.child!;
    final Size childSize = layoutChild(child, _childConstraints(child, constraints));
    if (childSize.height > childSize.width) {
      return Size(childSize.height, childSize.height);
    }
    return childSize;
  }

  @override
  @protected
  Size computeDryLayout(covariant BoxConstraints constraints) {
    return _computeSize(layoutChild: ChildLayoutHelper.dryLayoutChild, constraints: constraints);
  }

  @override
  double? computeDryBaseline(BoxConstraints constraints, TextBaseline baseline) {
    final RenderBox child = this.child!;
    return child.getDryBaseline(_childConstraints(child, constraints), baseline);
  }

  @override
  void performLayout() {
    size = _computeSize(layoutChild: ChildLayoutHelper.layoutChild, constraints: constraints);
  }
}

// BEGIN GENERATED TOKEN PROPERTIES - Badge

// Do not edit by hand. The code between the "BEGIN GENERATED" and
// "END GENERATED" comments are generated from data in the Material
// Design token database by the script:
//   dev/tools/gen_defaults/bin/gen_defaults.dart.

// dart format off
class _BadgeDefaultsM3 extends BadgeThemeData {
  _BadgeDefaultsM3(this.context) : super(
    smallSize: 6.0,
    largeSize: 16.0,
    padding: const EdgeInsets.symmetric(horizontal: 4),
    alignment: AlignmentDirectional.topEnd,
  );

  final BuildContext context;
  late final ThemeData _theme = Theme.of(context);
  late final ColorScheme _colors = _theme.colorScheme;

  @override
  Color? get backgroundColor => _colors.error;

  @override
  Color? get textColor => _colors.onError;

  @override
  TextStyle? get textStyle => Theme.of(context).textTheme.labelSmall;
}
// dart format on

// END GENERATED TOKEN PROPERTIES - Badge
