// Copyright (c) 2020, 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:meta/meta.dart';

/// A class representing a change on a [String], intended to be compatible with
/// `package:analysis_server`'s [SourceEdit].
///
/// For example, changing a string from
/// ```
/// foo: foobar
/// ```
/// to
/// ```
/// foo: barbar
/// ```
/// will be represented by
/// `SourceEdit(offset: 4, length: 3, replacement: 'bar')`
@sealed
class SourceEdit {
  /// The offset from the start of the string where the modification begins.
  final int offset;

  /// The length of the substring to be replaced.
  final int length;

  /// The replacement string to be used.
  final String replacement;

  /// Creates a new [SourceEdit] instance. [offset], [length] and [replacement]
  /// must be non-null, and [offset] and [length] must be non-negative.
  factory SourceEdit(int offset, int length, String replacement) =>
      SourceEdit._(offset, length, replacement);

  SourceEdit._(this.offset, this.length, this.replacement) {
    RangeError.checkNotNegative(offset);
    RangeError.checkNotNegative(length);
  }

  @override
  bool operator ==(Object other) {
    if (other is SourceEdit) {
      return offset == other.offset &&
          length == other.length &&
          replacement == other.replacement;
    }

    return false;
  }

  @override
  int get hashCode => offset.hashCode ^ length.hashCode ^ replacement.hashCode;

  /// Constructs a SourceEdit from JSON.
  ///
  /// **Example:**
  /// ```dart
  /// final edit = {
  ///   'offset': 1,
  ///   'length': 2,
  ///   'replacement': 'replacement string'
  /// };
  ///
  /// final sourceEdit = SourceEdit.fromJson(edit);
  /// ```
  factory SourceEdit.fromJson(Map<String, dynamic> json) {
    final offset = json['offset'];
    final length = json['length'];
    final replacement = json['replacement'];

    if (offset is int && length is int && replacement is String) {
      return SourceEdit(offset, length, replacement);
    }

    throw const FormatException('Invalid JSON passed to SourceEdit');
  }

  /// Encodes this object as JSON-compatible structure.
  ///
  /// **Example:**
  /// ```dart
  /// import 'dart:convert' show jsonEncode;
  ///
  /// final edit = SourceEdit(offset, length, 'replacement string');
  /// final jsonString = jsonEncode(edit.toJson());
  /// print(jsonString);
  /// ```
  Map<String, dynamic> toJson() {
    return {'offset': offset, 'length': length, 'replacement': replacement};
  }

  @override
  String toString() => 'SourceEdit($offset, $length, "$replacement")';

  /// Applies a series of [SourceEdit]s to an original string, and return the
  /// final output.
  ///
  /// [edits] should be in order i.e. the first [SourceEdit] in [edits] should
  /// be the first edit applied to [original].
  ///
  /// **Example:**
  /// ```dart
  /// const original = 'YAML: YAML';
  /// final sourceEdits = [
  ///        SourceEdit(6, 4, "YAML Ain't Markup Language"),
  ///        SourceEdit(6, 4, "YAML Ain't Markup Language"),
  ///        SourceEdit(0, 4, "YAML Ain't Markup Language")
  ///      ];
  /// final result = SourceEdit.applyAll(original, sourceEdits);
  /// ```
  /// **Expected result:**
  /// ```dart
  /// "YAML Ain't Markup Language: YAML Ain't Markup Language Ain't Markup
  /// Language"
  /// ```
  static String applyAll(String original, Iterable<SourceEdit> edits) {
    return edits.fold(original, (current, edit) => edit.apply(current));
  }

  /// Applies one [SourceEdit]s to an original string, and return the final
  /// output.
  ///
  /// **Example:**
  /// ```dart
  /// final edit = SourceEdit(4, 3, 'bar');
  /// final originalString = 'foo: foobar';
  /// print(edit.apply(originalString)); // 'foo: barbar'
  /// ```
  String apply(String original) {
    return original.replaceRange(offset, offset + length, replacement);
  }
}
