export const base64ToBytes = (b64: string): Uint8Array => {
  const binaryString = atob(b64);
  const bytes = new Uint8Array(binaryString.length);
  for (let i = 0; i < binaryString.length; i++) {
      bytes[i] = binaryString.charCodeAt(i);
  }
  return bytes;
}

export const bytesToBase64 = (buf: Uint8Array): string => {
  const bytes: string[] = new Array(buf.length);
  for (let i = 0; i < buf.length; i++) {
    bytes[i] = String.fromCharCode(buf[i]);
  }
  return btoa(bytes.join(''));
}

/**
 * Do semi constant time comparison of two Uint8Arrays to prevent timing attacks.
 * Returns true if the arrays are equal, false otherwise.
 *
 * Note: This is not truly constant time, but it is designed to take the same amount of time regardless of where the first difference is.
 * It will still take longer for longer arrays, but it will not short-circuit on the first difference.
 */
export const constantTimeEqual = (a: Uint8Array, b: Uint8Array): boolean => {
  if (a.length !== b.length) return false;

  let result = 0;
  for (let i = 0; i < a.length; i++) {
    result |= a[i] ^ b[i];
  }
  return result === 0;
}

/* BufferReader is a utility class for reading and writing binary data from a Uint8Array buffer.
 * It maintains an internal offset and length to track the current position in the buffer. It
 * provides methods for reading unsigned integers in little-endian format, as well as arbitrary
 * byte arrays. It also allows writing unsigned integers back into the buffer. This class is
 * useful for parsing and constructing binary protocols.
 */
export class BufferReader {
  protected buffer: Uint8Array;
  protected offset: number = 0;
  public length: number = 0;

  constructor(buffer: Uint8Array) {
    this.buffer = new Uint8Array(buffer.length);
    for (let i = 0; i < buffer.length; i++) {
      this.buffer[i] = buffer[i];
    }
    this.length = buffer.length;
  }

  public reset(buffer: Uint8Array) {
    this.buffer = new Uint8Array(buffer.length);
    for (let i = 0; i < buffer.length; i++) {
      this.buffer[i] = buffer[i];
    }
    this.offset = 0;
    this.length = buffer.length;
  }

  public readUint8(): number {
    const value = this.buffer[this.offset];
    this.offset++;
    this.length--;
    return value;
  }

  public readUint16LE(): number {
    const value = this.buffer[this.offset] | (this.buffer[this.offset + 1] << 8);
    this.offset += 2;
    this.length -= 2;
    return value;
  }

  public readUint32LE(): number {
    const value = this.buffer[this.offset] |
      (this.buffer[this.offset + 1] << 8) |
      (this.buffer[this.offset + 2] << 16) |
      (this.buffer[this.offset + 3] << 24);
    this.offset += 4;
    this.length -= 4;
    return value;
  }

  public readBytes(size?: number): Uint8Array {
    size ||= (this.buffer.length - this.offset);
    const value = this.buffer.slice(this.offset, this.offset + size);
    this.offset += size;
    this.length -= size;
    return value;
  }

  public writeUint16LE(value: number): void {
    this.buffer[this.offset] = value & 0xFF;
    this.buffer[this.offset + 1] = (value >> 8) & 0xFF;
    this.offset += 2;
    this.length += 2;
  }

  public writeUint32LE(value: number): void {
    this.buffer[this.offset] = value & 0xFF;
    this.buffer[this.offset + 1] = (value >> 8) & 0xFF;
    this.buffer[this.offset + 2] = (value >> 16) & 0xFF;
    this.buffer[this.offset + 3] = (value >> 24) & 0xFF;
    this.offset += 4;
    this.length += 4;
  }
}
