#define _GNU_SOURCE // for gettid
#include <emscripten/console.h>
#include <emscripten/webaudio.h>
#include <emscripten/threading.h>
#include <assert.h>
#include <unistd.h>

/* Steps to use Wasm-based AudioWorklets:
  1. Create a Web Audio AudioContext either via manual JS code and calling
     emscriptenRegisterAudioObject() from JS, or by calling
     emscripten_create_audio_context() (shown in this sample)
  2. Initialize a Wasm AudioWorklet scope on the audio context by calling
     emscripten_start_wasm_audio_worklet_thread_async(). This shares the Wasm
     Module, Memory, etc. to the AudioWorklet scope, and establishes the stack
     space for the Audio Worklet.
     This needs to be called exactly once during page's lifetime. There is no
     mechanism in Web Audio to shut down/uninitialize the scope.
  3. Create one or more of Audio Worklet Processors with the desired name and
     AudioParam configuration.
  4. Instantiate Web Audio audio graph nodes from the above created worklet
     processors, specifying the desired input-output configurations and Wasm-side
     function callbacks to call for each node.
  5. Add the graph nodes to the Web Audio graph, and the audio callbacks should
     begin to fire.
*/

EMSCRIPTEN_WEBAUDIO_T context;

// TEST_AND_EXIT is defined when running in the Emscripten test harness. You can
// strip these out in your own project (otherwise playback will end quickly).
#ifdef TEST_AND_EXIT
_Thread_local int testTlsVariable = 1;
int lastTlsVariableValueInAudioThread = 1;
#endif

bool worklet_started = false;
pid_t main_tid = 0;

// This function will be called for every fixed-size buffer of audio samples to be processed.
bool ProcessAudio(int numInputs,
                  const AudioSampleFrame* inputs,
                  int numOutputs,
                  AudioSampleFrame* outputs,
                  int numParams,
                  const AudioParamFrame* params,
                  void* userData) {
  assert(!emscripten_is_main_browser_thread());
  assert(emscripten_current_thread_is_audio_worklet());
  if (!worklet_started) {
    worklet_started = true;
    pid_t worklet_tid = gettid();
    emscripten_outf("worklet thread tid: %d\n", worklet_tid);
    assert(worklet_tid && worklet_tid > main_tid);
#ifdef __EMSCRIPTEN_PTHREADS__
    emscripten_outf("worklet thread pthread_self: %p\n", pthread_self());
    assert(pthread_self());
#endif
  }

#ifdef TEST_AND_EXIT
  // Only running in the test harness, see main_thread_tls_access()
  assert(testTlsVariable == lastTlsVariableValueInAudioThread);
  ++testTlsVariable;
  lastTlsVariableValueInAudioThread = testTlsVariable;
#endif

  // Verify that getentropy fails gracefully (i.e. returns non-zero) when called
  // inside a worklet (where crypto.getRandomValues is not supported);
  unsigned char bytes[16];
  int rtn = getentropy(bytes, sizeof(bytes));
  assert(rtn != 0);

  // Produce noise in all output channels.
  for (int i = 0; i < numOutputs; ++i) {
    for (int j = 0; j < outputs[i].samplesPerChannel*outputs[i].numberOfChannels; ++j) {
      outputs[i].data[j] = (rand() / (float)RAND_MAX * 2.0f - 1.0f) * 0.3f;
    }
  }

  // We generated audio and want to keep this processor going. Return false here to shut down.
  return true;
}

EM_JS(void, InitHtmlUi, (EMSCRIPTEN_WEBAUDIO_T audioContext), {
  // Add a button on the page to toggle playback as a response to user click.
  let startButton = document.createElement('button');
  startButton.innerHTML = 'Toggle playback';
  document.body.appendChild(startButton);

  audioContext = emscriptenGetAudioObject(audioContext);
  startButton.onclick = () => {
    if (audioContext.state != 'running') {
      audioContext.resume();
    } else {
      audioContext.suspend();
    }
  };
});

