// ------------------------------------------------------------------------ //
// ------------------------------------------------------------------------ //
// ------------------------------------------------------------------------ //
// ------------------------------------------------------------------------ //
// ------------------------- wasm-audio-helper.js ------------------------- //

/**
 * Copyright 2018 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

// Basic byte unit of WASM heap. (16 bit = 2 bytes)
const BYTES_PER_UNIT = Uint16Array.BYTES_PER_ELEMENT;

// Byte per audio sample. (32 bit float)
const BYTES_PER_SAMPLE = Float32Array.BYTES_PER_ELEMENT;

// The max audio channel on Chrome is 32.
const MAX_CHANNEL_COUNT = 32;

// WebAudio's render quantum size.
const RENDER_QUANTUM_FRAMES = 128;


/**
 * A WASM HEAP wrapper for AudioBuffer class. This breaks down the AudioBuffer
 * into an Array of Float32Array for the convinient WASM opearion.
 *
 * @class
 * @dependency Module A WASM module generated by the emscripten glue code.
 */
class HeapAudioBuffer {
  /**
   * @constructor
   * @param  {object} wasmModule WASM module generated by Emscripten.
   * @param  {number} length Buffer frame length.
   * @param  {number} channelCount Number of channels.
   * @param  {number=} maxChannelCount Maximum number of channels.
   */
  constructor(wasmModule, length, channelCount, maxChannelCount) {
    // The |channelCount| must be greater than 0, and less than or equal to
    // the maximum channel count.
    this._isInitialized = false;
    this._module = wasmModule;
    this._length = length;
    this._maxChannelCount = maxChannelCount
        ? Math.min(maxChannelCount, MAX_CHANNEL_COUNT)
        : channelCount;
    this._channelCount = channelCount;
    this._allocateHeap();
    this._isInitialized = true;
  }

  /**
   * Allocates memory in the WASM heap and set up Float32Array views for the
   * channel data.
   *
   * @private
   */
  _allocateHeap() {
    const channelByteSize = this._length * BYTES_PER_SAMPLE;
    const dataByteSize = this._channelCount * channelByteSize;
    this._dataPtr = this._module._malloc(dataByteSize);
    this._channelData = [];
    for (let i = 0; i < this._channelCount; ++i) {
      let startByteOffset = this._dataPtr + i * channelByteSize;
      let endByteOffset = startByteOffset + channelByteSize;
      // Get the actual array index by dividing the byte offset by 2 bytes.
      this._channelData[i] =
          this._module.HEAPF32.subarray(startByteOffset >> BYTES_PER_UNIT,
                                        endByteOffset >> BYTES_PER_UNIT);
    }
  }

  /**
   * Adapt the current channel count to the new input buffer.
   *
   * @param  {number} newChannelCount The new channel count.
   */
  adaptChannel(newChannelCount) {
    if (newChannelCount < this._maxChannelCount) {
      this._channelCount = newChannelCount;
    }
    // reform subArrays just in case memory has grown since we last did this
    const channelByteSize = this._length * BYTES_PER_SAMPLE;
    for (let i = 0; i < this._channelCount; ++i) 
    {
      let startByteOffset = this._dataPtr + i * channelByteSize;
      let endByteOffset = startByteOffset + channelByteSize;
      // Get the actual array index by dividing the byte offset by 2 bytes.
      this._channelData[i] =
          this._module.HEAPF32.subarray(startByteOffset >> BYTES_PER_UNIT,
                                        endByteOffset >> BYTES_PER_UNIT);
    }
  }

  /**
   * Getter for the buffer length in frames.
   *
   * @return {?number} Buffer length in frames.
   */
  get length() {
    return this._isInitialized ? this._length : null;
  }

  /**
   * Getter for the number of channels.
   *
   * @return {?number} Buffer length in frames.
   */
  get numberOfChannels() {
    return this._isInitialized ? this._channelCount : null;
  }

  /**
   * Getter for the maxixmum number of channels allowed for the instance.
   *
   * @return {?number} Buffer length in frames.
   */
  get maxChannelCount() {
    return this._isInitialized ? this._maxChannelCount : null;
  }

  /**
   * Returns a Float32Array object for a given channel index. If the channel
   * index is undefined, it returns the reference to the entire array of channel
   * data.
   *
   * @param  {number|undefined} channelIndex Channel index.
   * @return {?Array} a channel data array or an
   * array of channel data.
   */
  getChannelData(channelIndex) {
    if (channelIndex >= this._channelCount) {
      return null;
    }
    return typeof channelIndex === 'undefined'
        ? this._channelData : this._channelData[channelIndex];
  }

  /**
   * Returns the base address of the allocated memory space in the WASM heap.
   *
   * @return {number} WASM Heap address.
   */
  getHeapAddress() {
    return this._dataPtr;
  }

  /**
   * Frees the allocated memory space in the WASM heap.
   */
  free() {
    this._isInitialized = false;
    this._module._free(this._dataPtr);
    this._module._free(this._pointerArrayPtr);
    this._channelData = null;
  }
} // class HeapAudioBuffer


