// 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 'span.dart';

// TODO(nweiz): Use SourceLocationMixin once we decide to cut a release with
// breaking changes. See SourceLocationMixin for details.

/// A class that describes a single location within a source file.
///
/// This class should not be extended. Instead, [SourceLocationBase] should be
/// extended instead.
class SourceLocation implements Comparable<SourceLocation> {
  /// URL of the source containing this location.
  ///
  /// This may be null, indicating that the source URL is unknown or
  /// unavailable.
  final Uri? sourceUrl;

  /// The 0-based offset of this location in the source.
  final int offset;

  /// The 0-based line of this location in the source.
  final int line;

  /// The 0-based column of this location in the source
  final int column;

  /// Returns a representation of this location in the `source:line:column`
  /// format used by text editors.
  ///
  /// This prints 1-based lines and columns.
  String get toolString {
    final source = sourceUrl ?? 'unknown source';
    return '$source:${line + 1}:${column + 1}';
  }

  /// Creates a new location indicating [offset] within [sourceUrl].
  ///
  /// [line] and [column] default to assuming the source is a single line. This
  /// means that [line] defaults to 0 and [column] defaults to [offset].
  ///
  /// [sourceUrl] may be either a [String], a [Uri], or `null`.
  SourceLocation(this.offset, {Object? sourceUrl, int? line, int? column})
      : sourceUrl =
            sourceUrl is String ? Uri.parse(sourceUrl) : sourceUrl as Uri?,
        line = line ?? 0,
        column = column ?? offset {
    if (offset < 0) {
      throw RangeError('Offset may not be negative, was $offset.');
    } else if (line != null && line < 0) {
      throw RangeError('Line may not be negative, was $line.');
    } else if (column != null && column < 0) {
      throw RangeError('Column may not be negative, was $column.');
    }
  }

  /// Returns the distance in characters between `this` and [other].
  ///
  /// This always returns a non-negative value.
  int distance(SourceLocation other) {
    if (sourceUrl != other.sourceUrl) {
      throw ArgumentError('Source URLs "$sourceUrl" and '
          "\"${other.sourceUrl}\" don't match.");
    }
    return (offset - other.offset).abs();
  }

  /// Returns a span that covers only a single point: this location.
  SourceSpan pointSpan() => SourceSpan(this, this, '');

  /// Compares two locations.
  ///
  /// [other] must have the same source URL as `this`.
  @override
  int compareTo(SourceLocation other) {
    if (sourceUrl != other.sourceUrl) {
      throw ArgumentError('Source URLs "$sourceUrl" and '
          "\"${other.sourceUrl}\" don't match.");
    }
    return offset - other.offset;
  }

  @override
  bool operator ==(Object other) =>
      other is SourceLocation &&
      sourceUrl == other.sourceUrl &&
      offset == other.offset;

  @override
  int get hashCode => (sourceUrl?.hashCode ?? 0) + offset;

  @override
  String toString() => '<$runtimeType: $offset $toolString>';
}

/// A base class for source locations with [offset], [line], and [column] known
/// at construction time.
class SourceLocationBase extends SourceLocation {
  SourceLocationBase(super.offset, {super.sourceUrl, super.line, super.column});
}
