part of dart._skwasm_impl;
// Copyright 2013 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.



const String _kWeightAxisTag = 'wght';

final List<String> _testFonts = <String>['FlutterTest', 'Ahem'];
List<String> _computeEffectiveFontFamilies(List<String> fontFamilies) {
  if (!ui_web.TestEnvironment.instance.forceTestFonts) {
    return fontFamilies;
  }
  final Iterable<String> filteredFonts = fontFamilies.where(_testFonts.contains);
  return filteredFonts.isEmpty ? _testFonts : filteredFonts.toList();
}

int getFourByteTag(String s) {
  assert(s.length == 4);
  return s.codeUnitAt(0) << 24 | s.codeUnitAt(1) << 16 | s.codeUnitAt(2) << 8 | s.codeUnitAt(3);
}

class SkwasmLineMetrics implements ui.LineMetrics {
  SkwasmLineMetrics({
    required this.hardBreak,
    required this.ascent,
    required this.descent,
    required this.unscaledAscent,
    required this.height,
    required this.width,
    required this.left,
    required this.baseline,
    required this.lineNumber,
    this.startIndex = 0,
    this.endIndex = 0,
  });

  factory SkwasmLineMetrics._fromHandle(LineMetricsHandle handle) {
    return SkwasmLineMetrics(
      hardBreak: lineMetricsGetHardBreak(handle),
      ascent: lineMetricsGetAscent(handle),
      descent: lineMetricsGetDescent(handle),
      unscaledAscent: lineMetricsGetUnscaledAscent(handle),
      height: lineMetricsGetHeight(handle),
      width: lineMetricsGetWidth(handle),
      left: lineMetricsGetLeft(handle),
      baseline: lineMetricsGetBaseline(handle),
      lineNumber: lineMetricsGetLineNumber(handle),
      startIndex: lineMetricsGetStartIndex(handle),
      endIndex: lineMetricsGetEndIndex(handle),
    );
  }

  @override
  final bool hardBreak;

  @override
  final double ascent;

  @override
  final double descent;

  @override
  final double unscaledAscent;

  @override
  final double height;

  @override
  final double width;

  @override
  final double left;

  @override
  final double baseline;

  @override
  final int lineNumber;

  final int startIndex;
  final int endIndex;
}

class SkwasmParagraph extends SkwasmObjectWrapper<RawParagraph> implements ui.Paragraph {
  SkwasmParagraph(ParagraphHandle handle) : super(handle, _registry);

  static final SkwasmFinalizationRegistry<RawParagraph> _registry =
      SkwasmFinalizationRegistry<RawParagraph>(
        (ParagraphHandle handle) => paragraphDispose(handle),
      );

  bool _hasCheckedForMissingCodePoints = false;

  @override
  double get width => paragraphGetWidth(handle);

  @override
  double get height => paragraphGetHeight(handle);

  @override
  double get longestLine => paragraphGetLongestLine(handle);

  @override
  double get minIntrinsicWidth => paragraphGetMinIntrinsicWidth(handle);

  @override
  double get maxIntrinsicWidth => paragraphGetMaxIntrinsicWidth(handle);

  @override
  double get alphabeticBaseline => paragraphGetAlphabeticBaseline(handle);

  @override
  double get ideographicBaseline => paragraphGetIdeographicBaseline(handle);

  @override
  bool get didExceedMaxLines => paragraphGetDidExceedMaxLines(handle);

  @override
  int get numberOfLines => paragraphGetLineCount(handle);

  @override
  int? getLineNumberAt(int codeUnitOffset) {
    final int lineNumber = paragraphGetLineNumberAt(handle, codeUnitOffset);
    return lineNumber >= 0 ? lineNumber : null;
  }

  @override
  void layout(ui.ParagraphConstraints constraints) {
    paragraphLayout(handle, constraints.width);
    if (!ui_web.TestEnvironment.instance.disableFontFallbacks && !_hasCheckedForMissingCodePoints) {
      _hasCheckedForMissingCodePoints = true;
      final int missingCodePointCount = paragraphGetUnresolvedCodePoints(handle, nullptr, 0);
      if (missingCodePointCount > 0) {
        withStackScope((StackScope scope) {
          final Pointer<Uint32> codePointBuffer = scope.allocUint32Array(missingCodePointCount);
          final int returnedCodePointCount = paragraphGetUnresolvedCodePoints(
            handle,
            codePointBuffer,
            missingCodePointCount,
          );
          assert(missingCodePointCount == returnedCodePointCount);
          renderer.fontCollection.fontFallbackManager!.addMissingCodePoints(
            List<int>.generate(missingCodePointCount, (int index) => codePointBuffer[index]),
          );
        });
      }
    }
  }

  List<ui.TextBox> _convertTextBoxList(TextBoxListHandle listHandle) {
    final int length = textBoxListGetLength(listHandle);
    return withStackScope((StackScope scope) {
      final RawRect tempRect = scope.allocFloatArray(4);
      return List<ui.TextBox>.generate(length, (int index) {
        final int textDirectionIndex = textBoxListGetBoxAtIndex(listHandle, index, tempRect);
        return ui.TextBox.fromLTRBD(
          tempRect[0],
          tempRect[1],
          tempRect[2],
          tempRect[3],
          ui.TextDirection.values[textDirectionIndex],
        );
      });
    });
  }

