/* This file is under the regular MIT license. 

Copyright 2024 Charles Lohr (cnlohr)

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

*/

const expectedProductName = "CNLohr lolra ch32v203 goertzel test";
const filter = { vendorId : 0x1209, productId : 0xd035 };
let dev = null;
let loopAbort = false;

const IQHistoryLen = 4096;
var IQHistoryArray = new Uint32Array(IQHistoryLen);
var MPHistoryArray = new Float32Array(IQHistoryLen*2);
var IQHistoryHead = 0|0;
var lastIntensity = 1.0;
var lastNumQ = 0;
var lastTotalTime = 1;
var lastTimeUsed = 1;
var didSetupGeneralData = false;
var graphIsClicked = false;

function graphClick( e )
{
	if( e.type == "mousedown" ) graphIsClicked = true;
	if( e.type == "mouseup" ) graphIsClicked = false;
	return true;
}

function setStatus( msg )
{
	document.getElementById( "STATUS" ).innerHTML = msg;
	console.log( msg );
}

function setStatusError( msg )
{
	setStatus( "<FONT COLOR=RED>" + msg + "</FONT>" );
	console.log( msg );
	console.trace();
}

function tryConnect()
{
	if( !navigator.hid )
	{
		return;
	}
	
	if( !dev )
	{
		navigator.hid.getDevices().then( (devices) =>
		{
			if( devices.length == 0 )
				setStatusError( "No devices found. Open a device." );
			else
				devices.forEach( tryOpen );
		});
	}
}

async function closeDeviceTool()
{
	loopAbort = false;
	setStatusError( "Disconnected" );
	dev = null;
}

var playingAudioProcessor = null;
var audioContext = null;

var targetModulation = 0;
var targetGain = 0.0;

function UpdateButtonNames()
{
	document.getElementById( "ToggleAudioButton" ).value = ( targetGain > 0.0 ) ? "Stop Audio" : "Start Audio";
	document.getElementById( "ToggleAudioModulationButton" ).value = ( targetModulation > 0.5 ) ? "FM" : "AM";
}

async function toggleAudioModulation()
{
	if( playingAudioProcessor != null )
	{
		var newVal = 1 - targetModulation;
		targetModulation = newVal;
	}
	UpdateButtonNames();
}

