/**
 * Pitch Shifter AudioWorkletProcessor
 *
 * Phase vocoder implementation for real-time pitch shifting.
 * Used for "monster voice" effect in Synops mixer.
 *
 * Algorithm: overlap-add with phase vocoder frequency-domain processing.
 * - FFT size: 2048 samples (good balance of quality vs latency at 48kHz)
 * - Hop size: 512 samples (4x overlap)
 * - Latency: ~42ms at 48kHz
 *
 * Parameters:
 * - pitchFactor: 0.5 (octave down) to 2.0 (octave up), default 0.7 (monster)
 */

class PitchShifterProcessor extends AudioWorkletProcessor {
	constructor() {
		super();

		this.pitchFactor = 0.7; // default monster voice
		this.enabled = false;

		// FFT parameters
		this.fftSize = 2048;
		this.hopSize = 512; // fftSize / 4
		this.overlap = 4;

		// Circular input buffer
		this.inputBuffer = new Float32Array(this.fftSize * 2);
		this.inputWritePos = 0;
		this.inputSamplesReady = 0;

		// Output overlap-add buffer
		this.outputBuffer = new Float32Array(this.fftSize * 2);
		this.outputReadPos = 0;

		// Phase tracking for vocoder
		this.lastInputPhase = new Float32Array(this.fftSize);
		this.lastOutputPhase = new Float32Array(this.fftSize);

		// Hann window
		this.window = new Float32Array(this.fftSize);
		for (let i = 0; i < this.fftSize; i++) {
			this.window[i] = 0.5 * (1 - Math.cos((2 * Math.PI * i) / this.fftSize));
		}

		// Working buffers for FFT
		this.fftReal = new Float32Array(this.fftSize);
		this.fftImag = new Float32Array(this.fftSize);
		this.synthReal = new Float32Array(this.fftSize);
		this.synthImag = new Float32Array(this.fftSize);

		// Pre-compute bit-reversal table
		this.bitRev = new Uint32Array(this.fftSize);
		const bits = Math.log2(this.fftSize);
		for (let i = 0; i < this.fftSize; i++) {
			let rev = 0;
			let val = i;
			for (let b = 0; b < bits; b++) {
				rev = (rev << 1) | (val & 1);
				val >>= 1;
			}
			this.bitRev[i] = rev;
		}

		// Pre-compute twiddle factors
		this.twiddleReal = new Float32Array(this.fftSize / 2);
		this.twiddleImag = new Float32Array(this.fftSize / 2);
		for (let i = 0; i < this.fftSize / 2; i++) {
			const angle = (-2 * Math.PI * i) / this.fftSize;
			this.twiddleReal[i] = Math.cos(angle);
			this.twiddleImag[i] = Math.sin(angle);
		}

		// Listen for parameter changes
		this.port.onmessage = (e) => {
			if (e.data.pitchFactor !== undefined) {
				this.pitchFactor = Math.max(0.5, Math.min(2.0, e.data.pitchFactor));
			}
			if (e.data.enabled !== undefined) {
				this.enabled = e.data.enabled;
				if (!this.enabled) {
					// Clear buffers on disable
					this.inputBuffer.fill(0);
					this.outputBuffer.fill(0);
					this.lastInputPhase.fill(0);
					this.lastOutputPhase.fill(0);
					this.inputSamplesReady = 0;
				}
			}
		};
	}

	// In-place FFT (Cooley-Tukey radix-2 DIT)
	fft(real, imag, inverse) {
		const n = this.fftSize;

		// Bit-reversal permutation
		for (let i = 0; i < n; i++) {
			const j = this.bitRev[i];
			if (i < j) {
				let tmp = real[i]; real[i] = real[j]; real[j] = tmp;
				tmp = imag[i]; imag[i] = imag[j]; imag[j] = tmp;
			}
		}

		// Butterfly stages
		for (let size = 2; size <= n; size *= 2) {
			const halfSize = size / 2;
			const step = n / size;

			for (let i = 0; i < n; i += size) {
				for (let j = 0; j < halfSize; j++) {
					const twIdx = j * step;
					let twR = this.twiddleReal[twIdx];
					let twI = this.twiddleImag[twIdx];
					if (inverse) twI = -twI;

					const idx1 = i + j;
					const idx2 = i + j + halfSize;

					const tR = twR * real[idx2] - twI * imag[idx2];
					const tI = twR * imag[idx2] + twI * real[idx2];

					real[idx2] = real[idx1] - tR;
					imag[idx2] = imag[idx1] - tI;
					real[idx1] += tR;
					imag[idx1] += tI;
				}
			}
		}

		if (inverse) {
			for (let i = 0; i < n; i++) {
				real[i] /= n;
				imag[i] /= n;
			}
		}
	}