#ifdef TEST_AND_EXIT
bool main_thread_tls_access(double time, void *userData) {
  // Try to mess the TLS variable on the main thread, with the expectation that
  // it should not change the TLS value on the AudioWorklet thread, asserted in
  // ProcessAudio().
  testTlsVariable = (int)time;
  // Exit to the test harness after enough calls to ProcessAudio()
  if (lastTlsVariableValueInAudioThread >= 100) {
    emscripten_destroy_audio_context(context);
    emscripten_force_exit(0);
  }
  return true;
}
#endif

// This callback will fire after the Audio Worklet Processor has finished being
// added to the Worklet global scope.
void AudioWorkletProcessorCreated(EMSCRIPTEN_WEBAUDIO_T audioContext, bool success, void *userData) {
  assert(success && "Stopped in AudioWorkletProcessorCreated");

  // Specify the input and output node configurations for the Wasm Audio
  // Worklet. A simple setup with single mono output channel here, and no
  // inputs.
  int outputChannelCounts[1] = { 1 };

  EmscriptenAudioWorkletNodeCreateOptions options = {
    .numberOfInputs = 0,
    .numberOfOutputs = 1,
    .outputChannelCounts = outputChannelCounts,
    .channelCount = 1,
    .channelCountMode = WEBAUDIO_CHANNEL_COUNT_MODE_EXPLICIT,
    .channelInterpretation = WEBAUDIO_CHANNEL_INTERPRETATION_SPEAKERS,
  };

  // Instantiate the noise-generator Audio Worklet Processor.
  EMSCRIPTEN_WEBAUDIO_T wasmAudioWorklet = emscripten_create_wasm_audio_worklet_node(audioContext, "noise-generator", &options, &ProcessAudio, 0);
  // Connect the audio worklet node to the graph.
  emscripten_audio_node_connect(wasmAudioWorklet, audioContext, 0, 0);

#ifdef TEST_AND_EXIT
  // Schedule this to exit after ProcessAudio() has been called 100 times
  emscripten_set_timeout_loop(main_thread_tls_access, 10, 0);
#endif

  InitHtmlUi(audioContext);
}

// This callback will fire when the Wasm Module has been shared to the
// AudioWorklet global scope, and is now ready to begin adding Audio Worklet
// Processors.
void WebAudioWorkletThreadInitialized(EMSCRIPTEN_WEBAUDIO_T audioContext, bool success, void *userData) {
  assert(success && "Stopped in WebAudioWorkletThreadInitialized");

  WebAudioWorkletProcessorCreateOptions opts = {
    .name = "noise-generator",
  };
  emscripten_create_wasm_audio_worklet_processor_async(audioContext, &opts, AudioWorkletProcessorCreated, 0);
}

// Define a global stack space for the AudioWorkletGlobalScope. Note that all
// AudioWorkletProcessors and/or AudioWorkletNodes on the given Audio Context
// all share the same AudioWorkerGlobalScope, i.e. they all run on the same one
// audio thread (multiple nodes/processors do not each get their own thread).
// Hence one stack is enough.
uint8_t wasmAudioWorkletStack[4096];

int main() {
  assert(emscripten_is_main_browser_thread());
  assert(!emscripten_current_thread_is_audio_worklet());
  main_tid = gettid();
  emscripten_outf("main thread tid: %d\n", main_tid);
  emscripten_outf("wasmAudioWorkletStack: %p\n", wasmAudioWorkletStack);
  assert(main_tid);
#ifdef __EMSCRIPTEN_PTHREADS__
  emscripten_outf("main thread pthread_self: %p\n", pthread_self());
  assert(pthread_self());
#endif

  // Create an audio context
  context = emscripten_create_audio_context(0 /* use default constructor options */);

  // and kick off Audio Worklet scope initialization, which shares the Wasm
  // Module and Memory to the AudioWorklet scope and initializes its stack.
  emscripten_start_wasm_audio_worklet_thread_async(context, wasmAudioWorkletStack, sizeof(wasmAudioWorkletStack), WebAudioWorkletThreadInitialized, 0);

#ifdef TEST_AND_EXIT
  // We're in the test harness and exiting is via main_thread_tls_access()
  emscripten_exit_with_live_runtime();
#endif
}