async function toggleAudio()
{
	if( playingAudioProcessor == null )
	{
		var bypass = '\
		class PlayingAudioProcessor extends AudioWorkletProcessor {\
			static get parameterDescriptors() {\
			  return [\
			  	{ name: "gain", defaultValue: 0, },\
			  	{ name: "sampleAdvance", defaultValue: 0.5, },\
				]\
			};\
			constructor() {\
				super();\
				this.rbuffer = new Float32Array(8192); \
				this.rbufferhead = 0|0; \
				this.rbuffertail = 0|0; \
				this.sampleplace = 0.0; \
				this.dcoffset = 0.0; \
				this.totalsampcount = 0|0; \
				\
				this.port.onmessage = (e) => { \
					for( var i = 0|0; i < e.data.length|0; i++ ) \
					{ \
						let n = (this.rbufferhead + (1|0))%(8192|0); \
						if( n == this.rbuffertail ) \
						{ \
							this.rbuffertail = (this.rbuffertail + (1|0))%(8192|0); \
							/*console.log( `Overflow` ); */ \
						} \
						var vv = e.data[i]; \
						this.dcoffset = this.dcoffset * 0.995 + vv * 0.005; \
						this.rbuffer[this.rbufferhead] = vv - this.dcoffset; \
						this.rbufferhead = n; \
					} \
				}; \
			}\
			\
			process(inputs, outputs, parameters) {\
				/*console.log( parameters.gain[0] );*/ \
				/*console.log( this.ingestData );*/ \
				let len = outputs[0][0].length; \
				const sa = Math.fround( parameters.sampleAdvance[0] ); /*float*/ \
				var s = Math.fround( this.sampleplace );      /*float*/ \
				var tail = this.rbuffertail | 0;              /* int*/  \
				var tailnext = this.rbuffertail | 0;          /* int*/  \
				if( tail == this.rbufferhead ) { /*console.log( "Underflow " );*/ return true; }\
				var tsamp = Math.fround( this.rbuffer[tail] ); \
				var nsamp = Math.fround( this.rbuffer[tailnext] ); \
				this.totalsampcount += len|0; \
				for (let b = 0|0; b < len|0; b++) { \
					s += sa; \
					var excess = Math.floor( s ) | 0; \
					if( excess > 0 ) \
					{ \
						s -= excess; \
						tail = ( tail + (excess|0) ) % (8192|0); \
						tailnext = ( tail + (1|0) ) % (8192|0); \
						if( tail == this.rbufferhead ) { /* console.log( "Underflow" ); */ break; } \
						tsamp = Math.fround( this.rbuffer[tail] ); \
						nsamp = Math.fround( this.rbuffer[tailnext] ); \
					} \
					var valv = tsamp * (1.0-s) + nsamp * s; \
					outputs[0][0][b] = valv*parameters.gain[0]; \
				} \
				/*console.log( tail + " " + this.rbuffertail + " " + tsamp + " " + nsamp );*/ \
				this.rbuffertail = tail; \
				this.sampleplace = s; \
				return true; \
			} \
		} \
		\
		registerProcessor("playing-audio-processor", PlayingAudioProcessor);';

		// The following mechanism does not work on Chrome.
		//	const dataURI = URL.createObjectURL( new Blob([bypass], { type: 'text/javascript', } ) );


		// Extremely tricky trick to side-step local file:// CORS issues.
		// https://stackoverflow.com/a/67125196/2926815
		// https://stackoverflow.com/a/72180421/2926815
		let blob = new Blob([bypass], {type: 'application/javascript'});
		let reader = new FileReader();
		await reader.readAsDataURL(blob);
		let dataURI = await new Promise((res) => {
			reader.onloadend = function () {
				res(reader.result);
			}
		});

		audioContext = new AudioContext();

		await audioContext.audioWorklet.addModule(dataURI);

		playingAudioProcessor = new AudioWorkletNode(
			audioContext,
			"playing-audio-processor"
		);
		playingAudioProcessor.connect(audioContext.destination);
		audioContext.resume();

		let gainParam = playingAudioProcessor.parameters.get( "gain" );
		gainParam.setValueAtTime( 0, audioContext.currentTime );
	}

	var newVal = 1.0 - targetGain;
	console.log( "Setting gain to: " + newVal );
	let gainParam = playingAudioProcessor.parameters.get("gain");
	gainParam.setValueAtTime( newVal, audioContext.currentTime);
	targetGain = newVal;
	document.getElementById( "ToggleAudioButton" ).value = ( newVal > 0.5 ) ? "Stop Audio" : "Start Audio";
	UpdateButtonNames();
}

function onLoadWebHidControl()
{
	liveGraph = document.getElementById( "LiveGraph" );
	liveGraph.addEventListener( "mousedown", graphClick );
	liveGraph.addEventListener( "mouseup", graphClick );

	UpdateButtonNames();
	setTimeout( sendLoop, 1 );

	if( !navigator.hid )
	{
		setStatusError( "Browser does not support HID." );
		document.getElementById( "connectButton" ).hidden = true;
	}
	else
	{
		navigator.hid.addEventListener("disconnect", (event) => { if( event.device.productName == expectedProductName ) closeDeviceTool(); } );
	}

	setTimeout( () => { elapsedOK = true; }, 3000 );

}

function reqConnect()
{
	loopAbort = true;
	const initialization = navigator.hid.requestDevice( { filters: [ filter ] } );
	initialization.then( gotUSBDevice );
	initialization.catch( setStatusError );
}

function gotUSBDevice(result)
{
	if( result.length < 1 )
	{
		setStatusError( "Error: No devices found" );
		return;
	}

	if( result[0].productName != expectedProductName )
	{
		setStatusError( "Error: Wrong device name.  Got " + result[0].productName + " Expected " + expectedProductName );
		return;
	}

	const thisDev = result[0];
	
	tryOpen( thisDev );
}

function tryOpen( thisDev )
{
	thisDev.open().then( ( result ) => {
		if( result === undefined )
		{
			if( dev ) dev.close();
			loopAbort = false;
			dev = thisDev;
			setStatus( "Connected." );
		}
		else
		{
			setStatusError( "Error: Could not open; " + result );
		}
	} ).catch( (e) => setStatusError( "Error: Could not open; " + e ) );
}

