import { ReadonlyUint8Array } from './readonly-uint8array';

/**
 * Concatenates an array of `Uint8Array`s into a single `Uint8Array`.
 * Reuses the original byte array when applicable.
 *
 * @param byteArrays - The array of byte arrays to concatenate.
 *
 * @example
 * ```ts
 * const bytes1 = new Uint8Array([0x01, 0x02]);
 * const bytes2 = new Uint8Array([]);
 * const bytes3 = new Uint8Array([0x03, 0x04]);
 * const bytes = mergeBytes([bytes1, bytes2, bytes3]);
 * //    ^ [0x01, 0x02, 0x03, 0x04]
 * ```
 */
export const mergeBytes = (byteArrays: Uint8Array[]): Uint8Array => {
    const nonEmptyByteArrays = byteArrays.filter(arr => arr.length);
    if (nonEmptyByteArrays.length === 0) {
        return byteArrays.length ? byteArrays[0] : new Uint8Array();
    }

    if (nonEmptyByteArrays.length === 1) {
        return nonEmptyByteArrays[0];
    }

    const totalLength = nonEmptyByteArrays.reduce((total, arr) => total + arr.length, 0);
    const result = new Uint8Array(totalLength);
    let offset = 0;
    nonEmptyByteArrays.forEach(arr => {
        result.set(arr, offset);
        offset += arr.length;
    });
    return result;
};

/**
 * Pads a `Uint8Array` with zeroes to the specified length.
 * If the array is longer than the specified length, it is returned as-is.
 *
 * @param bytes - The byte array to pad.
 * @param length - The desired length of the byte array.
 *
 * @example
 * Adds zeroes to the end of the byte array to reach the desired length.
 * ```ts
 * const bytes = new Uint8Array([0x01, 0x02]);
 * const paddedBytes = padBytes(bytes, 4);
 * //    ^ [0x01, 0x02, 0x00, 0x00]
 * ```
 *
 * @example
 * Returns the original byte array if it is already at the desired length.
 * ```ts
 * const bytes = new Uint8Array([0x01, 0x02]);
 * const paddedBytes = padBytes(bytes, 2);
 * // bytes === paddedBytes
 * ```
 */
export function padBytes(bytes: Uint8Array, length: number): Uint8Array;
export function padBytes(bytes: ReadonlyUint8Array, length: number): ReadonlyUint8Array;
export function padBytes(bytes: ReadonlyUint8Array, length: number): ReadonlyUint8Array {
    if (bytes.length >= length) return bytes;
    const paddedBytes = new Uint8Array(length).fill(0);
    paddedBytes.set(bytes);
    return paddedBytes;
}

/**
 * Fixes a `Uint8Array` to the specified length.
 * If the array is longer than the specified length, it is truncated.
 * If the array is shorter than the specified length, it is padded with zeroes.
 *
 * @param bytes - The byte array to truncate or pad.
 * @param length - The desired length of the byte array.
 *
 * @example
 * Truncates the byte array to the desired length.
 * ```ts
 * const bytes = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
 * const fixedBytes = fixBytes(bytes, 2);
 * //    ^ [0x01, 0x02]
 * ```
 *
 * @example
 * Adds zeroes to the end of the byte array to reach the desired length.
 * ```ts
 * const bytes = new Uint8Array([0x01, 0x02]);
 * const fixedBytes = fixBytes(bytes, 4);
 * //    ^ [0x01, 0x02, 0x00, 0x00]
 * ```
 *
 * @example
 * Returns the original byte array if it is already at the desired length.
 * ```ts
 * const bytes = new Uint8Array([0x01, 0x02]);
 * const fixedBytes = fixBytes(bytes, 2);
 * // bytes === fixedBytes
 * ```
 */
export const fixBytes = (bytes: ReadonlyUint8Array | Uint8Array, length: number): ReadonlyUint8Array | Uint8Array =>
    padBytes(bytes.length <= length ? bytes : bytes.slice(0, length), length);

/**
 * Returns true if and only if the provided `data` byte array contains
 * the provided `bytes` byte array at the specified `offset`.
 *
 * @param data - The byte array in which to search for `bytes`.
 * @param bytes - The byte sequence to search for.
 * @param offset - The position in `data` where the search begins.
 *
 * @example
 * ```ts
 * const data = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
 * const bytes = new Uint8Array([0x02, 0x03]);
 * containsBytes(data, bytes, 1); // true
 * containsBytes(data, bytes, 2); // false
 * ```
 */
export function containsBytes(
    data: ReadonlyUint8Array | Uint8Array,
    bytes: ReadonlyUint8Array | Uint8Array,
    offset: number,
): boolean {
    const slice =
        (offset === 0 || offset <= -data.byteLength) && data.length === bytes.length
            ? data
            : data.slice(offset, offset + bytes.length);
    return bytesEqual(slice, bytes);
}

/**
 * Returns true if and only if the provided `bytes1` and `bytes2` byte arrays are equal.
 *
 * @param bytes1 - The first byte array to compare.
 * @param bytes2 - The second byte array to compare.
 *
 * @example
 * ```ts
 * const bytes1 = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
 * const bytes2 = new Uint8Array([0x01, 0x02, 0x03, 0x04]);
 * bytesEqual(bytes1, bytes2); // true
 * ```
 */
export function bytesEqual(bytes1: ReadonlyUint8Array | Uint8Array, bytes2: ReadonlyUint8Array | Uint8Array): boolean {
    return bytes1.length === bytes2.length && bytes1.every((value, index) => value === bytes2[index]);
}