/**
 * A JS FIFO implementation for the AudioWorklet. 3 assumptions for the
 * simpler operation:
 *  1. the push and the pull operation are done by 128 frames. (Web Audio
 *    API's render quantum size in the speficiation)
 *  2. the channel count of input/output cannot be changed dynamically.
 *    The AudioWorkletNode should be configured with the `.channelCount = k`
 *    (where k is the channel count you want) and
 *    `.channelCountMode = explicit`.
 *  3. This is for the single-thread operation. (obviously)
 *
 * @class
 */
class RingBuffer {
  /**
   * @constructor
   * @param  {number} length Buffer length in frames.
   * @param  {number} channelCount Buffer channel count.
   */
  constructor(length, channelCount) {
    this._readIndex = 0;
    this._writeIndex = 0;
    this._framesAvailable = 0;

    this._channelCount = channelCount;
    this._length = length;
    this._channelData = [];
    for (let i = 0; i < this._channelCount; ++i) {
      this._channelData[i] = new Float32Array(length);
    }
  }

  /**
   * Getter for Available frames in buffer.
   *
   * @return {number} Available frames in buffer.
   */
  get framesAvailable() {
    return this._framesAvailable;
  }

  /**
   * Push a sequence of Float32Arrays to buffer.
   *
   * @param  {array} arraySequence A sequence of Float32Arrays.
   */
  push(arraySequence) {
    // The channel count of arraySequence and the length of each channel must
    // match with this buffer obejct.

    // Transfer data from the |arraySequence| storage to the internal buffer.
    let sourceLength = arraySequence[0].length;
    for (let i = 0; i < sourceLength; ++i) {
      let writeIndex = (this._writeIndex + i) % this._length;
      for (let channel = 0; channel < this._channelCount; ++channel) {
        this._channelData[channel][writeIndex] = arraySequence[channel][i];
      }
    }

    this._writeIndex += sourceLength;
    if (this._writeIndex >= this._length) {
      this._writeIndex = 0;
    }

    // For excessive frames, the buffer will be overwritten.
    this._framesAvailable += sourceLength;
    if (this._framesAvailable > this._length) {
      this._framesAvailable = this._length;
    }
  }

  /**
   * Pull data out of buffer and fill a given sequence of Float32Arrays.
   *
   * @param  {array} arraySequence An array of Float32Arrays.
   */
  pull(arraySequence) {
    // The channel count of arraySequence and the length of each channel must
    // match with this buffer obejct.

    // If the FIFO is completely empty, do nothing.
    if (this._framesAvailable === 0) {
      return;
    }

    let destinationLength = arraySequence[0].length;

    // Transfer data from the internal buffer to the |arraySequence| storage.
    for (let i = 0; i < destinationLength; ++i) {
      let readIndex = (this._readIndex + i) % this._length;
      for (let channel = 0; channel < this._channelCount; ++channel) {
        arraySequence[channel][i] = this._channelData[channel][readIndex];
      }
    }

    this._readIndex += destinationLength;
    if (this._readIndex >= this._length) {
      this._readIndex = 0;
    }

    this._framesAvailable -= destinationLength;
    if (this._framesAvailable < 0) {
      this._framesAvailable = 0;
    }
  }
} // class RingBuffer

// ------------------------------------------------------------------------ //
// ------------------------------------------------------------------------ //
// ------------------------------------------------------------------------ //
// ------------------------------------------------------------------------ //




// ------------------------------------------------------------------------ //
// ------------------------------------------------------------------------ //
// ------------------------------------------------------------------------ //
// ------------------------------------------------------------------------ //
// --------------------------- chucknode.js ------------------------------- //

var globalInit = false;
var globalPromise = undefined;
var chucks = {};

