import 'dart:typed_data';

import '../io/file_handle.dart';
import '../util/abstract_file_handle.dart';
import '../util/byte_order.dart';
import '../util/input_stream.dart';
import '../util/output_stream.dart';
import '../util/ram_file_handle.dart';

class OutputFileStream extends OutputStreamBase {
  final int byteOrder;
  int _length;
  final AbstractFileHandle _fileHandle;
  final Uint8List _buffer;
  int _bufferPosition;
  bool _isOpen;

  static const int kDefaultBufferSize = 1024 * 1024; // 1MB

  OutputFileStream.withFileHandle(
    this._fileHandle, {
    this.byteOrder = LITTLE_ENDIAN,
    int? bufferSize,
  })  : _length = 0,
        _buffer = Uint8List(bufferSize == null
            ? kDefaultBufferSize
            : bufferSize < 1
                ? 1
                : bufferSize),
        _bufferPosition = 0,
        _isOpen = true;

  factory OutputFileStream(
    String path, {
    int byteOrder = LITTLE_ENDIAN,
    int? bufferSize,
  }) {
    return OutputFileStream.withFileHandle(
      FileHandle(path, openMode: AbstractFileOpenMode.write),
      byteOrder: byteOrder,
      bufferSize: bufferSize,
    );
  }

  factory OutputFileStream.toRamFile(
    RamFileHandle ramFileHandle, {
    int byteOrder = LITTLE_ENDIAN,
    int? bufferSize,
  }) {
    return OutputFileStream.withFileHandle(
      ramFileHandle,
      byteOrder: byteOrder,
      bufferSize: bufferSize,
    );
  }

  @override
  int get length => _length;

  @override
  void flush() {
    if (_bufferPosition > 0) {
      if (_isOpen) {
        _fileHandle.writeFromSync(_buffer, 0, _bufferPosition);
      }
      _bufferPosition = 0;
    }
  }

  Future<void> close() async {
    if (!_isOpen) {
      return;
    }
    flush();
    _isOpen = false;
    await _fileHandle.close();
  }

  void closeSync() {
    if (!_isOpen) {
      return;
    }
    flush();
    _isOpen = false;
    _fileHandle.closeSync();
  }

  /// Write a byte to the end of the buffer.
  @override
  void writeByte(int value) {
    _buffer[_bufferPosition++] = value;
    if (_bufferPosition == _buffer.length) {
      flush();
    }
    _length++;
  }

  /// Write a set of bytes to the end of the buffer.
  @override
  void writeBytes(List<int> bytes, [int? len]) {
    len ??= bytes.length;
    if (_bufferPosition + len >= _buffer.length) {
      flush();

      if (_bufferPosition + len < _buffer.length) {
        for (int i = 0, j = _bufferPosition; i < len; ++i, ++j) {
          _buffer[j] = bytes[i];
        }
        _bufferPosition += len;
        _length += len;
        return;
      }
    }

    flush();
    _fileHandle.writeFromSync(bytes, 0, len);
    _length += len;
  }

  @override
  void writeInputStream(InputStreamBase stream) {
    if (stream is InputStream) {
      final len = stream.length;

      if (_bufferPosition + len >= _buffer.length) {
        flush();

        if (_bufferPosition + len < _buffer.length) {
          for (int i = 0, j = _bufferPosition, k = stream.offset;
              i < len;
              ++i, ++j, ++k) {
            _buffer[j] = stream.buffer[k];
          }
          _bufferPosition += len;
          _length += len;
          return;
        }
      }

      if (_bufferPosition > 0) {
        flush();
      }
      _fileHandle.writeFromSync(
          stream.buffer, stream.offset, stream.offset + stream.length);
      _length += stream.length;
    } else {
      var size = stream.length;
      const chunkSize = 1024 * 1024;
      Uint8List? bytes;
      while (size > chunkSize) {
        bytes = stream.readBytes(chunkSize).toUint8List(bytes);
        writeBytes(bytes);
        size -= chunkSize;
      }
      if (size > 0) {
        bytes = stream.readBytes(size).toUint8List(bytes);
        writeBytes(bytes);
      }
    }
  }

  /// Write a 16-bit word to the end of the buffer.
  @override
  void writeUint16(int value) {
    if (byteOrder == BIG_ENDIAN) {
      writeByte((value >> 8) & 0xff);
      writeByte((value) & 0xff);
      return;
    }
    writeByte((value) & 0xff);
    writeByte((value >> 8) & 0xff);
  }

  /// Write a 32-bit word to the end of the buffer.
  @override
  void writeUint32(int value) {
    if (byteOrder == BIG_ENDIAN) {
      writeByte((value >> 24) & 0xff);
      writeByte((value >> 16) & 0xff);
      writeByte((value >> 8) & 0xff);
      writeByte((value) & 0xff);
      return;
    }
    writeByte((value) & 0xff);
    writeByte((value >> 8) & 0xff);
    writeByte((value >> 16) & 0xff);
    writeByte((value >> 24) & 0xff);
  }

  /// Write a 64-bit word to the end of the buffer.
  @override
  void writeUint64(int value) {
    var topBit = 0x00;
    if (value & 0x8000000000000000 != 0) {
      topBit = 0x80;
      value ^= 0x8000000000000000;
    }
    if (byteOrder == BIG_ENDIAN) {
      writeByte(topBit | ((value >> 56) & 0xff));
      writeByte((value >> 48) & 0xff);
      writeByte((value >> 40) & 0xff);
      writeByte((value >> 32) & 0xff);
      writeByte((value >> 24) & 0xff);
      writeByte((value >> 16) & 0xff);
      writeByte((value >> 8) & 0xff);
      writeByte((value) & 0xff);
      return;
    }
    writeByte((value) & 0xff);
    writeByte((value >> 8) & 0xff);
    writeByte((value >> 16) & 0xff);
    writeByte((value >> 24) & 0xff);
    writeByte((value >> 32) & 0xff);
    writeByte((value >> 40) & 0xff);
    writeByte((value >> 48) & 0xff);
    writeByte(topBit | ((value >> 56) & 0xff));
  }

  List<int> subset(int start, [int? end]) {
    if (_bufferPosition > 0) {
      flush();
    }

    final pos = _fileHandle.position;
    if (start < 0) {
      start = pos + start;
    }
    var length = 0;
    if (end == null) {
      end = pos;
    } else if (end < 0) {
      end = pos + end;
    }
    length = (end - start);
    _fileHandle.position = start;
    final buffer = Uint8List(length);
    _fileHandle.readInto(buffer);
    _fileHandle.position = pos;
    return buffer;
  }
}
