// 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:ui' show TextLeadingDistribution;

import 'package:flutter/foundation.dart';

import 'basic_types.dart';
import 'text_style.dart';

/// Defines the strut, which sets the minimum height a line can be
/// relative to the baseline.
///
/// Strut applies to all lines in the paragraph. Strut is a feature that
/// allows minimum line heights to be set. The effect is as if a zero
/// width space was included at the beginning of each line in the
/// paragraph. This imaginary space is 'shaped' according the properties
/// defined in this class. Flutter's strut is based on
/// [typesetting strut](https://en.wikipedia.org/wiki/Strut_(typesetting))
/// and CSS's [line-height](https://www.w3.org/TR/CSS2/visudet.html#line-height).
///
/// No lines may be shorter than the strut. The ascent and descent of the
/// strut are calculated, and any laid out text that has a shorter ascent or
/// descent than the strut's ascent or descent will take the ascent and
/// descent of the strut. Text with ascents or descents larger than the
/// strut's ascent or descent will layout as normal and extend past the strut.
///
/// Strut is defined independently from any text content or [TextStyle]s.
///
/// The vertical components of strut are as follows:
///
///  * Half the font-defined leading
///  * `ascent * height`
///  * `descent * height`
///  * Half the font-defined leading
///
/// The sum of these four values is the total height of the line.
///
/// Ascent is the font's spacing above the baseline without leading and
/// descent is the spacing below the baseline without leading. Leading is
/// split evenly between the top and bottom. The values for `ascent` and
/// `descent` are provided by the font named by [fontFamily]. If no
/// [fontFamily] or [fontFamilyFallback] is provided, then the platform's
/// default family will be used. Many fonts will have leading values of
/// zero, so in practice, the leading component is often irrelevant.
///
/// When [height] is omitted or null, then the font defined ascent and descent
/// will be used. The font's combined ascent and descent may be taller or
/// shorter than the [fontSize]. When [height] is provided, the line's EM-square
/// ascent and descent (which sums to [fontSize]) will be scaled by [height] to
/// achieve a final line height of `fontSize * height + fontSize * leading`
/// logical pixels. The proportion of ascent:descent with [height] specified
/// is the same as the font metrics defined ascent:descent ratio.
///
/// ![Text height diagram](https://flutter.github.io/assets-for-api-docs/assets/painting/text_height_diagram.png)
///
/// Each line's spacing above the baseline will be at least as tall as the
/// half leading plus ascent. Each line's spacing below the baseline will
/// be at least as tall as the half leading plus descent.
///
/// See also:
///
///  * [StrutStyle](dart-ui/StrutStyle-class.html), the class in the [dart:ui] library.
///
/// ### Fields and their default values.

// ///////////////////////////////////////////////////////////////////////////
// The defaults are noted here for convenience. The actual place where they //
// are defined is in the engine paragraph_style.h of LibTxt. The values here//
// should be updated should it change in the engine. The engine specifies   //
// the defaults in order to reduce the amount of data we pass to native as  //
// strut will usually be unspecified.                                       //
// ///////////////////////////////////////////////////////////////////////////

