import { assert } from './assert';
import { add0x, assertIsHexString, remove0x } from './hex';
// '0'.charCodeAt(0) === 48
const HEX_MINIMUM_NUMBER_CHARACTER = 48;
// '9'.charCodeAt(0) === 57
const HEX_MAXIMUM_NUMBER_CHARACTER = 58;
const HEX_CHARACTER_OFFSET = 87;
/**
 * Memoized function that returns an array to be used as a lookup table for
 * converting bytes to hexadecimal values.
 *
 * The array is created lazily and then cached for future use. The benefit of
 * this approach is that the performance of converting bytes to hex is much
 * better than if we were to call `toString(16)` on each byte.
 *
 * The downside is that the array is created once and then never garbage
 * collected. This is not a problem in practice because the array is only 256
 * elements long.
 *
 * @returns A function that returns the lookup table.
 */ function getPrecomputedHexValuesBuilder() {
    // To avoid issues with tree shaking, we need to use a function to return the
    // array. This is because the array is only used in the `bytesToHex` function
    // and if we were to use a global variable, the array might be removed by the
    // tree shaker.
    const lookupTable = [];
    return ()=>{
        if (lookupTable.length === 0) {
            for(let i = 0; i < 256; i++){
                lookupTable.push(i.toString(16).padStart(2, '0'));
            }
        }
        return lookupTable;
    };
}
/**
 * Function implementation of the {@link getPrecomputedHexValuesBuilder}
 * function.
 */ const getPrecomputedHexValues = getPrecomputedHexValuesBuilder();
