// 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";

/// An instance of the default implementation of the [AsciiCodec].
///
/// This instance provides a convenient access to the most common ASCII
/// use cases.
///
/// Examples:
/// ```dart
/// var encoded = ascii.encode("This is ASCII!");
/// var decoded = ascii.decode([0x54, 0x68, 0x69, 0x73, 0x20, 0x69, 0x73,
///                             0x20, 0x41, 0x53, 0x43, 0x49, 0x49, 0x21]);
/// ```
const AsciiCodec ascii = AsciiCodec();

const int _asciiMask = 0x7F;

/// Encoding and decoding of ASCII characters.
///
/// An [Encoding] (conversion between strings and bytes) that converts
/// ASCII characters (U+0000..U+007F) to and from their corresponding
/// byte values.
///
/// Treats any non-ASCII character (U+0080..U+10FFFF) as an invalid input
/// to endcoding, and any byte &ge; 128 as an invalid input to decoding.
final class AsciiCodec extends Encoding {
  final bool _allowInvalid;

  /// Instantiates a new [AsciiCodec].
  ///
  /// If [_allowInvalid] is `true`, the [decode] method and the converter
  /// returned by [decoder] will default to allowing invalid values,
  /// which are byte values greater than 127.
  /// If allowing invalid values, invalid values will be decoded to the Unicode
  /// Replacement character (U+FFFD). If not, a [FormatException] is be thrown.
  /// Calls to the [decode] method can choose to override this default.
  ///
  /// Encoders will not accept invalid (non-ASCII) characters.
  const AsciiCodec({this._allowInvalid = false});

  /// The name of this codec is "us-ascii".
  String get name => "us-ascii";

  Uint8List encode(String source) => encoder.convert(source);

  /// Decodes the ASCII [bytes] (a list of unsigned 7-bit integers) to the
  /// corresponding string.
  ///
  /// If [bytes] contains values that are not in the range 0 .. 127, the decoder
  /// will eventually throw a [FormatException].
  ///
  /// If [allowInvalid] is not provided, it defaults to the value used to create
  /// this [AsciiCodec].
  String decode(List<int> bytes, {bool? allowInvalid}) {
    if (allowInvalid ?? _allowInvalid) {
      return const AsciiDecoder(allowInvalid: true).convert(bytes);
    } else {
      return const AsciiDecoder(allowInvalid: false).convert(bytes);
    }
  }

  AsciiEncoder get encoder => const AsciiEncoder();

  AsciiDecoder get decoder => _allowInvalid
      ? const AsciiDecoder(allowInvalid: true)
      : const AsciiDecoder(allowInvalid: false);
}

// Superclass for [AsciiEncoder] and [Latin1Encoder].
// Generalizes common operations that only differ by a mask;
class _UnicodeSubsetEncoder extends Converter<String, List<int>> {
  final int _subsetMask;

  const _UnicodeSubsetEncoder(this._subsetMask);

  /// Converts the [String] into a list of its code units.
  ///
  /// If [start] and [end] are provided, only the substring
  /// `string.substring(start, end)` is used as input to the conversion.
  Uint8List convert(String string, [int start = 0, int? end]) {
    var stringLength = string.length;
    end = RangeError.checkValidRange(start, end, stringLength);
    var length = end - start;
    var result = Uint8List(length);
    for (var i = 0; i < length; i++) {
      var codeUnit = string.codeUnitAt(start + i);
      if ((codeUnit & ~_subsetMask) != 0) {
        throw ArgumentError.value(
          string,
          "string",
          "Contains invalid characters.",
        );
      }
      result[i] = codeUnit;
    }
    return result;
  }

  /// Starts a chunked conversion.
  ///
  /// The converter works more efficiently if the given [sink] is a
  /// [ByteConversionSink].
  StringConversionSink startChunkedConversion(Sink<List<int>> sink) {
    return _UnicodeSubsetEncoderSink(
      _subsetMask,
      sink is ByteConversionSink ? sink : ByteConversionSink.from(sink),
    );
  }

  // Override the base-class' bind, to provide a better type.
  Stream<List<int>> bind(Stream<String> stream) => super.bind(stream);
}

/// Converts strings of only ASCII characters to bytes.
///
/// Example:
/// ```dart import:typed_data
/// const asciiEncoder = AsciiEncoder();
/// const sample = 'Dart';
/// final asciiValues = asciiEncoder.convert(sample);
/// print(asciiValues); // [68, 97, 114, 116]
/// ```
final class AsciiEncoder extends _UnicodeSubsetEncoder {
  const AsciiEncoder() : super(_asciiMask);
}

/// This class encodes chunked strings to bytes (unsigned 8-bit
/// integers).
class _UnicodeSubsetEncoderSink extends StringConversionSink {
  final ByteConversionSink _sink;
  final int _subsetMask;

  _UnicodeSubsetEncoderSink(this._subsetMask, this._sink);

  void close() {
    _sink.close();
  }

  void addSlice(String source, int start, int end, bool isLast) {
    RangeError.checkValidRange(start, end, source.length);
    for (var i = start; i < end; i++) {
      var codeUnit = source.codeUnitAt(i);
      if ((codeUnit & ~_subsetMask) != 0) {
        throw ArgumentError(
          "Source contains invalid character with code point: $codeUnit.",
        );
      }
    }
    _sink.add(source.codeUnits.sublist(start, end));
    if (isLast) {
      close();
    }
  }
}

/// This class converts Latin-1 bytes (lists of unsigned 8-bit integers)
/// to a string.
abstract class _UnicodeSubsetDecoder extends Converter<List<int>, String> {
  final bool _allowInvalid;
  final int _subsetMask;

