'use strict';

var cryptoKey = false; // 'aabbccddeeff00112233445566778899';
const IV_LENGTH = 16; // AES-CBC requires a 16-byte IV

var lyraCodecModule = false;

var EncryptionFailedMessage = null;
async function encodeFunction(encodedFrame, controller) {
	if (cryptoKey){
		try {
			const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
			encodedFrame.data = await encryptAES(encodedFrame.data, cryptoKey, iv);
			if (EncryptionFailedMessage){
				console.error("👍 e2e encryption is now working! whew.");
				DecryptionFailedMessage = false;
			}
		} catch(e){
			if (!EncryptionFailedMessage){
				EncryptionFailedMessage = true;
				console.error("😢 e2e encryption failed");  // didn't encode. we'll just send it anyways. Insecure or user friendly? :|
			}
		}
	}
	controller.enqueue(encodedFrame);
}

var DecryptionFailedMessage = null;
async function decodeFunction(encodedFrame, controller) {
	if (cryptoKey){
		try {
			const iv = new Uint8Array(encodedFrame.data.slice(0, IV_LENGTH));
			const encryptedData = new Uint8Array(encodedFrame.data.slice(IV_LENGTH));
			encodedFrame.data = await decryptAES(encryptedData, cryptoKey, iv);
			if (DecryptionFailedMessage){
				console.error("👍 Decryption is now working");
				DecryptionFailedMessage = false;
			}
		} catch(e){
			if (!DecryptionFailedMessage){
				DecryptionFailedMessage = true;
				console.error("😢 Decryption failed. Will try without E2EE."); // lets try playing it, just in case it wasn't encoded. Insecure or user friendly? :|
			}
		}
	}
	controller.enqueue(encodedFrame); 
}

async function encryptAES(data, key, iv) {
	const encodedKey = new TextEncoder().encode(key);
	const importedKey = await crypto.subtle.importKey(
		'raw',
		encodedKey,
		'AES-CBC',
		false,
		['encrypt']
	);

	const encryptedData = await crypto.subtle.encrypt(
		{ name: 'AES-CBC', iv: iv },
		importedKey,
		data
	);

	const encryptedBytes = new Uint8Array(encryptedData);
	return new Uint8Array([...iv, ...encryptedBytes]).buffer;
}

async function decryptAES(encryptedData, key, iv) {
	const encodedKey = new TextEncoder().encode(key);
	const importedKey = await crypto.subtle.importKey(
		'raw',
		encodedKey,
		'AES-CBC',
		false,
		['decrypt']
	);

	return await crypto.subtle.decrypt(
		{ name: 'AES-CBC', iv: iv },
		importedKey,
		encryptedData
	);
}

function lyraEncodeFunction(encodedFrame, controller) { // Snippet is Apache 2.0 licenced. Source: https://github.com/Flash-Meeting/lyra-webrtc
	const inputDataArray = new Uint8Array(encodedFrame.data);

	const inputBufferPtr = lyraCodecModule._malloc(encodedFrame.data.byteLength);
	const encodedBufferPtr = lyraCodecModule._malloc(1024);

	lyraCodecModule.HEAPU8.set(inputDataArray, inputBufferPtr);
	const length = lyraCodecModule.encode(inputBufferPtr, inputDataArray.length, 16000, encodedBufferPtr);

	const newData = new ArrayBuffer(length);
	if (length > 0){
		const newDataArray = new Uint8Array(newData);
		newDataArray.set(lyraCodecModule.HEAPU8.subarray(encodedBufferPtr, encodedBufferPtr + length));
	}
	lyraCodecModule._free(inputBufferPtr);
	lyraCodecModule._free(encodedBufferPtr);
	encodedFrame.data = newData;
	controller.enqueue(encodedFrame);
}
function lyraDecodeFunction(encodedFrame, controller) { // Apache 2.0 licenced. Source: https://github.com/Flash-Meeting/lyra-webrtc
	const newData = new ArrayBuffer(16000 * 0.02 * 2);
	if (encodedFrame.data.byteLength > 0) {
		const inputDataArray = new Uint8Array(encodedFrame.data);
		const inputBufferPtr = lyraCodecModule._malloc(encodedFrame.data.byteLength);
		const outputBufferPtr = lyraCodecModule._malloc(2048);
		lyraCodecModule.HEAPU8.set(inputDataArray, inputBufferPtr);
		const length = lyraCodecModule.decode(inputBufferPtr,
			inputDataArray.length, 16000,
			outputBufferPtr);
		const newDataArray = new Uint8Array(newData);
		newDataArray.set(lyraCodecModule.HEAPU8.subarray(outputBufferPtr, outputBufferPtr + length));
		lyraCodecModule._free(inputBufferPtr);
		lyraCodecModule._free(outputBufferPtr);
	}
	encodedFrame.data = newData;
	controller.enqueue(encodedFrame);
}

