// 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 '../dart_formatter.dart';
import '../debug.dart' as debug;
import 'chunk.dart';
import 'line_splitting/line_splitter.dart';

/// Given a series of chunks, splits them into lines and writes the result to
/// a buffer.
final class LineWriter {
  final _buffer = StringBuffer();

  final List<Chunk> _chunks;

  final String _lineEnding;

  /// The number of characters allowed in a single line.
  final int pageWidth;

  /// The number of characters of additional indentation to apply to each line.
  ///
  /// This is used when formatting blocks to get the output into the right
  /// column based on where the block appears.
  final int _blockIndentation;

  /// The cache of blocks that have already been formatted.
  final Map<_BlockKey, FormatResult> _blockCache;

  /// The offset in [_buffer] where the selection starts in the formatted code.
  ///
  /// This will be `null` if there is no selection or the writer hasn't reached
  /// the beginning of the selection yet.
  int? _selectionStart;

  /// The offset in [_buffer] where the selection ends in the formatted code.
  ///
  /// This will be `null` if there is no selection or the writer hasn't reached
  /// the end of the selection yet.
  int? _selectionEnd;

  /// The number of characters that have been written to the output.
  int get length => _buffer.length;

  LineWriter(DartFormatter formatter, this._chunks)
    : _lineEnding = formatter.lineEnding!,
      pageWidth = formatter.pageWidth,
      _blockIndentation = 0,
      _blockCache = {};

  /// Creates a line writer for a block.
  LineWriter._(
    this._chunks,
    this._lineEnding,
    this.pageWidth,
    this._blockIndentation,
    this._blockCache,
  );

  /// Gets the results of formatting the child block of [chunk] at starting
  /// [column].
  ///
  /// If that block has already been formatted, reuses the results.
  ///
  /// The column is the column for the delimiters. The contents of the block
  /// are always implicitly one level deeper than that.
  ///
  ///     main() {
  ///       function(() {
  ///         block;
  ///       });
  ///     }
  ///
  /// When we format the function expression's body, [column] will be 2, not 4.
  FormatResult formatBlock(BlockChunk chunk, int column) {
    var key = _BlockKey(chunk, column);

    // Use the cached one if we have it.
    var cached = _blockCache[key];
    if (cached != null) return cached;

    var writer = LineWriter._(
      chunk.children,
      _lineEnding,
      pageWidth,
      column,
      _blockCache,
    );
    return _blockCache[key] = writer.writeLines();
  }

  /// Takes all of the chunks and divides them into sublists and line splits
  /// each list.
  ///
  /// Since this is linear and line splitting is worse it's good to feed the
  /// line splitter smaller lists of chunks when possible.
  FormatResult writeLines({bool isCompilationUnit = false}) {
    // Now that we know what hard splits there will be, break the chunks into
    // independently splittable lines.
    var totalCost = 0;
    var start = 0;

    for (var i = 0; i < _chunks.length; i++) {
      var chunk = _chunks[i];
      if (!chunk.canDivide) continue;

      totalCost += _completeLine(start, i);
      start = i;
    }

    if (start < _chunks.length) {
      totalCost += _completeLine(start, _chunks.length);
    }

    // Be a good citizen, end with a newline.
    if (isCompilationUnit) _buffer.write(_lineEnding);

    return FormatResult(
      _buffer.toString(),
      totalCost,
      _selectionStart,
      _selectionEnd,
    );
  }

  /// Takes the chunks from [start] to [end], removes them, and runs the
  /// [LineSplitter] on them.
  int _completeLine(int start, int end) {
    var chunks = _chunks.sublist(start, end);

    if (debug.traceLineWriter) {
      debug.log(debug.green('\nWriting:'));
      debug.dumpChunks(0, chunks);
      debug.log();
    }

    // Run the line splitter.
    var splitter = LineSplitter(this, chunks, _blockIndentation);
    var splits = splitter.apply();

    // Write each chunk with the appropriate splits between them.
    for (var i = 0; i < chunks.length; i++) {
      var chunk = chunks[i];

      // Write the block chunk's children first.
      if (chunk is BlockChunk) {
        if (!splits.shouldSplitAt(i)) {
          // This block didn't split (which implies none of the child blocks
          // of that block split either, recursively), so write them all inline.
          _writeChunksUnsplit(chunk);
        } else {
          _buffer.write(_lineEnding);

          // Include the formatted block contents.
          var block = formatBlock(chunk, splits.getColumn(i));

          // If this block contains one of the selection markers, tell the
          // writer where it ended up in the final output.
          if (block.selectionStart != null) {
            _selectionStart = length + block.selectionStart!;
          }

          if (block.selectionEnd != null) {
            _selectionEnd = length + block.selectionEnd!;
          }

          _buffer.write(block.text);
        }
      }

      if (splits.shouldSplitAt(i)) {
        // Don't write an initial single newline at the beginning of the output.
        // If this is for a block, then the newline will be written before
        // writing the block. If it's the top level output, then it shouldn't
        // have an extra leading newline.
        if (_buffer.isNotEmpty) {
          _buffer.write(_lineEnding);
          if (chunk.isDouble) _buffer.write(_lineEnding);
        }

        _buffer.write(' ' * (splits.getColumn(i)));
      } else {
        if (chunk.spaceWhenUnsplit) _buffer.write(' ');
      }

      _writeChunk(chunk);
    }

    return splits.cost;
  }

  /// Writes the block chunks of [block] (and any child chunks of them,
  /// recursively) without any splitting.
  void _writeChunksUnsplit(BlockChunk block) {
    for (var chunk in block.children) {
      if (chunk.spaceWhenUnsplit) _buffer.write(' ');

      // Recurse into the block.
      if (chunk is BlockChunk) _writeChunksUnsplit(chunk);

      _writeChunk(chunk);
    }
  }

  /// Writes [chunk] to the output and updates the selection if the chunk
  /// contains a selection marker.
  void _writeChunk(Chunk chunk) {
    if (chunk.selectionStart != null) {
      _selectionStart = length + chunk.selectionStart!;
    }

    if (chunk.selectionEnd != null) {
      _selectionEnd = length + chunk.selectionEnd!;
    }

    _buffer.write(chunk.text);
  }
}

/// Key type for the formatted block cache.
///
/// To cache formatted blocks, we just need to know which block it is (the
/// index of its parent chunk) and how far it was indented when we formatted it
/// (the starting column).
final class _BlockKey {
  /// The index of the chunk in the surrounding chunk list that contains this
  /// block.
  final Chunk chunk;

  /// The absolute zero-based column number where the block starts.
  final int column;

  _BlockKey(this.chunk, this.column);

  @override
  bool operator ==(Object other) {
    if (other is! _BlockKey) return false;
    return chunk == other.chunk && column == other.column;
  }

  @override
  int get hashCode => chunk.hashCode ^ column.hashCode;
}

/// The result of formatting a series of chunks.
final class FormatResult {
  /// The resulting formatted text, including newlines and leading whitespace
  /// to reach the proper column.
  final String text;

  /// The numeric cost of the chosen solution.
  final int cost;

  /// Where in the resulting buffer the selection starting point should appear
  /// if it was contained within this split list of chunks.
  ///
  /// Otherwise, this is `null`.
  final int? selectionStart;

  /// Where in the resulting buffer the selection end point should appear if it
  /// was contained within this split list of chunks.
  ///
  /// Otherwise, this is `null`.
  final int? selectionEnd;

  FormatResult(this.text, this.cost, this.selectionStart, this.selectionEnd);
}
