import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import '../util/archive_exception.dart';
import '../util/byte_order.dart';
import '../util/input_stream.dart';

class InputFileStream extends InputStreamBase {
  String path;
  RandomAccessFile _file;
  final int byteOrder;
  int _fileSize = 0;
  int _filePosition = 0;
  List<int> _buffer;
  int _bufferSize = 0;
  int _bufferPosition = 0;
  int _maxBufferSize;
  static const int _kDefaultBufferSize = 4096;

  InputFileStream(this.path, {this.byteOrder: LITTLE_ENDIAN,
    int bufferSize: _kDefaultBufferSize}) {
    _maxBufferSize = bufferSize;
    _buffer = new Uint8List(_maxBufferSize);
    _file = new File(path).openSync();
    _fileSize = _file.lengthSync();

    _readBuffer();
  }

  InputFileStream.file(File file, {this.byteOrder: LITTLE_ENDIAN,
    int bufferSize: _kDefaultBufferSize}) {
    _maxBufferSize = bufferSize;
    _buffer = new Uint8List(_maxBufferSize);
    _file = file.openSync();
    _fileSize = _file.lengthSync();
    _readBuffer();
  }

  void close() {
    _file.closeSync();
    _fileSize = 0;
  }

  int get length => _fileSize;

  int get position => _filePosition;

  bool get isEOS => (_filePosition >= _fileSize) &&
      (_bufferPosition >= _bufferSize);

  int get bufferSize => _bufferSize;

  int get bufferPosition => _bufferPosition;

  int get bufferRemaining => _bufferSize - _bufferPosition;

  int get fileRemaining => _fileSize - _filePosition;

  void reset() {
    _filePosition = 0;
    _file.setPositionSync(0);
    _readBuffer();
  }

  void skip(int length) {
    if ((_bufferPosition + length) < _bufferSize) {
      _bufferPosition += length;
    } else {
      int remaining = length - (_bufferSize - _bufferPosition);
      while (!isEOS) {
        _readBuffer();
        if (remaining < _bufferSize) {
          _bufferPosition += remaining;
          break;
        }
        remaining -= _bufferSize;
      }
    }
  }

  /**
   * Read [count] bytes from an [offset] of the current read position, without
   * moving the read position.
   */
  InputStream peekBytes(int count, [int offset = 0]) {
    int end = _bufferPosition + offset + count;
    if (end > 0 && end < _bufferSize) {
      List<int> bytes = _buffer.sublist(_bufferPosition + offset, end);
      return new InputStream(bytes);
    }

    Uint8List bytes = new Uint8List(count);

    int remaining = _bufferSize - (_bufferPosition + offset);
    if (remaining > 0) {
      List<int> bytes1 = _buffer.sublist(_bufferPosition + offset, _bufferSize);
      bytes.setRange(0, remaining, bytes1);
    }

    _file.readIntoSync(bytes, remaining, count);
    _file.setPositionSync(_filePosition);

    return new InputStream(bytes);
  }

  void rewind([int count = 1]) {
    if (_bufferPosition - count < 0) {
      int remaining = (_bufferPosition - count).abs();
      _filePosition = _filePosition - _bufferSize - remaining;
      if (_filePosition < 0) {
        _filePosition = 0;
      }
      _file.setPositionSync(_filePosition);
      _readBuffer();
      return;
    }
    _bufferPosition -= count;
  }

  int readByte() {
    if (isEOS) {
      return 0;
    }
    if (_bufferPosition >= _bufferSize) {
      _readBuffer();
    }
    if (_bufferPosition >= _bufferSize) {
      return 0;
    }
    return _buffer[_bufferPosition++] & 0xff;
  }

  /**
   * Read a 16-bit word from the stream.
   */
  int readUint16() {
    int b1 = 0;
    int b2 = 0;
    if ((_bufferPosition + 2) < _bufferSize) {
      b1 = _buffer[_bufferPosition++] & 0xff;
      b2 = _buffer[_bufferPosition++] & 0xff;
    } else {
      b1 = readByte();
      b2 = readByte();
    }
    if (byteOrder == BIG_ENDIAN) {
      return (b1 << 8) | b2;
    }
    return (b2 << 8) | b1;
  }

