// ---------------------------------------------------------------------------
// Pixel pack/unpack helpers
// ---------------------------------------------------------------------------

/**
 * Pack 1-byte-per-pixel array into 1bpp.
 * Excess bits in the last byte are set to 0.
 * Size = ceil(width * height / 8).
 */
export function packPixels(pixels: Uint8Array, width: number, height: number): Uint8Array {
  const total = width * height;
  const packed = new Uint8Array((total + 7) >> 3);
  for (let i = 0; i < total; i++) {
    if (pixels[i]) packed[i >> 3] = (packed[i >> 3] ?? 0) | (1 << (i & 7));
  }
  return packed;
}

/**
 * Unpack 1bpp data to 1-byte-per-pixel. Excess bits beyond width*height are ignored.
 */
export function unpackPixels(packed: Uint8Array, width: number, height: number): Uint8Array {
  const total = width * height;
  const pixels = new Uint8Array(total);
  for (let i = 0; i < total; i++) {
    pixels[i] = ((packed[i >> 3] ?? 0) >> (i & 7)) & 1;
  }
  return pixels;
}

/** Byte count of packed 1bpp image */
export function packedSize(width: number, height: number): number {
  return (width * height + 7) >> 3;
}

/** Padding needed to align `byteCount` to a 4-byte boundary */
export function pad32(byteCount: number): number {
  return (4 - (byteCount & 3)) & 3;
}

// ---------------------------------------------------------------------------
// BinaryReader — little-endian cursor over an ArrayBuffer
// ---------------------------------------------------------------------------

export class BinaryReader {
  private view: DataView;
  private pos: number = 0;

  constructor(buffer: ArrayBuffer | ArrayBufferView) {
    this.view = buffer instanceof ArrayBuffer
      ? new DataView(buffer)
      : new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength);
  }

  get offset():     number { return this.pos; }
  get byteLength(): number { return this.view.byteLength; }
  get remaining():  number { return this.view.byteLength - this.pos; }

  readByte():   number { this.#need(1); return this.view.getUint8(this.pos++); }
  readUint16(): number { this.#need(2); const v = this.view.getUint16(this.pos, true); this.pos += 2; return v; }
  readShort():  number { return this.readUint16(); }
  readInt16():  number { this.#need(2); const v = this.view.getInt16(this.pos, true);  this.pos += 2; return v; }

  readUint24(): number {
    this.#need(3);
    const v = this.view.getUint8(this.pos) | (this.view.getUint8(this.pos+1) << 8) | (this.view.getUint8(this.pos+2) << 16);
    this.pos += 3;
    return v;
  }

  readUint32(): number { this.#need(4); const v = this.view.getUint32(this.pos, true); this.pos += 4; return v; }

  readUint64(): bigint {
    this.#need(8);
    const lo = BigInt(this.view.getUint32(this.pos,   true));
    const hi = BigInt(this.view.getUint32(this.pos+4, true));
    this.pos += 8;
    return (hi << 32n) | lo;
  }

  readBytes(n: number): Uint8Array {
    this.#need(n);
    const s = new Uint8Array(this.view.buffer, this.pos, n);
    this.pos += n;
    return s;
  }

  readUtf8(n: number): string { return new TextDecoder().decode(this.readBytes(n)); }

  seek(pos: number): void {
    if (pos < 0 || pos > this.view.byteLength) {
      throw new RangeError(`seek(${pos}) out of [0, ${this.view.byteLength}]`);
    }
    this.pos = pos;
  }

  #need(n: number): void {
    if (this.remaining < n) throw new RangeError(
      `BinaryReader: need ${n} byte(s) at offset ${this.pos}, ${this.remaining} remain`
    );
  }
}
