/*
 * Copyright 2021 The Emscripten Authors.  All rights reserved.
 * Emscripten is available under two separate licenses, the MIT license and the
 * University of Illinois/NCSA Open Source License.  Both these licenses can be
 * found in the LICENSE file.
 */

#pragma once

#include <functional>
#include <thread>

#include <emscripten/proxying.h>
#include <emscripten/threading.h>

extern "C" {
void _wasmfs_thread_utils_heartbeat(em_proxying_queue* ctx);
}

namespace emscripten {

// Helper class for synchronously proxying work to a dedicated worker thread,
// including where the work is asynchronous.
class ProxyWorker {
  // The queue we use to proxy work and the dedicated worker.
  ProxyingQueue queue;

  // Used to notify the calling thread once the worker has been started.
  bool started = false;
  std::mutex mutex;
  std::condition_variable cond;
  // Declare the thread last since it's dependent on the above member variables.
  // Declaring it last isn't strictly needed since the thread is initialized in
  // the body of the constructor, but is done out of caution.
  std::thread thread;

public:
  // Spawn the worker thread.
  ProxyWorker() : queue() {
    // Initialize the thread in the constructor to ensure the object has been
    // fully constructed before thread starts using the object to avoid a data
    // race. See #24370.
    thread = std::thread([&]() {
      // Notify the caller that we have started.
      {
        std::unique_lock<std::mutex> lock(mutex);
        started = true;
      }
      cond.notify_all();

      // Sometimes the main thread is spinning, waiting on a WasmFS lock held
      // by a thread trying to proxy work to this dedicated worker. In that
      // case, the proxying message won't be relayed by the main thread and
      // the system will deadlock. This heartbeat ensures that proxying work
      // eventually gets done so the thread holding the lock can make forward
      // progress even if the main thread is blocked.
      //
      // TODO: Remove this once we can postMessage directly between workers
      // without involving the main thread or once all browsers ship
      // Atomics.waitAsync.
      //
      // Note that this requires adding _emscripten_proxy_execute_queue to
      // EXPORTED_FUNCTIONS.
      _wasmfs_thread_utils_heartbeat(queue.queue);

      // Sit in the event loop performing work as it comes in.
      emscripten_exit_with_live_runtime();
    });

    // Make sure the thread has actually started before returning. This allows
    // subsequent code to assume the thread has already been spawned and not
    // worry about potential deadlocks where it holds a lock while proxying an
    // operation and meanwhile the main thread is blocked trying to acquire the
    // same lock so is never able to spawn the worker thread.
    //
    // Unfortunately, this solution would cause the main thread to deadlock on
    // itself, so for now assert that we are not on the main thread. In the
    // future, we could provide an asynchronous version of this utility that
    // calls a user callback once the worker has been started. This asynchronous
    // version would be safe to use on the main thread.
    assert(
      !emscripten_is_main_browser_thread() &&
      "cannot safely spawn dedicated workers from the main browser thread");
    {
      std::unique_lock<std::mutex> lock(mutex);
      cond.wait(lock, [&]() { return started; });
    }
  }

  // Kill the worker thread.
  ~ProxyWorker() {
    pthread_cancel(thread.native_handle());
    thread.join();
  }

  // Proxy synchronous work.
  void operator()(const std::function<void()>& func) {
    queue.proxySync(thread.native_handle(), func);
  }
  // Proxy asynchronous work that calls `finish()` on the ctx parameter to mark
  // its end.
  void operator()(const std::function<void(ProxyingQueue::ProxyingCtx)>& func) {
    queue.proxySyncWithCtx(thread.native_handle(), func);
  }
};

} // namespace emscripten
