// 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.

library dart_style.src.line_writer;

import 'chunk.dart';
import 'dart_formatter.dart';
import 'debug.dart' as debug;
import 'line_splitting/line_splitter.dart';
import 'whitespace.dart';

/// Given a series of chunks, splits them into lines and writes the result to
/// a buffer.
class LineWriter {
  final _buffer = new 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) {
    // There is always a newline after the opening delimiter.
    _buffer.write(_lineEnding);
  }

  /// Gets the results of formatting the child block of [chunk] at with
  /// 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 anonymous lambda, [column] will be 2, not 4.
  FormatResult formatBlock(Chunk chunk, int column) {
    var key = new _BlockKey(chunk, column);

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

    var writer = new LineWriter._(
        chunk.block.chunks, _lineEnding, pageWidth, column, _blockCache);

    // TODO(rnystrom): Passing in an initial indent here is hacky. The
    // LineWriter ensures all but the first chunk have a block indent, and this
    // handles the first chunk. Do something cleaner.
    var result = writer.writeLines(Indent.block, flushLeft: chunk.flushLeft);
    return _blockCache[key] = result;
  }

  /// 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(int firstLineIndent,
      {bool isCompilationUnit: false, bool flushLeft: false}) {
    // Now that we know what hard splits there will be, break the chunks into
    // independently splittable lines.
    var newlines = 0;
    var indent = firstLineIndent;
    var totalCost = 0;
    var start = 0;

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

      totalCost +=
          _completeLine(newlines, indent, start, i + 1, flushLeft: flushLeft);

      // Get ready for the next line.
      newlines = chunk.isDouble ? 2 : 1;
      indent = chunk.indent;
      flushLeft = chunk.flushLeft;
      start = i + 1;
    }

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

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

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

  /// Takes the chunks from [start] to [end] with leading [indent], removes
  /// them, and runs the [LineSplitter] on them.
  int _completeLine(int newlines, int indent, int start, int end,
      {bool flushLeft}) {
    // Write the newlines required by the previous line.
    for (var j = 0; j < newlines; j++) {
      _buffer.write(_lineEnding);
    }

    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 = new LineSplitter(this, chunks, _blockIndentation, indent,
        flushLeft: flushLeft);
    var splits = splitter.apply();

    // Write the indentation of the first line.
    if (!flushLeft) {
      _buffer.write(" " * (indent + _blockIndentation));
    }

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

      if (chunk.isBlock) {
        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 {
          // 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 (i == chunks.length - 1) {
        // Don't write trailing whitespace after the last chunk.
      } else if (splits.shouldSplitAt(i)) {
        _buffer.write(_lineEnding);
        if (chunk.isDouble) _buffer.write(_lineEnding);

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

    return splits.cost;
  }

  /// Writes the block chunks of [chunk] (and any child chunks of them,
  /// recursively) without any splitting.
  void _writeChunksUnsplit(Chunk chunk) {
    if (!chunk.isBlock) return;

    for (var blockChunk in chunk.block.chunks) {
      _writeChunk(blockChunk);

      if (blockChunk.spaceWhenUnsplit) _buffer.write(" ");

      // Recurse into the block.
      _writeChunksUnsplit(blockChunk);
    }
  }

  /// 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).
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);

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

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

/// The result of formatting a series of chunks.
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);
}