var setDataDir, setLogLevel, initChuckInstance,

    clearChuckInstance, clearGlobals, cleanupChuckInstance,
    cleanRegisteredChucks, 
    
    runChuckCode, runChuckCodeWithReplacementDac, runChuckFile,
    runChuckFileWithReplacementDac, runChuckFileWithArgs,
    runChuckFileWithArgsWithReplacementDac,
    
    replaceChuckCode, replaceChuckCodeWithReplacementDac,
    replaceChuckFile, replaceChuckFileWithReplacementDac,
    replaceChuckFileWithArgs, replaceChuckFileWithArgsWithReplacementDac,
    
    isShredActive,
    removeShred,

    // don't care about these because emscripten has another mechanism
    // for rerouting print statements
    // setChoutCallback = 
    // setCherrCallback = 
    // setStdoutCallback = 
    // setStderrCallback = 

    // set/get global int, float, string
    setChuckInt, getChuckInt, setChuckFloat, getChuckFloat,
    setChuckString, getChuckString,

    // global Events
    signalChuckEvent, broadcastChuckEvent, listenForChuckEventOnce,
    startListeningForChuckEvent, stopListeningForChuckEvent,

    // note: array is SAMPLE == Float32
    // NOTE ALSO anything using arrays cannot use cwrap; use in similar manner to Module._chuckManualAudioCallback
    // TODO
    // getGlobalUGenSamples = Module.cwrap( 'getGlobalUGenSamples', 'number', ['number', 'string', 'array', 'number'] );

    // set/get global int arrays
    // note: anything using arrays cannot use cwrap (since 'array' is an Int64Array)
    //  called manually with heap manipulation below
    //  setGlobalIntArray = Module.cwrap( 'setGlobalIntArray', 'number', ['number', 'string', 'array', 'number'] );
    getGlobalIntArray, setGlobalIntArrayValue, getGlobalIntArrayValue,
    setGlobalAssociativeIntArrayValue, getGlobalAssociativeIntArrayValue,

    // set/get global float arrays
    // note: anything using arrays cannot use cwrap. called manually with heap manipulation below
    //  setGlobalFloatArray = Module.cwrap( 'setGlobalFloatArray', 'number', ['number', 'string', 'array', 'number'] );
    getGlobalFloatArray, setGlobalFloatArrayValue, getGlobalFloatArrayValue,
    setGlobalAssociativeFloatArrayValue, getGlobalAssociativeFloatArrayValue;

var initGlobals = function( Module )
{
    // metaproperties (e.g. setDataDir must be called before init_chuck
    setDataDir = Module.cwrap( 'setDataDir', 'number', ['string'] );
    setLogLevel = Module.cwrap( 'setLogLevel', 'number', ['number'] );

    // initialize a VM
    initChuckInstance = Module.cwrap( 'initChuckInstance', 'number', ['number', 'number', 'number', 'number'] );
    
    
    
    // cleanup 
    clearChuckInstance = Module.cwrap( 'clearChuckInstance', 'number', ['number'] );
    clearGlobals = Module.cwrap( 'clearGlobals', 'number', ['number'] );
    cleanupChuckInstance = Module.cwrap( 'cleanupChuckInstance', 'number', ['number'] );
    cleanRegisteredChucks = Module.cwrap( 'cleanRegisteredChucks', null );

    // running code 
    runChuckCode = Module.cwrap( 'runChuckCode', 'number', ['number', 'string'] );
    runChuckCodeWithReplacementDac = Module.cwrap( 'runChuckCodeWithReplacementDac', 'number', ['number', 'string', 'string'] );
    runChuckFile = Module.cwrap( 'runChuckFile', 'number', ['number', 'string'] );
    runChuckFileWithReplacementDac = Module.cwrap( 'runChuckFileWithReplacementDac', 'number', ['number', 'string', 'string'] );
    runChuckFileWithArgs = Module.cwrap( 'runChuckFileWithArgs', 'number', ['number', 'string', 'string'] );
    runChuckFileWithArgsWithReplacementDac = Module.cwrap( 'runChuckFileWithArgsWithReplacementDac', 'number', ['number', 'string', 'string', 'string'] );
    
    replaceChuckCode = Module.cwrap( 'replaceChuckCode', 'number', ['number', 'number', 'string'] );
    replaceChuckCodeWithReplacementDac = Module.cwrap( 'replaceChuckCodeWithReplacementDac', 'number', ['number', 'number', 'string', 'string'] );
    replaceChuckFile = Module.cwrap( 'replaceChuckFile', 'number', ['number', 'number', 'string'] );
    replaceChuckFileWithReplacementDac = Module.cwrap( 'replaceChuckFileWithReplacementDac', 'number', ['number', 'number', 'string', 'string'] );
    replaceChuckFileWithArgs = Module.cwrap( 'replaceChuckFileWithArgs', 'number', ['number', 'number', 'string', 'string'] );
    replaceChuckFileWithArgsWithReplacementDac = Module.cwrap( 'replaceChuckFileWithArgsWithReplacementDac', 'number', ['number', 'number', 'string', 'string', 'string'] );
    
    
    isShredActive = Module.cwrap( 'isShredActive', 'number', ['number', 'number'] );
    removeShred = Module.cwrap( 'removeShred', 'number', ['number', 'number'] );

    // don't care about these because emscripten has another mechanism
    // for rerouting print statements
    // setChoutCallback = 
    // setCherrCallback = 
    // setStdoutCallback = 
    // setStderrCallback = 

    // set/get global int, float, string
    setChuckInt = Module.cwrap( 'setChuckInt', 'number', ['number', 'string', 'number'] );
    getChuckInt = Module.cwrap( 'getChuckInt', 'number', ['number', 'string'] );
    setChuckFloat = Module.cwrap( 'setChuckFloat', 'number', ['number', 'string', 'number'] );
    getChuckFloat = Module.cwrap( 'getChuckFloat', 'number', ['number', 'string'] );
    setChuckString = Module.cwrap( 'setChuckString', 'number', ['number', 'string', 'string'] );
    getChuckString = Module.cwrap( 'getChuckString', 'number', ['number', 'string', 'number'] );

    // global Events
    signalChuckEvent = Module.cwrap( 'signalChuckEvent', 'number', ['number', 'string'] );
    broadcastChuckEvent = Module.cwrap( 'broadcastChuckEvent', 'number', ['number', 'string'] );
    listenForChuckEventOnce = Module.cwrap( 'listenForChuckEventOnce', 'number', ['number', 'string', 'number'] );
    startListeningForChuckEvent = Module.cwrap( 'startListeningForChuckEvent', 'number', ['number', 'string', 'number']);
    stopListeningForChuckEvent = Module.cwrap( 'stopListeningForChuckEvent', 'number', ['number', 'string', 'number']);

    // note: array is SAMPLE == Float32
    // NOTE ALSO anything using arrays cannot use cwrap; use in similar manner to Module._chuckManualAudioCallback
    // TODO
    // getGlobalUGenSamples = Module.cwrap( 'getGlobalUGenSamples', 'number', ['number', 'string', 'array', 'number'] );

    // set/get global int arrays
    // note: anything using arrays cannot use cwrap (since 'array' is an Int64Array)
    //  called manually with heap manipulation below
    //  setGlobalIntArray = Module.cwrap( 'setGlobalIntArray', 'number', ['number', 'string', 'array', 'number'] );
    getGlobalIntArray = Module.cwrap( 'getGlobalIntArray', 'number', ['number', 'string', 'number'] );
    setGlobalIntArrayValue = Module.cwrap( 'setGlobalIntArrayValue', 'number', ['number', 'string', 'number', 'number'] );
    getGlobalIntArrayValue = Module.cwrap( 'getGlobalIntArrayValue', 'number', ['number', 'string', 'number'] );
    setGlobalAssociativeIntArrayValue = Module.cwrap( 'setGlobalAssociativeIntArrayValue', 'number', ['number', 'string', 'string', 'number'] );
    getGlobalAssociativeIntArrayValue = Module.cwrap( 'getGlobalAssociativeIntArrayValue', 'number', ['number', 'string', 'string'] );

    // set/get global float arrays
    // note: anything using arrays cannot use cwrap. called manually with heap manipulation below
    //  setGlobalFloatArray = Module.cwrap( 'setGlobalFloatArray', 'number', ['number', 'string', 'array', 'number'] );
    getGlobalFloatArray = Module.cwrap( 'getGlobalFloatArray', 'number', ['number', 'string', 'number'] );
    setGlobalFloatArrayValue = Module.cwrap( 'setGlobalFloatArrayValue', 'number', ['number', 'string', 'number', 'number'] );
    getGlobalFloatArrayValue = Module.cwrap( 'getGlobalFloatArrayValue', 'number', ['number', 'string', 'number'] );
    setGlobalAssociativeFloatArrayValue = Module.cwrap( 'setGlobalAssociativeFloatArrayValue', 'number', ['number', 'string', 'string', 'number'] );
    getGlobalAssociativeFloatArrayValue = Module.cwrap( 'getGlobalAssociativeFloatArrayValue', 'number', ['number', 'string', 'string'] );

    // set data dir to "/" for embedded files
    setDataDir( "/" );
}