///
/// Omitted or null properties will take the default values specified below:
///
///  * [fontFamily]: the name of the font to use when calculating the strut
///    (e.g., Roboto). No glyphs from the font will be drawn and the font will
///    be used purely for metrics.
///
///  * [fontFamilyFallback]: an ordered list of font family names that will
///    be searched for when the font in [fontFamily] cannot be found. When
///    all specified font families have been exhausted an no match was found,
///    the default platform font will be used.
///
///  * [fontSize]: the size of the ascent plus descent in logical pixels. This
///    is also used as the basis of the custom leading calculation. This value
///    cannot be negative.
///    Default is 14 logical pixels.
///
///  * [height]: the multiple of [fontSize] the line's height should be.
///    The line's height will take the font's ascent and descent values if
///    [height] is omitted or null. If provided, the EM-square ascent and
///    descent (which sum to [fontSize]) is scaled by [height].
///    The [height] will impact the spacing above and below the baseline
///    differently depending on the ratios between the font's ascent and
///    descent. This property is separate from the leading multiplier, which
///    is controlled through [leading].
///    Default is null.
///
///  * [leading]: the custom leading to apply to the strut as a multiple of
///    [fontSize]. Leading is additional spacing between lines. Half of the
///    leading is added to the top and the other half to the bottom of the
///    line height. This differs from [height] since the spacing is equally
///    distributed above and below the baseline.
///    Default is null, which will use the font-specified leading.
///
///  * [fontWeight]: the typeface thickness to use when calculating the strut
///    (e.g., bold).
///    Default is [FontWeight.w400].
///
///  * [fontStyle]: the typeface variant to use when calculating the strut
///    (e.g., italic).
///    Default is [FontStyle.normal].
///
///  * [forceStrutHeight]: when true, all lines will be laid out with the
///    height of the strut. All line and run-specific metrics will be
///    ignored/overridden and only strut metrics will be used instead.
///    This property guarantees uniform line spacing, however text in
///    adjacent lines may overlap. This property should be enabled with
///    caution as it bypasses a large portion of the vertical layout system.
///    The default value is false.
///
/// ### Examples
///
/// {@tool snippet}
/// In this simple case, the text will be rendered at font size 10, however,
/// the vertical height of each line will be the strut height (Roboto in
/// font size 30 * 1.5) as the text itself is shorter than the strut.
///
/// ```dart
/// const Text(
///   'Hello, world!\nSecond line!',
///   style: TextStyle(
///     fontSize: 10,
///     fontFamily: 'Raleway',
///   ),
///   strutStyle: StrutStyle(
///     fontFamily: 'Roboto',
///     fontSize: 30,
///     height: 1.5,
///   ),
/// ),
/// ```
/// {@end-tool}
///
/// {@tool snippet}
/// Here, strut is used to absorb the additional line height in the second line.
/// The strut [height] was defined as 1.5 (the default font size is 14), which
/// caused all lines to be laid out taller than without strut. This extra space
/// was able to accommodate the larger font size of `Second line!` without
/// causing the line height to change for the second line only. All lines in
/// this example are thus the same height (`14 * 1.5`).
///
/// ```dart
/// const Text.rich(
///   TextSpan(
///     text: 'First line!\n',
///     style: TextStyle(
///       fontSize: 14,
///       fontFamily: 'Roboto'
///     ),
///     children: <TextSpan>[
///       TextSpan(
///         text: 'Second line!\n',
///         style: TextStyle(
///           fontSize: 16,
///           fontFamily: 'Roboto',
///         ),
///       ),
///       TextSpan(
///         text: 'Third line!\n',
///         style: TextStyle(
///           fontSize: 14,
///           fontFamily: 'Roboto',
///         ),
///       ),
///     ],
///   ),
///   strutStyle: StrutStyle(
///     fontFamily: 'Roboto',
///     height: 1.5,
///   ),
/// ),
/// ```
/// {@end-tool}
///
/// {@tool snippet}
/// Here, strut is used to enable strange and overlapping text to achieve unique
/// effects. The `M`s in lines 2 and 3 are able to extend above their lines and
/// fill empty space in lines above. The [forceStrutHeight] is enabled and functions
/// as a 'grid' for the glyphs to draw on.
///
/// ![The result of the example below.](https://flutter.github.io/assets-for-api-docs/assets/painting/strut_force_example.png)
///
/// ```dart
/// const Text.rich(
///   TextSpan(
///     text: '---------         ---------\n',
///     style: TextStyle(
///       fontSize: 14,
///       fontFamily: 'Roboto',
///     ),
///     children: <TextSpan>[
///       TextSpan(
///         text: '^^^M^^^\n',
///         style: TextStyle(
///           fontSize: 30,
///           fontFamily: 'Roboto',
///         ),
///       ),
///       TextSpan(
///         text: 'M------M\n',
///         style: TextStyle(
///           fontSize: 30,
///           fontFamily: 'Roboto',
///         ),
///       ),
///     ],
///   ),
///   strutStyle: StrutStyle(
///     fontFamily: 'Roboto',
///     fontSize: 14,
///     height: 1,
///     forceStrutHeight: true,
///   ),
/// ),
/// ```
/// {@end-tool}
///
/// {@tool snippet}
/// This example uses forceStrutHeight to create a 'drop cap' for the 'T' in 'The'.
/// By locking the line heights to the metrics of the 14pt serif font, we are able
/// to lay out a large 37pt 'T' on the second line to take up space on both the first
/// and second lines.
///
/// ![The result of the example below.](https://flutter.github.io/assets-for-api-docs/assets/painting/strut_force_example_2.png)
///
/// ```dart
/// const Text.rich(
///   TextSpan(
///     text: '       he candle flickered\n',
///     style: TextStyle(
///       fontSize: 14,
///       fontFamily: 'Serif'
///     ),
///     children: <TextSpan>[
///       TextSpan(
///         text: 'T',
///         style: TextStyle(
///           fontSize: 37,
///           fontFamily: 'Serif'
///         ),
///       ),
///       TextSpan(
///         text: 'in the moonlight as\n',
///         style: TextStyle(
///           fontSize: 14,
///           fontFamily: 'Serif'
///         ),
///       ),
///       TextSpan(
///         text: 'Dash the bird fluttered\n',
///         style: TextStyle(
///           fontSize: 14,
///           fontFamily: 'Serif'
///         ),
///       ),
///       TextSpan(
///         text: 'off into the distance.',
///         style: TextStyle(
///           fontSize: 14,
///           fontFamily: 'Serif'
///         ),
///       ),
///     ],
///   ),
///   strutStyle: StrutStyle(
///     fontFamily: 'Serif',
///     fontSize: 14,
///     forceStrutHeight: true,
///   ),
/// ),
/// ```
/// {@end-tool}
///
@immutable
class StrutStyle with Diagnosticable {
  /// Creates a strut style.
  ///
  /// The `package` argument must be non-null if the font family is defined in a
  /// package. It is combined with the `fontFamily` argument to set the
  /// [fontFamily] property.
  ///
  /// If provided, fontSize must be positive and non-zero, leading must be
  /// zero or positive.
  const StrutStyle({
    String? fontFamily,
    List<String>? fontFamilyFallback,
    this.fontSize,
    this.height,
    this.leadingDistribution,
    this.leading,
    this.fontWeight,
    this.fontStyle,
    this.forceStrutHeight,
    this.debugLabel,
    String? package,
  }) : fontFamily = package == null ? fontFamily : 'packages/$package/$fontFamily',
       _fontFamilyFallback = fontFamilyFallback,
       _package = package,
       assert(fontSize == null || fontSize > 0),
       assert(leading == null || leading >= 0),
       assert(package == null || (fontFamily != null || fontFamilyFallback != null));

