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

import 'dart:math' as math;

import 'package:ui/ui.dart' as ui;

import '../util.dart';
import 'canvas_paragraph.dart';
import 'fragmenter.dart';
import 'layout_service.dart';
import 'line_breaker.dart';
import 'paragraph.dart';
import 'text_direction.dart';

/// Splits [text] into fragments that are ready to be laid out by
/// [TextLayoutService].
///
/// This fragmenter takes into account line breaks, directionality and styles.
class LayoutFragmenter extends TextFragmenter {
  const LayoutFragmenter(super.text, this.paragraphSpans);

  final List<ParagraphSpan> paragraphSpans;

  @override
  List<LayoutFragment> fragment() {
    final List<LayoutFragment> fragments = <LayoutFragment>[];

    int fragmentStart = 0;

    final Iterator<LineBreakFragment> lineBreakFragments = LineBreakFragmenter(text).fragment().iterator..moveNext();
    final Iterator<BidiFragment> bidiFragments = BidiFragmenter(text).fragment().iterator..moveNext();
    final Iterator<ParagraphSpan> spans = paragraphSpans.iterator..moveNext();

    LineBreakFragment currentLineBreakFragment = lineBreakFragments.current;
    BidiFragment currentBidiFragment = bidiFragments.current;
    ParagraphSpan currentSpan = spans.current;

    while (true) {
      final int fragmentEnd = math.min(
        currentLineBreakFragment.end,
        math.min(
          currentBidiFragment.end,
          currentSpan.end,
        ),
      );

      final int distanceFromLineBreak = currentLineBreakFragment.end - fragmentEnd;

      final LineBreakType lineBreakType = distanceFromLineBreak == 0
          ? currentLineBreakFragment.type
          : LineBreakType.prohibited;

      final int trailingNewlines = currentLineBreakFragment.trailingNewlines - distanceFromLineBreak;
      final int trailingSpaces = currentLineBreakFragment.trailingSpaces - distanceFromLineBreak;

      final int fragmentLength = fragmentEnd - fragmentStart;
      fragments.add(LayoutFragment(
        fragmentStart,
        fragmentEnd,
        lineBreakType,
        currentBidiFragment.textDirection,
        currentBidiFragment.fragmentFlow,
        currentSpan,
        trailingNewlines: clampInt(trailingNewlines, 0, fragmentLength),
        trailingSpaces: clampInt(trailingSpaces, 0, fragmentLength),
      ));

      fragmentStart = fragmentEnd;

      bool moved = false;
      if (currentLineBreakFragment.end == fragmentEnd) {
        if (lineBreakFragments.moveNext()) {
          moved = true;
          currentLineBreakFragment = lineBreakFragments.current;
        }
      }
      if (currentBidiFragment.end == fragmentEnd) {
        if (bidiFragments.moveNext()) {
          moved = true;
          currentBidiFragment = bidiFragments.current;
        }
      }
      if (currentSpan.end == fragmentEnd) {
        if (spans.moveNext()) {
          moved = true;
          currentSpan = spans.current;
        }
      }

      // Once we reached the end of all fragments, exit the loop.
      if (!moved) {
        break;
      }
    }

    return fragments;
  }
}

abstract class _CombinedFragment extends TextFragment {
  _CombinedFragment(
    super.start,
    super.end,
    this.type,
    this._textDirection,
    this.fragmentFlow,
    this.span, {
    required this.trailingNewlines,
    required this.trailingSpaces,
  })  : assert(trailingNewlines >= 0),
        assert(trailingSpaces >= trailingNewlines);

  final LineBreakType type;

  ui.TextDirection? get textDirection => _textDirection;
  ui.TextDirection? _textDirection;

  final FragmentFlow fragmentFlow;

  final ParagraphSpan span;

  final int trailingNewlines;

  final int trailingSpaces;

  @override
  int get hashCode => Object.hash(
    start,
    end,
    type,
    textDirection,
    fragmentFlow,
    span,
    trailingNewlines,
    trailingSpaces,
  );

