// Copyright (c) 2013, 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.

part of dart.convert;

/**
 * This class provides an interface for converters to
 * efficiently transmit String data.
 *
 * Instead of limiting the interface to one non-chunked String it accepts
 * partial strings or can be transformed into a byte sink that
 * accepts UTF-8 code units.
 *
 * This abstract class will likely get more methods over time. Implementers are
 * urged to extend [StringConversionSinkBase] or to mix in
 * [StringConversionSinkMixin], to ensure that their class covers the newly
 * added methods.
 */
abstract class StringConversionSink
    extends ChunkedConversionSink<String> {
  StringConversionSink();
  factory StringConversionSink.withCallback(void callback(String accumulated))
      = _StringCallbackSink;
  factory StringConversionSink.from(ChunkedConversionSink<String> sink)
      = _StringAdapterSink;

  /**
   * Creates a new instance wrapping the given [sink].
   *
   * Every string that is added to the returned instance is forwarded to
   * the [sink]. The instance is allowed to buffer and is not required to
   * forward immediately.
   */
  factory StringConversionSink.fromStringSink(StringSink sink) =
      _StringSinkConversionSink;

  /**
   * Adds the next [chunk] to `this`.
   *
   * Adds the substring defined by [start] and [end]-exclusive to `this`.
   *
   * If [isLast] is `true` closes `this`.
   */
  void addSlice(String chunk, int start, int end, bool isLast);

  /**
   * Returns `this` as a sink that accepts UTF-8 input.
   *
   * If used, this method must be the first and only call to `this`. It
   * invalidates `this`. All further operations must be performed on the result.
   */
  ByteConversionSink asUtf8Sink(bool allowMalformed);
  // - asRuneSink
  // - asCodeUnitsSink

  /**
   * Returns `this` as a [ClosableStringSink].
   *
   * If used, this method must be the first and only call to `this`. It
   * invalidates `this`. All further operations must be performed on the result.
   */
  ClosableStringSink asStringSink();
}

/**
 * A [ClosableStringSink] extends the [StringSink] interface by adding a
 * `close` method.
 */
abstract class ClosableStringSink extends StringSink {
  /**
   * Creates a new instance combining a [StringSink] [sink] and a callback
   * [onClose] which is invoked when the returned instance is closed.
   */
  factory ClosableStringSink.fromStringSink(StringSink sink, void onClose())
      = _ClosableStringSink;

  /**
   * Closes `this` and flushes any outstanding data.
   */
  void close();
}

typedef void _StringSinkCloseCallback();

/**
 * This class wraps an existing [StringSink] and invokes a
 * closure when [close] is invoked.
 */
class _ClosableStringSink implements ClosableStringSink {
  final _StringSinkCloseCallback _callback;
  final StringSink _sink;

  _ClosableStringSink(this._sink, this._callback);

  void close() => _callback();

  void writeCharCode(int charCode) => _sink.writeCharCode(charCode);
  void write(Object o) => _sink.write(o);
  void writeln([Object o = ""]) => _sink.writeln(o);
  void writeAll(Iterable objects, [String separator = ""])
      => _sink.writeAll(objects, separator);
}

/**
 * This class wraps an existing [StringConversionSink] and exposes a
 * [ClosableStringSink] interface. The wrapped sink only needs to implement
 * `add` and `close`.
 */
// TODO(floitsch): make this class public?
class _StringConversionSinkAsStringSinkAdapter implements ClosableStringSink {
  static const _MIN_STRING_SIZE = 16;

  StringBuffer _buffer;
  StringConversionSink _chunkedSink;

  _StringConversionSinkAsStringSinkAdapter(this._chunkedSink)
      : _buffer = new StringBuffer();

  void close() {
    if (_buffer.isNotEmpty) _flush();
    _chunkedSink.close();
  }

  void writeCharCode(int charCode) {
    _buffer.writeCharCode(charCode);
    if (_buffer.length > _MIN_STRING_SIZE) _flush();
  }

  void write(Object o) {
    if (_buffer.isNotEmpty) _flush();
    String str = o.toString();
    _chunkedSink.add(o.toString());
  }

  void writeln([Object o = ""]) {
    _buffer.writeln(o);
    if (_buffer.length > _MIN_STRING_SIZE) _flush();
  }

  void writeAll(Iterable objects, [String separator = ""]) {
    if (_buffer.isNotEmpty) _flush();
    Iterator iterator = objects.iterator;
    if (!iterator.moveNext()) return;
    if (separator.isEmpty) {
      do {
        _chunkedSink.add(iterator.current.toString());
      } while (iterator.moveNext());
    } else {
      _chunkedSink.add(iterator.current.toString());
      while (iterator.moveNext()) {
        write(separator);
        _chunkedSink.add(iterator.current.toString());
      }
    }
  }

  void _flush() {
    String accumulated = _buffer.toString();
    _buffer.clear();
    _chunkedSink.add(accumulated);
  }
}

/**
 * This class provides a base-class for converters that need to accept String
 * inputs.
 */
abstract class StringConversionSinkBase extends StringConversionSinkMixin {
}

/**
 * This class provides a mixin for converters that need to accept String
 * inputs.
 */