  /// Builds a StrutStyle that contains values of the equivalent properties in
  /// the provided [textStyle].
  ///
  /// The [textStyle] parameter must not be null.
  ///
  /// The named parameters override the [textStyle]'s argument's properties.
  /// Since TextStyle does not contain [leading] or [forceStrutHeight], these
  /// values will take on default values (null and false) unless otherwise
  /// specified.
  ///
  /// If provided, fontSize must be positive and non-zero, leading must be
  /// zero or positive.
  ///
  /// When [textStyle] has a package and a new [package] is also specified,
  /// the entire font family fallback list should be redefined since the
  /// [textStyle]'s package data is inherited by being prepended onto the
  /// font family names. If [fontFamilyFallback] is meant to be empty, pass
  /// an empty list instead of null. This prevents the previous package name
  /// from being prepended twice.
  StrutStyle.fromTextStyle(
    TextStyle textStyle, {
    String? fontFamily,
    List<String>? fontFamilyFallback,
    double? fontSize,
    double? height,
    TextLeadingDistribution? leadingDistribution,
    this.leading, // TextStyle does not have an equivalent (yet).
    FontWeight? fontWeight,
    FontStyle? fontStyle,
    this.forceStrutHeight,
    String? debugLabel,
    String? package,
  }) : assert(textStyle != null),
       assert(fontSize == null || fontSize > 0),
       assert(leading == null || leading >= 0),
       assert(package == null || fontFamily != null || fontFamilyFallback != null),
       fontFamily = fontFamily != null ? (package == null ? fontFamily : 'packages/$package/$fontFamily') : textStyle.fontFamily,
       _fontFamilyFallback = fontFamilyFallback ?? textStyle.fontFamilyFallback,
       height = height ?? textStyle.height,
       leadingDistribution = leadingDistribution ?? textStyle.leadingDistribution,
       fontSize = fontSize ?? textStyle.fontSize,
       fontWeight = fontWeight ?? textStyle.fontWeight,
       fontStyle = fontStyle ?? textStyle.fontStyle,
       debugLabel = debugLabel ?? textStyle.debugLabel,
       _package = package; // the textStyle._package data is embedded in the
                           // fontFamily names, so we no longer need it.