  @override
  bool operator ==(Object other) {
    return other is LayoutFragment &&
        other.start == start &&
        other.end == end &&
        other.type == type &&
        other.textDirection == textDirection &&
        other.fragmentFlow == fragmentFlow &&
        other.span == span &&
        other.trailingNewlines == trailingNewlines &&
        other.trailingSpaces == trailingSpaces;
  }
}

class LayoutFragment extends _CombinedFragment with _FragmentMetrics, _FragmentPosition, _FragmentBox {
  LayoutFragment(
    super.start,
    super.end,
    super.type,
    super.textDirection,
    super.fragmentFlow,
    super.span, {
    required super.trailingNewlines,
    required super.trailingSpaces,
  });

  int get length => end - start;
  bool get isSpaceOnly => length == trailingSpaces;
  bool get isPlaceholder => span is PlaceholderSpan;
  bool get isBreak => type != LineBreakType.prohibited;
  bool get isHardBreak => type == LineBreakType.mandatory || type == LineBreakType.endOfText;
  EngineTextStyle get style => span.style;

  /// Returns the substring from [paragraph] that corresponds to this fragment,
  /// excluding new line characters.
  String getText(CanvasParagraph paragraph) {
    return paragraph.plainText.substring(start, end - trailingNewlines);
  }

  /// Splits this fragment into two fragments with the split point being the
  /// given [index].
  // TODO(mdebbar): If we ever get multiple return values in Dart, we should use it!
  //                See: https://github.com/dart-lang/language/issues/68
  List<LayoutFragment?> split(int index) {
    assert(start <= index);
    assert(index <= end);

    if (start == index) {
      return <LayoutFragment?>[null, this];
    }

    if (end == index) {
      return <LayoutFragment?>[this, null];
    }

    // The length of the second fragment after the split.
    final int secondLength = end - index;

    // Trailing spaces/new lines go to the second fragment. Any left over goes
    // to the first fragment.
    final int secondTrailingNewlines = math.min(trailingNewlines, secondLength);
    final int secondTrailingSpaces = math.min(trailingSpaces, secondLength);

    return <LayoutFragment>[
      LayoutFragment(
        start,
        index,
        LineBreakType.prohibited,
        textDirection,
        fragmentFlow,
        span,
        trailingNewlines: trailingNewlines - secondTrailingNewlines,
        trailingSpaces: trailingSpaces - secondTrailingSpaces,
      ),
      LayoutFragment(
        index,
        end,
        type,
        textDirection,
        fragmentFlow,
        span,
        trailingNewlines: secondTrailingNewlines,
        trailingSpaces: secondTrailingSpaces,
      ),
    ];
  }

  @override
  String toString() {
    return '$LayoutFragment($start, $end, $type, $textDirection)';
  }
}

mixin _FragmentMetrics on _CombinedFragment {
  late Spanometer _spanometer;

  /// The rise from the baseline as calculated from the font and style for this text.
  double get ascent => _ascent;
  late double _ascent;

  /// The drop from the baseline as calculated from the font and style for this text.
  double get descent => _descent;
  late double _descent;

  /// The width of the measured text, not including trailing spaces.
  double get widthExcludingTrailingSpaces => _widthExcludingTrailingSpaces;
  late double _widthExcludingTrailingSpaces;

  /// The width of the measured text, including any trailing spaces.
  double get widthIncludingTrailingSpaces => _widthIncludingTrailingSpaces + _extraWidthForJustification;
  late double _widthIncludingTrailingSpaces;

  double _extraWidthForJustification = 0.0;

  /// The total height as calculated from the font and style for this text.
  double get height => ascent + descent;

  double get widthOfTrailingSpaces => widthIncludingTrailingSpaces - widthExcludingTrailingSpaces;

  /// Set measurement values for the fragment.
  void setMetrics(Spanometer spanometer, {
    required double ascent,
    required double descent,
    required double widthExcludingTrailingSpaces,
    required double widthIncludingTrailingSpaces,
  }) {
    _spanometer = spanometer;
    _ascent = ascent;
    _descent = descent;
    _widthExcludingTrailingSpaces = widthExcludingTrailingSpaces;
    _widthIncludingTrailingSpaces = widthIncludingTrailingSpaces;
  }
}

