export default class Stream {
  private stream: Uint8Array;
  private readPointer = 0;  // bits
  private writePointer = 0; // bits

  constructor(stream?: Uint8Array | number[]) {
    if (!stream) {
      this.stream = new Uint8Array(0);
    } else {
      const src = stream instanceof Uint8Array ? stream : Uint8Array.from(stream);
      this.stream = src;
      this.writePointer = this.stream.length << 3;
    }
  }

  getStream(): Uint8Array {
    return this.stream.slice(0, this.length());
  }

  setStream(stream?: Uint8Array | number[]) {
    if (!stream) {
      this.stream = new Uint8Array(0);
      this.readPointer = 0;
      this.writePointer = 0;
      return;
    }
    const src = stream instanceof Uint8Array ? stream : Uint8Array.from(stream);
    this.stream = src;
    this.readPointer = 0;
    this.writePointer = this.stream.length << 3;
  }

  getBuffer(): Uint8Array {
    return this.getStream();
  }

  isEmpty(): boolean {
    return this.writePointer === 0;
  }

  length(): number {
    return (this.writePointer + 7) >> 3;
  }

  // ---------- bit / boolean ----------

  writeBit(value: number) {
    this.writeBits(BigInt(value & 1), 1);
  }

  readBit(): number {
    return Number(this.readBits(1));
  }

  writeBoolean(value: boolean) {
    this.writeBit(value ? 1 : 0);
  }

  readBoolean(): boolean {
    return this.readBit() === 1;
  }

  // ---------- byte ----------

  writeByte(b: number) {
    this.writeUInt8(b & 0xff);
  }

  readByte(): number {
    const v = this.readUInt8();
    return (v << 24) >> 24; // signed byte
  }

  // ---------- UInt / Int 8 ----------

  writeUInt8(value: number) {
    const v = value & 0xff;

    if ((this.writePointer & 7) === 0) {
      this.reserveBits(8);
      this.stream[this.writePointer >> 3] = v;
      this.writePointer += 8;
      return;
    }

    this.writeBits(BigInt(v), 8);
  }

  readUInt8(): number {
    if (this.remainingBits() < 8n) {
      throw new Error("Not enough bits to read UInt8");
    }

    if ((this.readPointer & 7) === 0) {
      const v = this.stream[this.readPointer >> 3] & 0xff;
      this.readPointer += 8;
      return v;
    }

    return Number(this.readBits(8));
  }

  writeInt8(value: number) {
    this.writeUInt8(value);
  }

  readInt8(): number {
    const u = this.readUInt8();
    return (u << 24) >> 24;
  }

  // ---------- UInt / Int 16 ----------

  writeUInt16(value: number) {
    const v = value & 0xffff;
    this.writeUInt8((v >>> 8) & 0xff);
    this.writeUInt8(v & 0xff);
  }

  readUInt16(): number {
    const hi = this.readUInt8();
    const lo = this.readUInt8();
    return (hi << 8) | lo;
  }

  writeInt16(value: number) {
    this.writeUInt16(value);
  }

  readInt16(): number {
    const u = this.readUInt16();
    return (u << 16) >> 16;
  }

  // ---------- UInt / Int 32 ----------

  writeUInt32(value: number) {
    if (!Number.isFinite(value) || value < 0 || value > 0xffffffff) {
      throw new Error(`UInt32 out of range: ${value}`);
    }

    const v = Math.floor(value);
    this.writeUInt8((v >>> 24) & 0xff);
    this.writeUInt8((v >>> 16) & 0xff);
    this.writeUInt8((v >>> 8) & 0xff);
    this.writeUInt8(v & 0xff);
  }

  readUInt32(): number {
    const b1 = this.readUInt8();
    const b2 = this.readUInt8();
    const b3 = this.readUInt8();
    const b4 = this.readUInt8();
    return (((b1 * 0x1000000) + (b2 << 16) + (b3 << 8) + b4) >>> 0);
  }

  writeInt32(value: number) {
    this.writeUInt32(value >>> 0);
  }

  readInt32(): number {
    return this.readUInt32() | 0;
  }

  // ---------- UInt / Int 64 ----------

  writeUInt64(value: bigint) {
    if (value < 0n || value > 0xffff_ffff_ffff_ffffn) {
      throw new Error(`UInt64 out of range: ${value.toString()}`);
    }

    this.writeUInt8(Number((value >> 56n) & 0xffn));
    this.writeUInt8(Number((value >> 48n) & 0xffn));
    this.writeUInt8(Number((value >> 40n) & 0xffn));
    this.writeUInt8(Number((value >> 32n) & 0xffn));
    this.writeUInt8(Number((value >> 24n) & 0xffn));
    this.writeUInt8(Number((value >> 16n) & 0xffn));
    this.writeUInt8(Number((value >> 8n) & 0xffn));
    this.writeUInt8(Number(value & 0xffn));
  }

  readUInt64(): bigint {
    const high = BigInt(this.readUInt32() >>> 0);
    const low = BigInt(this.readUInt32() >>> 0);
    return (high << 32n) | low;
  }

  writeInt64(value: bigint) {
    const u = BigInt.asUintN(64, value);
    this.writeUInt64(u);
  }

  readInt64(): bigint {
    return BigInt.asIntN(64, this.readUInt64());
  }

  // ---------- float ----------