class ChuckNode extends AudioWorkletProcessor
{
    constructor( options ) 
    {
        super();
        
        this.srate = options.processorOptions.srate;
        this.inChannels = options.outputChannelCount[0];
        this.outChannels = options.outputChannelCount[0];
        
        this.myID = options.processorOptions.chuckID;
        
        chucks[this.myID] = this;
        
        // do this in response to an incoming message
        this.port.onmessage = this.handle_message.bind(this);
                    
        this.myPointers = {}
        this.myActiveShreds = [];
        this.haveInit = false;

        this.init( options.processorOptions.preloadedFiles, 
                   options.processorOptions.wasm );
    }
    
    init( preloadedFiles, wasm )
    {
        if( !globalInit )
        {
            var PreModule = {
                wasmBinary: wasm,
                print: (function( self ) 
                    {
                        return function( text )
                        {
                            console.log( text );
                            self.port.postMessage( { type: "console print", message: text } );
                        }
                    })( this ),
                printErr: (function( self )
                    {
                        return function( text )
                        {
                            console.error( text );
                            self.port.postMessage( { type: "console print", message: text } );
                        }
                    })( this ),
                // don't try to decode audio files; I'm really truly trying to copy the binaries only I promise
                noAudioDecoding: true,
                // don't try to decode images either, just so it will stop printing warnings it can't do it
                noImageDecoding: true
            };
    
            PreModule[ "preRun" ] = [
                (function( filesToPreload, Module )
                {
                    return function()
                    {
                        for( var i = 0; i < filesToPreload.length; i++ )
                        {
                            Module["FS_createPreloadedFile"]( "/", 
                                filesToPreload[i].filename, 
                                filesToPreload[i].data, true, true );
                        }
                    }
                })( preloadedFiles, PreModule )
            ]
            
            globalPromise = ChucK( PreModule );
            globalInit = true;
        }
        globalPromise.then( (function( self )
        {
            return function( Module ) 
            {
                self.Module = Module;

                initGlobals( Module );

                // Allocate the buffer for the heap access. Start with stereo, but it can
                // be expanded up to 32 channels.
                self._heapInputBuffer = new HeapAudioBuffer(Module, RENDER_QUANTUM_FRAMES,
                    self.inChannels, MAX_CHANNEL_COUNT);
                self._heapOutputBuffer = new HeapAudioBuffer(Module, RENDER_QUANTUM_FRAMES,
                    self.outChannels, MAX_CHANNEL_COUNT);

                initChuckInstance( self.myID, self.srate, self.inChannels, self.outChannels );
                // setLogLevel( 10 );
                self.haveInit = true;
                
                self.port.postMessage( { type: 'initCallback' } );
            }
        })( this ) );   
    }
    
