const atob = globalThis.atob;
const btoa = globalThis.btoa;

/**
 * Basic utilities for the RealtimeAPI
 * @class
 */
export class RealtimeUtils {
    /**
     * Converts Float32Array of amplitude data to ArrayBuffer in Int16Array format
     * @param {Float32Array} float32Array
     * @returns {ArrayBuffer}
     */
    static floatTo16BitPCM(float32Array) {
        const buffer = new ArrayBuffer(float32Array.length * 2);
        const view = new DataView(buffer);
        let offset = 0;
        for (let i = 0; i < float32Array.length; i++, offset += 2) {
            let s = Math.max(-1, Math.min(1, float32Array[i]));
            view.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7fff, true);
        }
        return buffer;
    }

    /**
     * Converts a base64 string to an ArrayBuffer
     * @param {string} base64
     * @returns {ArrayBuffer}
     */
    static base64ToArrayBuffer(base64) {
        const binaryString = atob(base64);
        const len = binaryString.length;
        const bytes = new Uint8Array(len);
        for (let i = 0; i < len; i++) {
            bytes[i] = binaryString.charCodeAt(i);
        }
        return bytes.buffer;
    }

    /**
     * Converts an ArrayBuffer, Int16Array or Float32Array to a base64 string
     * @param {ArrayBuffer|Int16Array|Float32Array} arrayBuffer
     * @returns {string}
     */
    static arrayBufferToBase64(arrayBuffer) {
        if (arrayBuffer instanceof Float32Array) {
            arrayBuffer = this.floatTo16BitPCM(arrayBuffer);
        } else if (arrayBuffer instanceof Int16Array) {
            arrayBuffer = arrayBuffer.buffer;
        }
        let binary = '';
        let bytes = new Uint8Array(arrayBuffer);
        const chunkSize = 0x8000; // 32KB chunk size
        for (let i = 0; i < bytes.length; i += chunkSize) {
            let chunk = bytes.subarray(i, i + chunkSize);
            binary += String.fromCharCode.apply(null, chunk);
        }
        return btoa(binary);
    }

    /**
     * Merge two Int16Arrays from Int16Arrays or ArrayBuffers
     * @param {ArrayBuffer|Int16Array} left
     * @param {ArrayBuffer|Int16Array} right
     * @returns {Int16Array}
     */
    static mergeInt16Arrays(left, right) {
        if (left instanceof ArrayBuffer) {
            left = new Int16Array(left);
        }
        if (right instanceof ArrayBuffer) {
            right = new Int16Array(right);
        }
        if (!(left instanceof Int16Array) || !(right instanceof Int16Array)) {
            throw new Error(`Both items must be Int16Array`);
        }
        const newValues = new Int16Array(left.length + right.length);
        for (let i = 0; i < left.length; i++) {
            newValues[i] = left[i];
        }
        for (let j = 0; j < right.length; j++) {
            newValues[left.length + j] = right[j];
        }
        return newValues;
    }

    /**
     * Generates an id to send with events and messages
     * @param {string} prefix
     * @param {number} [length]
     * @returns {string}
     */
    static generateId(prefix, length = 21) {
        // base58; non-repeating chars
        const chars = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
        const str = Array(length - prefix.length)
            .fill(0)
            .map((_) => chars[Math.floor(Math.random() * chars.length)])
            .join('');
        return `${prefix}${str}`;
    }
}