  @override
  List<ui.TextBox> getBoxesForRange(
    int start,
    int end, {
    ui.BoxHeightStyle boxHeightStyle = ui.BoxHeightStyle.tight,
    ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight,
  }) {
    final TextBoxListHandle listHandle = paragraphGetBoxesForRange(
      handle,
      start,
      end,
      boxHeightStyle.index,
      boxWidthStyle.index,
    );
    final List<ui.TextBox> boxes = _convertTextBoxList(listHandle);
    textBoxListDispose(listHandle);
    return boxes;
  }

  @override
  ui.TextPosition getPositionForOffset(ui.Offset offset) => withStackScope((StackScope scope) {
    final Pointer<Int32> outAffinity = scope.allocInt32Array(1);
    final int position = paragraphGetPositionForOffset(handle, offset.dx, offset.dy, outAffinity);
    return ui.TextPosition(offset: position, affinity: ui.TextAffinity.values[outAffinity[0]]);
  });

  @override
  ui.GlyphInfo? getGlyphInfoAt(int codeUnitOffset) {
    return withStackScope((StackScope scope) {
      final Pointer<Float> outRect = scope.allocFloatArray(4);
      final Pointer<Uint32> outRange = scope.allocUint32Array(2);
      final Pointer<Bool> outBooleanFlags = scope.allocBoolArray(1);
      return paragraphGetGlyphInfoAt(handle, codeUnitOffset, outRect, outRange, outBooleanFlags)
          ? ui.GlyphInfo(
              scope.convertRectFromNative(outRect),
              ui.TextRange(start: outRange[0], end: outRange[1]),
              outBooleanFlags[0] ? ui.TextDirection.ltr : ui.TextDirection.rtl,
            )
          : null;
    });
  }

  @override
  ui.GlyphInfo? getClosestGlyphInfoForOffset(ui.Offset offset) {
    return withStackScope((StackScope scope) {
      final Pointer<Float> outRect = scope.allocFloatArray(4);
      final Pointer<Uint32> outRange = scope.allocUint32Array(2);
      final Pointer<Bool> outBooleanFlags = scope.allocBoolArray(1);
      return paragraphGetClosestGlyphInfoAtCoordinate(
            handle,
            offset.dx,
            offset.dy,
            outRect,
            outRange,
            outBooleanFlags,
          )
          ? ui.GlyphInfo(
              scope.convertRectFromNative(outRect),
              ui.TextRange(start: outRange[0], end: outRange[1]),
              outBooleanFlags[0] ? ui.TextDirection.ltr : ui.TextDirection.rtl,
            )
          : null;
    });
  }

  @override
  ui.TextRange getWordBoundary(ui.TextPosition position) => withStackScope((StackScope scope) {
    final Pointer<Int32> outRange = scope.allocInt32Array(2);
    final int characterPosition = switch (position.affinity) {
      ui.TextAffinity.upstream => position.offset - 1,
      ui.TextAffinity.downstream => position.offset,
    };
    paragraphGetWordBoundary(handle, characterPosition, outRange);
    return ui.TextRange(start: outRange[0], end: outRange[1]);
  });

  @override
  ui.TextRange getLineBoundary(ui.TextPosition position) {
    final int offset = position.offset;
    for (final SkwasmLineMetrics metrics in computeLineMetrics()) {
      if (offset >= metrics.startIndex && offset <= metrics.endIndex) {
        return ui.TextRange(start: metrics.startIndex, end: metrics.endIndex);
      }
    }
    return ui.TextRange.empty;
  }

  @override
  List<ui.TextBox> getBoxesForPlaceholders() {
    final TextBoxListHandle listHandle = paragraphGetBoxesForPlaceholders(handle);
    final List<ui.TextBox> boxes = _convertTextBoxList(listHandle);
    textBoxListDispose(listHandle);
    return boxes;
  }

  @override
  List<SkwasmLineMetrics> computeLineMetrics() {
    final int lineCount = paragraphGetLineCount(handle);
    return List<SkwasmLineMetrics>.generate(lineCount, (int index) {
      final metricsHandle = paragraphGetLineMetricsAtIndex(handle, index);
      final metrics = SkwasmLineMetrics._fromHandle(metricsHandle);
      lineMetricsDispose(metricsHandle);
      return metrics;
    });
  }

  @override
  ui.LineMetrics? getLineMetricsAt(int index) {
    final LineMetricsHandle metricsHandle = paragraphGetLineMetricsAtIndex(handle, index);
    if (metricsHandle == nullptr) {
      return null;
    }
    final metrics = SkwasmLineMetrics._fromHandle(metricsHandle);
    lineMetricsDispose(metricsHandle);
    return metrics;
  }
}