/**
 * Check if a value is a `Uint8Array`.
 *
 * @param value - The value to check.
 * @returns Whether the value is a `Uint8Array`.
 */ export function isBytes(value) {
    return value instanceof Uint8Array;
}
/**
 * Assert that a value is a `Uint8Array`.
 *
 * @param value - The value to check.
 * @throws If the value is not a `Uint8Array`.
 */ export function assertIsBytes(value) {
    assert(isBytes(value), 'Value must be a Uint8Array.');
}
/**
 * Convert a `Uint8Array` to a hexadecimal string.
 *
 * @param bytes - The bytes to convert to a hexadecimal string.
 * @returns The hexadecimal string.
 */ export function bytesToHex(bytes) {
    assertIsBytes(bytes);
    if (bytes.length === 0) {
        return '0x';
    }
    const lookupTable = getPrecomputedHexValues();
    const hexadecimal = new Array(bytes.length);
    for(let i = 0; i < bytes.length; i++){
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        hexadecimal[i] = lookupTable[bytes[i]];
    }
    return add0x(hexadecimal.join(''));
}
/**
 * Convert a `Uint8Array` to a `bigint`.
 *
 * To convert a `Uint8Array` to a `number` instead, use {@link bytesToNumber}.
 * To convert a two's complement encoded `Uint8Array` to a `bigint`, use
 * {@link bytesToSignedBigInt}.
 *
 * @param bytes - The bytes to convert to a `bigint`.
 * @returns The `bigint`.
 */ export function bytesToBigInt(bytes) {
    assertIsBytes(bytes);
    const hexadecimal = bytesToHex(bytes);
    return BigInt(hexadecimal);
}
/**
 * Convert a `Uint8Array` to a signed `bigint`. This assumes that the bytes are
 * encoded in two's complement.
 *
 * To convert a `Uint8Array` to an unsigned `bigint` instead, use
 * {@link bytesToBigInt}.
 *
 * @see https://en.wikipedia.org/wiki/Two%27s_complement
 * @param bytes - The bytes to convert to a signed `bigint`.
 * @returns The signed `bigint`.
 */ export function bytesToSignedBigInt(bytes) {
    assertIsBytes(bytes);
    let value = BigInt(0);
    for (const byte of bytes){
        // eslint-disable-next-line no-bitwise
        value = (value << BigInt(8)) + BigInt(byte);
    }
    return BigInt.asIntN(bytes.length * 8, value);
}
/**
 * Convert a `Uint8Array` to a `number`.
 *
 * To convert a `Uint8Array` to a `bigint` instead, use {@link bytesToBigInt}.
 *
 * @param bytes - The bytes to convert to a number.
 * @returns The number.
 * @throws If the resulting number is not a safe integer.
 */ export function bytesToNumber(bytes) {
    assertIsBytes(bytes);
    const bigint = bytesToBigInt(bytes);
    assert(bigint <= BigInt(Number.MAX_SAFE_INTEGER), 'Number is not a safe integer. Use `bytesToBigInt` instead.');
    return Number(bigint);
}
/**
 * Convert a UTF-8 encoded `Uint8Array` to a `string`.
 *
 * @param bytes - The bytes to convert to a string.
 * @returns The string.
 */ export function bytesToString(bytes) {
    assertIsBytes(bytes);
    return new TextDecoder().decode(bytes);
}
/**
 * Convert a hexadecimal string to a `Uint8Array`. The string can optionally be
 * prefixed with `0x`. It accepts even and odd length strings.
 *
 * If the value is "0x", an empty `Uint8Array` is returned.
 *
 * @param value - The hexadecimal string to convert to bytes.
 * @returns The bytes as `Uint8Array`.
 */ export function hexToBytes(value) {
    // "0x" is often used as empty byte array.
    if (value?.toLowerCase?.() === '0x') {
        return new Uint8Array();
    }
    assertIsHexString(value);
    // Remove the `0x` prefix if it exists, and pad the string to have an even
    // number of characters.
    const strippedValue = remove0x(value).toLowerCase();
    const normalizedValue = strippedValue.length % 2 === 0 ? strippedValue : `0${strippedValue}`;
    const bytes = new Uint8Array(normalizedValue.length / 2);
    for(let i = 0; i < bytes.length; i++){
        // While this is not the prettiest way to convert a hexadecimal string to a
        // `Uint8Array`, it is a lot faster than using `parseInt` to convert each
        // character.
        const c1 = normalizedValue.charCodeAt(i * 2);
        const c2 = normalizedValue.charCodeAt(i * 2 + 1);
        const n1 = c1 - (c1 < HEX_MAXIMUM_NUMBER_CHARACTER ? HEX_MINIMUM_NUMBER_CHARACTER : HEX_CHARACTER_OFFSET);
        const n2 = c2 - (c2 < HEX_MAXIMUM_NUMBER_CHARACTER ? HEX_MINIMUM_NUMBER_CHARACTER : HEX_CHARACTER_OFFSET);
        bytes[i] = n1 * 16 + n2;
    }
    return bytes;
}
/**
 * Convert a `bigint` to a `Uint8Array`.
 *
 * This assumes that the `bigint` is an unsigned integer. To convert a signed
 * `bigint` instead, use {@link signedBigIntToBytes}.
 *
 * @param value - The bigint to convert to bytes.
 * @returns The bytes as `Uint8Array`.
 */ export function bigIntToBytes(value) {
    assert(typeof value === 'bigint', 'Value must be a bigint.');
    assert(value >= BigInt(0), 'Value must be a non-negative bigint.');
    const hexadecimal = value.toString(16);
    return hexToBytes(hexadecimal);
}
/**
 * Check if a `bigint` fits in a certain number of bytes.
 *
 * @param value - The `bigint` to check.
 * @param bytes - The number of bytes.
 * @returns Whether the `bigint` fits in the number of bytes.
 */ function bigIntFits(value, bytes) {
    assert(bytes > 0);
    /* eslint-disable no-bitwise */ const mask = value >> BigInt(31);
    return !((~value & mask) + (value & ~mask) >> BigInt(bytes * 8 + ~0));
/* eslint-enable no-bitwise */ }
/**
 * Convert a signed `bigint` to a `Uint8Array`. This uses two's complement
 * encoding to represent negative numbers.
 *
 * To convert an unsigned `bigint` to a `Uint8Array` instead, use
 * {@link bigIntToBytes}.
 *
 * @see https://en.wikipedia.org/wiki/Two%27s_complement
 * @param value - The number to convert to bytes.
 * @param byteLength - The length of the resulting `Uint8Array`. If the number
 * is larger than the maximum value that can be represented by the given length,
 * an error is thrown.
 * @returns The bytes as `Uint8Array`.
 */ export function signedBigIntToBytes(value, byteLength) {
    assert(typeof value === 'bigint', 'Value must be a bigint.');
    assert(typeof byteLength === 'number', 'Byte length must be a number.');
    assert(byteLength > 0, 'Byte length must be greater than 0.');
    assert(bigIntFits(value, byteLength), 'Byte length is too small to represent the given value.');
    // ESLint doesn't like mutating function parameters, so to avoid having to
    // disable the rule, we create a new variable.
    let numberValue = value;
    const bytes = new Uint8Array(byteLength);
    for(let i = 0; i < bytes.length; i++){
        bytes[i] = Number(BigInt.asUintN(8, numberValue));
        // eslint-disable-next-line no-bitwise
        numberValue >>= BigInt(8);
    }
    return bytes.reverse();
}
/**
 * Convert a `number` to a `Uint8Array`.
 *
 * @param value - The number to convert to bytes.
 * @returns The bytes as `Uint8Array`.
 * @throws If the number is not a safe integer.
 */ export function numberToBytes(value) {
    assert(typeof value === 'number', 'Value must be a number.');
    assert(value >= 0, 'Value must be a non-negative number.');
    assert(Number.isSafeInteger(value), 'Value is not a safe integer. Use `bigIntToBytes` instead.');
    const hexadecimal = value.toString(16);
    return hexToBytes(hexadecimal);
}
/**
 * Convert a `string` to a UTF-8 encoded `Uint8Array`.
 *
 * @param value - The string to convert to bytes.
 * @returns The bytes as `Uint8Array`.
 */ export function stringToBytes(value) {
    assert(typeof value === 'string', 'Value must be a string.');
    return new TextEncoder().encode(value);
}
/**
 * Convert a byte-like value to a `Uint8Array`. The value can be a `Uint8Array`,
 * a `bigint`, a `number`, or a `string`.
 *
 * This will attempt to guess the type of the value based on its type and
 * contents. For more control over the conversion, use the more specific
 * conversion functions, such as {@link hexToBytes} or {@link stringToBytes}.
 *
 * If the value is a `string`, and it is prefixed with `0x`, it will be
 * interpreted as a hexadecimal string. Otherwise, it will be interpreted as a
 * UTF-8 string. To convert a hexadecimal string to bytes without interpreting
 * it as a UTF-8 string, use {@link hexToBytes} instead.
 *
 * If the value is a `bigint`, it is assumed to be unsigned. To convert a signed
 * `bigint` to bytes, use {@link signedBigIntToBytes} instead.
 *
 * If the value is a `Uint8Array`, it will be returned as-is.
 *
 * @param value - The value to convert to bytes.
 * @returns The bytes as `Uint8Array`.
 */ export function valueToBytes(value) {
    if (typeof value === 'bigint') {
        return bigIntToBytes(value);
    }
    if (typeof value === 'number') {
        return numberToBytes(value);
    }
    if (typeof value === 'string') {
        if (value.startsWith('0x')) {
            return hexToBytes(value);
        }
        return stringToBytes(value);
    }
    if (isBytes(value)) {
        return value;
    }
    throw new TypeError(`Unsupported value type: "${typeof value}".`);
}
/**
 * Concatenate multiple byte-like values into a single `Uint8Array`. The values
 * can be `Uint8Array`, `bigint`, `number`, or `string`. This uses
 * {@link valueToBytes} under the hood to convert each value to bytes. Refer to
 * the documentation of that function for more information.
 *
 * @param values - The values to concatenate.
 * @returns The concatenated bytes as `Uint8Array`.
 */ export function concatBytes(values) {
    const normalizedValues = new Array(values.length);
    let byteLength = 0;
    for(let i = 0; i < values.length; i++){
        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
        const value = valueToBytes(values[i]);
        normalizedValues[i] = value;
        byteLength += value.length;
    }
    const bytes = new Uint8Array(byteLength);
    for(let i = 0, offset = 0; i < normalizedValues.length; i++){
        // While we could simply spread the values into an array and use
        // `Uint8Array.from`, that is a lot slower than using `Uint8Array.set`.
        bytes.set(normalizedValues[i], offset);
        offset += normalizedValues[i].length;
    }
    return bytes;
}
/**
 * Create a {@link DataView} from a {@link Uint8Array}. This is a convenience
 * function that avoids having to create a {@link DataView} manually, which
 * requires passing the `byteOffset` and `byteLength` parameters every time.
 *
 * Not passing the `byteOffset` and `byteLength` parameters can result in
 * unexpected behavior when the {@link Uint8Array} is a view of a larger
 * {@link ArrayBuffer}, e.g., when using {@link Uint8Array.subarray}.
 *
 * This function also supports Node.js {@link Buffer}s.
 *
 * @example
 * ```typescript
 * const bytes = new Uint8Array([1, 2, 3]);
 *
 * // This is equivalent to:
 * // const dataView = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
 * const dataView = createDataView(bytes);
 * ```
 * @param bytes - The bytes to create the {@link DataView} from.
 * @returns The {@link DataView}.
 */ export function createDataView(bytes) {
    // To maintain compatibility with Node.js, we need to check if the bytes are
    // a Buffer. If so, we need to slice the buffer to get the underlying
    // ArrayBuffer.
    // eslint-disable-next-line no-restricted-globals
    if (typeof Buffer !== 'undefined' && bytes instanceof Buffer) {
        const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
        return new DataView(buffer);
    }
    return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
}

//# sourceMappingURL=bytes.js.map