  /// A [StrutStyle] that will have no impact on the text layout.
  ///
  /// Equivalent to having no strut at all. All lines will be laid out according to
  /// the properties defined in [TextStyle].
  static const StrutStyle disabled = StrutStyle(
    height: 0.0,
    leading: 0.0,
  );

  /// The name of the font to use when calculating the strut (e.g., Roboto). If
  /// the font is defined in a package, this will be prefixed with
  /// 'packages/package_name/' (e.g. 'packages/cool_fonts/Roboto'). The
  /// prefixing is done by the constructor when the `package` argument is
  /// provided.
  ///
  /// The value provided in [fontFamily] will act as the preferred/first font
  /// family that will be searched for, followed in order by the font families
  /// in [fontFamilyFallback]. If all font families are exhausted and no match
  /// was found, the default platform font family will be used instead. Unlike
  /// [TextStyle.fontFamilyFallback], the font does not need to contain the
  /// desired glyphs to match.
  final String? fontFamily;

  /// The ordered list of font families to fall back on when a higher priority
  /// font family cannot be found.
  ///
  /// The value provided in [fontFamily] will act as the preferred/first font
  /// family that will be searched for, followed in order by the font families
  /// in [fontFamilyFallback]. If all font families are exhausted and no match
  /// was found, the default platform font family will be used instead. Unlike
  /// [TextStyle.fontFamilyFallback], the font does not need to contain the
  /// desired glyphs to match.
  ///
  /// When [fontFamily] is null or not provided, the first value in [fontFamilyFallback]
  /// acts as the preferred/first font family. When neither is provided, then
  /// the default platform font will be used. Providing and empty list or null
  /// for this property is the same as omitting it.
  ///
  /// If the font is defined in a package, each font family in the list will be
  /// prefixed with 'packages/package_name/' (e.g. 'packages/cool_fonts/Roboto').
  /// The package name should be provided by the `package` argument in the
  /// constructor.
  List<String>? get fontFamilyFallback {
    if (_package != null && _fontFamilyFallback != null)
      return _fontFamilyFallback!.map((String family) => 'packages/$_package/$family').toList();
    return _fontFamilyFallback;
  }
  final List<String>? _fontFamilyFallback;

  // This is stored in order to prefix the fontFamilies in _fontFamilyFallback
  // in the [fontFamilyFallback] getter.
  final String? _package;

  /// The size of text (in logical pixels) to use when obtaining metrics from the font.
  ///
  /// The [fontSize] is used to get the base set of metrics that are then used to calculated
  /// the metrics of strut. The height and leading are expressed as a multiple of
  /// [fontSize].
  ///
  /// The default fontSize is 14 logical pixels.
  final double? fontSize;

