const FLOAT = new Float64Array(1);
const FLOAT_BYTES = new Uint8Array(FLOAT.buffer, 0, 8);

FLOAT[0] = -1;
// Little endian [0, 0, 0, 0, 0, 0,  240, 191]
// Big endian    [191, 240, 0, 0, 0, 0, 0, 0]
const isBigEndian = FLOAT_BYTES[7] === 0;

/**
 * @experimental
 * @public
 *
 * A collection of functions that get or set various numeric types and bit widths from a Uint8Array.
 */
export type NumberUtils = {
  /** Is true if the current system is big endian. */
  isBigEndian: boolean;
  /**
   * Parses a signed int32 at offset. Throws a `RangeError` if value is negative.
   */
  getNonnegativeInt32LE: (source: Uint8Array, offset: number) => number;
  getInt32LE: (source: Uint8Array, offset: number) => number;
  getUint32LE: (source: Uint8Array, offset: number) => number;
  getUint32BE: (source: Uint8Array, offset: number) => number;
  getBigInt64LE: (source: Uint8Array, offset: number) => bigint;
  getFloat64LE: (source: Uint8Array, offset: number) => number;
  setInt32BE: (destination: Uint8Array, offset: number, value: number) => 4;
  setInt32LE: (destination: Uint8Array, offset: number, value: number) => 4;
  setBigInt64LE: (destination: Uint8Array, offset: number, value: bigint) => 8;
  setFloat64LE: (destination: Uint8Array, offset: number, value: number) => 8;
};

/**
 * Number parsing and serializing utilities.
 *
 * @experimental
 * @public
 */
export const NumberUtils: NumberUtils = {
  isBigEndian,

  getNonnegativeInt32LE(source: Uint8Array, offset: number): number {
    if (source[offset + 3] > 127) {
      throw new RangeError(`Size cannot be negative at offset: ${offset}`);
    }
    return (
      source[offset] |
      (source[offset + 1] << 8) |
      (source[offset + 2] << 16) |
      (source[offset + 3] << 24)
    );
  },

  /** Reads a little-endian 32-bit integer from source */
  getInt32LE(source: Uint8Array, offset: number): number {
    return (
      source[offset] |
      (source[offset + 1] << 8) |
      (source[offset + 2] << 16) |
      (source[offset + 3] << 24)
    );
  },

  /** Reads a little-endian 32-bit unsigned integer from source */
  getUint32LE(source: Uint8Array, offset: number): number {
    return (
      source[offset] +
      source[offset + 1] * 256 +
      source[offset + 2] * 65536 +
      source[offset + 3] * 16777216
    );
  },

  /** Reads a big-endian 32-bit integer from source */
  getUint32BE(source: Uint8Array, offset: number): number {
    return (
      source[offset + 3] +
      source[offset + 2] * 256 +
      source[offset + 1] * 65536 +
      source[offset] * 16777216
    );
  },

  /** Reads a little-endian 64-bit integer from source */
  getBigInt64LE(source: Uint8Array, offset: number): bigint {
    const hi = BigInt(
      source[offset + 4] +
        source[offset + 5] * 256 +
        source[offset + 6] * 65536 +
        (source[offset + 7] << 24)
    ); // Overflow

    const lo = BigInt(
      source[offset] +
        source[offset + 1] * 256 +
        source[offset + 2] * 65536 +
        source[offset + 3] * 16777216
    );

    return (hi << 32n) + lo;
  },

  /** Reads a little-endian 64-bit float from source */
  getFloat64LE: isBigEndian
    ? (source: Uint8Array, offset: number) => {
        FLOAT_BYTES[7] = source[offset];
        FLOAT_BYTES[6] = source[offset + 1];
        FLOAT_BYTES[5] = source[offset + 2];
        FLOAT_BYTES[4] = source[offset + 3];
        FLOAT_BYTES[3] = source[offset + 4];
        FLOAT_BYTES[2] = source[offset + 5];
        FLOAT_BYTES[1] = source[offset + 6];
        FLOAT_BYTES[0] = source[offset + 7];
        return FLOAT[0];
      }
    : (source: Uint8Array, offset: number) => {
        FLOAT_BYTES[0] = source[offset];
        FLOAT_BYTES[1] = source[offset + 1];
        FLOAT_BYTES[2] = source[offset + 2];
        FLOAT_BYTES[3] = source[offset + 3];
        FLOAT_BYTES[4] = source[offset + 4];
        FLOAT_BYTES[5] = source[offset + 5];
        FLOAT_BYTES[6] = source[offset + 6];
        FLOAT_BYTES[7] = source[offset + 7];
        return FLOAT[0];
      },

  /** Writes a big-endian 32-bit integer to destination, can be signed or unsigned */
  setInt32BE(destination: Uint8Array, offset: number, value: number): 4 {
    destination[offset + 3] = value;
    value >>>= 8;
    destination[offset + 2] = value;
    value >>>= 8;
    destination[offset + 1] = value;
    value >>>= 8;
    destination[offset] = value;
    return 4;
  },

  /** Writes a little-endian 32-bit integer to destination, can be signed or unsigned */
  setInt32LE(destination: Uint8Array, offset: number, value: number): 4 {
    destination[offset] = value;
    value >>>= 8;
    destination[offset + 1] = value;
    value >>>= 8;
    destination[offset + 2] = value;
    value >>>= 8;
    destination[offset + 3] = value;
    return 4;
  },

  /** Write a little-endian 64-bit integer to source */
  setBigInt64LE(destination: Uint8Array, offset: number, value: bigint): 8 {
    const mask32bits = 0xffff_ffffn;

    /** lower 32 bits */
    let lo = Number(value & mask32bits);
    destination[offset] = lo;
    lo >>= 8;
    destination[offset + 1] = lo;
    lo >>= 8;
    destination[offset + 2] = lo;
    lo >>= 8;
    destination[offset + 3] = lo;

    let hi = Number((value >> 32n) & mask32bits);
    destination[offset + 4] = hi;
    hi >>= 8;
    destination[offset + 5] = hi;
    hi >>= 8;
    destination[offset + 6] = hi;
    hi >>= 8;
    destination[offset + 7] = hi;

    return 8;
  },

  /** Writes a little-endian 64-bit float to destination */
  setFloat64LE: isBigEndian
    ? (destination: Uint8Array, offset: number, value: number) => {
        FLOAT[0] = value;
        destination[offset] = FLOAT_BYTES[7];
        destination[offset + 1] = FLOAT_BYTES[6];
        destination[offset + 2] = FLOAT_BYTES[5];
        destination[offset + 3] = FLOAT_BYTES[4];
        destination[offset + 4] = FLOAT_BYTES[3];
        destination[offset + 5] = FLOAT_BYTES[2];
        destination[offset + 6] = FLOAT_BYTES[1];
        destination[offset + 7] = FLOAT_BYTES[0];
        return 8;
      }
    : (destination: Uint8Array, offset: number, value: number) => {
        FLOAT[0] = value;
        destination[offset] = FLOAT_BYTES[0];
        destination[offset + 1] = FLOAT_BYTES[1];
        destination[offset + 2] = FLOAT_BYTES[2];
        destination[offset + 3] = FLOAT_BYTES[3];
        destination[offset + 4] = FLOAT_BYTES[4];
        destination[offset + 5] = FLOAT_BYTES[5];
        destination[offset + 6] = FLOAT_BYTES[6];
        destination[offset + 7] = FLOAT_BYTES[7];
        return 8;
      }
};