void withScopedFontList(
  List<String> fontFamilies,
  void Function(Pointer<SkStringHandle>, int) callback,
) {
  withStackScope((StackScope scope) {
    final Pointer<SkStringHandle> familiesPtr = scope
        .allocPointerArray(fontFamilies.length)
        .cast<SkStringHandle>();
    int nativeIndex = 0;
    for (int i = 0; i < fontFamilies.length; i++) {
      familiesPtr[nativeIndex] = skStringFromDartString(fontFamilies[i]);
      nativeIndex++;
    }
    callback(familiesPtr, fontFamilies.length);
    for (int i = 0; i < fontFamilies.length; i++) {
      skStringFree(familiesPtr[i]);
    }
  });
}

class SkwasmNativeTextStyle extends SkwasmObjectWrapper<RawTextStyle> {
  SkwasmNativeTextStyle(TextStyleHandle handle) : super(handle, _registry);

  factory SkwasmNativeTextStyle.defaultTextStyle() => SkwasmNativeTextStyle(textStyleCreate());

  static final SkwasmFinalizationRegistry<RawTextStyle> _registry =
      SkwasmFinalizationRegistry<RawTextStyle>(
        (TextStyleHandle handle) => textStyleDispose(handle),
      );

  SkwasmNativeTextStyle copy() {
    return SkwasmNativeTextStyle(textStyleCopy(handle));
  }
}

class SkwasmTextStyle implements ui.TextStyle {
  SkwasmTextStyle({
    this.color,
    this.decoration,
    this.decorationColor,
    this.decorationStyle,
    this.decorationThickness,
    this.fontWeight,
    this.fontStyle,
    this.textBaseline,
    this.fontFamily,
    this.fontFamilyFallback,
    this.fontSize,
    this.letterSpacing,
    this.wordSpacing,
    this.height,
    this.leadingDistribution,
    this.locale,
    this.background,
    this.foreground,
    this.shadows,
    this.fontFeatures,
    this.fontVariations,
  }) : assert(
         color == null || foreground == null,
         'Cannot provide both a color and a foreground\n'
         'The color argument is just a shorthand for "foreground: Paint()..color = color".',
       );

  void applyToNative(SkwasmNativeTextStyle style) {
    final TextStyleHandle handle = style.handle;
    if (color != null) {
      textStyleSetColor(handle, color!.value);
    }
    if (decoration != null) {
      textStyleSetDecoration(handle, decoration!.maskValue);
    }
    if (decorationColor != null) {
      textStyleSetDecorationColor(handle, decorationColor!.value);
    }
    if (decorationStyle != null) {
      textStyleSetDecorationStyle(handle, decorationStyle!.index);
    }
    if (decorationThickness != null) {
      textStyleSetDecorationThickness(handle, decorationThickness!);
    }
    if (fontWeight != null || fontStyle != null) {
      textStyleSetFontStyle(
        handle,
        (fontWeight ?? ui.FontWeight.normal).value,
        (fontStyle ?? ui.FontStyle.normal).index,
      );
    }
    if (textBaseline != null) {
      textStyleSetTextBaseline(handle, textBaseline!.index);
    }

    final List<String> effectiveFontFamilies = _computeEffectiveFontFamilies(fontFamilies);
    if (effectiveFontFamilies.isNotEmpty) {
      withScopedFontList(
        effectiveFontFamilies,
        (Pointer<SkStringHandle> families, int count) =>
            textStyleAddFontFamilies(handle, families, count),
      );
    }

    if (fontSize != null) {
      textStyleSetFontSize(handle, fontSize!);
    }
    if (letterSpacing != null) {
      textStyleSetLetterSpacing(handle, letterSpacing!);
    }
    if (wordSpacing != null) {
      textStyleSetWordSpacing(handle, wordSpacing!);
    }
    if (height != null) {
      textStyleSetHeight(handle, height!);
    }
    if (leadingDistribution != null) {
      textStyleSetHalfLeading(handle, leadingDistribution == ui.TextLeadingDistribution.even);
    }
    if (locale != null) {
      final SkStringHandle localeHandle = skStringFromDartString(locale!.toLanguageTag());
      textStyleSetLocale(handle, localeHandle);
      skStringFree(localeHandle);
    }
    if (background != null) {
      final backgroundPaint = (background! as SkwasmPaint).toRawPaint();
      textStyleSetBackground(handle, backgroundPaint);
      paintDispose(backgroundPaint);
    }
    if (foreground != null) {
      final foregroundPaint = (foreground! as SkwasmPaint).toRawPaint();
      textStyleSetForeground(handle, foregroundPaint);
      paintDispose(foregroundPaint);
    }
    if (shadows != null) {
      for (final ui.Shadow shadow in shadows!) {
        textStyleAddShadow(
          handle,
          shadow.color.value,
          shadow.offset.dx,
          shadow.offset.dy,
          shadow.blurSigma,
        );
      }
    }
    if (fontFeatures != null) {
      for (final ui.FontFeature feature in fontFeatures!) {
        final SkStringHandle featureName = skStringFromDartString(feature.feature);
        textStyleAddFontFeature(handle, featureName, feature.value);
        skStringFree(featureName);
      }
    }

    Iterable<ui.FontVariation> allFontVariations;
    if (fontVariations != null &&
        fontVariations!.any((ui.FontVariation v) => v.axis == _kWeightAxisTag)) {
      allFontVariations = fontVariations!;
    } else {
      final int weightValue = fontWeight?.value ?? ui.FontWeight.normal.value;
      final List<ui.FontVariation> weightVariation = <ui.FontVariation>[
        ui.FontVariation(_kWeightAxisTag, weightValue.toDouble()),
      ];
      if (fontVariations == null) {
        allFontVariations = weightVariation;
      } else {
        allFontVariations = fontVariations!.followedBy(weightVariation);
      }
    }
    final int variationCount = allFontVariations.length;
    withStackScope((StackScope scope) {
      final Pointer<Uint32> axisBuffer = scope.allocUint32Array(variationCount);
      final Pointer<Float> valueBuffer = scope.allocFloatArray(variationCount);
      int i = 0;
      for (final ui.FontVariation variation in allFontVariations) {
        axisBuffer[i] = getFourByteTag(variation.axis);
        valueBuffer[i] = variation.value;
        i++;
      }
      textStyleSetFontVariations(handle, axisBuffer, valueBuffer, variationCount);
    });
  }

