content: // Copyright 2018 Google Inc. Use of this source code is governed by an
  // MIT-style license that can be found in the LICENSE file or at
  // https://opensource.org/licenses/MIT.
  
  import 'package:charcode/charcode.dart';
  import 'package:source_maps/source_maps.dart';
  import 'package:source_span/source_span.dart';
  
  import '../utils.dart';
  
  /// A [StringBuffer] that builds a [SourceMap] for the file being written.
  class SourceMapBuffer implements StringBuffer {
    /// The buffer that contains the text of the target file.
    final _buffer = StringBuffer();
  
    /// The source map entries that map the source files to [_buffer].
    final _entries = <Entry>[];
  
    /// The index of the current line in [_buffer].
    var _line = 0;
  
    /// The index of the current column in [_buffer].
    var _column = 0;
  
    /// Whether the text currently being written should be encompassed by a
    /// [SourceSpan].
    var _inSpan = false;
  
    /// The current location in [_buffer].
    SourceLocation get _targetLocation =>
        SourceLocation(_buffer.length, line: _line, column: _column);
  
    bool get isEmpty => _buffer.isEmpty;
    bool get isNotEmpty => _buffer.isNotEmpty;
    int get length => _buffer.length;
  
    /// Runs [callback] and associates all text written within it with [span].
    ///
    /// Specifically, this associates the point at the beginning of the written
    /// text with [span.start] and the point at the end of the written text with
    /// [span.end].
    T forSpan<T>(FileSpan span, T callback()) {
      var wasInSpan = _inSpan;
      _inSpan = true;
      _addEntry(span.start, _targetLocation);
      try {
        return callback();
      } finally {
        // We could map [span.end] to [_targetLocation] here, but in practice
        // browsers don't care about where a span ends as long as it covers at
        // least the entity that they're looking up. Avoiding end mappings halves
        // the size of the source maps we generate.
  
        _inSpan = wasInSpan;
      }
    }
  
    /// Adds an entry to [_entries] unless it's redundant with the last entry.
    void _addEntry(FileLocation source, SourceLocation target) {
      if (_entries.isNotEmpty) {
        var entry = _entries.last;
  
        // Browsers don't care about the position of a value within a line, so
        // it's redundant to have two entries on the same target line that both
        // point to the same source line, even if they point to different
        // columns in that line.
        if (entry.source.line == source.line &&
            entry.target.line == target.line) {
          return;
        }
  
        // Since source maps are only used to look up the source from the target
        // and not vice versa, we don't need multiple mappings to the same target.
        if (entry.target.offset == target.offset) return;
      }
  
      _entries.add(Entry(source, target, null));
    }
  
    void clear() =>
        throw UnsupportedError("SourceMapBuffer.clear() is not supported.");
  
    void write(Object? object) {
      var string = object.toString();
      _buffer.write(string);
  
      for (var i = 0; i < string.length; i++) {
        if (string.codeUnitAt(i) == $lf) {
          _writeLine();
        } else {
          _column++;
        }
      }
    }
  
    void writeAll(Iterable<Object?> objects, [String separator = ""]) =>
        write(objects.join(separator));
  
    void writeCharCode(int charCode) {
      _buffer.writeCharCode(charCode);
      if (charCode == $lf) {
        _writeLine();
      } else {
        _column++;
      }
    }
  
    void writeln([Object? object = ""]) {
      // Special-case the common case.
      if (identical(object, "")) {
        _buffer.writeln();
        _writeLine();
        return;
      }
  
      var string = object.toString();
      _buffer.writeln(string);
  
      var newlines = countOccurrences(string, $lf) + 1;
      for (var i = 0; i < newlines; i++) {
        _writeLine();
      }
    }
  
    /// Records that a line has been passed.
    ///
    /// If we're in the middle of a source span, indicate that at the beginning of
    /// the new line. This is necessary because source maps consider each line
    /// separately.
    void _writeLine() {
      // Trim useless entries.
      if (_entries.last.target.line == _line &&
          _entries.last.target.column == _column) {
        _entries.removeLast();
      }
  
      _line++;
      _column = 0;
      if (_inSpan) {
        _entries.add(Entry(_entries.last.source, _targetLocation, null));
      }
    }
  
    String toString() => _buffer.toString();
  
    /// Returns the source map for the file being written.
    ///
    /// If [prefix] is passed, all the entries in the source map will be moved
    /// forward by the number of characters and lines in [prefix].
    ///
    /// [SingleMapping.targetUrl] will be `null`.
    SingleMapping buildSourceMap({String? prefix}) {
      if (prefix == null || prefix.isEmpty) {
        return SingleMapping.fromEntries(_entries);
      }
  
      var prefixLength = prefix.length;
      var prefixLines = 0;
      var prefixColumn = 0;
      for (var i = 0; i < prefix.length; i++) {
        if (prefix.codeUnitAt(i) == $lf) {
          prefixLines++;
          prefixColumn = 0;
        } else {
          prefixColumn++;
        }
      }
  
      return SingleMapping.fromEntries(
        _entries.map(
          (entry) => Entry(
            entry.source,
            SourceLocation(
              entry.target.offset + prefixLength,
              line: entry.target.line + prefixLines,
              // Only adjust the column for entries that are on the same line as
              // the last chunk of the prefix.
              column: entry.target.column +
                  (entry.target.line == 0 ? prefixColumn : 0),
            ),
            entry.identifierName,
          ),
        ),
      );
    }
  }
  