// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:math';

import 'package:characters/characters.dart' show CharacterRange;

import 'text_layout_metrics.dart';

// Examples can assume:
// late TextLayoutMetrics textLayout;
// late TextSpan text;
// bool isWhitespace(int? codeUnit) => true;

/// Signature for a predicate that takes an offset into a UTF-16 string, and a
/// boolean that indicates the search direction.
typedef UntilPredicate = bool Function(int offset, bool forward);

/// An interface for retrieving the logical text boundary (as opposed to the
/// visual boundary) at a given code unit offset in a document.
///
/// Either the [getTextBoundaryAt] method, or both the
/// [getLeadingTextBoundaryAt] method and the [getTrailingTextBoundaryAt] method
/// must be implemented.
abstract class TextBoundary {
  /// A constant constructor to enable subclass override.
  const TextBoundary();

  /// Returns the offset of the closest text boundary before or at the given
  /// `position`, or null if no boundaries can be found.
  ///
  /// The return value, if not null, is usually less than or equal to `position`.
  ///
  /// The range of the return value is given by the closed interval
  /// `[0, string.length]`.
  int? getLeadingTextBoundaryAt(int position) {
    if (position < 0) {
      return null;
    }
    final int start = getTextBoundaryAt(position).start;
    return start >= 0 ? start : null;
  }

  /// Returns the offset of the closest text boundary after the given
  /// `position`, or null if there is no boundary can be found after `position`.
  ///
  /// The return value, if not null, is usually greater than `position`.
  ///
  /// The range of the return value is given by the closed interval
  /// `[0, string.length]`.
  int? getTrailingTextBoundaryAt(int position) {
    final int end = getTextBoundaryAt(max(0, position)).end;
    return end >= 0 ? end : null;
  }

  /// Returns the text boundary range that encloses the input position.
  ///
  /// The returned [TextRange] may contain `-1`, which indicates no boundaries
  /// can be found in that direction.
  TextRange getTextBoundaryAt(int position) {
    final int start = getLeadingTextBoundaryAt(position) ?? -1;
    final int end = getTrailingTextBoundaryAt(position) ?? -1;
    return TextRange(start: start, end: end);
  }
}

/// A [TextBoundary] subclass for retrieving the range of the grapheme the given
/// `position` is in.
///
/// The class is implemented using the
/// [characters](https://pub.dev/packages/characters) package.
class CharacterBoundary extends TextBoundary {
  /// Creates a [CharacterBoundary] with the text.
  const CharacterBoundary(this._text);

  final String _text;

  @override
  int? getLeadingTextBoundaryAt(int position) {
    if (position < 0) {
      return null;
    }
    final int graphemeStart = CharacterRange.at(
      _text,
      min(position, _text.length),
    ).stringBeforeLength;
    assert(CharacterRange.at(_text, graphemeStart).isEmpty);
    return graphemeStart;
  }

  @override
  int? getTrailingTextBoundaryAt(int position) {
    if (position >= _text.length) {
      return null;
    }
    final rangeAtPosition = CharacterRange.at(_text, max(0, position + 1));
    final int nextBoundary = rangeAtPosition.stringBeforeLength + rangeAtPosition.current.length;
    assert(nextBoundary == _text.length || CharacterRange.at(_text, nextBoundary).isEmpty);
    return nextBoundary;
  }

  @override
  TextRange getTextBoundaryAt(int position) {
    if (position < 0) {
      return TextRange(start: -1, end: getTrailingTextBoundaryAt(position) ?? -1);
    } else if (position >= _text.length) {
      return TextRange(start: getLeadingTextBoundaryAt(position) ?? -1, end: -1);
    }
    final rangeAtPosition = CharacterRange.at(_text, position);
    return rangeAtPosition.isNotEmpty
        ? TextRange(
            start: rangeAtPosition.stringBeforeLength,
            end: rangeAtPosition.stringBeforeLength + rangeAtPosition.current.length,
          )
        // rangeAtPosition is empty means `position` is a grapheme boundary.
        : TextRange(
            start: rangeAtPosition.stringBeforeLength,
            end: getTrailingTextBoundaryAt(position) ?? -1,
          );
  }
}

/// A [TextBoundary] subclass for locating closest line breaks to a given
/// `position`.
///
/// When the given `position` points to a hard line break, the returned range
/// is the line's content range before the hard line break, and does not contain
/// the given `position`. For instance, the line breaks at `position = 1` for
/// "a\nb" is `[0, 1)`, which does not contain the position `1`.
class LineBoundary extends TextBoundary {
  /// Creates a [LineBoundary] with the text and layout information.
  const LineBoundary(this._textLayout);

  final TextLayoutMetrics _textLayout;

  @override
  TextRange getTextBoundaryAt(int position) =>
      _textLayout.getLineAtOffset(TextPosition(offset: max(position, 0)));
}

/// A text boundary that uses paragraphs as logical boundaries.
///
/// A paragraph is defined as the range between line terminators. If no
/// line terminators exist then the paragraph boundary is the entire document.
class ParagraphBoundary extends TextBoundary {
  /// Creates a [ParagraphBoundary] with the text.
  const ParagraphBoundary(this._text);

  final String _text;

  /// Returns the [int] representing the start position of the paragraph that
  /// bounds the given `position`. The returned [int] is the position of the code unit
  /// that follows the line terminator that encloses the desired paragraph.
  @override
  int? getLeadingTextBoundaryAt(int position) {
    if (position < 0 || _text.isEmpty) {
      return null;
    }

    if (position >= _text.length) {
      return _text.length;
    }

    if (position == 0) {
      return 0;
    }

    var index = position;

    if (index > 1 && _text.codeUnitAt(index) == 0x0A && _text.codeUnitAt(index - 1) == 0x0D) {
      index -= 2;
    } else if (TextLayoutMetrics.isLineTerminator(_text.codeUnitAt(index))) {
      index -= 1;
    }

    while (index > 0) {
      if (TextLayoutMetrics.isLineTerminator(_text.codeUnitAt(index))) {
        return index + 1;
      }
      index -= 1;
    }

    return max(index, 0);
  }

  /// Returns the [int] representing the end position of the paragraph that
  /// bounds the given `position`. The returned [int] is the position of the
  /// code unit representing the trailing line terminator that encloses the
  /// desired paragraph.
  @override
  int? getTrailingTextBoundaryAt(int position) {
    if (position >= _text.length || _text.isEmpty) {
      return null;
    }

    if (position < 0) {
      return 0;
    }

    var index = position;

    while (!TextLayoutMetrics.isLineTerminator(_text.codeUnitAt(index))) {
      index += 1;
      if (index == _text.length) {
        return index;
      }
    }

    return index < _text.length - 1 &&
            _text.codeUnitAt(index) == 0x0D &&
            _text.codeUnitAt(index + 1) == 0x0A
        ? index + 2
        : index + 1;
  }
}

/// A text boundary that uses the entire document as logical boundary.
class DocumentBoundary extends TextBoundary {
  /// Creates a [DocumentBoundary] with the text.
  const DocumentBoundary(this._text);

  final String _text;

  @override
  int? getLeadingTextBoundaryAt(int position) => position < 0 ? null : 0;
  @override
  int? getTrailingTextBoundaryAt(int position) => position >= _text.length ? null : _text.length;
}
