export class Utils {
  public static numberToHex(n: number): string {
    const hex = n.toString(16);
    return hex.length === 1 ? `0${hex}` : hex;
  }

  public static bufToHex(buf: DataView | Uint8Array | number[], separator: string = " "): string {
    const arr: number[] = buf instanceof DataView ? this.dataViewToNumberArray(buf) : Array.from(buf);
    return arr.map((n) => Utils.numberToHex(n)).join(separator);
  }

  public static hexToBuf(str: string): Uint8Array {
    const match = str.match(/[\da-f]{2}/gi);

    if (!match) {
      return new Uint8Array();
    }
    
    return new Uint8Array(
      match.map((h) => {
        return parseInt(h, 16);
      })
    );
  }

  public static dataViewToNumberArray(dw: DataView): number[] {
    const a: number[] = [];
    for (let i = 0; i < dw.byteLength; i++) {
      a.push(dw.getUint8(i));
    }
    return a;
  }

  public static dataViewToU8Array(dw: DataView): Uint8Array {
    return Uint8Array.from(this.dataViewToNumberArray(dw));
  }

  public static u8ArrayToString(arr: Uint8Array): string {
    return new TextDecoder().decode(arr);
  }

  /** Count non-zero bits in the byte array */
  public static countSetBits(arr: Uint8Array): number {
    // not so efficient, but readable
    let count: number = 0;

    arr.forEach((value) => {
      // shift until value becomes zero
      while (value > 0) {
        // check last bit
        if ((value & 1) === 1) {
          count++;
        }
        value >>= 1;
      }
    });

    return count;
  }

  /** Big endian  */
  public static u16ToBytes(n: number): [number, number] {
    const h = (n >> 8) & 0xff;
    const l = n % 256 & 0xff;
    return [h, l];
  }

  /** Big endian  */
  public static bytesToI16(arr: Uint8Array): number {
    Validators.u8ArrayLengthEquals(arr, 2);
    return arr[0] * 256 + arr[1];
  }

  public static u8ArraysEqual(a: Uint8Array, b: Uint8Array): boolean {
    return a.length === b.length && a.every((el, i) => el === b[i]);
  }

  public static sleep(ms: number): Promise<undefined> {
    return new Promise((resolve) => setTimeout(resolve, ms));
  }

  public static isBluetoothSupported(): boolean {
    return typeof navigator.bluetooth?.requestDevice !== "undefined";
  }

  public static isSerialSupported(): boolean {
    return typeof navigator.serial?.requestPort !== "undefined";
  }
}

export class Validators {
  public static u8ArraysEqual(a: Uint8Array, b: Uint8Array, message?: string): void {
    if (!Utils.u8ArraysEqual(a, b)) {
      throw new Error(message ?? "Arrays must be equal");
    }
  }
  public static u8ArrayLengthEquals(a: Uint8Array, len: number, message?: string): void {
    if (a.length !== len) {
      throw new Error(message ?? `Array length must be ${len}`);
    }
  }
  public static u8ArrayLengthAtLeast(a: Uint8Array, len: number, message?: string): void {
    if (a.length < len) {
      throw new Error(message ?? `Array length must be at least ${len}`);
    }
  }
}