  List<String> get fontFamilies => <String>[?fontFamily, ...?fontFamilyFallback];

  final ui.Color? color;
  final ui.TextDecoration? decoration;
  final ui.Color? decorationColor;
  final ui.TextDecorationStyle? decorationStyle;
  final double? decorationThickness;
  final ui.FontWeight? fontWeight;
  final ui.FontStyle? fontStyle;
  final ui.TextBaseline? textBaseline;
  final String? fontFamily;
  final List<String>? fontFamilyFallback;
  final double? fontSize;
  final double? letterSpacing;
  final double? wordSpacing;
  final double? height;
  final ui.TextLeadingDistribution? leadingDistribution;
  final ui.Locale? locale;
  final ui.Paint? background;
  final ui.Paint? foreground;
  final List<ui.Shadow>? shadows;
  final List<ui.FontFeature>? fontFeatures;
  final List<ui.FontVariation>? fontVariations;

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) {
      return true;
    }
    return other is SkwasmTextStyle &&
        other.color == color &&
        other.decoration == decoration &&
        other.decorationColor == decorationColor &&
        other.decorationStyle == decorationStyle &&
        other.fontWeight == fontWeight &&
        other.fontStyle == fontStyle &&
        other.textBaseline == textBaseline &&
        other.leadingDistribution == leadingDistribution &&
        other.fontFamily == fontFamily &&
        other.fontSize == fontSize &&
        other.letterSpacing == letterSpacing &&
        other.wordSpacing == wordSpacing &&
        other.height == height &&
        other.decorationThickness == decorationThickness &&
        other.locale == locale &&
        other.background == background &&
        other.foreground == foreground &&
        listEquals<ui.Shadow>(other.shadows, shadows) &&
        listEquals<String>(other.fontFamilyFallback, fontFamilyFallback) &&
        listEquals<ui.FontFeature>(other.fontFeatures, fontFeatures) &&
        listEquals<ui.FontVariation>(other.fontVariations, fontVariations);
  }

  @override
  int get hashCode {
    final List<ui.Shadow>? shadows = this.shadows;
    final List<ui.FontFeature>? fontFeatures = this.fontFeatures;
    final List<ui.FontVariation>? fontVariations = this.fontVariations;
    final List<String>? fontFamilyFallback = this.fontFamilyFallback;
    return Object.hash(
      color,
      decoration,
      decorationColor,
      decorationStyle,
      fontWeight,
      fontStyle,
      textBaseline,
      leadingDistribution,
      fontFamily,
      fontFamilyFallback == null ? null : Object.hashAll(fontFamilyFallback),
      fontSize,
      letterSpacing,
      wordSpacing,
      height,
      locale,
      background,
      foreground,
      shadows == null ? null : Object.hashAll(shadows),
      decorationThickness,
      // Object.hash goes up to 20 arguments, but we have 21
      Object.hash(
        fontFeatures == null ? null : Object.hashAll(fontFeatures),
        fontVariations == null ? null : Object.hashAll(fontVariations),
      ),
    );
  }

  @override
  String toString() {
    String result = super.toString();
    assert(() {
      final List<String>? fontFamilyFallback = this.fontFamilyFallback;
      final double? fontSize = this.fontSize;
      final double? height = this.height;
      result =
          'TextStyle('
          'color: ${color ?? "unspecified"}, '
          'decoration: ${decoration ?? "unspecified"}, '
          'decorationColor: ${decorationColor ?? "unspecified"}, '
          'decorationStyle: ${decorationStyle ?? "unspecified"}, '
          'decorationThickness: ${decorationThickness ?? "unspecified"}, '
          'fontWeight: ${fontWeight ?? "unspecified"}, '
          'fontStyle: ${fontStyle ?? "unspecified"}, '
          'textBaseline: ${textBaseline ?? "unspecified"}, '
          'fontFamily: ${fontFamily ?? "unspecified"}, '
          'fontFamilyFallback: ${fontFamilyFallback != null && fontFamilyFallback.isNotEmpty ? fontFamilyFallback : "unspecified"}, '
          'fontSize: ${fontSize != null ? fontSize.toStringAsFixed(1) : "unspecified"}, '
          'letterSpacing: ${letterSpacing != null ? "${letterSpacing}x" : "unspecified"}, '
          'wordSpacing: ${wordSpacing != null ? "${wordSpacing}x" : "unspecified"}, '
          'height: ${height != null ? "${height.toStringAsFixed(1)}x" : "unspecified"}, '
          'leadingDistribution: ${leadingDistribution ?? "unspecified"}, '
          'locale: ${locale ?? "unspecified"}, '
          'background: ${background ?? "unspecified"}, '
          'foreground: ${foreground ?? "unspecified"}, '
          'shadows: ${shadows ?? "unspecified"}, '
          'fontFeatures: ${fontFeatures ?? "unspecified"}, '
          'fontVariations: ${fontVariations ?? "unspecified"}'
          ')';
      return true;
    }());
    return result;
  }
}