let sendReport = null;
let receiveReport = null;

async function sendLoopError( e )
{
console.log( "SEND LOOP ERROR" );
	sendReport = null;
	receiveReport = null;
	if( dev ) await dev.close();
	dev = null;
	setStatusError( e );
}

function updateWebHidDeviceWithParameters( paramlist )
{
	var i = 0|0;
	var arraySend = new Uint8Array(63);
	for( var i = 0|0; i < paramlist.length|0; i++ )
	{
		var vv = paramlist[i] | 0;
		arraySend[i*4+7] = (vv>>0)&0xff;
		arraySend[i*4+8] = (vv>>8)&0xff;
		arraySend[i*4+9] = (vv>>16)&0xff;
		arraySend[i*4+10] = (vv>>24)&0xff;
	}
	arraySend[3] = paramlist.length | 0;
	sendReport = dev.sendFeatureReport( 0xAC, arraySend ).catch( sendLoopError );
	if( !sendReport ) sendLoopError( "error creating sendFeatureReport" );
}

FMiirphase = 0.0; /* for FM */
FMlastphase = 0.0; /* for FM */
FMphaseout = 0.0; /* for FM */

lastadc = 0|0;


remote_clock_mhz = 0;
remote_clock_last_timestamp = 0|0;
remote_clock_last_timems = 0.0;
remote_clock_initted = false;
remote_clock_refinement = 1.0;
remote_clock_total_s = 0;
remote_clock_total_ticks = 0.0;

function ComputeRemoteClock( remote_time_ticks, now_ms )
{
	if( !remote_clock_initted )
	{
		remote_clock_last_timestamp = remote_time_ticks;
		remote_clock_last_timems = now_ms;
		remote_clock_refinement = 1.0;
		remote_clock_initted = true;
		remote_clock_total_ms = 0;
		remote_clock_total_ticks = 0|0;
		return;
	}

	var delta_s = (now_ms - remote_clock_last_timems)/1000.0;
	var delta_clock = ((remote_time_ticks - remote_clock_last_timestamp)|0)*2; // convert 144MHz to 288MHz
	var this_mhz = delta_clock / delta_s;

	remote_clock_total_s += delta_s;
	remote_clock_total_ticks += delta_clock * 1.0;

	remote_clock_mhz = remote_clock_total_ticks / remote_clock_total_s;

	remote_clock_total_s *= 0.99999;
	remote_clock_total_ticks *= 0.99999;

	remote_clock_last_timestamp = remote_time_ticks;
	remote_clock_last_timems = now_ms;
}

function updateCrystalWithMHz()
{
	document.getElementById('crystalmhz').value = remote_clock_mhz/1000000.0;
}

