import type { AsyncReadMethod } from "./types";

export class BinaryStreamReader {
  chunks: { view: ArrayBuffer, index: number }[] = [];
  readStack: { size: number, resolve: (value: DataView | PromiseLike<DataView>) => void, reject: (reason?: any) => void }[] = [];

  constructor() { }

  push(view: ArrayBuffer): void {
    this.chunks.push({ view, index: 0 });

    let next = this.readStack[0];

    while (next && this.hasFree(next.size)) {
      this.readStack.shift()!.resolve(this.get(next.size));

      next = this.readStack[0];
    }
  }

  static fromReader(reader: ReadableStreamDefaultReader): BinaryStreamReader {
    const streamReader = new BinaryStreamReader();

    (async () => {
      while (true) {
        const data = await reader.read();
        streamReader.push(data.value);
      }
    })();

    return streamReader;
  }

  private get(size: number): DataView {
    const b = new Uint8Array(size);
    let bHead: number = 0;

    for (let i = 0; i < this.chunks.length; i++) {
      const buf = this.chunks[i];

      let wantedBytes = (size - bHead)
      let availableBytes = (buf.view.byteLength - buf.index);

      if (wantedBytes < availableBytes) {
        b.set(new Uint8Array(buf.view.slice(buf.index, buf.index += wantedBytes)), bHead);
        bHead += wantedBytes;
        break;
      } else {
        b.set(new Uint8Array(buf.view.slice(buf.index, buf.index += availableBytes)), bHead);
        bHead += availableBytes;
      }
    }

    while (this.chunks[0] && this.chunks[0].view.byteLength == this.chunks[0].index) {
      this.chunks.shift();
    }

    return new DataView(b.buffer);
  }

  private hasFree(size: number) {
    let accumulated: number = 0;

    for (let i = 0; i < this.chunks.length; i++) {
      accumulated += this.chunks[i].view.byteLength - this.chunks[i].index;

      if (accumulated >= size) return true;
    }

    return false;
  }

  async bytes(size: number): Promise<DataView> {
    return new Promise((res, rej) => {
      if (this.hasFree(size)) {
        return res(this.get(size));
      }

      this.readStack.push({ size, resolve: res, reject: rej });
    })
  }

  async readBoolean(): Promise<boolean> {
    return await this.readUInt8() != 0;
  }

  async readSInt8(): Promise<number> {
    return (await this.bytes(1)).getInt8(0);
  }

  async readUInt8(): Promise<number> {
    return (await this.bytes(1)).getUint8(0);
  }

  async readSInt16(): Promise<number> {
    return (await this.bytes(2)).getInt16(0, true);
  }

  async readUInt16(): Promise<number> {
    return (await this.bytes(2)).getUint16(0, true);
  }

  async readSInt32(): Promise<number> {
    return (await this.bytes(4)).getInt32(0, true);
  }

  async readUInt32(): Promise<number> {
    return (await this.bytes(4)).getUint32(0, true);
  }

  async readSInt64(): Promise<bigint> {
    return (await this.bytes(8)).getBigInt64(0, true);
  }

  async readUInt64(): Promise<bigint> {
    return (await this.bytes(8)).getBigUint64(0, true);
  }

  async readFloat32(): Promise<number> {
    return (await this.bytes(4)).getFloat32(0, true);
  }

  async readFloat64(): Promise<number> {
    return (await this.bytes(8)).getFloat64(0, true);
  }

  async readString(length: number, encoding?: string): Promise<string> {
    const decoder = new TextDecoder(encoding);

    return decoder.decode(await this.bytes(length));
  }

  async readFixedString(length: number, encoding?: string): Promise<string> {
    const decoder = new TextDecoder(encoding);

    const bytes = await this.bytes(length);
    const slice = new Uint8Array(bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength));
    const foundIndex = slice.findIndex(byte => byte == 0);
    const end = foundIndex === -1 ? slice.byteLength : foundIndex;

    return decoder.decode(slice.slice(0, end));
  }

  async readArray<T>(size: number, readMethod: AsyncReadMethod<T, this>): Promise<T[]> {
    const array = new Array<T>(size);

    for (let i = 0; i < size; i++) {
      array[i] = await this.read(readMethod);
    }

    return array;
  }

  async read<T>(readMethod: AsyncReadMethod<T, this>): Promise<T> {
    if ("deserialize" in readMethod) {
      return await readMethod.deserialize(this);
    }

    return await readMethod(this);
  }

  async readDate(): Promise<Date> {
    return new Date(Number(await this.readUInt64()));
  }
}