final class SkwasmStrutStyle implements ui.StrutStyle {
  SkwasmStrutStyle({
    String? fontFamily,
    List<String>? fontFamilyFallback,
    double? fontSize,
    double? height,
    double? leading,
    ui.FontWeight? fontWeight,
    ui.FontStyle? fontStyle,
    bool? forceStrutHeight,
    ui.TextLeadingDistribution? leadingDistribution,
  }) : _fontFamily = fontFamily,
       _fontFamilyFallback = fontFamilyFallback,
       _fontSize = fontSize,
       _height = height,
       _leading = leading,
       _fontWeight = fontWeight,
       _fontStyle = fontStyle,
       _forceStrutHeight = forceStrutHeight,
       _leadingDistribution = leadingDistribution;

  final String? _fontFamily;
  final List<String>? _fontFamilyFallback;
  final double? _fontSize;
  final double? _height;
  final double? _leading;
  final ui.FontWeight? _fontWeight;
  final ui.FontStyle? _fontStyle;
  final bool? _forceStrutHeight;
  final ui.TextLeadingDistribution? _leadingDistribution;

  StrutStyleHandle createNativeHandle() {
    final StrutStyleHandle handle = strutStyleCreate();
    final List<String> effectiveFontFamilies = _computeEffectiveFontFamilies(<String>[
      ?_fontFamily,
      ...?_fontFamilyFallback,
    ]);
    if (effectiveFontFamilies.isNotEmpty) {
      withScopedFontList(
        effectiveFontFamilies,
        (Pointer<SkStringHandle> families, int count) =>
            strutStyleSetFontFamilies(handle, families, count),
      );
    }
    if (_fontSize != null) {
      strutStyleSetFontSize(handle, _fontSize);
    }
    if (_height != null && _height != ui.kTextHeightNone) {
      strutStyleSetHeight(handle, _height);
    }
    if (_leadingDistribution != null) {
      strutStyleSetHalfLeading(handle, _leadingDistribution == ui.TextLeadingDistribution.even);
    }
    if (_leading != null) {
      strutStyleSetLeading(handle, _leading);
    }
    if (_fontWeight != null || _fontStyle != null) {
      strutStyleSetFontStyle(
        handle,
        (_fontWeight ?? ui.FontWeight.normal).value,
        (_fontStyle ?? ui.FontStyle.normal).index,
      );
    }
    if (_forceStrutHeight != null) {
      strutStyleSetForceStrutHeight(handle, _forceStrutHeight);
    }
    return handle;
  }

  @override
  bool operator ==(Object other) {
    return other is SkwasmStrutStyle &&
        other._fontFamily == _fontFamily &&
        other._fontSize == _fontSize &&
        other._height == _height &&
        other._leading == _leading &&
        other._leadingDistribution == _leadingDistribution &&
        other._fontWeight == _fontWeight &&
        other._fontStyle == _fontStyle &&
        other._forceStrutHeight == _forceStrutHeight &&
        listEquals<String>(other._fontFamilyFallback, _fontFamilyFallback);
  }

  @override
  int get hashCode {
    final List<String>? fontFamilyFallback = _fontFamilyFallback;
    return Object.hash(
      _fontFamily,
      fontFamilyFallback != null ? Object.hashAll(fontFamilyFallback) : null,
      _fontSize,
      _height,
      _leading,
      _leadingDistribution,
      _fontWeight,
      _fontStyle,
      _forceStrutHeight,
    );
  }