/// Encapsulates positioning of the fragment relative to the line.
///
/// The coordinates are all relative to the line it belongs to. For example,
/// [left] is the distance from the left edge of the line to the left edge of
/// the fragment.
///
/// This is what the various measurements/coordinates look like for a fragment
/// in an LTR paragraph:
///
///          *------------------------line.width-----------------*
///                            *---width----*
///          ┌─────────────────┬────────────┬────────────────────┐
///          │                 │--FRAGMENT--│                    │
///          └─────────────────┴────────────┴────────────────────┘
///          *---startOffset---*
///          *------left-------*
///          *--------endOffset-------------*
///          *----------right---------------*
///
///
/// And in an RTL paragraph, [startOffset] and [endOffset] are flipped because
/// the line starts from the right. Here's what they look like:
///
///          *------------------------line.width-----------------*
///                            *---width----*
///          ┌─────────────────┬────────────┬────────────────────┐
///          │                 │--FRAGMENT--│                    │
///          └─────────────────┴────────────┴────────────────────┘
///                                         *----startOffset-----*
///          *------left-------*
///                            *-----------endOffset-------------*
///          *----------right---------------*
///
mixin _FragmentPosition on _CombinedFragment, _FragmentMetrics {
  /// The distance from the beginning of the line to the beginning of the fragment.
  double get startOffset => _startOffset;
  late double _startOffset;

  /// The width of the line that contains this fragment.
  late ParagraphLine line;

  /// The distance from the beginning of the line to the end of the fragment.
  double get endOffset => startOffset + widthIncludingTrailingSpaces;

  /// The distance from the left edge of the line to the left edge of the fragment.
  double get left => line.textDirection == ui.TextDirection.ltr
      ? startOffset
      : line.width - endOffset;

  /// The distance from the left edge of the line to the right edge of the fragment.
  double get right => line.textDirection == ui.TextDirection.ltr
      ? endOffset
      : line.width - startOffset;

  /// Set the horizontal position of this fragment relative to the [line] that
  /// contains it.
  void setPosition({
    required double startOffset,
    required ui.TextDirection textDirection,
  }) {
    _startOffset = startOffset;
    _textDirection ??= textDirection;
  }

  /// Adjust the width of this fragment for paragraph justification.
  void justifyTo({required double paragraphWidth}) {
    // Only justify this fragment if it's not a trailing space in the line.
    if (end > line.endIndex - line.trailingSpaces) {
      // Don't justify fragments that are part of trailing spaces of the line.
      return;
    }

    if (trailingSpaces == 0) {
      // If this fragment has no spaces, there's nothing to justify.
      return;
    }

    final double justificationTotal = paragraphWidth - line.width;
    final double justificationPerSpace = justificationTotal / line.nonTrailingSpaces;
    _extraWidthForJustification = justificationPerSpace * trailingSpaces;
  }
}