async function sendLoop()
{
	const sleep = ms => new Promise(r => setTimeout(r, ms));
	var frameNo = 0|0;
	var lastTime = performance.now();
	let goodCount = 0;
	let badCount = 0;
	let kBsecAvg = 0;
	let xActionSecAvg = 0;

	while( true )
	{
		if( dev && dev !== null && !loopAbort )
		{
			receiveReport = dev.receiveFeatureReport( 0xAD ).catch( sendLoopError );
			if( !receiveReport ) sendLoopError( "error creating receiveReport" );

			frameNo++

			const updateStatsPerfPer = 4;
			if( frameNo % updateStatsPerfPer == 0 )
			{
				let thisTime = performance.now();
				let deltaTime = thisTime - lastTime;
				let kBsec = (255*1000/1024*updateStatsPerfPer)/(deltaTime);
				let xActionSec = (2*updateStatsPerfPer*1000)/(deltaTime);
				kBsecAvg = kBsecAvg * 0.9 + kBsec * 0.1;
				xActionSecAvg = xActionSecAvg * 0.9 + xActionSec * 0.1;

				document.getElementById( "StatusPerf" ).innerHTML = 
					(kBsecAvg).toFixed(2) + " kB/s<br>" +
					(xActionSecAvg).toFixed(2)  + "x/s<br>";

				if( !didSetupGeneralData )
				{
					document.getElementById( "GeneralData" ).innerHTML =
						"<TABLE WIDTH=100% border=1><TR><TD width=25% ID=gdCount>Count: " + goodCount + " / " + badCount + "</td>" +
						"<td width=20% ID=gdInten>Inten: " + ((Math.log( lastIntensity * lastIntensity )/Math.log(10)) * 10-120).toFixed(2) + "db (" + lastIntensity + ")</td>" +
						"<td width=20% ID=gdADCs>ADCs: " + (lastadc>>16).toFixed(0)  + " / " + (lastadc&0xffff).toFixed(0)  + "</td>" +
						"<td width=20%><INPUT TYPE=submit ID=gdButton style='padding:.1em' VALUE=" + (remote_clock_mhz/1000000.0).toFixed(6) + ` ONMOUSEDOWN="updateCrystalWithMHz()"> MHz ` + "</TD>" +
						"<td width=20% ID=gdPPM>" + ((remote_clock_mhz-288000000)/288).toFixed(3) + " PPM</td>" +
						"</tr></table>";
					didSetupGeneralData = true;
				}
				else
				{

					document.getElementById( "gdCount" ).innerHTML = "Count: " + goodCount + " / " + badCount;
					document.getElementById( "gdInten" ).innerHTML = "Inten: " + ((Math.log( lastIntensity * lastIntensity )/Math.log(10)) * 10-120).toFixed(2) + "db (" + lastIntensity + ")";
					document.getElementById( "gdADCs" ).innerHTML = "ADC:" + (lastadc>>16).toFixed(0)  + " / " + (lastadc&0xffff).toFixed(0);
					document.getElementById( "gdButton" ).value = "" + (remote_clock_mhz/1000000.0).toFixed(6);
					document.getElementById( "gdPPM" ).innerHTML = "" + ((remote_clock_mhz-288000000)/288).toFixed(3) + " PPM";
					didSetupGeneralData = true;
				}

				lastTime = thisTime;
			}
			else if( frameNo % updateStatsPerfPer == 2 )
			{
				const ctx = liveGraph.getContext("2d");

				if( !graphIsClicked )
				{
					let liveGraphContainer = document.getElementById( "LiveGraphContainer" );
					liveGraph.width = liveGraphContainer.clientWidth;
					liveGraph.style.position = 'absolute';

					ctx.clearRect(0, 0, liveGraph.width, liveGraph.height);

					let grey = darkmode?"rgb( 240 240 240 )":"rgb(16 16 16)";
					let mark = darkmode?"rgb( 255 255 255 )":"rgb(0 0 0)";

					var filledness = lastNumQ * 198 / 120;
					ctx.fillStyle = grey;
					ctx.fillRect( 2, 2 + 198 - filledness, 18, filledness - 2);

					filledness = ( lastTimeUsed * 1.0 / lastTotalTime ) * 198;
					ctx.fillStyle = grey;
					ctx.fillRect( 26, 2 + 198 - filledness, 18, filledness - 2 );

					ctx.fillStyle = mark;

					let mulcoeff = 2500.0 / lastIntensity;
					var lot = 1.2;
					var x = 253;
					for( var i = (IQHistoryHead-1) & (IQHistoryLen-1); i != IQHistoryHead|0; i = (i - 1 + IQHistoryLen) & (IQHistoryLen-1) )
					{
						let power = MPHistoryArray[i*2+0]; //Math.sqrt( real * real + imag * imag ) * mulcoeff;
						let phase = MPHistoryArray[i*2+1]; //Math.atan2( real, imag ) * 0.159155078*0.5;
						ctx.fillRect(x,power*120+10,2,2);
						ctx.fillRect(x,phase*80+110,2,2);

						let v = IQHistoryArray[i];
						let real = (v >> 16);
						let imag = (v & 0xffff);
						if( real > 32767 ) real -= 65536;
						if( imag > 32767 ) imag -= 65536;
						real = real * mulcoeff + 100;
						imag = imag * mulcoeff + 100;
						if( real < 0 ) real = 0; if( real > 255 ) real = 255;
						if( imag < 0 ) imag = 0; if( imag > 255 ) imag = 255;
						x++;
						if( lot > 0 )
						{
							ctx.globalAlpha = lot;
							ctx.fillRect(real+50,imag,2,2);
						}
						ctx.globalAlpha = 1.0;
						lot -= 0.0015;
					}

					ctx.strokeStyle = "rgb( 128 128 128 )";
					ctx.fillStyle = "rgb( 128 0 0 )";
					ctx.strokeRect( 1, 1, 20, 198 );
					ctx.strokeRect( 25, 1, 20, 198 );
					ctx.strokeRect( 49, 1, 200, 198 );
					ctx.strokeRect( 253, 1, liveGraph.width, 198 );
				}
			}


			if( sendReport )
			{
				await sendReport;
			}
			if( receiveReport )
			{
				let receiveData = await receiveReport;
				if( receiveData && receiveData.buffer )
				{
					// Receive data from USB (from HandleHidUserGetReportSetup)
					let data = new Uint32Array( receiveData.buffer.slice( 0, 508 ) );
					let intensity = data[0]>>8;
					let numq = data[0] & 0xff;
					let time_total = data[1]>>16;
					let time_used  = data[1]&0xffff;
					let sample_divisor  = data[2]&0xffff;
					let remote_time = data[3];
					ComputeRemoteClock( remote_time, performance.now() );
					lastadc = data[4];
					let demodbuffer = new Float32Array(numq);
					let mulcoeff = 16.0 / lastIntensity;
					for( var i = 0|0; i < numq; i++ )
					{
						let vv = IQHistoryArray[IQHistoryHead] = data[i+5];
						let vi = vv >> 16;
						let vq = vv & 0xffff;
						if( vi >= 32768 ) vi = vi-65535;
						if( vq >= 32768 ) vq = vq-65535;
						let power = Math.sqrt( vi * vi + vq * vq ) * mulcoeff;
						let phase = Math.atan2( vi, vq ) * 0.159155078 + 0.5;
						MPHistoryArray[IQHistoryHead*2+0] = power; //Math.sqrt( real * real + imag * imag ) * mulcoeff;
						MPHistoryArray[IQHistoryHead*2+1] = phase; //Math.atan2( real, imag ) * 0.159155078*0.5;
						if( targetModulation == 0 )
						{ /* AM */
							demodbuffer[i] = power;
						}
						else if( targetModulation == 1 )
						{ /* FM */
							var diffphase = phase - FMlastphase;
							FMlastphase = phase;
							if( diffphase - FMiirphase < 0.0 ) diffphase += 1.0;
							if( diffphase - FMiirphase > 1.0 ) diffphase -= 1.0;
							FMiirphase = FMiirphase * 0.999 + diffphase * 0.001;
							diffphase -= FMiirphase;
							if( diffphase > 0.5 ) diffphase -= 1;
							if( diffphase <-0.5 ) diffphase += 1;
							var po = FMphaseout = FMphaseout * 0.993 + diffphase;
							if( po < 0.0 ) po += 1.0;
							if( po > 1.0 ) po -= 1.0;
							demodbuffer[i] = po * .3; // Turn down FM it's really loud.
						}

						IQHistoryHead = (IQHistoryHead+1)%IQHistoryLen;
					}
					lastIntensity = intensity;
					lastNumQ = numq;
					lastTotalTime = time_total;
					lastTimeUsed = time_used;

					if( audioContext != null && playingAudioProcessor != null )
					{
						let sampleAdvance = (system_rate/sample_divisor) / audioContext.sampleRate;
						let sampleAdvanceParam = playingAudioProcessor.parameters.get("sampleAdvance");
						sampleAdvanceParam.setValueAtTime( sampleAdvance, audioContext.currentTime);
						playingAudioProcessor.port.postMessage( demodbuffer );
					}
					goodCount++;
				}
				else
				{
					badCount++;
				}
			}
		}
		else if( loopAbort )
		{
			if( dev )
			{
				console.log( "Loop Aborting, Dev Closing" );
				await dev.close();
				console.log( "Loop Aborting, Dev Closed." );
				dev = null;
			}
			await sleep(100);
		}
		else
		{
			// Try opening dev.
			console.log( "Attempting reconnect." );
			tryConnect();
			goodCount = 0;
			badCount = 0;
			let i = 0|0;
			for( i = 0; i < 10; i++ )
			{
				await sleep(100);
				if( dev )
				{
					//break;
				}
			}
		}
	}
	console.log( "ABORT\n" );
}