function handleTransform(operation, readable, writable) {
	if (operation === 'encode') {
		const transformStream = new TransformStream({
			transform: encodeFunction,
		});
		readable.pipeThrough(transformStream).pipeTo(writable);
	} else if (operation === 'decode') {
		const transformStream = new TransformStream({
			transform: decodeFunction,
		});
		readable.pipeThrough(transformStream).pipeTo(writable);
		
	} else if (operation === 'lyradecode') {
		const transformStream = new TransformStream({
			transform: lyraDecodeFunction,
		});
		readable.pipeThrough(transformStream).pipeTo(writable);	
	} else if (operation === 'lyraencode') {
		const transformStream = new TransformStream({
			transform: lyraEncodeFunction,
		});
		readable.pipeThrough(transformStream).pipeTo(writable);	
	} else if (operation === 'redencode') {
        const transformStream = new TransformStream({
            transform: (chunk, controller) => {
				controller.enqueue(chunk);
				/* const clonedChunk = new EncodedAudioChunk({
				  type: chunk.type,
				  timestamp: chunk.timestamp,
				  // .. i need a way to clone the binary data here
				  data: cloneChunkData(chunk)
				});
				controller.enqueue(clonedChunk); */
            },
        });
        readable.pipeThrough(transformStream).pipeTo(writable);
    } else if (operation === 'reddecode') {
        const receivedChunks = new Set();
        const transformStream = new TransformStream({
            transform: (chunk, controller) => {
                const chunkId = chunk.timestamp;
                if (receivedChunks.has(chunkId)) {
                    // If it's a duplicate, ignore it
                    return;
                }
                receivedChunks.add(chunkId);
                controller.enqueue(chunk);
            },
        });
        readable.pipeThrough(transformStream).pipeTo(writable);
		
    } else if (operation === 'pass') {
        const transformStream = new TransformStream({
            transform: (chunk, controller) => {
                controller.enqueue(chunk);
            },
        });
        readable.pipeThrough(transformStream).pipeTo(writable);
    } 
}

if (self.RTCTransformEvent) {
	self.onrtctransform = (event) => {
		const transformer = event.transformer;
		handleTransform(transformer.options.operation, transformer.readable, transformer.writable);
	};
}
onmessage = async (event) => {
	if (event.data.cryptoKey) {
		cryptoKey = event.data.cryptoKey;
		if (cryptoKey){
			self.postMessage('crypto key Loaded. e2ee will be used where possible');
		} else {
			self.postMessage('crypto key Un-loaded. ?');
		}
	} else if (event.data.cryptoPhrase) {
		try {
		const encoder = new TextEncoder();
			const data = encoder.encode(event.data.cryptoPhrase);
			const hashBuffer = await crypto.subtle.digest('SHA-256', data);
			const keyBytes = new Uint8Array(hashBuffer, 0, IV_LENGTH);
			cryptoKey = Array.from(keyBytes).map(byte => byte.toString(16).padStart(2, '0')).join('');
		} catch(e){
			console.error(e);
			cryptoKey = false;
		}
		if (cryptoKey){
			self.postMessage('crypto key Generated from a password. e2ee will be used where possible');
		} else {
			self.postMessage('WARNING: crypto key could Not be generated');
		}
	} else if (event.data.lyraCodecModule) {
		lyraCodecModule = event.data.lyraCodecModule;
	}
	if (event.data.operation) {
		return handleTransform(event.data.operation, event.data.readable, event.data.writable);
	}
};

self.postMessage('insertableStreamWorkerLoaded');
