/**
 * @license
 * Copyright 2023 The Emscripten Authors
 * SPDX-License-Identifier: MIT
 */

assert(SHARED_MEMORY);

addToLibrary({
// Chrome 87 shipped Atomics.waitAsync:
//   https://www.chromestatus.com/feature/6243382101803008
// However its implementation is faulty:
//   https://bugs.chromium.org/p/chromium/issues/detail?id=1167541
// Firefox Nightly 86.0a1 (2021-01-15) does not yet have it:
//   https://bugzilla.mozilla.org/show_bug.cgi?id=1467846
// And at the time of writing, no other browser has it either.
#if MIN_CHROME_VERSION < 91 || MIN_SAFARI_VERSION != TARGET_NOT_SUPPORTED || MIN_FIREFOX_VERSION != TARGET_NOT_SUPPORTED || ENVIRONMENT_MAY_BE_NODE
  // Partially polyfill Atomics.waitAsync() if not available in the browser.
  // Also polyfill for old Chrome-based browsers, where Atomics.waitAsync is
  // broken until Chrome 91, see:
  //   https://bugs.chromium.org/p/chromium/issues/detail?id=1167541
  //   https://github.com/tc39/proposal-atomics-wait-async/blob/master/PROPOSAL.md
  // This polyfill performs polling with setTimeout() to observe a change in the
  // target memory location.
  $waitAsyncPolyfilled: '=(!Atomics.waitAsync || (globalThis.navigator?.userAgent && Number((navigator.userAgent.match(/Chrom(e|ium)\\/([0-9]+)\\./)||[])[2]) < 91));',
  $polyfillWaitAsync__deps: ['$waitAsyncPolyfilled'],
  $polyfillWaitAsync__postset: `if (waitAsyncPolyfilled) {
  let __Atomics_waitAsyncAddresses = [/*[i32a, index, value, maxWaitMilliseconds, promiseResolve]*/];
  function __Atomics_pollWaitAsyncAddresses() {
    let now = performance.now();
    let l = __Atomics_waitAsyncAddresses.length;
    for (let i = 0; i < l; ++i) {
      let a = __Atomics_waitAsyncAddresses[i];
      let expired = (now > a[3]);
      let awoken = (Atomics.load(a[0], a[1]) != a[2]);
      if (expired || awoken) {
        __Atomics_waitAsyncAddresses[i--] = __Atomics_waitAsyncAddresses[--l];
        __Atomics_waitAsyncAddresses.length = l;
        a[4](awoken ? 'ok': 'timed-out');
      }
    }
    if (l) {
      // If we still have addresses to wait, loop the timeout handler to continue polling.
      setTimeout(__Atomics_pollWaitAsyncAddresses, 10);
    }
  }
  #if ASSERTIONS && WASM_WORKERS
    if (!ENVIRONMENT_IS_WASM_WORKER) err('Current environment does not support Atomics.waitAsync(): polyfilling it, but this is going to be suboptimal.');
  #endif
  Atomics.waitAsync = (i32a, index, value, maxWaitMilliseconds) => {
    let val = Atomics.load(i32a, index);
    if (val != value) return { async: false, value: 'not-equal' };
    if (maxWaitMilliseconds <= 0) return { async: false, value: 'timed-out' };
    maxWaitMilliseconds = performance.now() + (maxWaitMilliseconds || Infinity);
    let promiseResolve;
    let promise = new Promise((resolve) => { promiseResolve = resolve; });
    if (!__Atomics_waitAsyncAddresses[0]) setTimeout(__Atomics_pollWaitAsyncAddresses, 10);
    __Atomics_waitAsyncAddresses.push([i32a, index, value, maxWaitMilliseconds, promiseResolve]);
    return { async: true, value: promise };
  };
}`,
#else
  $waitAsyncPolyfilled: false,
#endif

  $polyfillWaitAsync__internal: true,
  $polyfillWaitAsync: () => {
    // nop, used for its postset to ensure `Atomics.waitAsync()` polyfill is
    // included exactly once and only included when needed.
    // Any function using Atomics.waitAsync should depend on this.
  },

#if ASYNCIFY
  _emscripten_atomic_wait_promise__deps: ['$polyfillWaitAsync', '$atomicWaitStates', '$addPromise'],
  _emscripten_atomic_wait_promise: (addr, val, maxWaitMilliseconds) => {
    var wait = Atomics.waitAsync(HEAP32, {{{ getHeapOffset('addr', 'i32') }}}, val, maxWaitMilliseconds);
    if (wait.async) {
      // In the async case return the promise ID.
      var chainedPromise = wait.value.then((value) => atomicWaitStates.indexOf(value));
      var id = addPromise(chainedPromise);
      return id;
    }
    // In the synchronous case return the negative result code
    return -atomicWaitStates.indexOf(wait.value);
  },
#else
  _emscripten_atomic_wait_promise: (addr, val, maxWaitMilliseconds) => {
    abort('Please compile your program with async support in order to use asynchronous operations like emscripten_atomic_wait_suspending');
  },
#endif

  $atomicWaitStates__internal: true,
  $atomicWaitStates: ['ok', 'not-equal', 'timed-out'],
  $liveAtomicWaitAsyncs: {},
  $liveAtomicWaitAsyncs__internal: true,
  $liveAtomicWaitAsyncCounter: 0,
  $liveAtomicWaitAsyncCounter__internal: true,

  emscripten_atomic_wait_async__deps: ['$atomicWaitStates', '$liveAtomicWaitAsyncs', '$liveAtomicWaitAsyncCounter', '$polyfillWaitAsync', '$callUserCallback'],
  emscripten_atomic_wait_async: (addr, val, asyncWaitFinished, userData, maxWaitMilliseconds) => {
    let wait = Atomics.waitAsync(HEAP32, {{{ getHeapOffset('addr', 'i32') }}}, val, maxWaitMilliseconds);
    if (!wait.async) return atomicWaitStates.indexOf(wait.value);
    // Increment waitAsync generation counter, account for wraparound in case
    // application does huge amounts of waitAsyncs per second (not sure if
    // possible?)
    // Valid counter range: 0...2^31-1
    let counter = liveAtomicWaitAsyncCounter;
    liveAtomicWaitAsyncCounter = Math.max(0, (liveAtomicWaitAsyncCounter+1)|0);
    liveAtomicWaitAsyncs[counter] = addr;
    {{{ runtimeKeepalivePush() }}}
    wait.value.then((value) => {
      if (liveAtomicWaitAsyncs[counter]) {
        {{{ runtimeKeepalivePop() }}}
        delete liveAtomicWaitAsyncs[counter];
        callUserCallback(() => {{{ makeDynCall('vpiip', 'asyncWaitFinished') }}}(addr, val, atomicWaitStates.indexOf(value), userData));
      }
    });
    return -counter;
  },

  emscripten_atomic_cancel_wait_async__deps: ['$liveAtomicWaitAsyncs'],
  emscripten_atomic_cancel_wait_async: (waitToken) => {
#if ASSERTIONS
    if (waitToken == {{{ cDefs.ATOMICS_WAIT_NOT_EQUAL }}}) {
      warnOnce('Attempted to call emscripten_atomic_cancel_wait_async() with a value ATOMICS_WAIT_NOT_EQUAL (1) that is not a valid wait token! Check success in return value from call to emscripten_atomic_wait_async()');
    } else if (waitToken == {{{ cDefs.ATOMICS_WAIT_TIMED_OUT }}}) {
      warnOnce('Attempted to call emscripten_atomic_cancel_wait_async() with a value ATOMICS_WAIT_TIMED_OUT (2) that is not a valid wait token! Check success in return value from call to emscripten_atomic_wait_async()');
    } else if (waitToken > 0) {
      warnOnce(`Attempted to call emscripten_atomic_cancel_wait_async() with an invalid wait token value ${waitToken}`);
    }
#endif
    var address = liveAtomicWaitAsyncs[waitToken];
    if (address) {
      // Notify the waitAsync waiters on the memory location, so that JavaScript
      // garbage collection can occur.
      // See https://github.com/WebAssembly/threads/issues/176
      // This has the unfortunate effect of causing spurious wakeup of all other
      // waiters at the address (which causes a small performance loss).
      Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
      delete liveAtomicWaitAsyncs[waitToken];
      {{{ runtimeKeepalivePop() }}}
      return {{{ cDefs.EMSCRIPTEN_RESULT_SUCCESS }}};
    }
    // This waitToken does not exist.
    return {{{ cDefs.EMSCRIPTEN_RESULT_INVALID_PARAM }}};
  },

  emscripten_atomic_cancel_all_wait_asyncs__deps: ['$liveAtomicWaitAsyncs'],
  emscripten_atomic_cancel_all_wait_asyncs: () => {
    let waitAsyncs = Object.values(liveAtomicWaitAsyncs);
    for (var address of waitAsyncs) {
      Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
    }
    liveAtomicWaitAsyncs = {};
    return waitAsyncs.length;
  },

  emscripten_atomic_cancel_all_wait_asyncs_at_address__deps: ['$liveAtomicWaitAsyncs'],
  emscripten_atomic_cancel_all_wait_asyncs_at_address: (address) => {
    let numCancelled = 0;
    for (var [waitToken, waitAddress] of Object.entries(liveAtomicWaitAsyncs)) {
      if (waitAddress == address) {
        Atomics.notify(HEAP32, {{{ getHeapOffset('address', 'i32') }}});
        delete liveAtomicWaitAsyncs[waitToken];
        numCancelled++;
      }
    }
    return numCancelled;
  },

  emscripten_has_threading_support: () => !!globalThis.SharedArrayBuffer,

  emscripten_num_logical_cores: () =>
#if ENVIRONMENT_MAY_BE_NODE
    ENVIRONMENT_IS_NODE ? require('node:os').cpus().length :
#endif
    navigator['hardwareConcurrency'],

  emscripten_atomics_is_lock_free: (width) => Atomics.isLockFree(width),
});
