class StreamProcessor extends AudioWorkletProcessor {
    constructor() {
        super();
        // Each incoming packet/block is a chunk of audio data, often 1024 samples or more
        // The process() function currently processes data 128 samples at a time
        // Rather than copy into a big circular buffer, we'll just process the data straight
        // out of this incoming data. It's allocated and needs to be garbage collected anyway
        this.queue = [];
        this.currentBuffer = null;
        this.currentBufferConsumed = 0;
        this.encoding = null;
        this.buffering = 1;           // Every time the queue fully empties, we wait for it to fill before resuming
        this.minBufferedMillis = 100; // We will not begin playback until we have this much data buffered
        this.maxBufferedMillis = 200; // If this much data is already buffered and new data arrives, we'll drop old data til just above minBufferedMillis
        this.bufferedMillis = 0;      // The sum of lengths of all buffers in milliseconds (excluding currentBuffer)
        this.bits = 16;

        // Listen for messages from the main thread to receive audio data
        this.port.onmessage = (event) => {
            let { data, encoding, minimumAudioBuffer, maximumAudioBuffer } = event.data;
            if( minimumAudioBuffer ) {
                // ReplayKit usually provides data in 4096 byte chunks (stereo, 16 bit) for 44khz audio,
                // which is  1024 mono samples.  At 44khz, that's 24.97 milliseconds of audio. So, "50" below would mean we buffer
                // two packets, and start playback when we receive the 3rd. Less than this, and the chance of jitter gets quite
                // high.  Even 50 is pretty low unless you're on a local network.
                if( minimumAudioBuffer < 100 ) {
                    minimumAudioBuffer = 100;
                }
                // There is no technical reason to limit the buffer size, but playback won't begin till the buffer is
                // filled.  So, we'll enforce a max just to prevent accidentally disabling sound entirely.
                if( minimumAudioBuffer > 5000 ) {
                    minimumAudioBuffer = 5000;
                }
                this.minBufferedMillis = minimumAudioBuffer;
            }
            if( maximumAudioBuffer ) {
                if( maximumAudioBuffer < this.minBufferedMillis ) {
                    maximumAudioBuffer = this.minBufferedMillis;
                }
                this.maxBufferedMillis = maximumAudioBuffer;
            }
            if( this.maxBufferedMillis < this.minBufferedMillis ) {
                this.maxBufferedMillis = this.minBufferedMillis + 100;
            }
            // The max buffer being much larger than minbuffer doesn't gain you a lot.
            // You really need an artificial scenario, such as being in a debugger at
            // a breakpoint, to accumulate audio faster than you're playing it back.
            //
            // For very small values (~100ms, for example) of minBuffer, you do risk "clumping up"
            // a few hundred ms of audio data before playback due to a spotty network connection.
            // In that case, you probably do want to accept the extra data and play it back. So
            // something like 100/300 min/max could easily make sense.
            if( this.maxBufferedMillis > this.minBufferedMillis + 1000 ) {
                this.maxBufferedMillis = this.minBufferedMillis + 1000;
            }

            // User has switched between stereo and mono, 32 and 16 bit, or whatever...
            // Just discard current data, there's no need to make this (rare) transition
            // It's also a very small blip when we're getting data at 20~100 frames/second
            if( encoding != null && encoding != this.encoding ) {
                this.queue = [];
                this.currentBuffer = null;
                this.inputChannels = 2;
                this.scale = 32768.0;
                this.bits = 16;
                if( encoding.includes("1ch") ) {
                    this.inputChannels = 1;
                }
                if( encoding.includes("PCM8") ) {
                    this.scale = 128.0;
                    this.bits = 8;
                }
                this.encoding = encoding;
            }
            if( data != null ) {
                if( this.encoding == null ) {
                    return;
                }
                if( this.bits == 8 ) {
                    data = new Int8Array( data );
                } else {
                    data = new Int16Array( data );
                }
                this.bufferedMillis +=  data.length * 1000.0 / (sampleRate * this.inputChannels);
                this.queue.push( data );
                // If we have accumulated too much data, start discarding old data.
                // Never shrink the queue to have fewer than 2 chunks.
                // Don't discard data if it puts us under this.bufferedMillis
                // Note: we could, but aren't, considering the amount of data remaining in the
                // current buffer.  This isn't hyper-precise.
                while( this.bufferedMillis > this.maxBufferedMillis && this.queue.length > 2 ) {
                    let discarded = this.queue[0]; // discard the oldest data
                    let m = discarded.length * 1000 / (sampleRate * this.inputChannels);
                    // Only a very odd config would let this happen.  If we have too much data buffered,
                    // but removing one block of data would put us UNDER the mininimum buffer, keep the data.
                    // We are usually expecting VERY tiny buffers, we're not going to bother trying to subdivide
                    // them to hit arbitrary min/max targets.
                    if( this.bufferedMillis - m < this.minBufferedMillis ) {
                        break;
                    }
                    this.queue.shift();
                    this.bufferedMillis -= m;
                }
            }
        };
    }