  @override
  String toString() {
    String result = super.toString();
    assert(() {
      final List<String>? fontFamilyFallback = _fontFamilyFallback;
      final double? fontSize = _fontSize;
      final double? height = _height;
      final double? leading = _leading;
      result =
          'StrutStyle('
          'fontFamily: ${_fontFamily ?? "unspecified"}, '
          'fontFamilyFallback: ${_fontFamilyFallback ?? "unspecified"}, '
          'fontFamilyFallback: ${fontFamilyFallback != null && fontFamilyFallback.isNotEmpty ? fontFamilyFallback : "unspecified"}, '
          'fontSize: ${fontSize != null ? fontSize.toStringAsFixed(1) : "unspecified"}, '
          'height: ${height != null ? "${height.toStringAsFixed(1)}x" : "unspecified"}, '
          'leading: ${leading != null ? "${leading.toStringAsFixed(1)}x" : "unspecified"}, '
          'fontWeight: ${_fontWeight ?? "unspecified"}, '
          'fontStyle: ${_fontStyle ?? "unspecified"}, '
          'forceStrutHeight: ${_forceStrutHeight ?? "unspecified"}, '
          'leadingDistribution: ${_leadingDistribution ?? "unspecified"}, '
          ')';
      return true;
    }());
    return result;
  }
}

class SkwasmParagraphStyle implements ui.ParagraphStyle {
  SkwasmParagraphStyle({
    ui.TextAlign? textAlign,
    ui.TextDirection? textDirection,
    int? maxLines,
    String? fontFamily,
    double? fontSize,
    double? height,
    ui.TextHeightBehavior? textHeightBehavior,
    ui.FontWeight? fontWeight,
    ui.FontStyle? fontStyle,
    ui.StrutStyle? strutStyle,
    String? ellipsis,
    ui.Locale? locale,
  }) : _textAlign = textAlign,
       _textDirection = textDirection,
       _maxLines = maxLines,
       _fontFamily = fontFamily,
       _fontSize = fontSize,
       _height = height,
       _textHeightBehavior = textHeightBehavior,
       _fontWeight = fontWeight,
       _fontStyle = fontStyle,
       _strutStyle = strutStyle,
       _ellipsis = ellipsis,
       _locale = locale;

  (ParagraphStyleHandle, SkwasmNativeTextStyle) createNative() {
    final ParagraphStyleHandle handle = paragraphStyleCreate();
    if (_textAlign != null) {
      paragraphStyleSetTextAlign(handle, _textAlign.index);
    }
    if (_textDirection != null) {
      paragraphStyleSetTextDirection(handle, _textDirection.index);
    }
    if (_maxLines != null) {
      paragraphStyleSetMaxLines(handle, _maxLines);
    }
    if (_height != null && _height != ui.kTextHeightNone) {
      paragraphStyleSetHeight(handle, _height);
    }
    if (_textHeightBehavior != null) {
      paragraphStyleSetTextHeightBehavior(
        handle,
        _textHeightBehavior.applyHeightToFirstAscent,
        _textHeightBehavior.applyHeightToLastDescent,
      );
    }
    if (_ellipsis != null) {
      final SkStringHandle ellipsisHandle = skStringFromDartString(_ellipsis);
      paragraphStyleSetEllipsis(handle, ellipsisHandle);
      skStringFree(ellipsisHandle);
    }
    if (_strutStyle != null) {
      _strutStyle as SkwasmStrutStyle;
      final strutStyleHandle = _strutStyle.createNativeHandle();
      paragraphStyleSetStrutStyle(handle, strutStyleHandle);
      strutStyleDispose(strutStyleHandle);
    }
    final SkwasmNativeTextStyle textStyle = (renderer.fontCollection as SkwasmFontCollection)
        .defaultTextStyle
        .copy();
    final TextStyleHandle textStyleHandle = textStyle.handle;

    final List<String> effectiveFontFamilies = _computeEffectiveFontFamilies(<String>[
      ?_fontFamily,
    ]);
    if (effectiveFontFamilies.isNotEmpty) {
      withScopedFontList(
        effectiveFontFamilies,
        (Pointer<SkStringHandle> families, int count) =>
            textStyleAddFontFamilies(textStyleHandle, families, count),
      );
    }
    if (_fontSize != null) {
      textStyleSetFontSize(textStyleHandle, _fontSize);
    }
    if (_height != null) {
      textStyleSetHeight(textStyleHandle, _height);
    }
    if (_fontWeight != null || _fontStyle != null) {
      textStyleSetFontStyle(
        textStyleHandle,
        (_fontWeight ?? ui.FontWeight.normal).value,
        (_fontStyle ?? ui.FontStyle.normal).index,
      );
    }
    withStackScope((StackScope scope) {
      final Pointer<Uint32> axisBuffer = scope.allocUint32Array(1);
      final Pointer<Float> valueBuffer = scope.allocFloatArray(1);
      axisBuffer[0] = getFourByteTag(_kWeightAxisTag);
      final int weightValue = _fontWeight?.value ?? ui.FontWeight.normal.value;
      valueBuffer[0] = weightValue.toDouble();
      textStyleSetFontVariations(textStyleHandle, axisBuffer, valueBuffer, 1);
    });
    if (_textHeightBehavior != null) {
      textStyleSetHalfLeading(
        textStyleHandle,
        _textHeightBehavior.leadingDistribution == ui.TextLeadingDistribution.even,
      );
    }
    if (_locale != null) {
      final SkStringHandle localeHandle = skStringFromDartString(_locale.toLanguageTag());
      textStyleSetLocale(textStyleHandle, localeHandle);
      skStringFree(localeHandle);
    }
    paragraphStyleSetTextStyle(handle, textStyleHandle);
    paragraphStyleSetApplyRoundingHack(handle, false);
    return (handle, textStyle);
  }