    handleNewShredID( newShredID, shredCallback )
    {
        if( newShredID > 0 )
        {
            // keep track for myself
            this.myActiveShreds.push( newShredID );
        }
        else
        {
            // compilation failed
        }
        // tell the host
        this.port.postMessage( { 
            type: "newShredCallback", 
            callback: shredCallback,
            shred: newShredID
        } );
    }
    
    handleReplacedShredID( oldShredID, newShredID, shredCallback )
    {
        if( newShredID > 0 )
        {
            // keep track for myself
            this.myActiveShreds.push( newShredID );
        }
        else
        {
            // compilation failed --> we did not actually remove oldShredID
            this.myActiveShreds.push( oldShredID );
        }
        // tell the host
        this.port.postMessage( { 
            type: "replacedShredCallback", 
            callback: shredCallback,
            newShred: newShredID,
            oldShred: oldShredID
        } );
    }
    
    handleRemoveShred( shredID, callback )
    {
        if( removeShred( this.myID, shredID ) )
        {
            this.handleRemovedShredID( shredID, callback );
        }
        else
        {
            this.handleRemovedShredID( 0, callback );
        }
    }
    
    handleRemovedShredID( shredID, shredCallback )
    {
        this.port.postMessage( { 
            type: "removedShredCallback", 
            callback: shredCallback,
            shred: shredID
        } );
    }
    
    findMostRecentActiveShred()
    {
        // find the most recent shred that is still active,
        // and forget about all the more recently shredded ones
        // that are no longer active
        var shredID = this.myActiveShreds.pop();
        while( shredID && !isShredActive( this.myID, shredID ) )
        {
            shredID = this.myActiveShreds.pop();
        }
        return shredID;
    }
    
    findShredToReplace()
    {
        var shredToReplace = this.findMostRecentActiveShred();
        if( !shredToReplace )
        {
            this.port.postMessage( { 
                type: "console print", 
                message: "[chuck](VM): no shreds to replace..." 
            } );
        }
        return shredToReplace;
    }
    
