class ERR_INVALID_ARG_TYPE extends TypeError {
    constructor(name, type, value) {
        super(`${name} variable is not of type ${type} (value: '${value}')`);
    }

    code = 'ERR_INVALID_ARG_TYPE';
}

function validateBoolean(value, name) {
    if (typeof value !== 'boolean') throw new ERR_INVALID_ARG_TYPE(name, 'boolean', value);
}

function validateObject(value, name) {
    if (value === null || Array.isArray(value) || typeof value !== 'object') {
        throw new ERR_INVALID_ARG_TYPE(name, 'Object', value);
    }
}

const randomFillSync = window.crypto.getRandomValues.bind(window.crypto);

// Implements an RFC 4122 version 4 random UUID.
// To improve performance, random data is generated in batches
// large enough to cover kBatchSize UUID's at a time. The uuidData
// and uuid buffers are reused. Each call to randomUUID() consumes
// 16 bytes from the buffer.

const kHexDigits = [48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 97, 98, 99, 100, 101, 102];
const kBatchSize = 128;
let uuidData;
let uuidNotBuffered;
let uuid;
let uuidBatch = 0;

function getBufferedUUID() {
    if (uuidData === undefined) {
        uuidData = new Uint8Array(16 * kBatchSize);
    }

    if (uuidBatch === 0) randomFillSync(uuidData);
    uuidBatch = (uuidBatch + 1) % kBatchSize;
    return uuidData.slice(uuidBatch * 16, uuidBatch * 16 + 16);
}

function randomUUID(options) {
    if (options !== undefined) validateObject(options, 'options');
    const { disableEntropyCache = false } = { ...options };

    validateBoolean(disableEntropyCache, 'options.disableEntropyCache');

    if (uuid === undefined) {
        uuid = new Uint8Array(36);
        uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-'.charCodeAt(0);
        uuid[14] = 52; // '4', identifies the UUID version
    }

    let uuidBuf;
    if (!disableEntropyCache) {
        uuidBuf = getBufferedUUID();
    } else {
        uuidBuf = uuidNotBuffered;
        if (uuidBuf === undefined) uuidBuf = uuidNotBuffered = new Uint8Array(16);
        randomFillSync(uuidBuf);
    }

    // Variant byte: 10xxxxxx (variant 1)
    uuidBuf[8] = (uuidBuf[8] & 0x3f) | 0x80;

    // This function is structured the way it is for performance.
    // The uuid buffer stores the serialization of the random
    // bytes from uuidData.
    // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
    let n = 0;
    uuid[0] = kHexDigits[uuidBuf[n] >> 4];
    uuid[1] = kHexDigits[uuidBuf[n++] & 0xf];
    uuid[2] = kHexDigits[uuidBuf[n] >> 4];
    uuid[3] = kHexDigits[uuidBuf[n++] & 0xf];
    uuid[4] = kHexDigits[uuidBuf[n] >> 4];
    uuid[5] = kHexDigits[uuidBuf[n++] & 0xf];
    uuid[6] = kHexDigits[uuidBuf[n] >> 4];
    uuid[7] = kHexDigits[uuidBuf[n++] & 0xf];
    // -
    uuid[9] = kHexDigits[uuidBuf[n] >> 4];
    uuid[10] = kHexDigits[uuidBuf[n++] & 0xf];
    uuid[11] = kHexDigits[uuidBuf[n] >> 4];
    uuid[12] = kHexDigits[uuidBuf[n++] & 0xf];
    // -
    // 4, uuid[14] is set already...
    uuid[15] = kHexDigits[uuidBuf[n++] & 0xf];
    uuid[16] = kHexDigits[uuidBuf[n] >> 4];
    uuid[17] = kHexDigits[uuidBuf[n++] & 0xf];
    // -
    uuid[19] = kHexDigits[uuidBuf[n] >> 4];
    uuid[20] = kHexDigits[uuidBuf[n++] & 0xf];
    uuid[21] = kHexDigits[uuidBuf[n] >> 4];
    uuid[22] = kHexDigits[uuidBuf[n++] & 0xf];
    // -
    uuid[24] = kHexDigits[uuidBuf[n] >> 4];
    uuid[25] = kHexDigits[uuidBuf[n++] & 0xf];
    uuid[26] = kHexDigits[uuidBuf[n] >> 4];
    uuid[27] = kHexDigits[uuidBuf[n++] & 0xf];
    uuid[28] = kHexDigits[uuidBuf[n] >> 4];
    uuid[29] = kHexDigits[uuidBuf[n++] & 0xf];
    uuid[30] = kHexDigits[uuidBuf[n] >> 4];
    uuid[31] = kHexDigits[uuidBuf[n++] & 0xf];
    uuid[32] = kHexDigits[uuidBuf[n] >> 4];
    uuid[33] = kHexDigits[uuidBuf[n++] & 0xf];
    uuid[34] = kHexDigits[uuidBuf[n] >> 4];
    uuid[35] = kHexDigits[uuidBuf[n] & 0xf];

    return String.fromCharCode.apply(null, uuid);
}

export function initialize() {
    if (typeof window.crypto.randomUUID === 'undefined') {
        window.crypto.randomUUID = randomUUID;
    }
}

export default {
    initialize,
};
