/** @internal NOTE: Internal APIs. Subject to change. Use of these APIs in production applications is not supported. */
import { TypedArray } from './bim-format-types';

enum TypedArrayType {
    Uint8 = 0,
    Uint16 = 1,
    Uint32 = 2,
    Float32 = 3
}

const TYPED_ARRAYS = {
    [TypedArrayType.Uint8]: Uint8Array,
    [TypedArrayType.Uint16]: Uint16Array,
    [TypedArrayType.Uint32]: Uint32Array,
    [TypedArrayType.Float32]: Float32Array
} as const;

// TODO: It would probably be good to validate that we're staying within bounds.
// TDOO: Data view might be a good option
export class ArrayBufferReader {
    private static readonly _typedArrayByteAlignment: number = 4;

    constructor(
        public readonly buffer: ArrayBuffer,
        public offsetInBytes: number = 0
    ) {}

    uint8(n: number): Uint8Array {
        const result = new Uint8Array(this.buffer, this.offsetInBytes, n);
        this.offsetInBytes += n * Uint8Array.BYTES_PER_ELEMENT;
        return result;
    }

    uint16(n: number): Uint16Array {
        const result = new Uint16Array(this.buffer, this.offsetInBytes, n);
        this.offsetInBytes += n * Uint16Array.BYTES_PER_ELEMENT;
        return result;
    }

    uint32(n: number): Uint32Array {
        const result = new Uint32Array(this.buffer, this.offsetInBytes, n);
        this.offsetInBytes += n * Uint32Array.BYTES_PER_ELEMENT;
        return result;
    }

    float32(n: number): Float32Array {
        const result = new Float32Array(this.buffer, this.offsetInBytes, n);
        this.offsetInBytes += n * Float32Array.BYTES_PER_ELEMENT;
        return result;
    }

    float64(n: number): Float64Array {
        const result = new Float64Array(this.buffer, this.offsetInBytes, n);
        this.offsetInBytes += n * Float64Array.BYTES_PER_ELEMENT;
        return result;
    }

    typedArray(n: number): { array: TypedArray; sizeInBytes: number } {
        const arrayType = this.uint32(1)[0] as TypedArrayType;
        const constructor = TYPED_ARRAYS[arrayType];
        if (constructor == null) {
            throw new Error(`Unsupported array type: ${arrayType}`);
        }
        const array = new constructor(this.buffer, this.offsetInBytes, n);
        const sizeInBytes = n * constructor.BYTES_PER_ELEMENT;
        this.offsetInBytes += sizeInBytes;
        this.readPadding(ArrayBufferReader._typedArrayByteAlignment, true);
        return { array, sizeInBytes };
    }

    readPadding(alignmentInBytes: number, throwOnInvalidPadding: boolean): number {
        const padCount = this.getPadByteCount(alignmentInBytes);
        if (padCount === 0) {
            return 0;
        }
        if (throwOnInvalidPadding) {
            const bytes = this.uint8(padCount);
            for (let i = 0; i < padCount; ++i) {
                const isValid = bytes[i] === 1 << i % 8;
                if (!isValid) {
                    throw new Error('Invalid padding');
                }
            }
        }
        return padCount;
    }

    private getPadByteCount(alignmentInBytes: number): number {
        if (this.offsetInBytes % alignmentInBytes === 0) {
            return 0;
        }

        return alignmentInBytes - (this.offsetInBytes % alignmentInBytes);
    }
}