  /// Instantiates a new decoder.
  ///
  /// The [_allowInvalid] argument defines how [convert] deals
  /// with invalid bytes.
  ///
  /// The [_subsetMask] argument is a bit mask used to define the subset
  /// of Unicode being decoded. Use [_LATIN1_MASK] for Latin-1 (8-bit) or
  /// [_asciiMask] for ASCII (7-bit).
  ///
  /// If [_allowInvalid] is `true`, [convert] replaces invalid bytes with the
  /// Unicode Replacement character `U+FFFD` (�).
  /// Otherwise it throws a [FormatException].
  const _UnicodeSubsetDecoder(this._allowInvalid, this._subsetMask);

  /// Converts the [bytes] (a list of unsigned 7- or 8-bit integers) to the
  /// corresponding string.
  ///
  /// If [start] and [end] are provided, only the sub-list of bytes from
  /// `start` to `end` (`end` not inclusive) is used as input to the conversion.
  String convert(List<int> bytes, [int start = 0, int? end]) {
    end = RangeError.checkValidRange(start, end, bytes.length);
    for (var i = start; i < end; i++) {
      var byte = bytes[i];
      if ((byte & ~_subsetMask) != 0) {
        if (!_allowInvalid) {
          throw FormatException("Invalid value in input: $byte");
        }
        return _convertInvalid(bytes, start, end);
      }
    }
    return String.fromCharCodes(bytes, start, end);
  }

  String _convertInvalid(List<int> bytes, int start, int end) {
    var buffer = StringBuffer();
    for (var i = start; i < end; i++) {
      var value = bytes[i];
      if ((value & ~_subsetMask) != 0) value = 0xFFFD;
      buffer.writeCharCode(value);
    }
    return buffer.toString();
  }

  /// Starts a chunked conversion.
  ///
  /// The converter works more efficiently if the given [sink] is a
  /// [StringConversionSink].
  ByteConversionSink startChunkedConversion(Sink<String> sink);

  // Override the base-class's bind, to provide a better type.
  Stream<String> bind(Stream<List<int>> stream) => super.bind(stream);
}

/// Converts ASCII bytes to string.
///
/// Example:
/// ```dart
/// const asciiDecoder = AsciiDecoder();
/// final asciiValues = [68, 97, 114, 116];
/// final result = asciiDecoder.convert(asciiValues);
/// print(result); // Dart
/// ```
/// Throws a [FormatException] if [bytes] contains values that are not
/// in the range 0 .. 127, and [allowInvalid] is `false` (the default).
///
/// If [allowInvalid] is `true`, any byte outside the range 0..127 is replaced
/// by the Unicode replacement character, U+FFFD ('�').
///
/// Example with `allowInvalid` set to true:
/// ```dart
/// const asciiDecoder = AsciiDecoder(allowInvalid: true);
/// final asciiValues = [68, 97, 114, 116, 20, 0xFF];
/// final result = asciiDecoder.convert(asciiValues);
/// print(result); // Dart �
/// print(result.codeUnits.last.toRadixString(16)); // fffd
/// ```
final class AsciiDecoder extends _UnicodeSubsetDecoder {
  const AsciiDecoder({bool allowInvalid = false})
    : super(allowInvalid, _asciiMask);

  /// Starts a chunked conversion.
  ///
  /// The converter works more efficiently if the given [sink] is a
  /// [StringConversionSink].
  ByteConversionSink startChunkedConversion(Sink<String> sink) {
    StringConversionSink stringSink;
    if (sink is StringConversionSink) {
      stringSink = sink;
    } else {
      stringSink = StringConversionSink.from(sink);
    }
    // TODO(lrn): Use asUtf16Sink when it becomes available. It
    // works just as well, is likely to have less decoding overhead,
    // and make adding U+FFFD easier.
    // At that time, merge this with _Latin1DecoderSink;
    if (_allowInvalid) {
      return _ErrorHandlingAsciiDecoderSink(stringSink.asUtf8Sink(false));
    } else {
      return _SimpleAsciiDecoderSink(stringSink);
    }
  }
}

class _ErrorHandlingAsciiDecoderSink extends ByteConversionSink {
  final ByteConversionSink _utf8Sink;
  _ErrorHandlingAsciiDecoderSink(this._utf8Sink);

  void close() {
    _utf8Sink.close();
  }

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

  void addSlice(List<int> source, int start, int end, bool isLast) {
    RangeError.checkValidRange(start, end, source.length);
    for (var i = start; i < end; i++) {
      if ((source[i] & ~_asciiMask) != 0) {
        if (i > start) _utf8Sink.addSlice(source, start, i, false);
        // Add UTF-8 encoding of U+FFFD.
        _utf8Sink.add(const <int>[0xEF, 0xBF, 0xBD]);
        start = i + 1;
      }
    }
    if (start < end) {
      _utf8Sink.addSlice(source, start, end, isLast);
    } else if (isLast) {
      close();
    }
  }
}

class _SimpleAsciiDecoderSink extends ByteConversionSink {
  final Sink _sink;
  _SimpleAsciiDecoderSink(this._sink);

  void close() {
    _sink.close();
  }

  void add(List<int> source) {
    for (var i = 0; i < source.length; i++) {
      if ((source[i] & ~_asciiMask) != 0) {
        throw FormatException("Source contains non-ASCII bytes.");
      }
    }
    _sink.add(String.fromCharCodes(source));
  }

  void addSlice(List<int> source, int start, int end, bool isLast) {
    final length = source.length;
    RangeError.checkValidRange(start, end, length);
    if (start < end) {
      if (start != 0 || end != length) {
        source = source.sublist(start, end);
      }
      add(source);
    }
    if (isLast) close();
  }
}