    handle_message( event )
    {
        switch( event.data.type )
        {
        // ================== Filesystem ===================== //
            case 'loadFile':
                this.Module.FS_createDataFile( '/' + event.data.directory, 
                    event.data.filename, event.data.data, true, true, true );
                break;
        // ================== Run / Compile ================== //
            case 'runChuckCode':
                var shredID = runChuckCode( this.myID, event.data.code );
                this.handleNewShredID( shredID, event.data.callback );
                break;
            case 'runChuckCodeWithReplacementDac':
                var shredID = runChuckCodeWithReplacementDac( this.myID, event.data.code, event.data.dac_name );
                this.handleNewShredID( shredID, event.data.callback );
                break;
            case 'runChuckFile':
                var shredID = runChuckFile( this.myID, event.data.filename );
                this.handleNewShredID( shredID, event.data.callback );
                break;
            case 'runChuckFileWithReplacementDac':
                var shredID = runChuckFileWithReplacementDac( this.myID, event.data.filename, event.data.dac_name );
                this.handleNewShredID( shredID, event.data.callback );
                break;
            case 'runChuckFileWithArgs':
                var shredID = runChuckFileWithArgs( this.myID, event.data.filename, event.data.colon_separated_args );
                this.handleNewShredID( shredID, event.data.callback );
                break;
            case 'runChuckFileWithArgsWithReplacementDac':
                var shredID = runChuckFileWithArgsWithReplacementDac( this.myID, event.data.filename, event.data.colon_separated_args, event.data.dac_name );
                this.handleNewShredID( shredID, event.data.callback );
                break;
            case 'replaceChuckCode':
                var shredToReplace = this.findShredToReplace();
                if( shredToReplace )
                {
                    var shredID = replaceChuckCode( this.myID, shredToReplace, event.data.code );
                    this.handleReplacedShredID( shredToReplace, shredID, event.data.callback );
                }
                break;
            case 'replaceChuckCodeWithReplacementDac':
                var shredToReplace = this.findShredToReplace();
                if( shredToReplace )
                {
                    var shredID = replaceChuckCodeWithReplacementDac( this.myID, shredToReplace, event.data.code, event.data.dac_name );
                    this.handleReplacedShredID( shredToReplace, shredID, event.data.callback );
                }                    
                break;
            case 'replaceChuckFile':
                var shredToReplace = this.findShredToReplace();
                if( shredToReplace )
                {
                    var shredID = replaceChuckFile( this.myID, shredToReplace, event.data.filename );
                    this.handleReplacedShredID( shredToReplace, shredID, event.data.callback );
                }
                break;
            case 'replaceChuckFileWithReplacementDac':
                var shredToReplace = this.findShredToReplace();
                if( shredToReplace )
                {
                    var shredID = replaceChuckFileWithReplacementDac( this.myID, shredToReplace, event.data.filename, event.data.dac_name );
                    this.handleReplacedShredID( shredToReplace, shredID, event.data.callback );
                }
                break;
            case 'replaceChuckFileWithArgs':
                var shredToReplace = this.findShredToReplace();
                if( shredToReplace )
                {
                    var shredID = replaceChuckFileWithArgs( this.myID, shredToReplace, event.data.filename, event.data.colon_separated_args );
                    this.handleReplacedShredID( shredToReplace, shredID, event.data.callback );
                }   
                break;
            case 'replaceChuckFileWithArgsWithReplacementDac':
                var shredToReplace = this.findShredToReplace();
                if( shredToReplace )
                {
                    var shredID = replaceChuckFileWithArgsWithReplacementDac( this.myID, shredToReplace, event.data.filename, event.data.colon_separated_args, event.data.dac_name );
                    this.handleReplacedShredID( shredToReplace, shredID, event.data.callback );
                }                    
                break;
            case 'removeLastCode':
                var shredID = this.findMostRecentActiveShred();
                // if we found a shred, remove it, otherwise,
                // there are no shreds left to remove
                if( shredID )
                {
                    this.handleRemoveShred( shredID, event.data.callback );
                }
                else
                {
                    this.port.postMessage( { 
                        type: "console print", 
                        message: "[chuck](VM): no shreds to remove..." 
                    } );
                }
                break;
            case 'removeShred':
                this.handleRemoveShred( event.data.shred, event.data.callback );
                break;
            case 'isShredActive':
                this.port.postMessage({
                    type: "intCallback",
                    callback: event.data.callback,
                    result: isShredActive( this.myID, event.data.shred )
                });
                break;
        // ================== Int, Float, String ============= //
            case 'setChuckInt':
                setChuckInt( this.myID, event.data.variable, event.data.value );
                break;
            case 'getChuckInt':
                var result = getChuckInt( this.myID, event.data.variable );
                this.port.postMessage( { type: "intCallback", callback: event.data.callback, result: result } );
                break;
            case 'setChuckFloat':
                setChuckFloat( this.myID, event.data.variable, event.data.value );
                break;
            case 'getChuckFloat':
                var result = getChuckFloat( this.myID, event.data.variable );
                this.port.postMessage( { type: "floatCallback", callback: event.data.callback, result: result } );
                break;
            case 'setChuckString':
                setChuckString( this.myID, event.data.variable, event.data.value );
                break;
            case 'getChuckString':
                (function( thePort, theCallback, theVariable, theID, Module ) 
                {
                    var pointer = Module.addFunction( (function(thePort, theCallback)
                    {   
                        return function( string_ptr )
                        {
                            thePort.postMessage( { 
                                type: "stringCallback", 
                                callback: theCallback, 
                                result: Module.UTF8ToString( string_ptr )
                            } );
                            Module.removeFunction( pointer );
                        }
                    })(thePort, theCallback), 'vi' );
                    getChuckString( theID, theVariable, pointer );
                })(this.port, event.data.callback, event.data.variable, this.myID, this.Module);
                break;
        // ================== Event =================== //
            case 'signalChuckEvent':
                signalChuckEvent( this.myID, event.data.variable );
                break;
            case 'broadcastChuckEvent':
                broadcastChuckEvent( this.myID, event.data.variable );
                break;
            case 'listenForChuckEventOnce':
                (function( thePort, theCallback, theVariable, theID, Module ) 
                {
                    var pointer = Module.addFunction( (function(thePort, theCallback)
                    {   
                        return function()
                        {
                            thePort.postMessage( { type: "eventCallback", callback: theCallback } );
                            Module.removeFunction( pointer );
                        }
                    })(thePort, theCallback), 'v' );
                    listenForChuckEventOnce( theID, theVariable, pointer );
                })(this.port, event.data.callback, event.data.variable, this.myID, this.Module);
                break;
            case 'startListeningForChuckEvent':
                this.myPointers[event.data.callback] = this.Module.addFunction( (function(thePort, theCallback)
                {
                    return function() 
                    {
                        thePort.postMessage( { type: "eventCallback", callback: theCallback } );
                    }
                })(this.port, event.data.callback), 'v' );
                startListeningForChuckEvent( this.myID, event.data.variable, this.myPointers[event.data.callback] );
                break;
            case 'stopListeningForChuckEvent':
                stopListeningForChuckEvent( this.myID, event.data.variable, this.myPointers[event.data.callback] );
                this.Module.removeFunction( this.myPointers[event.data.callback] );
                break;
        // ================== Int[] =================== //
            case 'setGlobalIntArray':
                // convert to Int32Array
                var values = new Int32Array( event.data.values );
                // put onto heap
                var valuesPtr = this.Module._malloc( values.length * values.BYTES_PER_ELEMENT );
                var heapview = this.Module.HEAP32.subarray( (valuesPtr >> 2), (valuesPtr >> 2) + values.length );
                heapview.set( values );
                
                // put variable name on heap as well
                var stringBytes = event.data.variable.length << 2 + 1;
                var stringPtr = this.Module._malloc( stringBytes );
                this.Module.stringToUTF8( event.data.variable, stringPtr, stringBytes );
                
                // call
                this.Module._setGlobalIntArray( this.myID, stringPtr, valuesPtr, values.length );
                
                // free
                this.Module._free( valuesPtr );
                this.Module._free( stringPtr );
                break;
            case 'getGlobalIntArray':
                (function( thePort, theCallback, theVariable, theID, Module ) 
                {
                    var pointer = Module.addFunction( (function(thePort, theCallback)
                    {   
                        return function( int32_ptr, len )
                        {
                            var result = new Int32Array(
                                Module.HEAPU8.buffer,
                                int32_ptr,
                                len
                            );
                            thePort.postMessage( { 
                                type: "intArrayCallback", 
                                callback: theCallback, 
                                result: Array.from(result)
                            } );
                            Module.removeFunction( pointer );
                        }
                    })(thePort, theCallback), 'vii' );
                    getGlobalIntArray( theID, theVariable, pointer );
                })(this.port, event.data.callback, event.data.variable, this.myID, this.Module);
                break;
            case 'setGlobalIntArrayValue':
                setGlobalIntArrayValue( this.myID, event.data.variable, event.data.index, event.data.value );
                break;
            case 'getGlobalIntArrayValue':
                var result = getGlobalIntArrayValue( this.myID, event.data.variable, event.data.index );
                this.port.postMessage( { type: "intCallback", callback: event.data.callback, result: result } );
                break;
            case 'setGlobalAssociativeIntArrayValue':
                setGlobalAssociativeIntArrayValue( this.myID, event.data.variable, event.data.key, event.data.value );
                break;
            case 'getGlobalAssociativeIntArrayValue':
                var result = getGlobalAssociativeIntArrayValue( this.myID, event.data.variable, event.data.key );
                this.port.postMessage( { type: "intCallback", callback: event.data.callback, result: result } );
                break;
        // ================== Float[] =================== //
            case 'setGlobalFloatArray':
                // convert to Float64Array
                var values = new Float64Array( event.data.values );
                // put onto heap
                var valuesPtr = this.Module._malloc( values.length * values.BYTES_PER_ELEMENT );
                var heapview = this.Module.HEAPF64.subarray( (valuesPtr >> 3), (valuesPtr >> 3) + values.length );
                heapview.set( values );
                
                // put variable name on heap as well
                var stringBytes = event.data.variable.length << 2 + 1;
                var stringPtr = this.Module._malloc( stringBytes );
                this.Module.stringToUTF8( event.data.variable, stringPtr, stringBytes );
                
                // call
                this.Module._setGlobalFloatArray( this.myID, stringPtr, valuesPtr, values.length );
                
                // free
                this.Module._free( valuesPtr );
                this.Module._free( stringPtr );
                break;
            case 'getGlobalFloatArray':
                (function( thePort, theCallback, theVariable, theID, Module ) 
                {
                    var pointer = Module.addFunction( (function(thePort, theCallback)
                    {   
                        return function( float64_ptr, len )
                        {
                            var result = new Float64Array(
                                Module.HEAPU8.buffer,
                                float64_ptr,
                                len
                            );
                            thePort.postMessage( { 
                                type: "floatArrayCallback", 
                                callback: theCallback, 
                                result: Array.from(result)
                            } );
                            Module.removeFunction( pointer );
                        }
                    })(thePort, theCallback), 'vii' );
                    getGlobalFloatArray( theID, theVariable, pointer );
                })(this.port, event.data.callback, event.data.variable, this.myID, this.Module);
                break;
            case 'setGlobalFloatArrayValue':
                setGlobalFloatArrayValue( this.myID, event.data.variable, event.data.index, event.data.value );
                break;
            case 'getGlobalFloatArrayValue':
                var result = getGlobalFloatArrayValue( this.myID, event.data.variable, event.data.index );
                this.port.postMessage( { type: "floatCallback", callback: event.data.callback, result: result } );
                break;
            case 'setGlobalAssociativeFloatArrayValue':
                setGlobalAssociativeFloatArrayValue( this.myID, event.data.variable, event.data.key, event.data.value );
                break;
            case 'getGlobalAssociativeFloatArrayValue':
                var result = getGlobalAssociativeFloatArrayValue( this.myID, event.data.variable, event.data.key );
                this.port.postMessage( { type: "floatCallback", callback: event.data.callback, result: result } );
                break;
        // ==================== VM Functions ====================== //
            case 'clearChuckInstance':
                clearChuckInstance( this.myID );
                break;
            case 'clearGlobals':
                clearGlobals( this.myID );
                this.port.postMessage( { 
                    type: "console print", 
                    message: "[chuck](VM): resetting all global variables" 
                } );
                break;
            default:
                break;
        }
        
    }