  final ui.TextAlign? _textAlign;
  final ui.TextDirection? _textDirection;
  final ui.FontWeight? _fontWeight;
  final ui.FontStyle? _fontStyle;
  final int? _maxLines;
  final String? _fontFamily;
  final double? _fontSize;
  final double? _height;
  final ui.TextHeightBehavior? _textHeightBehavior;
  final ui.StrutStyle? _strutStyle;
  final String? _ellipsis;
  final ui.Locale? _locale;

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) {
      return true;
    }
    if (other.runtimeType != runtimeType) {
      return false;
    }
    return other is SkwasmParagraphStyle &&
        other._textAlign == _textAlign &&
        other._textDirection == _textDirection &&
        other._fontWeight == _fontWeight &&
        other._fontStyle == _fontStyle &&
        other._maxLines == _maxLines &&
        other._fontFamily == _fontFamily &&
        other._fontSize == _fontSize &&
        other._height == _height &&
        other._textHeightBehavior == _textHeightBehavior &&
        other._strutStyle == _strutStyle &&
        other._ellipsis == _ellipsis &&
        other._locale == _locale;
  }

  @override
  int get hashCode {
    return Object.hash(
      _textAlign,
      _textDirection,
      _fontWeight,
      _fontStyle,
      _maxLines,
      _fontFamily,
      _fontSize,
      _height,
      _textHeightBehavior,
      _strutStyle,
      _ellipsis,
      _locale,
    );
  }

  @override
  String toString() {
    String result = super.toString();
    assert(() {
      final double? fontSize = _fontSize;
      final double? height = _height;
      result =
          'ParagraphStyle('
          'textAlign: ${_textAlign ?? "unspecified"}, '
          'textDirection: ${_textDirection ?? "unspecified"}, '
          'fontWeight: ${_fontWeight ?? "unspecified"}, '
          'fontStyle: ${_fontStyle ?? "unspecified"}, '
          'maxLines: ${_maxLines ?? "unspecified"}, '
          'textHeightBehavior: ${_textHeightBehavior ?? "unspecified"}, '
          'fontFamily: ${_fontFamily ?? "unspecified"}, '
          'fontSize: ${fontSize != null ? fontSize.toStringAsFixed(1) : "unspecified"}, '
          'height: ${height != null ? "${height.toStringAsFixed(1)}x" : "unspecified"}, '
          'strutStyle: ${_strutStyle ?? "unspecified"}, '
          'ellipsis: ${_ellipsis != null ? '"$_ellipsis"' : "unspecified"}, '
          'locale: ${_locale ?? "unspecified"}'
          ')';
      return true;
    }());
    return result;
  }
}

