// Copyright (c) 2014, the Dart project authors.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:path/path.dart' as p;
import 'package:term_glyph/term_glyph.dart' as glyph;

import 'file.dart';
import 'highlighter.dart';
import 'location.dart';
import 'span_mixin.dart';
import 'span_with_context.dart';
import 'utils.dart';

/// A class that describes a segment of source text.
abstract class SourceSpan implements Comparable<SourceSpan> {
  /// The start location of this span.
  SourceLocation get start;

  /// The end location of this span, exclusive.
  SourceLocation get end;

  /// The source text for this span.
  String get text;

  /// The URL of the source (typically a file) of this span.
  ///
  /// This may be null, indicating that the source URL is unknown or
  /// unavailable.
  Uri? get sourceUrl;

  /// The length of this span, in characters.
  int get length;

  /// Creates a new span from [start] to [end] (exclusive) containing [text].
  ///
  /// [start] and [end] must have the same source URL and [start] must come
  /// before [end]. [text] must have a number of characters equal to the
  /// distance between [start] and [end].
  factory SourceSpan(SourceLocation start, SourceLocation end, String text) =>
      SourceSpanBase(start, end, text);

  /// Creates a new span that's the union of `this` and [other].
  ///
  /// The two spans must have the same source URL and may not be disjoint.
  /// [text] is computed by combining `this.text` and `other.text`.
  SourceSpan union(SourceSpan other);

  /// Compares two spans.
  ///
  /// [other] must have the same source URL as `this`. This orders spans by
  /// [start] then [length].
  @override
  int compareTo(SourceSpan other);

  /// Formats [message] in a human-friendly way associated with this span.
  ///
  /// [color] may either be a [String], a [bool], or `null`. If it's a string,
  /// it indicates an [ANSI terminal color escape][] that should
  /// be used to highlight the span's text (for example, `"\u001b[31m"` will
  /// color red). If it's `true`, it indicates that the text should be
  /// highlighted using the default color. If it's `false` or `null`, it
  /// indicates that the text shouldn't be highlighted.
  ///
  /// This uses the full range of Unicode characters to highlight the source
  /// span if [glyph.ascii] is `false` (the default), but only uses ASCII
  /// characters if it's `true`.
  ///
  /// [ANSI terminal color escape]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
  String message(String message, {Object? color});

  /// Prints the text associated with this span in a user-friendly way.
  ///
  /// This is identical to [message], except that it doesn't print the file
  /// name, line number, column number, or message. If [length] is 0 and this
  /// isn't a [SourceSpanWithContext], returns an empty string.
  ///
  /// [color] may either be a [String], a [bool], or `null`. If it's a string,
  /// it indicates an [ANSI terminal color escape][] that should
  /// be used to highlight the span's text (for example, `"\u001b[31m"` will
  /// color red). If it's `true`, it indicates that the text should be
  /// highlighted using the default color. If it's `false` or `null`, it
  /// indicates that the text shouldn't be highlighted.
  ///
  /// This uses the full range of Unicode characters to highlight the source
  /// span if [glyph.ascii] is `false` (the default), but only uses ASCII
  /// characters if it's `true`.
  ///
  /// [ANSI terminal color escape]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
  String highlight({Object? color});
}

/// A base class for source spans with [start], [end], and [text] known at
/// construction time.
class SourceSpanBase extends SourceSpanMixin {
  @override
  final SourceLocation start;
  @override
  final SourceLocation end;
  @override
  final String text;

  SourceSpanBase(this.start, this.end, this.text) {
    if (end.sourceUrl != start.sourceUrl) {
      throw ArgumentError('Source URLs "${start.sourceUrl}" and '
          " \"${end.sourceUrl}\" don't match.");
    } else if (end.offset < start.offset) {
      throw ArgumentError('End $end must come after start $start.');
    } else if (text.length != start.distance(end)) {
      throw ArgumentError('Text "$text" must be ${start.distance(end)} '
          'characters long.');
    }
  }
}

// TODO(#52): Move these to instance methods in the next breaking release.
/// Extension methods on the base [SourceSpan] API.
extension SourceSpanExtension on SourceSpan {
  /// Like [SourceSpan.message], but also highlights [secondarySpans] to provide
  /// the user with additional context.
  ///
  /// Each span takes a label ([label] for this span, and the values of the
  /// [secondarySpans] map for the secondary spans) that's used to indicate to
  /// the user what that particular span represents.
  ///
  /// If [color] is `true`, [ANSI terminal color escapes][] are used to color
  /// the resulting string. By default this span is colored red and the
  /// secondary spans are colored blue, but that can be customized by passing
  /// ANSI escape strings to [primaryColor] or [secondaryColor].
  ///
  /// [ANSI terminal color escapes]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
  ///
  /// Each span in [secondarySpans] must refer to the same document as this
  /// span. Throws an [ArgumentError] if any secondary span has a different
  /// source URL than this span.
  ///
  /// Note that while this will work with plain [SourceSpan]s, it will produce
  /// much more useful output with [SourceSpanWithContext]s (including
  /// [FileSpan]s).
  String messageMultiple(
      String message, String label, Map<SourceSpan, String> secondarySpans,
      {bool color = false, String? primaryColor, String? secondaryColor}) {
    final buffer = StringBuffer()
      ..write('line ${start.line + 1}, column ${start.column + 1}');
    if (sourceUrl != null) buffer.write(' of ${p.prettyUri(sourceUrl)}');
    buffer
      ..writeln(': $message')
      ..write(highlightMultiple(label, secondarySpans,
          color: color,
          primaryColor: primaryColor,
          secondaryColor: secondaryColor));
    return buffer.toString();
  }

  /// Like [SourceSpan.highlight], but also highlights [secondarySpans] to
  /// provide the user with additional context.
  ///
  /// Each span takes a label ([label] for this span, and the values of the
  /// [secondarySpans] map for the secondary spans) that's used to indicate to
  /// the user what that particular span represents.
  ///
  /// If [color] is `true`, [ANSI terminal color escapes][] are used to color
  /// the resulting string. By default this span is colored red and the
  /// secondary spans are colored blue, but that can be customized by passing
  /// ANSI escape strings to [primaryColor] or [secondaryColor].
  ///
  /// [ANSI terminal color escapes]: https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
  ///
  /// Each span in [secondarySpans] must refer to the same document as this
  /// span. Throws an [ArgumentError] if any secondary span has a different
  /// source URL than this span.
  ///
  /// Note that while this will work with plain [SourceSpan]s, it will produce
  /// much more useful output with [SourceSpanWithContext]s (including
  /// [FileSpan]s).
  String highlightMultiple(String label, Map<SourceSpan, String> secondarySpans,
          {bool color = false, String? primaryColor, String? secondaryColor}) =>
      Highlighter.multiple(this, label, secondarySpans,
              color: color,
              primaryColor: primaryColor,
              secondaryColor: secondaryColor)
          .highlight();

  /// Returns a span from [start] code units (inclusive) to [end] code units
  /// (exclusive) after the beginning of this span.
  SourceSpan subspan(int start, [int? end]) {
    RangeError.checkValidRange(start, end, length);
    if (start == 0 && (end == null || end == length)) return this;

    final locations = subspanLocations(this, start, end);
    return SourceSpan(locations[0], locations[1], text.substring(start, end));
  }
}