    process(inputs, outputs, parameters) 
    {
        if( !this.haveInit ) { return true; }
        // structure for two-channel audio is [[Float32Array,Float32Array]]
        // since it's ONE output (outer array) with TWO channels (inner array of arrays)
        let input = inputs[0];
        let output = outputs[0];
        // recompute subarray views just in case memory has grown recently
        this._heapInputBuffer.adaptChannel( input.length );

        // copy input
        for (let channel = 0; channel < input.length; channel++)
        {
            // ... but only if they actually gave us something
            if( input[channel].length > 0 )
            {
                this._heapInputBuffer.getChannelData(channel).set(input[channel]);
            }
        }
        
        // process
        // for multichannel, WebAudio uses planar buffers.
        // this version of ChucK has been specially compiled to do the same
        // (ordinarily, ChucK uses interleaved buffers since it processes
        //  sample by sample)
        this.Module._chuckManualAudioCallback( 
            this.myID,        // chuck ID
            this._heapInputBuffer.getHeapAddress(),
            this._heapOutputBuffer.getHeapAddress(),
            output[0].length, // frame size (probably 128)
            this.inChannels,  // in channels
            this.outChannels  // out channels
        );
        
        // recompute subarray views just in case memory grew while we
        // were calling chuck audio callback
        this._heapOutputBuffer.adaptChannel( output.length );
        
        // copy output
        for (let channel = 0; channel < output.length; channel++) 
        {
            output[channel].set(this._heapOutputBuffer.getChannelData(channel));
        }
        
        return true;
    }

}