  /// The minimum height of the strut, as a multiple of [fontSize].
  ///
  /// When [height] is omitted or null, then the strut's height will be the sum
  /// of the strut's font-defined ascent, its font-defined descent, and its
  /// [leading]. The font's combined ascent and descent may be taller or shorter
  /// than the [fontSize].
  ///
  /// When [height] is provided, the line's EM-square ascent and descent (which
  /// sums to [fontSize]) will be scaled by [height] to achieve a final strut
  /// height of `fontSize * height + fontSize * leading` logical pixels. The
  /// following diagram illustrates the differences between the font metrics
  /// defined height and the EM-square height:
  ///
  /// ![Text height diagram](https://flutter.github.io/assets-for-api-docs/assets/painting/text_height_diagram.png)
  ///
  /// The ratio of ascent:descent with [height] specified is the same as the
  /// font metrics defined ascent:descent ratio when [height] is null or omitted.
  ///
  /// See [TextStyle.height], which works in a similar manner.
  ///
  /// The default height is null.
  final double? height;

  /// How the vertical space added by the [height] multiplier should be
  /// distributed over and under the strut.
  ///
  /// When a non-null [height] is specified, after accommodating the imaginary
  /// strut glyph, the remaining vertical space from the allotted
  /// `fontSize * height` logical pixels will be distributed over and under the
  /// strut, according to the [leadingDistribution] property.
  ///
  /// The additional leading introduced by the [leading] property applies
  /// independently of [leadingDistribution]: it will always be distributed
  /// evenly over and under the strut, regardless of [leadingDistribution].
  ///
  /// Defaults to null, which defers to the paragraph's
  /// `ParagraphStyle.textHeightBehavior`'s `leadingDistribution`.
  final TextLeadingDistribution? leadingDistribution;

  /// The typeface thickness to use when calculating the strut (e.g., bold).
  ///
  /// The default fontWeight is [FontWeight.w400].
  final FontWeight? fontWeight;

  /// The typeface variant to use when calculating the strut (e.g., italics).
  ///
  /// The default fontStyle is [FontStyle.normal].
  final FontStyle? fontStyle;

  /// The additional leading to apply to the strut as a multiple of [fontSize],
  /// indepdent of [height] and [leadingDistribution].
  ///
  /// Leading is additional spacing between lines. Half of the leading is added
  /// to the top and the other half to the bottom of the line. This differs
  /// from [height] since the spacing is always equally distributed above and
  /// below the baseline, regardless of [leadingDistribution].
  ///
  /// The default leading is null, which will use the font-specified leading.
  final double? leading;

  /// Whether the strut height should be forced.
  ///
  /// When true, all lines will be laid out with the height of the
  /// strut. All line and run-specific metrics will be ignored/overridden
  /// and only strut metrics will be used instead. This property guarantees
  /// uniform line spacing, however text in adjacent lines may overlap.
  ///
  /// This property should be enabled with caution as
  /// it bypasses a large portion of the vertical layout system.
  ///
  /// This is equivalent to setting [TextStyle.height] to zero for all [TextStyle]s
  /// in the paragraph. Since the height of each line is calculated as a max of the
  /// metrics of each run of text, zero height [TextStyle]s cause the minimums
  /// defined by strut to always manifest, resulting in all lines having the height
  /// of the strut.
  ///
  /// The default is false.
  final bool? forceStrutHeight;

  /// A human-readable description of this strut style.
  ///
  /// This property is maintained only in debug builds.
  ///
  /// This property is not considered when comparing strut styles using `==` or
  /// [compareTo], and it does not affect [hashCode].
  final String? debugLabel;

  /// Describe the difference between this style and another, in terms of how
  /// much damage it will make to the rendering.
  ///
  /// See also:
  ///
  ///  * [TextSpan.compareTo], which does the same thing for entire [TextSpan]s.
  RenderComparison compareTo(StrutStyle other) {
    if (identical(this, other))
      return RenderComparison.identical;
    if (fontFamily != other.fontFamily ||
        fontSize != other.fontSize ||
        fontWeight != other.fontWeight ||
        fontStyle != other.fontStyle ||
        height != other.height ||
        leading != other.leading ||
        forceStrutHeight != other.forceStrutHeight ||
        !listEquals(fontFamilyFallback, other.fontFamilyFallback))
      return RenderComparison.layout;
    return RenderComparison.identical;
  }

