import CommonUtils from "./common.utils";
import ValidationUtils from "./validation.utils";
import * as base64 from "base64-js";

export default class BufferUtils {

  /**
   * Tests for both node's Buffer and Uint8Array
   *
   * @param arg
   * @return Returns true if the given argument is an instance of a Uint8Array. 
   */
  public static isBuffer(arg: unknown): arg is Uint8Array {
    return arg instanceof Uint8Array;
  }

  /**
   * Tests for both node's Buffer and Uint8Array
   *
   * @param arg
   * @return Returns true if the given argument is an instance of a hash160 or hash256 buffer. 
   */
  public static isHashBuffer(arg: unknown): boolean {
    return this.isBuffer(arg) && (arg.length === 20 || arg.length === 32);
  }

  /**
   * Reverse a Uint8Array
   * @param param
   * @return new reversed Uint8Array
   */
  public static reverse(param: Uint8Array): Uint8Array {
    return Uint8Array.from(param).reverse();
  }

  /**
   * Convert a Uint8Array to a UTF-8 string.
   *
   * @param buffer - Uint8Array containing UTF-8 encoded bytes
   * @returns Decoded string
   */
  public static bufferToUtf8(buffer: Uint8Array): string {
    return new TextDecoder("utf-8").decode(buffer);
  }

  /**
   * Convert a UTF-8 string to a Uint8Array.
   *
   * @param str - UTF-8 string
   * @returns Encoded Uint8Array
   */
  public static utf8ToBuffer(str: string): Uint8Array {
    return new TextEncoder().encode(str);
  }

  /**
   * Convert a Uint8Array to a Base64 string.
   *
   * @param buffer - Uint8Array containing Base64 encoded bytes
   * @returns Decoded string
   */
  public static bufferToBase64(buffer: Uint8Array): string {
    return base64.fromByteArray(buffer);
  }

  /**
   * Convert a Base64 string to a Uint8Array.
   *
   * @param str - Base64 string
   * @returns Encoded Uint8Array
   */
  public static base64ToBuffer(str: string): Uint8Array {
    return base64.toByteArray(str);
  }

  /**
   * Transforms a buffer into a string with a number in hexa representation
   *
   * Similar for <tt>buffer.toString('hex')</tt>
   *
   * @param buffer
   * @return string
   */
  public static bufferToHex(buffer: Uint8Array): string {
    ValidationUtils.validateArgumentType(buffer, Uint8Array, 'buffer');
    return Array.from(buffer)
      .map(b => b.toString(16).padStart(2, '0'))
      .join('');
  }

  /**
   * Convert a hexadecimal string into a Uint8Array.
   *
   * @param hex - Hex string (must have even length)
   * @returns Uint8Array representing the bytes
   */
  public static hexToBuffer(hex: string): Uint8Array {
    if (!CommonUtils.isHexa(hex)) {
      return new Uint8Array();
    }

    const length = hex.length / 2;
    const bytes = new Uint8Array(length);

    for (let i = 0; i < length; i++) {
      bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
    }

    return bytes;
  }

  /**
   * Concatenate multiple Uint8Arrays into a single Uint8Array.
   * 
   * Mimics Node.js Buffer.concat(list, totalLength?):
   * - list: Array of Uint8Arrays
   * - totalLength: Optional precomputed total length
   *
   * @param list - Array of Uint8Arrays to concatenate
   * @param totalLength - Optional total length to preallocate
   * @returns New Uint8Array containing all bytes from input arrays
   *
   * @example
   * const a = new Uint8Array([1,2]);
   * const b = new Uint8Array([3,4]);
   * const result = Uint8ArrayUtils.concat([a,b]);
   * console.log(result); // Uint8Array(4) [1,2,3,4]
   */
  public static concat(list: Uint8Array[], totalLength?: number): Uint8Array {
    const length = totalLength ?? list.reduce((sum, arr) => sum + arr.length, 0);
    const result = new Uint8Array(length);

    let offset = 0;
    for (const arr of list) {
      result.set(arr, offset);
      offset += arr.length;
    }

    return result;
  }

  /**
   * Compares two Uint8Arrays for byte-wise equality.
   *
   * This function checks whether the two arrays have the same length
   * and the same content.
   *
   * @param a - The first Uint8Array to compare.
   * @param b - The second Uint8Array to compare.
   * @returns `true` if the arrays have the same length and contents, `false` otherwise.
   */
  public static equals(a: Uint8Array, b: Uint8Array): boolean {
    if (a === b) return true;
    if (a.byteLength !== b.byteLength) return false;

    for (let i = 0; i < a.byteLength; i++) {
      if (a[i] !== b[i]) return false;
    }

    return true;
  }

  /**
   * Transforms a number from 0 to 255 into a Uint8Array of size 1 with that value
   *
   * @param integer
   * @return Uint8Array
   */
  public static integerAsSingleByteBuffer(integer: number): Uint8Array {
    ValidationUtils.validateArgumentType(integer, 'number', 'integer');
    return new Uint8Array([integer & 0xff]);
  }

  
  /**
   * Transforms the first byte of an array into a number ranging from -128 to 127
   * 
   * @param buffer
   * @return number
   */
  public static integerFromSingleByteBuffer(buffer: Uint8Array): number {
    ValidationUtils.validateArgumentType(buffer, Uint8Array, 'buffer');
    return buffer[0];
  }

  /**
   * Transform a 4-byte integer into a Uint8Array of length 4.
   *
   * @param integer
   * @return Uint8Array
   */
  public static integerAsBuffer(integer: number): Uint8Array {
    ValidationUtils.validateArgumentType(integer, 'number', 'integer');

    const bytes = new Uint8Array(4);
    bytes[0] = (integer >> 24) & 0xff;
    bytes[1] = (integer >> 16) & 0xff;
    bytes[2] = (integer >> 8) & 0xff;
    bytes[3] = integer & 0xff;

    return bytes;
  }

  /**
   * Transform the first 4 values of a Uint8Array into a number, in little endian encoding
   *
   * @param buffer
   * @return integer
   */
  public static integerFromBuffer(buffer: Uint8Array): number {
    ValidationUtils.validateArgumentType(buffer, Uint8Array, 'buffer');
    return buffer[0] << 24 | buffer[1] << 16 | buffer[2] << 8 | buffer[3];
  }

  /**
   * @return secure random bytes
   */
  public static getRandomBuffer(size: number): Uint8Array {
    const bytes = new Uint8Array(size);
    crypto.getRandomValues(bytes);
    return bytes;
  }
}