    process( inputList, outputList, parameters ) {
        if( this.buffering ) {
            if( this.bufferedMillis > this.minBufferedMillis ) {
                this.buffering = 0;
            } else {
                // While buffering, output silence
                const output = outputList[0];
                for( let channel = 0; channel < output.length; channel++ ) {
                    output[ channel ].fill(0);
                }
                return true;
            }
        }
        return this.processRawPCM( inputList, outputList, parameters );

        if( this.encoding == "decodedPCM" ) {
            return this.processDecodedPCM( inputList, outputList, parameters );
        }
    }

    // This is data as decoded by audioContext.decodeAudioData (for example, decoded AAC or MP3 data).
    // Each entry in this.queue is an array with 1 element per channel(2 elements = sterio, 1 element = mono) .
    // That element is an array of 32bit float samples (i.e. the format already anticipated
    // by the audio stream processing system).  We just copy the data across.
    processDecodedPCM( inputList, outputList, parameters ) {
        const output = outputList[0];
        const frameSize = output[0].length;

        let transferred = 0;
        let channelCount = output.length;
        while(1) {
            if( this.currentBuffer == null ) {
                if( this.queue.length > 0 ) {
                    this.currentBuffer = this.queue.shift();
                    this.currentBufferConsumed = 0;
                } else {
                    break;
                }
            }
            channelCount = Math.min( this.currentBuffer.length, channelCount );
            let write1 = Math.min( this.currentBuffer[0].length - this.currentBufferConsumed, frameSize - transferred );
            for( let channel = 0; channel < channelCount; channel++ ) {
                output[ channel ].set( this.currentBuffer[ channel ].subarray( this.currentBufferConsumed, this.currentBufferConsumed + write1 ), transferred );
            }
            this.currentBufferConsumed += write1;
            transferred += write1;
            if( this.currentBuffer[0].length == this.currentBufferConsumed ) {
                this.currentBuffer = null;
            }
            if( transferred == frameSize ) {
                return true;
            }
        }

        for( let channel = 0; channel < channelCount; channel++ ) {
            output[ channel ].fill( 0, transferred );
            this.buffering = 1;
        }
        return true;
    }

    // Each queue/buffer contains raw PCM data, with interleaved channel data.
    // Currently supporting 16 and 8 bit data.
    processRawPCM( inputList, outputList, parameters ) {
        const output = outputList[0];
        const frameSize = output[0].length;

        let transferred = 0;
        let channelCount = output.length;
        while(1) {
            if( this.currentBuffer == null ) {
                if( this.queue.length > 0 ) {
                    this.currentBuffer = this.queue.shift();
                    this.currentBufferConsumed = 0;
                } else {
                    break;
                }
            }

            // Requires that buffers are Int16Arrays for 16-bit PCM and Int8Arrays for 8-bit PCM
            // We also currently require the audioContext to have been initialized with the expected bit rate
            const currentSamplesAvailable = (this.currentBuffer.length - this.currentBufferConsumed) / this.inputChannels;
            let samplesToCopy = Math.min( currentSamplesAvailable, frameSize - transferred );

            while( samplesToCopy > 0 ) {
                output[0][ transferred ] = this.currentBuffer[ this.currentBufferConsumed ] / this.scale;
                if( channelCount > 1 ) {
                    if( this.inputChannels == 1 ) {
                        output[0][ transferred ] = this.currentBuffer[ this.currentBufferConsumed ] / this.scale;
                    } else {
                        output[1][ transferred ] = this.currentBuffer[ this.currentBufferConsumed + 1 ] / this.scale;
                    }
                }
                this.currentBufferConsumed += this.inputChannels;
                transferred++;
                samplesToCopy--;
            }

            if( this.currentBuffer.length == this.currentBufferConsumed ) {
                this.bufferedMillis -= (this.currentBuffer.length * 1000) / (sampleRate * this.inputChannels);
                this.currentBuffer = null;
            }
            if( transferred == frameSize ) {
                //console.log("Data....");
                return true;
            }
        }

        for( let channel = 0; channel < channelCount; channel++ ) {
            //console.log("Zeros....");
            output[ channel ].fill( 0, transferred );
            this.buffering = 1;
        }
        return true;
    }
}
// Register the worklet processor
registerProcessor( 'stream-processor', StreamProcessor );