  /// Returns a new strut style that inherits its null values from
  /// corresponding properties in the [other] [TextStyle].
  ///
  /// The "missing" properties of the this strut style are _filled_ by
  /// the properties of the provided [TextStyle]. This is possible because
  /// [StrutStyle] shares many of the same basic properties as [TextStyle].
  ///
  /// If the given text style is null, returns this strut style.
  StrutStyle inheritFromTextStyle(TextStyle? other) {
    if (other == null)
      return this;

    return StrutStyle(
      fontFamily: fontFamily ?? other.fontFamily,
      fontFamilyFallback: fontFamilyFallback ?? other.fontFamilyFallback,
      fontSize: fontSize ?? other.fontSize,
      height: height ?? other.height,
      leading: leading, // No equivalent property in TextStyle yet.
      fontWeight: fontWeight ?? other.fontWeight,
      fontStyle: fontStyle ?? other.fontStyle,
      forceStrutHeight: forceStrutHeight, // StrutStyle-unique property.
      debugLabel: debugLabel ?? other.debugLabel,
      // Package is embedded within the getters for fontFamilyFallback.
    );
  }

  @override
  bool operator ==(Object other) {
    if (identical(this, other))
      return true;
    if (other.runtimeType != runtimeType)
      return false;
    return other is StrutStyle
        && other.fontFamily == fontFamily
        && other.fontSize == fontSize
        && other.fontWeight == fontWeight
        && other.fontStyle == fontStyle
        && other.height == height
        && other.leading == leading
        && other.forceStrutHeight == forceStrutHeight;
  }

  @override
  int get hashCode {
    return hashValues(
      fontFamily,
      fontSize,
      fontWeight,
      fontStyle,
      height,
      leading,
      forceStrutHeight,
    );
  }

  @override
  String toStringShort() => objectRuntimeType(this, 'StrutStyle');

  /// Adds all properties prefixing property names with the optional `prefix`.
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties, { String prefix = '' }) {
    super.debugFillProperties(properties);
    if (debugLabel != null)
      properties.add(MessageProperty('${prefix}debugLabel', debugLabel!));
    final List<DiagnosticsNode> styles = <DiagnosticsNode>[
      StringProperty('${prefix}family', fontFamily, defaultValue: null, quoted: false),
      IterableProperty<String>('${prefix}familyFallback', fontFamilyFallback, defaultValue: null),
      DoubleProperty('${prefix}size', fontSize, defaultValue: null),
    ];
    String? weightDescription;
    if (fontWeight != null) {
      weightDescription = 'w${fontWeight!.index + 1}00';
    }
    // TODO(jacobr): switch this to use enumProperty which will either cause the
    // weight description to change to w600 from 600 or require existing
    // enumProperty to handle this special case.
    styles.add(DiagnosticsProperty<FontWeight>(
      '${prefix}weight',
      fontWeight,
      description: weightDescription,
      defaultValue: null,
    ));
    styles.add(EnumProperty<FontStyle>('${prefix}style', fontStyle, defaultValue: null));
    styles.add(DoubleProperty('${prefix}height', height, unit: 'x', defaultValue: null));
    styles.add(FlagProperty('${prefix}forceStrutHeight', value: forceStrutHeight, defaultValue: null, ifTrue: '$prefix<strut height forced>', ifFalse: '$prefix<strut height normal>'));

    final bool styleSpecified = styles.any((DiagnosticsNode n) => !n.isFiltered(DiagnosticLevel.info));
    styles.forEach(properties.add);

    if (!styleSpecified)
      properties.add(FlagProperty('forceStrutHeight', value: forceStrutHeight, ifTrue: '$prefix<strut height forced>', ifFalse: '$prefix<strut height normal>'));
  }
}