abstract class StringConversionSinkMixin implements StringConversionSink {

  void addSlice(String str, int start, int end, bool isLast);
  void close();

  void add(String str) => addSlice(str, 0, str.length, false);

  ByteConversionSink asUtf8Sink(bool allowMalformed) {
    return new _Utf8ConversionSink(this, allowMalformed);
  }

  ClosableStringSink asStringSink() {
    return new _StringConversionSinkAsStringSinkAdapter(this);
  }
}

/**
 * This class is a [StringConversionSink] that wraps a [StringSink].
 */
class _StringSinkConversionSink extends StringConversionSinkBase {
  StringSink _stringSink;
  _StringSinkConversionSink(StringSink this._stringSink);

  void close() {}
  void addSlice(String str, int start, int end, bool isLast) {
    if (start != 0 || end != str.length) {
      for (int i = start; i < end; i++) {
        _stringSink.writeCharCode(str.codeUnitAt(i));
      }
    } else {
      _stringSink.write(str);
    }
    if (isLast) close();
  }

  void add(String str) => _stringSink.write(str);

  ByteConversionSink asUtf8Sink(bool allowMalformed) {
    return new _Utf8StringSinkAdapter(this, _stringSink, allowMalformed);
  }

  ClosableStringSink asStringSink() {
    return new ClosableStringSink.fromStringSink(_stringSink, this.close);
  }
}

/**
 * This class accumulates all chunks into one string
 * and invokes a callback when the sink is closed.
 *
 * This class can be used to terminate a chunked conversion.
 */
class _StringCallbackSink extends _StringSinkConversionSink {
  final _ChunkedConversionCallback<String> _callback;
  _StringCallbackSink(this._callback) : super(new StringBuffer());

  void close() {
    StringBuffer buffer = _stringSink;
    String accumulated = buffer.toString();
    buffer.clear();
    _callback(accumulated);
  }

  ByteConversionSink asUtf8Sink(bool allowMalformed) {
    return new _Utf8StringSinkAdapter(
        this, _stringSink, allowMalformed);
  }
}

/**
 * This class adapts a simple [ChunkedConversionSink] to a
 * [StringConversionSink].
 *
 * All additional methods of the [StringConversionSink] (compared to the
 * ChunkedConversionSink) are redirected to the `add` method.
 */
class _StringAdapterSink extends StringConversionSinkBase {
  final ChunkedConversionSink<String> _sink;

  _StringAdapterSink(this._sink);

  void add(String str) => _sink.add(str);

  void addSlice(String str, int start, int end, bool isLast) {
    if (start == 0 && end == str.length) {
      add(str);
    } else {
      add(str.substring(start, end));
    }
    if (isLast) close();
  }

  void close() => _sink.close();
}


/**
 * Decodes UTF-8 code units and stores them in a [StringSink].
 */
class _Utf8StringSinkAdapter extends ByteConversionSink {
  final _Utf8Decoder _decoder;
  final ChunkedConversionSink _chunkedSink;

  _Utf8StringSinkAdapter(ChunkedConversionSink chunkedSink,
                         StringSink sink, bool allowMalformed)
      : _chunkedSink = chunkedSink,
        _decoder = new _Utf8Decoder(sink, allowMalformed);

  void close() {
    _decoder.close();
    if(_chunkedSink != null) _chunkedSink.close();
  }

  void add(List<int> chunk) {
    addSlice(chunk, 0, chunk.length, false);
  }

  void addSlice(List<int> codeUnits, int startIndex, int endIndex,
                bool isLast) {
    _decoder.convert(codeUnits, startIndex, endIndex);
    if (isLast) close();
  }
}

/**
 * Decodes UTF-8 code units.
 *
 * Forwards the decoded strings to the given [StringConversionSink].
 */
// TODO(floitsch): make this class public?
class _Utf8ConversionSink extends ByteConversionSink {

  final _Utf8Decoder _decoder;
  final StringConversionSink _chunkedSink;
  final StringBuffer _buffer;
  _Utf8ConversionSink(StringConversionSink sink, bool allowMalformed)
      : this._(sink, new StringBuffer(), allowMalformed);

  _Utf8ConversionSink._(this._chunkedSink, StringBuffer stringBuffer,
                       bool allowMalformed)
      : _decoder = new _Utf8Decoder(stringBuffer, allowMalformed),
        _buffer = stringBuffer;

  void close() {
    _decoder.close();
    if (_buffer.isNotEmpty) {
      String accumulated = _buffer.toString();
      _buffer.clear();
      _chunkedSink.addSlice(accumulated, 0, accumulated.length, true);
    } else  {
      _chunkedSink.close();
    }
  }

  void add(List<int> chunk) {
    addSlice(chunk, 0, chunk.length, false);
  }

  void addSlice(List<int> chunk, int startIndex, int endIndex, bool isLast) {
    _decoder.convert(chunk, startIndex, endIndex);
    if (_buffer.isNotEmpty) {
      String accumulated = _buffer.toString();
      _chunkedSink.addSlice(accumulated, 0, accumulated.length, isLast);
      _buffer.clear();
      return;
    }
    if (isLast) close();
  }
}
