import type { ObjectValue, ObjectValueType, WriteMethod } from "./types";

export class BinaryWriter {
  protected index: number = 0;
  protected buffer: DataView;

  constructor(baseSize?: number) {
    this.buffer = new DataView(new ArrayBuffer(baseSize ?? 0));
  }

  protected request(size: number) {
    let needSize = size - (this.buffer.byteLength - this.index);

    if (needSize > 0) {
      let newBuffer = new Uint8Array(this.buffer.byteLength + needSize);

      newBuffer.set(new Uint8Array(this.buffer.buffer), 0);

      this.buffer = new DataView(newBuffer.buffer);
    }
  }

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

  writeSInt8(value: number): void {
    this.request(1);

    this.buffer.setInt8(this.index, value);

    this.index++;
  }

  writeUInt8(value: number): void {
    this.request(1);

    this.buffer.setUint8(this.index, value);

    this.index++;
  }

  writeSInt16(value: number): void {
    this.request(2);

    this.buffer.setInt16(this.index, value, true);

    this.index += 2;
  }

  writeUInt16(value: number): void {
    this.request(2);

    this.buffer.setUint16(this.index, value, true);

    this.index += 2;
  }

  writeSInt32(value: number): void {
    this.request(4);

    this.buffer.setInt32(this.index, value, true);

    this.index += 4;
  }

  writeUInt32(value: number): void {
    this.request(4);

    this.buffer.setUint32(this.index, value, true);

    this.index += 4;
  }

  writeSInt64(value: bigint): void {
    this.request(8);

    this.buffer.setBigInt64(this.index, value, true);

    this.index += 8;
  }

  writeUInt64(value: bigint): void {
    this.request(8);

    this.buffer.setBigUint64(this.index, value, true);

    this.index += 8;
  }

  writeFloat32(value: number): void {
    this.request(4);

    this.buffer.setFloat32(this.index, value, true);

    this.index += 4;
  }

  writeFloat64(value: number): void {
    this.request(8);

    this.buffer.setFloat64(this.index, value, true);

    this.index += 8;
  }

  writeString(string: string): void {
    const encoder = new TextEncoder();
    const encoded = encoder.encode(string);
    this.request(encoded.byteLength);

    new Uint8Array(this.buffer.buffer).set(encoded, this.index);

    this.index += encoded.byteLength;
  }

  writePrefixedString(string: string): void {
    const encoder = new TextEncoder();
    const encoded = encoder.encode(string);
    this.writeUInt32(encoded.byteLength);
    this.request(encoded.byteLength);

    new Uint8Array(this.buffer.buffer).set(encoded, this.index);

    this.index += encoded.byteLength;
  }

  writeFixedString(size: number, string: string): void {
    this.request(size);

    const encoder = new TextEncoder();
    encoder.encodeInto(string, new Uint8Array(this.buffer.buffer, this.index, size));

    this.index += size;
  }

  writeArray<T>(array: T[], method: WriteMethod<T, BinaryWriter>): void {
    for (let i = 0; i < array.length; i++) {
      this.write(array[i], method);
    }
  }
  write<T>(value: T, writeMethod: WriteMethod<T, BinaryWriter>): void {
    if ("serialize" in writeMethod) {
      writeMethod.serialize(this, value);
      return;
    }

    writeMethod(this, value);
  }

  toBytes(): Uint8Array {
    return new Uint8Array(this.buffer.buffer.slice(this.buffer.byteOffset, this.buffer.byteOffset + this.index));
  }
}