class ChuckSubNode extends AudioWorkletProcessor
{
    constructor( options ) 
    {
        super();
        
        this.inChannels = 0;
        // SHOULD BE 1
        this.outChannels = options.outputChannelCount[0];
        
        
        this.myID = options.processorOptions.chuckID;
        this.myDac = options.processorOptions.dac;
        
        // do this in response to an incoming message
        this.port.onmessage = this.handle_message.bind(this);
        
        console.assert( globalInit && this.myID in chucks, "ChuckSubNode can't find its ChuckNode with ID " + this.myID );
        this.myChuck = chucks[ this.myID ];

        this.haveInit = false;
        this.init();
    }
    
    init()
    {
        globalPromise.then( (function( self )
        {
            return function( Module ) 
            {
                self.Module = Module;

                // Allocate the buffer for the heap access. Start with however many channels we have
                // (should be 1)
                self._heapOutputBuffer = new HeapAudioBuffer(Module, RENDER_QUANTUM_FRAMES,
                    self.outChannels, MAX_CHANNEL_COUNT);
                
                var stringBytes = self.myDac.length << 2 + 1;
                self.myDacCString = self.Module._malloc( stringBytes );
                self.Module.stringToUTF8( self.myDac, self.myDacCString, stringBytes );

                self.haveInit = true;
                
                self.port.postMessage( { type: 'initCallback' } );
            }
        })( this ) );   
    }

    handle_message( event )
    {
        switch( event.data.type )
        {
            default:
                break;
        }
        
    }

    process( inputs, outputs, parameters ) 
    {
        if( !this.haveInit ) { return true; }

        let output = outputs[0];
        
        // fetch
        this.Module._getGlobalUGenSamples(
            this.myID,
            this.myDacCString,
            this._heapOutputBuffer.getHeapAddress(),
            output[0].length // frame size (probably 128)
        );
        
        // recompute subarray views just in case memory grew recently
        this._heapOutputBuffer.adaptChannel( output.length );
        
        // copy output
        for (let channel = 0; channel < output.length; channel++) 
        {
            output[channel].set(this._heapOutputBuffer.getChannelData(channel));
        }
        
        return true;
    }

}

registerProcessor( 'chuck-node', ChuckNode );
registerProcessor( 'chuck-sub-node', ChuckSubNode );

// ------------------------------------------------------------------------ //
// ------------------------------------------------------------------------ //
// ------------------------------------------------------------------------ //
// ------------------------------------------------------------------------ //
