/**
 * @file pbkdf2.ts
 * @summary A pure-JS implementation of PBKDF2-HMAC-SHA256 using the Web Crypto API.
 * This is used as a fallback to test for platform inconsistencies in native PBKDF2.
 * Adapted from public domain examples and RFC 2898.
 */

/**
 * Performs HMAC-SHA256 on a given key and data.
 * @param key The HMAC key.
 * @param data The data to hash.
 * @returns A promise that resolves to the HMAC-SHA256 digest as an ArrayBuffer.
 */
async function hmacSha256(key: CryptoKey, data: ArrayBuffer): Promise<ArrayBuffer> {
  return crypto.subtle.sign('HMAC', key, data);
}

/**
 * The F function for PBKDF2 (PRF).
 * T_1 = F(P, S, c, 1)
 * T_2 = F(P, S, c, 2)
 * ...
 * F(P, S, c, i) = U_1 \xor U_2 \xor ... \xor U_c
 * U_1 = PRF(P, S || INT_32_BE(i))
 * U_2 = PRF(P, U_1)
 * ...
 * U_c = PRF(P, U_{c-1})
 */
async function F(passwordKey: CryptoKey, salt: Uint8Array, iterations: number, i: number): Promise<Uint8Array> {
  // S || INT_32_BE(i)
  const saltI = new Uint8Array(salt.length + 4);
  saltI.set(salt, 0);
  const i_be = new DataView(saltI.buffer, salt.length, 4);
  i_be.setUint32(0, i, false); // false for big-endian

  // U_1
  let U = new Uint8Array(await hmacSha256(passwordKey, saltI.buffer));
  // T
  let T = U.slice();

  for (let c = 1; c < iterations; c++) {
    // U_c = PRF(P, U_{c-1})
    U = new Uint8Array(await hmacSha256(passwordKey, U.buffer));
    // T = T \xor U_c
    for (let j = 0; j < T.length; j++) {
      T[j] ^= U[j];
    }
  }

  return T;
}

/**
 * Derives a key using PBKDF2-HMAC-SHA256.
 * @param password The password string.
 * @param salt The salt bytes.
 * @param iterations The number of iterations.
 * @param keyLenBytes The desired key length in bytes.
 * @returns A promise that resolves to the derived key as a Uint8Array.
 */
export async function pbkdf2HmacSha256(password: string, salt: Uint8Array, iterations: number, keyLenBytes: number): Promise<Uint8Array> {
  const passwordBytes = new TextEncoder().encode(password);
  const passwordKey = await crypto.subtle.importKey(
    'raw',
    passwordBytes,
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['sign']
  );

  const hLen = 32; // SHA-256 output length in bytes
  const l = Math.ceil(keyLenBytes / hLen);
  const r = keyLenBytes - (l - 1) * hLen;

  const blocks: Uint8Array[] = [];
  for (let i = 1; i <= l; i++) {
    blocks.push(await F(passwordKey, salt, iterations, i));
  }
  
  const T = new Uint8Array(keyLenBytes);
  for(let i = 0; i < l - 1; i++) {
    T.set(blocks[i], i * hLen);
  }
  T.set(blocks[l-1].slice(0, r), (l-1) * hLen);
  
  return T;
}