  /**
   * Read a 24-bit word from the stream.
   */
  int readUint24() {
    int b1 = 0;
    int b2 = 0;
    int b3 = 0;
    if ((_bufferPosition + 3) < _bufferSize) {
      b1 = _buffer[_bufferPosition++] & 0xff;
      b2 = _buffer[_bufferPosition++] & 0xff;
      b3 = _buffer[_bufferPosition++] & 0xff;
    } else {
      b1 = readByte();
      b2 = readByte();
      b3 = readByte();
    }

    if (byteOrder == BIG_ENDIAN) {
      return b3 | (b2 << 8) | (b1 << 16);
    }
    return b1 | (b2 << 8) | (b3 << 16);
  }

  /**
   * Read a 32-bit word from the stream.
   */
  int readUint32() {
    int b1 = 0;
    int b2 = 0;
    int b3 = 0;
    int b4 = 0;
    if ((_bufferPosition + 4) < _bufferSize) {
      b1 = _buffer[_bufferPosition++] & 0xff;
      b2 = _buffer[_bufferPosition++] & 0xff;
      b3 = _buffer[_bufferPosition++] & 0xff;
      b4 = _buffer[_bufferPosition++] & 0xff;
    } else {
      b1 = readByte();
      b2 = readByte();
      b3 = readByte();
      b4 = readByte();
    }

    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.
   */
  int readUint64() {
    int b1 = 0;
    int b2 = 0;
    int b3 = 0;
    int b4 = 0;
    int b5 = 0;
    int b6 = 0;
    int b7 = 0;
    int b8 = 0;
    if ((_bufferPosition + 8) < _bufferSize) {
      b1 = _buffer[_bufferPosition++] & 0xff;
      b2 = _buffer[_bufferPosition++] & 0xff;
      b3 = _buffer[_bufferPosition++] & 0xff;
      b4 = _buffer[_bufferPosition++] & 0xff;
      b5 = _buffer[_bufferPosition++] & 0xff;
      b6 = _buffer[_bufferPosition++] & 0xff;
      b7 = _buffer[_bufferPosition++] & 0xff;
      b8 = _buffer[_bufferPosition++] & 0xff;
    } else {
      b1 = readByte();
      b2 = readByte();
      b3 = readByte();
      b4 = readByte();
      b5 = readByte();
      b6 = readByte();
      b7 = readByte();
      b8 = readByte();
    }

    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;
  }

  InputStream readBytes(int length) {
    if (isEOS) {
      return null;
    }

    if (_bufferPosition == _bufferSize) {
      _readBuffer();
    }

    if (_remainingBufferSize >= length) {
      List<int> bytes = _buffer.sublist(_bufferPosition,
          _bufferPosition + length);
      _bufferPosition += length;
      return new InputStream(bytes);
    }

    int total_remaining = fileRemaining + _remainingBufferSize;
    if (length > total_remaining) {
      length = total_remaining;
    }

    Uint8List bytes = new Uint8List(length);

    int offset = 0;
    while (length > 0) {
      int remaining = _bufferSize - _bufferPosition;
      int end = (length > remaining) ? _bufferSize : (_bufferPosition + length);
      List<int> l = _buffer.sublist(_bufferPosition, end);
      // TODO probably better to use bytes.setRange here.
      for (int i = 0; i < l.length; ++i) {
        bytes[offset + i] = l[i];
      }
      offset += l.length;
      length -= l.length;
      _bufferPosition = end;
      if (length > 0 && _bufferPosition == _bufferSize) {
        _readBuffer();
        if (_bufferSize == 0) {
          break;
        }
      }
    }

    return new InputStream(bytes);
  }

  Uint8List toUint8List() {
    return readBytes(_fileSize).toUint8List();
  }

  /**
   * 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}) {
    if (size == null) {
      List<int> codes = [];
      while (!isEOS) {
        int c = readByte();
        if (c == 0) {
          return utf8 ? new Utf8Decoder().convert(codes) :
                 new String.fromCharCodes(codes);
        }
        codes.add(c);
      }
      throw new ArchiveException('EOF reached without finding string terminator');
    }

    InputStream s = readBytes(size);
    Uint8List bytes = s.toUint8List();
    String str = utf8 ? new Utf8Decoder().convert(bytes) :
                 new String.fromCharCodes(bytes);
    return str;
  }

  int get _remainingBufferSize => _bufferSize - _bufferPosition;

  void _readBuffer() {
    _bufferPosition = 0;
    _bufferSize = _file.readIntoSync(_buffer);
    if (_bufferSize == 0) {
      return;
    }
    _filePosition += _bufferSize;
  }
}