  writeFloat32(value: number) {
    const ab = new ArrayBuffer(4);
    const dv = new DataView(ab);
    dv.setFloat32(0, value, false); // big-endian
    this.writeUInt8(dv.getUint8(0));
    this.writeUInt8(dv.getUint8(1));
    this.writeUInt8(dv.getUint8(2));
    this.writeUInt8(dv.getUint8(3));
  }

  readFloat32(): number {
    const ab = new ArrayBuffer(4);
    const dv = new DataView(ab);
    dv.setUint8(0, this.readUInt8());
    dv.setUint8(1, this.readUInt8());
    dv.setUint8(2, this.readUInt8());
    dv.setUint8(3, this.readUInt8());
    return dv.getFloat32(0, false); // big-endian
  }

  // ---------- string / bytes ----------
  // String: length(UInt32) + chars(UInt16), как в Java charAt()

  writeString(value: string | null | undefined) {
    const s = value ?? "";
    this.writeUInt32(s.length);

    if (s.length === 0) return;

    this.reserveBits(BigInt(s.length) * 16n);
    for (let i = 0; i < s.length; i++) {
      this.writeUInt16(s.charCodeAt(i) & 0xffff);
    }
  }

  readString(): string {
    const len = this.readUInt32();
    if (len > 0x7fffffff) {
      throw new Error(`String length too large: ${len}`);
    }

    const requiredBits = BigInt(len) * 16n;
    if (requiredBits > this.remainingBits()) {
      throw new Error("Not enough bits to read string");
    }

    const chars = new Array<number>(len);
    for (let i = 0; i < len; i++) {
      chars[i] = this.readUInt16();
    }
    return String.fromCharCode(...chars);
  }

  // byte[]: length(UInt32) + payload
  writeBytes(value: Uint8Array | number[] | null | undefined) {
    const arr = value == null
      ? new Uint8Array(0)
      : (value instanceof Uint8Array ? value : Uint8Array.from(value));

    this.writeUInt32(arr.length);
    if (arr.length === 0) return;

    this.reserveBits(BigInt(arr.length) * 8n);

    if ((this.writePointer & 7) === 0) {
      const byteIndex = this.writePointer >> 3;
      this.ensureCapacity(byteIndex + arr.length - 1);
      this.stream.set(arr, byteIndex);
      this.writePointer += arr.length << 3;
      return;
    }

    for (let i = 0; i < arr.length; i++) {
      this.writeUInt8(arr[i]);
    }
  }

  readBytes(): Uint8Array {
    const len = this.readUInt32();
    if (len === 0) return new Uint8Array(0);

    const requiredBits = BigInt(len) * 8n;
    if (requiredBits > this.remainingBits()) {
      return new Uint8Array(0);
    }

    const out = new Uint8Array(len);

    if ((this.readPointer & 7) === 0) {
      const byteIndex = this.readPointer >> 3;
      out.set(this.stream.slice(byteIndex, byteIndex + len));
      this.readPointer += len << 3;
      return out;
    }

    for (let i = 0; i < len; i++) {
      out[i] = this.readUInt8();
    }
    return out;
  }

  // ---------- internals ----------

  private remainingBits(): bigint {
    return BigInt(this.writePointer - this.readPointer);
  }

  private writeBits(value: bigint, bits: number) {
    if (bits <= 0) return;

    this.reserveBits(bits);

    for (let i = bits - 1; i >= 0; i--) {
      const bit = Number((value >> BigInt(i)) & 1n);
      const byteIndex = this.writePointer >> 3;
      const shift = 7 - (this.writePointer & 7);

      if (bit === 1) {
        this.stream[byteIndex] = this.stream[byteIndex] | (1 << shift);
      } else {
        this.stream[byteIndex] = this.stream[byteIndex] & ~(1 << shift);
      }
      this.writePointer++;
    }
  }

  private readBits(bits: number): bigint {
    if (bits <= 0) return 0n;
    if (this.remainingBits() < BigInt(bits)) {
      throw new Error("Not enough bits to read");
    }

    let value = 0n;
    for (let i = 0; i < bits; i++) {
      const bit = (this.stream[this.readPointer >> 3] >> (7 - (this.readPointer & 7))) & 1;
      value = (value << 1n) | BigInt(bit);
      this.readPointer++;
    }
    return value;
  }

  private reserveBits(bitsToWrite: number | bigint) {
    const bits = typeof bitsToWrite === "number" ? BigInt(bitsToWrite) : bitsToWrite;
    if (bits <= 0n) return;

    const lastBitIndex = BigInt(this.writePointer) + bits - 1n;
    if (lastBitIndex < 0n) throw new Error("Bit index overflow");

    const byteIndex = lastBitIndex >> 3n;
    if (byteIndex > BigInt(Number.MAX_SAFE_INTEGER)) {
      throw new Error("Stream too large");
    }

    this.ensureCapacity(Number(byteIndex));
  }

  private ensureCapacity(byteIndex: number) {
    const requiredSize = byteIndex + 1;
    if (requiredSize <= this.stream.length) return;

    let newSize = this.stream.length === 0 ? 32 : this.stream.length;
    while (newSize < requiredSize) {
      if (newSize > (0x7fffffff >> 1)) {
        newSize = requiredSize;
        break;
      }
      newSize <<= 1;
    }

    const next = new Uint8Array(newSize);
    next.set(this.stream);
    this.stream = next;
  }
}