/// Encapsulates calculations related to the bounding box of the fragment
/// relative to the paragraph.
mixin _FragmentBox on _CombinedFragment, _FragmentMetrics, _FragmentPosition {
  double get top => line.baseline - ascent;
  double get bottom => line.baseline + descent;

  late final ui.TextBox _textBoxIncludingTrailingSpaces = ui.TextBox.fromLTRBD(
    line.left + left,
    top,
    line.left + right,
    bottom,
    textDirection!,
  );

  /// Whether or not the trailing spaces of this fragment are part of trailing
  /// spaces of the line containing the fragment.
  bool get _isPartOfTrailingSpacesInLine => end > line.endIndex - line.trailingSpaces;

  /// Returns a [ui.TextBox] for the purpose of painting this fragment.
  ///
  /// The coordinates of the resulting [ui.TextBox] are relative to the
  /// paragraph, not to the line.
  ///
  /// Trailing spaces in each line aren't painted on the screen, so they are
  /// excluded from the resulting text box.
  ui.TextBox toPaintingTextBox() {
    if (_isPartOfTrailingSpacesInLine) {
      // For painting, we exclude the width of trailing spaces from the box.
      return textDirection! == ui.TextDirection.ltr
          ? ui.TextBox.fromLTRBD(
              line.left + left,
              top,
              line.left + right - widthOfTrailingSpaces,
              bottom,
              textDirection!,
            )
          : ui.TextBox.fromLTRBD(
              line.left + left + widthOfTrailingSpaces,
              top,
              line.left + right,
              bottom,
              textDirection!,
            );
    }
    return _textBoxIncludingTrailingSpaces;
  }

  /// Returns a [ui.TextBox] representing this fragment.
  ///
  /// The coordinates of the resulting [ui.TextBox] are relative to the
  /// paragraph, not to the line.
  ///
  /// As opposed to [toPaintingTextBox], the resulting text box from this method
  /// includes trailing spaces of the fragment.
  ui.TextBox toTextBox({
    int? start,
    int? end,
  }) {
    start ??= this.start;
    end ??= this.end;

    if (start <= this.start && end >= this.end - trailingNewlines) {
      return _textBoxIncludingTrailingSpaces;
    }
    return _intersect(start, end);
  }

  /// Performs the intersection of this fragment with the range given by [start] and
  /// [end] indices, and returns a [ui.TextBox] representing that intersection.
  ///
  /// The coordinates of the resulting [ui.TextBox] are relative to the
  /// paragraph, not to the line.
  ui.TextBox _intersect(int start, int end) {
    // `_intersect` should only be called when there's an actual intersection.
    assert(start > this.start || end < this.end);

    final double before;
    if (start <= this.start) {
      before = 0.0;
    } else {
      _spanometer.currentSpan = span;
      before = _spanometer.measureRange(this.start, start);
    }

    final double after;
    if (end >= this.end - trailingNewlines) {
      after = 0.0;
    } else {
      _spanometer.currentSpan = span;
      after = _spanometer.measureRange(end, this.end - trailingNewlines);
    }

    final double left, right;
    if (textDirection! == ui.TextDirection.ltr) {
      // Example: let's say the text is "Loremipsum" and we want to get the box
      // for "rem". In this case, `before` is the width of "Lo", and `after`
      // is the width of "ipsum".
      //
      // Here's how the measurements/coordinates look like:
      //
      //              before         after
      //              |----|     |----------|
      //              +---------------------+
      //              | L o r e m i p s u m |
      //              +---------------------+
      //    this.left ^                     ^ this.right
      left = this.left + before;
      right = this.right - after;
    } else {
      // Example: let's say the text is "txet_werbeH" ("Hebrew_text" flowing from
      // right to left). Say we want to get the box for "brew". The `before` is
      // the width of "He", and `after` is the width of "_text".
      //
      //                 after           before
      //              |----------|       |----|
      //              +-----------------------+
      //              | t x e t _ w e r b e H |
      //              +-----------------------+
      //    this.left ^                       ^ this.right
      //
      // Notice how `before` and `after` are reversed in the RTL example. That's
      // because the text flows from right to left.
      left = this.left + after;
      right = this.right - before;
    }

    // The fragment's left and right edges are relative to the line. In order
    // to make them relative to the paragraph, we need to add the left edge of
    // the line.
    return ui.TextBox.fromLTRBD(
      line.left + left,
      top,
      line.left + right,
      bottom,
      textDirection!,
    );
  }

  /// Returns the text position within this fragment's range that's closest to
  /// the given [x] offset.
  ///
  /// The [x] offset is expected to be relative to the left edge of the fragment.
  ui.TextPosition getPositionForX(double x) {
    x = _makeXDirectionAgnostic(x);

    final int startIndex = start;
    final int endIndex = end - trailingNewlines;

    // Check some special cases to return the result quicker.

    final int length = endIndex - startIndex;
    if (length == 0) {
      return ui.TextPosition(offset: startIndex);
    }
    if (length == 1) {
      // Find out if `x` is closer to `startIndex` or `endIndex`.
      final double distanceFromStart = x;
      final double distanceFromEnd = widthIncludingTrailingSpaces - x;
      return distanceFromStart < distanceFromEnd
          ? ui.TextPosition(offset: startIndex)
          : ui.TextPosition(offset: endIndex, affinity: ui.TextAffinity.upstream,);
    }

    _spanometer.currentSpan = span;
    // The resulting `cutoff` is the index of the character where the `x` offset
    // falls. We should return the text position of either `cutoff` or
    // `cutoff + 1` depending on which one `x` is closer to.
    //
    //   offset x
    //      ↓
    // "A B C D E F"
    //     ↑
    //   cutoff
    final int cutoff = _spanometer.forceBreak(
      startIndex,
      endIndex,
      availableWidth: x,
      allowEmpty: true,
    );

    if (cutoff == endIndex) {
      return ui.TextPosition(
        offset: cutoff,
        affinity: ui.TextAffinity.upstream,
      );
    }

    final double lowWidth = _spanometer.measureRange(startIndex, cutoff);
    final double highWidth = _spanometer.measureRange(startIndex, cutoff + 1);

    // See if `x` is closer to `cutoff` or `cutoff + 1`.
    if (x - lowWidth < highWidth - x) {
      // The offset is closer to cutoff.
      return ui.TextPosition(offset: cutoff);
    } else {
      // The offset is closer to cutoff + 1.
      return ui.TextPosition(
        offset: cutoff + 1,
        affinity: ui.TextAffinity.upstream,
      );
    }
  }

  /// Transforms the [x] coordinate to be direction-agnostic.
  ///
  /// The X (input) is relative to the [left] edge of the fragment, and this
  /// method returns an X' (output) that's relative to beginning of the text.
  ///
  /// Here's how it looks for a fragment with LTR content:
  ///
  ///          *------------------------line width------------------*
  ///                      *-----X (input)
  ///          ┌───────────┬────────────────────────┬───────────────┐
  ///          │           │ ---text-direction----> │               │
  ///          └───────────┴────────────────────────┴───────────────┘
  ///                      *-----X' (output)
  ///          *---left----*
  ///          *---------------right----------------*
  ///
  ///
  /// And here's how it looks for a fragment with RTL content:
  ///
  ///          *------------------------line width------------------*
  ///                      *-----X (input)
  ///          ┌───────────┬────────────────────────┬───────────────┐
  ///          │           │ <---text-direction---- │               │
  ///          └───────────┴────────────────────────┴───────────────┘
  ///                   (output) X'-----------------*
  ///          *---left----*
  ///          *---------------right----------------*
  ///
  double _makeXDirectionAgnostic(double x) {
    if (textDirection == ui.TextDirection.rtl) {
      return widthIncludingTrailingSpaces - x;
    }
    return x;
  }
}

class EllipsisFragment extends LayoutFragment {
  EllipsisFragment(
    int index,
    ParagraphSpan span,
  ) : super(
          index,
          index,
          LineBreakType.endOfText,
          null,
          // The ellipsis is always at the end of the line, so it can't be
          // sandwiched. This means it'll always follow the paragraph direction.
          FragmentFlow.sandwich,
          span,
          trailingNewlines: 0,
          trailingSpaces: 0,
        );

  @override
  bool get isSpaceOnly => false;

  @override
  bool get isPlaceholder => false;

  @override
  String getText(CanvasParagraph paragraph) {
    return paragraph.paragraphStyle.ellipsis!;
  }

  @override
  List<LayoutFragment> split(int index) {
    throw Exception('Cannot split an EllipsisFragment');
  }
}
