import 'dart:convert';
import 'dart:typed_data';
import 'byte_order.dart';

abstract class InputStreamBase {
  ///  The current read position relative to the start of the buffer.
  int get position;

  set position(int v);

  /// How many bytes are left in the stream.
  int get length;

  /// Is the current position at the end of the stream?
  bool get isEOS;

  /// Asynchronously closes the input stream.
  Future<void> close() async {}

  /// Synchronously close the input stream
  void closeSync() {}

  /// Reset to the beginning of the stream.
  void reset();

  /// Rewind the read head of the stream by the given number of bytes.
  void rewind([int length = 1]);

  /// Move the read position by [count] bytes.
  void skip(int length);

  /// Read [count] bytes from an [offset] of the current read position, without
  /// moving the read position.
  InputStreamBase peekBytes(int count, [int offset = 0]);

  /// Read a single byte.
  int readByte();

  /// Read [count] bytes from the stream.
  InputStreamBase readBytes(int count);

  InputStreamBase subset([int? position, int? length]);

  /// Read a null-terminated string, or if [len] is provided, that number of
  /// bytes returned as a string.
  String readString({int? size, bool utf8 = true});

  /// Read a 16-bit word from the stream.
  int readUint16();

  /// Read a 24-bit word from the stream.
  int readUint24();

  /// Read a 32-bit word from the stream.
  int readUint32();

  /// Read a 64-bit word form the stream.
  int readUint64();

  Uint8List toUint8List([Uint8List? bytes]);
}

/// A buffer that can be read as a stream of bytes
class InputStream extends InputStreamBase {
  List<int> buffer;
  int offset;
  int start;
  int byteOrder;

  /// Create a InputStream for reading from a List<int>
  InputStream(dynamic data,
      {this.byteOrder = LITTLE_ENDIAN, this.start = 0, int? length})
      : buffer = data is TypedData
            ? Uint8List.view(
                data.buffer, data.offsetInBytes, data.lengthInBytes)
            : data is List<int>
                ? data
                : List<int>.from(data as Iterable<dynamic>),
        offset = start {
    _length = length ?? buffer.length;
  }

  /// Create a copy of [other].
  InputStream.from(InputStream other)
      : buffer = other.buffer,
        offset = other.offset,
        start = other.start,
        _length = other._length,
        byteOrder = other.byteOrder;

  ///  The current read position relative to the start of the buffer.
  @override
  int get position => offset - start;

  @override
  set position(int v) {
    offset = start + v;
  }

  /// How many bytes are left in the stream.
  @override
  int get length => _length - (offset - start);

  /// Is the current position at the end of the stream?
  @override
  bool get isEOS => offset >= (start + _length);

  /// Reset to the beginning of the stream.
  @override
  void reset() {
    offset = start;
  }

  @override
  Future<void> close() async {
    buffer = <int>[];
    _length = 0;
  }

  @override
  void closeSync() {
    buffer = <int>[];
    _length = 0;
  }

  /// Rewind the read head of the stream by the given number of bytes.
  @override
  void rewind([int length = 1]) {
    offset -= length;
    if (offset < 0) {
      offset = 0;
    }
  }

  /// Access the buffer relative from the current position.
  int operator [](int index) => buffer[offset + index];

  /// Return a InputStream to read a subset of this stream.  It does not
  /// move the read position of this stream.  [position] is specified relative
  /// to the start of the buffer.  If [position] is not specified, the current
  /// read position is used. If [length] is not specified, the remainder of this
  /// stream is used.
  @override
  InputStreamBase subset([int? position, int? length]) {
    if (position == null) {
      position = offset;
    } else {
      position += start;
    }

    if (length == null || length < 0) {
      length = _length - (position - start);
    }

    return InputStream(buffer,
        byteOrder: byteOrder, start: position, length: length);
  }

  /// Returns the position of the given [value] within the buffer, starting
  /// from the current read position with the given [offset].  The position
  /// returned is relative to the start of the buffer, or -1 if the [value]
  /// was not found.
  int indexOf(int value, [int offset = 0]) {
    for (var i = this.offset + offset, end = this.offset + length;
        i < end;
        ++i) {
      if (buffer[i] == value) {
        return i - start;
      }
    }
    return -1;
  }