	processFrame() {
		const n = this.fftSize;
		const hopSize = this.hopSize;
		const pitchFactor = this.pitchFactor;

		// Extract windowed frame from input buffer
		const readStart = ((this.inputWritePos - n) + this.inputBuffer.length) % this.inputBuffer.length;
		for (let i = 0; i < n; i++) {
			const idx = (readStart + i) % this.inputBuffer.length;
			this.fftReal[i] = this.inputBuffer[idx] * this.window[i];
			this.fftImag[i] = 0;
		}

		// Forward FFT
		this.fft(this.fftReal, this.fftImag, false);

		// Phase vocoder analysis + synthesis
		const freqPerBin = sampleRate / n;
		const expectedPhaseAdvance = (2 * Math.PI * hopSize) / n;

		for (let k = 0; k < n; k++) {
			// Analysis: get magnitude and phase
			const mag = Math.sqrt(this.fftReal[k] * this.fftReal[k] + this.fftImag[k] * this.fftImag[k]);
			const phase = Math.atan2(this.fftImag[k], this.fftReal[k]);

			// Phase difference from last frame
			let phaseDiff = phase - this.lastInputPhase[k];
			this.lastInputPhase[k] = phase;

			// Remove expected phase advance
			phaseDiff -= k * expectedPhaseAdvance;

			// Wrap to [-pi, pi]
			phaseDiff = phaseDiff - 2 * Math.PI * Math.round(phaseDiff / (2 * Math.PI));

			// True frequency of this bin
			const trueFreq = k * freqPerBin + (phaseDiff * freqPerBin) / expectedPhaseAdvance;

			// Synthesis: map to new bin position
			const newBin = Math.round(k * pitchFactor);
			if (newBin >= 0 && newBin < n) {
				// Accumulate phase for output
				const outputPhaseAdvance = (2 * Math.PI * hopSize * (trueFreq * pitchFactor)) / sampleRate;
				this.lastOutputPhase[newBin] += outputPhaseAdvance;

				this.synthReal[newBin] = mag * Math.cos(this.lastOutputPhase[newBin]);
				this.synthImag[newBin] = mag * Math.sin(this.lastOutputPhase[newBin]);
			}
		}

		// Inverse FFT
		this.fft(this.synthReal, this.synthImag, true);

		// Overlap-add to output buffer
		for (let i = 0; i < n; i++) {
			const idx = (this.outputReadPos + i) % this.outputBuffer.length;
			this.outputBuffer[idx] += this.synthReal[i] * this.window[i] / this.overlap;
		}

		// Clear synth buffers for next frame
		this.synthReal.fill(0);
		this.synthImag.fill(0);
	}

	process(inputs, outputs) {
		const input = inputs[0];
		const output = outputs[0];

		if (!input || !input[0] || !output || !output[0]) return true;

		const inputChannel = input[0];
		const outputChannel = output[0];
		const blockSize = inputChannel.length; // typically 128

		if (!this.enabled) {
			// Pass through when disabled
			outputChannel.set(inputChannel);
			return true;
		}

		// Feed input samples into circular buffer
		for (let i = 0; i < blockSize; i++) {
			this.inputBuffer[this.inputWritePos] = inputChannel[i];
			this.inputWritePos = (this.inputWritePos + 1) % this.inputBuffer.length;
			this.inputSamplesReady++;

			// Process a frame every hopSize samples, once we have enough data
			if (this.inputSamplesReady >= this.fftSize && this.inputSamplesReady % this.hopSize === 0) {
				this.processFrame();
			}
		}

		// Read from output buffer
		for (let i = 0; i < blockSize; i++) {
			outputChannel[i] = this.outputBuffer[this.outputReadPos];
			this.outputBuffer[this.outputReadPos] = 0; // clear after reading
			this.outputReadPos = (this.outputReadPos + 1) % this.outputBuffer.length;
		}

		return true;
	}
}

registerProcessor('pitch-shifter', PitchShifterProcessor);
