/**
 * @license
 * Copyright 2020 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

import {InvalidArgumentsException} from '../exception/invalid_arguments_exception';

/**
 * Does near constant time byte array comparison.
 * @param ba1 The first bytearray to check.
 * @param ba2 The second bytearray to check.
 * @return If the array are equal.
 */
export function isEqual(ba1: Uint8Array, ba2: Uint8Array): boolean {
  if (ba1.length !== ba2.length) {
    return false;
  }
  let result = 0;
  for (let i = 0; i < ba1.length; i++) {
    result |= ba1[i] ^ ba2[i];
  }
  return result == 0;
}

/**
 * Returns a new array that is the result of joining the arguments.
 */
export function concat(...var_args: Uint8Array[]): Uint8Array {
  let length = 0;
  for (let i = 0; i < arguments.length; i++) {
    length += arguments[i].length;
  }
  const result = new Uint8Array(length);
  let curOffset = 0;
  for (let i = 0; i < arguments.length; i++) {
    result.set(arguments[i], curOffset);
    curOffset += arguments[i].length;
  }
  return result;
}

/**
 * Converts a non-negative integer number to a 64-bit big-endian byte array.
 * @param value The number to convert.
 * @return The number as a big-endian byte array.
 * @throws {InvalidArgumentsException}
 * @static
 */
export function fromNumber(value: number): Uint8Array {
  if (Number.isNaN(value) || value % 1 !== 0) {
    throw new InvalidArgumentsException('cannot convert non-integer value');
  }
  if (value < 0) {
    throw new InvalidArgumentsException('cannot convert negative number');
  }
  if (value > Number.MAX_SAFE_INTEGER) {
    throw new InvalidArgumentsException(
        'cannot convert number larger than ' + Number.MAX_SAFE_INTEGER);
  }
  const twoPower32 = 2 ** 32;
  let low = value % twoPower32;
  let high = value / twoPower32;
  const result = new Uint8Array(8);
  for (let i = 7; i >= 4; i--) {
    result[i] = low & 255;
    low >>>= 8;
  }
  for (let i = 3; i >= 0; i--) {
    result[i] = high & 255;
    high >>>= 8;
  }
  return result;
}

/**
 * Converts the hex string to a byte array.
 *
 * @param hex the input
 * @return the byte array output
 * @throws {!InvalidArgumentsException}
 * @static
 */
export function fromHex(hex: string): Uint8Array {
  if (hex.length % 2 != 0) {
    throw new InvalidArgumentsException(
        'Hex string length must be multiple of 2');
  }
  const arr = new Uint8Array(hex.length / 2);
  for (let i = 0; i < hex.length; i += 2) {
    arr[i / 2] = parseInt(hex.substring(i, i + 2), 16);
  }
  return arr;
}

/**
 * Converts a byte array to hex.
 *
 * @param bytes the byte array input
 * @return hex the output
 * @static
 */
export function toHex(bytes: Uint8Array): string {
  let result = '';
  for (let i = 0; i < bytes.length; i++) {
    const hexByte = bytes[i].toString(16);
    result += hexByte.length > 1 ? hexByte : '0' + hexByte;
  }
  return result;
}

/**
 * Converts the Base64 string to a byte array.
 *
 * @param encoded the base64 string
 * @param opt_webSafe True indicates we should use the alternative
 *     alphabet, which does not require escaping for use in URLs.
 * @return the byte array output
 * @static
 */
export function fromBase64(encoded: string, opt_webSafe?: boolean): Uint8Array {
  if (opt_webSafe) {
    const normalBase64 = encoded.replace(/-/g, '+').replace(/_/g, '/');
    return fromByteString(window.atob(normalBase64));
  }
  return fromByteString(window.atob(encoded));
}

/**
 * Base64 encode a byte array.
 *
 * @param bytes the byte array input
 * @param opt_webSafe True indicates we should use the alternative
 *     alphabet, which does not require escaping for use in URLs.
 * @return base64 output
 * @static
 */
export function toBase64(bytes: Uint8Array, opt_webSafe?: boolean): string {
  const encoded = window
                      .btoa(
                          /* padding */
                          toByteString(bytes))
                      .replace(/=/g, '');
  if (opt_webSafe) {
    return encoded.replace(/\+/g, '-').replace(/\//g, '_');
  }
  return encoded;
}

/**
 * Converts a byte string to a byte array. Only support ASCII and Latin-1
 * strings, does not support multi-byte characters.
 *
 * @param str the input
 * @return the byte array output
 * @static
 */
export function fromByteString(str: string): Uint8Array {
  const output = [];
  let p = 0;
  for (let i = 0; i < str.length; i++) {
    const c = str.charCodeAt(i);
    output[p++] = c;
  }
  return new Uint8Array(output);
}

/**
 * Turns a byte array into the string given by the concatenation of the
 * characters to which the numbers correspond. Each byte is corresponding to a
 * character. Does not support multi-byte characters.
 *
 * @param bytes Array of numbers representing
 *     characters.
 * @return Stringification of the array.
 */
export function toByteString(bytes: Uint8Array): string {
  let str = '';
  for (let i = 0; i < bytes.length; i += 1) {
    str += String.fromCharCode(bytes[i]);
  }
  return str;
}

/**
 * Returns the element-wise XOR of two byte arrays of the same length
 */
export function xor(x: Uint8Array, y: Uint8Array): Uint8Array {
  if (x.length !== y.length) {
    throw new InvalidArgumentsException(
        'Both byte arrays should be of the same length');
  }

  const arr = new Uint8Array(x.length);

  for (let i = 0; i < arr.length; i++) {
    arr[i] = x[i] ^ y[i];
  }

  return arr;
}