  /// Read [count] bytes from an [offset] of the current read position, without
  /// moving the read position.
  @override
  InputStreamBase peekBytes(int count, [int offset = 0]) {
    return subset((this.offset - start) + offset, count);
  }

  /// Move the read position by [count] bytes.
  @override
  void skip(int count) {
    offset += count;
  }

  /// Read a single byte.
  @override
  int readByte() {
    return buffer[offset++];
  }

  /// Read [count] bytes from the stream.
  @override
  InputStreamBase readBytes(int count) {
    final bytes = subset(offset - start, count);
    offset += bytes.length;
    return bytes as InputStream;
  }

  /// Read a null-terminated string, or if [len] is provided, that number of
  /// bytes returned as a string.
  @override
  String readString({int? size, bool utf8 = true}) {
    if (size == null) {
      final codes = <int>[];
      if (isEOS) {
        return '';
      }
      while (!isEOS) {
        final c = readByte();
        if (c == 0) {
          break;
        }
        codes.add(c);
      }
      return utf8 ? Utf8Decoder().convert(codes) : String.fromCharCodes(codes);
    }

    final s = readBytes(size);
    final bytes = s.toUint8List();
    try {
      final str =
          utf8 ? Utf8Decoder().convert(bytes) : String.fromCharCodes(bytes);
      return str;
    } catch (err) {
      // If the string is not a valid UTF8 string, decode it as character codes.
      return String.fromCharCodes(bytes);
    }
  }

  /// Read a 16-bit word from the stream.
  @override
  int readUint16() {
    final b1 = buffer[offset++] & 0xff;
    final b2 = buffer[offset++] & 0xff;
    if (byteOrder == BIG_ENDIAN) {
      return (b1 << 8) | b2;
    }
    return (b2 << 8) | b1;
  }

  /// Read a 24-bit word from the stream.
  @override
  int readUint24() {
    final b1 = buffer[offset++] & 0xff;
    final b2 = buffer[offset++] & 0xff;
    final b3 = buffer[offset++] & 0xff;
    if (byteOrder == BIG_ENDIAN) {
      return b3 | (b2 << 8) | (b1 << 16);
    }
    return b1 | (b2 << 8) | (b3 << 16);
  }

  /// Read a 32-bit word from the stream.
  @override
  int readUint32() {
    final b1 = buffer[offset++] & 0xff;
    final b2 = buffer[offset++] & 0xff;
    final b3 = buffer[offset++] & 0xff;
    final b4 = buffer[offset++] & 0xff;
    if (byteOrder == BIG_ENDIAN) {
      return (b1 << 24) | (b2 << 16) | (b3 << 8) | b4;
    }
    return (b4 << 24) | (b3 << 16) | (b2 << 8) | b1;
  }

  /// Read a 64-bit word form the stream.
  @override
  int readUint64() {
    final b1 = buffer[offset++] & 0xff;
    final b2 = buffer[offset++] & 0xff;
    final b3 = buffer[offset++] & 0xff;
    final b4 = buffer[offset++] & 0xff;
    final b5 = buffer[offset++] & 0xff;
    final b6 = buffer[offset++] & 0xff;
    final b7 = buffer[offset++] & 0xff;
    final b8 = buffer[offset++] & 0xff;
    if (byteOrder == BIG_ENDIAN) {
      return (b1 << 56) |
          (b2 << 48) |
          (b3 << 40) |
          (b4 << 32) |
          (b5 << 24) |
          (b6 << 16) |
          (b7 << 8) |
          b8;
    }
    return (b8 << 56) |
        (b7 << 48) |
        (b6 << 40) |
        (b5 << 32) |
        (b4 << 24) |
        (b3 << 16) |
        (b2 << 8) |
        b1;
  }

  @override
  Uint8List toUint8List([Uint8List? bytes]) {
    var len = length;
    if (buffer is Uint8List) {
      final b = buffer as Uint8List;
      if ((offset + len) > b.length) {
        len = b.length - offset;
      }
      final bytes = Uint8List.view(b.buffer, b.offsetInBytes + offset, len);
      return bytes;
    }
    var end = offset + len;
    if (end > buffer.length) {
      end = buffer.length;
    }
    return Uint8List.fromList(buffer.sublist(offset, end));
  }

  late int _length;
}