class SkwasmParagraphBuilder extends SkwasmObjectWrapper<RawParagraphBuilder>
    implements ui.ParagraphBuilder {
  factory SkwasmParagraphBuilder(SkwasmParagraphStyle style, SkwasmFontCollection collection) {
    final (paragraphStyleHandle, nativeTextStyle) = style.createNative();
    final builder = SkwasmParagraphBuilder._(
      paragraphBuilderCreate(paragraphStyleHandle, collection.handle),
      style,
      nativeTextStyle,
    );
    paragraphStyleDispose(paragraphStyleHandle);
    return builder;
  }

  SkwasmParagraphBuilder._(
    ParagraphBuilderHandle handle,
    this.style,
    SkwasmNativeTextStyle baseTextStyle,
  ) : super(handle, _registry) {
    textStyleStack.add(baseTextStyle);
  }

  static final SkwasmFinalizationRegistry<RawParagraphBuilder> _registry =
      SkwasmFinalizationRegistry<RawParagraphBuilder>(
        (ParagraphBuilderHandle handle) => paragraphBuilderDispose(handle),
      );

  final SkwasmParagraphStyle style;
  final List<SkwasmNativeTextStyle> textStyleStack = <SkwasmNativeTextStyle>[];

  @override
  List<double> placeholderScales = <double>[];

  @override
  void addPlaceholder(
    double width,
    double height,
    ui.PlaceholderAlignment alignment, {
    double scale = 1.0,
    double? baselineOffset,
    ui.TextBaseline? baseline,
  }) {
    paragraphBuilderAddPlaceholder(
      handle,
      width * scale,
      height * scale,
      alignment.index,
      (baselineOffset ?? height) * scale,
      (baseline ?? ui.TextBaseline.alphabetic).index,
    );
    placeholderScales.add(scale);
  }

  @override
  void addText(String text) {
    final SkString16Handle stringHandle = skString16FromDartString(text);
    paragraphBuilderAddText(handle, stringHandle);
    skString16Free(stringHandle);
  }

  static final DomV8BreakIterator _v8BreakIterator = createV8BreakIterator();
  static final DomSegmenter _graphemeSegmenter = createIntlSegmenter(granularity: 'grapheme');
  static final DomSegmenter _wordSegmenter = createIntlSegmenter(granularity: 'word');
  static final DomTextDecoder _utf8Decoder = DomTextDecoder();

  void _addSegmenterData() => withStackScope((StackScope scope) {
    // Because some of the string processing is dealt with manually in dart,
    // and some is handled by the browser, we actually need both a dart string
    // and a JS string. Converting from linear memory to Dart strings or JS
    // strings is more efficient than directly converting to each other, so we
    // just create both up front here.
    final Pointer<Uint32> outSize = scope.allocUint32Array(1);
    final Pointer<Uint8> utf8Data = paragraphBuilderGetUtf8Text(handle, outSize);

    final String text;
    final JSString jsText;
    if (utf8Data == nullptr) {
      text = '';
      jsText = ''.toJS;
    } else {
      final List<int> codeUnitList = List<int>.generate(
        outSize.value,
        (int index) => utf8Data[index],
      );
      text = utf8.decode(codeUnitList);
      jsText = _utf8Decoder.decode(
        // In an ideal world we would just use a subview of wasm memory rather
        // than a slice, but the TextDecoder API doesn't work on shared buffer
        // sources yet.
        // See https://bugs.chromium.org/p/chromium/issues/detail?id=1012656
        JSUint8Array(
          skwasmInstance.wasmMemory.buffer,
        ).slice(utf8Data.address, utf8Data.address + outSize.value),
      );
    }

    _addGraphemeBreakData(text, jsText);
    _addWordBreakData(text, jsText);
    _addLineBreakData(text, jsText);
  });

  UnicodePositionBufferHandle _createBreakPositionBuffer(
    String text,
    JSString jsText,
    DomSegmenter segmenter,
  ) {
    final DomIteratorWrapper<DomSegment> iterator = segmenter.segmentRaw(jsText).iterator();
    final List<int> breaks = <int>[];
    while (iterator.moveNext()) {
      breaks.add(iterator.current.index);
    }
    breaks.add(text.length);

    final UnicodePositionBufferHandle positionBuffer = unicodePositionBufferCreate(breaks.length);
    final Pointer<Uint32> buffer = unicodePositionBufferGetDataPointer(positionBuffer);
    for (int i = 0; i < breaks.length; i++) {
      buffer[i] = breaks[i];
    }
    return positionBuffer;
  }

  void _addGraphemeBreakData(String text, JSString jsText) {
    final UnicodePositionBufferHandle positionBuffer = _createBreakPositionBuffer(
      text,
      jsText,
      _graphemeSegmenter,
    );
    paragraphBuilderSetGraphemeBreaksUtf16(handle, positionBuffer);
    unicodePositionBufferFree(positionBuffer);
  }

  void _addWordBreakData(String text, JSString jsText) {
    final UnicodePositionBufferHandle positionBuffer = _createBreakPositionBuffer(
      text,
      jsText,
      _wordSegmenter,
    );
    paragraphBuilderSetWordBreaksUtf16(handle, positionBuffer);
    unicodePositionBufferFree(positionBuffer);
  }

  void _addLineBreakData(String text, JSString jsText) {
    final List<LineBreakFragment> lineBreaks = breakLinesUsingV8BreakIterator(
      text,
      jsText,
      _v8BreakIterator,
    );
    final LineBreakBufferHandle lineBreakBuffer = lineBreakBufferCreate(lineBreaks.length + 1);
    final Pointer<LineBreak> lineBreakPointer = lineBreakBufferGetDataPointer(lineBreakBuffer);

    // First line break is always zero. The buffer is zero initialized, so we can just
    // skip the first one.
    for (int i = 0; i < lineBreaks.length; i++) {
      final LineBreakFragment fragment = lineBreaks[i];
      lineBreakPointer[i + 1].position = fragment.end;
      lineBreakPointer[i + 1].lineBreakType = fragment.type == LineBreakType.mandatory
          ? kHardLineBreak
          : kSoftLineBreak;
    }
    paragraphBuilderSetLineBreaksUtf16(handle, lineBreakBuffer);
    lineBreakBufferFree(lineBreakBuffer);
  }

  @override
  ui.Paragraph build() {
    if (!skwasmIsHeavy()) {
      _addSegmenterData();
    }
    final paragraph = SkwasmParagraph(paragraphBuilderBuild(handle));
    while (textStyleStack.isNotEmpty) {
      final style = textStyleStack.removeLast();
      style.dispose();
    }
    dispose();
    return paragraph;
  }

  @override
  int get placeholderCount => placeholderScales.length;

  @override
  void pop() {
    final SkwasmNativeTextStyle style = textStyleStack.removeLast();
    style.dispose();
    paragraphBuilderPop(handle);
  }

  @override
  void pushStyle(ui.TextStyle textStyle) {
    textStyle as SkwasmTextStyle;
    final SkwasmNativeTextStyle nativeStyle = textStyleStack.last.copy();
    textStyle.applyToNative(nativeStyle);
    textStyleStack.add(nativeStyle);
    paragraphBuilderPushStyle(handle, nativeStyle.handle);
  }
}
