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

//
// Async support via ASYNCIFY
//

addToLibrary({
  // error handling

  $runAndAbortIfError: (func) => {
    try {
      return func();
    } catch (e) {
      abort(e);
    }
  },

#if ASYNCIFY
  $Asyncify__deps: ['$runAndAbortIfError', '$callUserCallback',
#if ASSERTIONS
    '$createNamedFunction',
#endif
#if !MINIMAL_RUNTIME
    '$runtimeKeepalivePush', '$runtimeKeepalivePop',
#endif
#if ASYNCIFY == 1
    // Needed by allocateData and handleSleep respectively
    'malloc', 'free',
#endif
  ],

  $Asyncify: {
    //
    // Asyncify code that is shared between mode 1 (original) and mode 2 (JSPI).
    //
#if ASYNCIFY == 1 && MEMORY64
    rewindArguments: new Map(),
#endif
    instrumentWasmImports(imports) {
#if EMBIND_GEN_MODE
      // Instrumenting is not needed when generating code.
      return imports;
#endif
#if ASYNCIFY_DEBUG
      dbg('asyncify instrumenting imports');
#endif
#if ASSERTIONS && ASYNCIFY == 2
      assert('Suspending' in WebAssembly, 'JSPI not supported by current environment. Perhaps it needs to be enabled via flags?');
#endif
      var importPattern = {{{ new RegExp(`^(${ASYNCIFY_IMPORTS_EXCEPT_JS_LIBS.map(x => x.split('.')[1]).join('|').replace(/\*/g, '.*')})$`) }}};

      for (let [x, original] of Object.entries(imports)) {
        if (typeof original == 'function') {
          let isAsyncifyImport = original.isAsync || importPattern.test(x);
#if ASYNCIFY == 2
          // Wrap async imports with a suspending WebAssembly function.
          if (isAsyncifyImport) {
#if ASYNCIFY_DEBUG
            dbg('asyncify: suspendOnReturnedPromise for', x, original);
#endif
            imports[x] = original = new WebAssembly.Suspending(original);
          }
#endif
#if ASSERTIONS && ASYNCIFY != 2 // We cannot apply assertions with stack switching, as the imports must not be modified from suspender.suspendOnReturnedPromise TODO find a way
          imports[x] = (...args) => {
            var originalAsyncifyState = Asyncify.state;
            try {
              return original(...args);
            } finally {
              // Only asyncify-declared imports are allowed to change the
              // state.
              // Changing the state from normal to disabled is allowed (in any
              // function) as that is what shutdown does (and we don't have an
              // explicit list of shutdown imports).
              var changedToDisabled =
                    originalAsyncifyState === Asyncify.State.Normal &&
                    Asyncify.state        === Asyncify.State.Disabled;
              // invoke_* functions are allowed to change the state if we do
              // not ignore indirect calls.
              var ignoredInvoke = x.startsWith('invoke_') &&
                                  {{{ !ASYNCIFY_IGNORE_INDIRECT }}};
              if (Asyncify.state !== originalAsyncifyState &&
                  !isAsyncifyImport &&
                  !changedToDisabled &&
                  !ignoredInvoke) {
                abort(`import ${x} was not in ASYNCIFY_IMPORTS, but changed the state`);
              }
            }
          };
#if MAIN_MODULE
          // The dynamic library loader needs to be able to read .sig
          // properties, so that it knows function signatures when it adds
          // them to the table.
          imports[x].sig = original.sig;
#endif // MAIN_MODULE
#endif // ASSERTIONS
        }
      }
    },
#if ASYNCIFY == 1 && MEMORY64
    saveRewindArguments(func, passedArguments) {
      return Asyncify.rewindArguments.set(func, Array.from(passedArguments));
    },
    restoreRewindArguments(func) {
#if ASSERTIONS
      assert(Asyncify.rewindArguments.has(func));
#endif
      return Asyncify.rewindArguments.get(func);
    },
#endif

#if ASYNCIFY == 1
    instrumentFunction(original) {
      var wrapper = (...args) => {
#if ASYNCIFY_DEBUG >= 2
        dbg(`ASYNCIFY: ${'  '.repeat(Asyncify.exportCallStack.length)} try ${original}`);
#endif
        Asyncify.exportCallStack.push(original);
        try {
#if MEMORY64
          Asyncify.saveRewindArguments(original, args);
#endif
          return original(...args);
        } finally {
          if (!ABORT) {
            var top = Asyncify.exportCallStack.pop();
#if ASSERTIONS
            assert(top === original);
#endif
#if ASYNCIFY_DEBUG >= 2
            dbg(`ASYNCIFY: ${'  '.repeat(Asyncify.exportCallStack.length)} finally ${original}`);
#endif
            Asyncify.maybeStopUnwind();
          }
        }
      };
      Asyncify.funcWrappers.set(original, wrapper);
#if MAIN_MODULE
      wrapper.orig = original;
#endif
#if ASSERTIONS
      wrapper = createNamedFunction(`__asyncify_wrapper_${original.name}`, wrapper);
#endif
      return wrapper;
    },
#endif // ASYNCIFY == 1

    instrumentWasmExports(exports) {
#if EMBIND_GEN_MODE
      // Instrumenting is not needed when generating code.
      return exports;
#endif
#if ASYNCIFY_DEBUG
      dbg('asyncify instrumenting exports');
#endif
#if ASYNCIFY == 2
      var exportPattern = {{{ new RegExp(`^(${ASYNCIFY_EXPORTS.join('|').replace(/\*/g, '.*')})$`) }}};
      Asyncify.asyncExports = new Set();
#endif
      var ret = {};
      for (let [x, original] of Object.entries(exports)) {
        if (typeof original == 'function') {
 #if ASYNCIFY == 2
          // Wrap all exports with a promising WebAssembly function.
          let isAsyncifyExport = exportPattern.test(x);
          if (isAsyncifyExport) {
            Asyncify.asyncExports.add(original);
            original = Asyncify.makeAsyncFunction(original);
          }
          ret[x] = original;
#else
          var wrapper = Asyncify.instrumentFunction(original);
          ret[x] = wrapper;
#endif
        } else {
          ret[x] = original;
        }
      }
      return ret;
    },

#if ASYNCIFY == 1
    //
    // Original implementation of Asyncify.
    //
    State: {
      Normal: 0,
      Unwinding: 1,
      Rewinding: 2,
      Disabled: 3,
    },
    state: 0,
    StackSize: {{{ ASYNCIFY_STACK_SIZE }}},
    currData: null,
    // The return value passed to wakeUp() in
    // Asyncify.handleSleep((wakeUp) => {...}) is stored here,
    // so we can return it later from the C function that called
    // Asyncify.handleSleep() after rewinding finishes.
    handleSleepReturnValue: 0,
    // We must track which wasm exports are called into and
    // exited, so that we know where the call stack began,
    // which is where we must call to rewind it.
    // This list contains the original Wasm exports.
    exportCallStack: [],
    callstackFuncToId: new Map(),
    callStackIdToFunc: new Map(),
    // Maps wasm functions to their corresponding wrapper function.
    funcWrappers: new Map(),
    callStackId: 0,
    asyncPromiseHandlers: null, // { resolve, reject } pair for when *all* asynchronicity is done
    sleepCallbacks: [], // functions to call every time we sleep

    getCallStackId(func) {
#if ASSERTIONS
      assert(func);
#endif
      if (!Asyncify.callstackFuncToId.has(func)) {
        var id = Asyncify.callStackId++;
        Asyncify.callstackFuncToId.set(func, id);
        Asyncify.callStackIdToFunc.set(id, func);
      }
      return Asyncify.callstackFuncToId.get(func);
    },

    maybeStopUnwind() {
#if ASYNCIFY_DEBUG
      dbg('ASYNCIFY: maybe stop unwind', Asyncify.exportCallStack);
#endif
      if (Asyncify.currData &&
          Asyncify.state === Asyncify.State.Unwinding &&
          Asyncify.exportCallStack.length === 0) {
        // We just finished unwinding.
        // Be sure to set the state before calling any other functions to avoid
        // possible infinite recursion here (For example in debug pthread builds
        // the dbg() function itself can call back into WebAssembly to get the
        // current pthread_self() pointer).
        Asyncify.state = Asyncify.State.Normal;
#if ASYNCIFY_DEBUG
        dbg('ASYNCIFY: stop unwind');
#endif
        {{{ runtimeKeepalivePush(); }}}
        // Keep the runtime alive so that a re-wind can be done later.
        runAndAbortIfError(_asyncify_stop_unwind);
        if (typeof Fibers != 'undefined') {
          Fibers.trampoline();
        }
      }
    },

    whenDone() {
#if ASSERTIONS
      assert(Asyncify.currData, 'tried to wait for an async operation when none is in progress');
      assert(!Asyncify.asyncPromiseHandlers, 'cannot have multiple async operations in flight at once');
#endif
      return new Promise((resolve, reject) => {
        Asyncify.asyncPromiseHandlers = { resolve, reject };
      });
    },

    allocateData() {
      // An asyncify data structure has three fields:
      //  0  current stack pos
      //  4  max stack pos
      //  8  id of function at bottom of the call stack (callStackIdToFunc[id] == wasm func)
      //
      // The Asyncify ABI only interprets the first two fields, the rest is for the runtime.
      // We also embed a stack in the same memory region here, right next to the structure.
      // This struct is also defined as asyncify_data_t in emscripten/fiber.h
      var ptr = _malloc({{{ C_STRUCTS.asyncify_data_s.__size__ }}} + Asyncify.StackSize);
      Asyncify.setDataHeader(ptr, ptr + {{{ C_STRUCTS.asyncify_data_s.__size__ }}}, Asyncify.StackSize);
      Asyncify.setDataRewindFunc(ptr);
      return ptr;
    },

    setDataHeader(ptr, stack, stackSize) {
      {{{ makeSetValue('ptr', C_STRUCTS.asyncify_data_s.stack_ptr, 'stack', '*') }}};
      {{{ makeSetValue('ptr', C_STRUCTS.asyncify_data_s.stack_limit, 'stack + stackSize', '*') }}};
    },

    setDataRewindFunc(ptr) {
      var bottomOfCallStack = Asyncify.exportCallStack[0];
#if ASYNCIFY_DEBUG >= 2
      dbg(`ASYNCIFY: setDataRewindFunc(${ptr}), bottomOfCallStack is`, bottomOfCallStack, new Error().stack);
#endif
#if ASSERTIONS
      assert(bottomOfCallStack, 'exportCallStack is empty');
#endif
      var rewindId = Asyncify.getCallStackId(bottomOfCallStack);
      {{{ makeSetValue('ptr', C_STRUCTS.asyncify_data_s.rewind_id, 'rewindId', 'i32') }}};
    },

    getDataRewindFunc(ptr) {
      var id = {{{ makeGetValue('ptr', C_STRUCTS.asyncify_data_s.rewind_id, 'i32') }}};
      var func = Asyncify.callStackIdToFunc.get(id);
#if ASSERTIONS
      assert(func, `id ${id} not found in callStackIdToFunc`);
#endif
      return func;
    },

    doRewind(ptr) {
      var original = Asyncify.getDataRewindFunc(ptr);
#if ASYNCIFY_DEBUG
      dbg('ASYNCIFY: doRewind:', original);
#endif
      var func = Asyncify.funcWrappers.get(original);
#if ASSERTIONS
      assert(original);
      assert(func);
#endif
      // Once we have rewound and the stack we no longer need to artificially
      // keep the runtime alive.
      {{{ runtimeKeepalivePop(); }}}
#if MEMORY64
      // When re-winding, the arguments to a function are ignored.  For i32 arguments we
      // can just call the function with no args at all since the engine will produce zeros
      // for all arguments.  However, for i64 arguments we get `undefined cannot be converted to
      // BigInt`.
      func = func.bind(0, ...Asyncify.restoreRewindArguments(original));
#endif
      return callUserCallback(func);
    },

    // This receives a function to call to start the async operation, and
    // handles everything else for the user of this API. See emscripten_sleep()
    // and other async methods for simple examples of usage.
    handleSleep(startAsync) {
#if ASSERTIONS
      assert(Asyncify.state !== Asyncify.State.Disabled, 'handleSleep called after Asyncify was shut down');
#endif
      if (ABORT) return;
#if ASYNCIFY_DEBUG
      dbg(`ASYNCIFY: handleSleep ${Asyncify.state}`);
#endif
      if (Asyncify.state === Asyncify.State.Normal) {
        // Prepare to sleep. Call startAsync, and see what happens:
        // if the code decided to call our callback synchronously,
        // then no async operation was in fact begun, and we don't
        // need to do anything.
        var reachedCallback = false;
        var reachedAfterCallback = false;
        startAsync((handleSleepReturnValue = 0) => {
#if ASSERTIONS
          // old emterpretify API supported other stuff
          assert(['undefined', 'number', 'boolean', 'bigint'].includes(typeof handleSleepReturnValue), `invalid type for handleSleepReturnValue: '${typeof handleSleepReturnValue}'`);
#endif
          if (ABORT) return;
          Asyncify.handleSleepReturnValue = handleSleepReturnValue;
          reachedCallback = true;
          if (!reachedAfterCallback) {
            // We are happening synchronously, so no need for async.
            return;
          }
#if ASSERTIONS
          // This async operation did not happen synchronously, so we did
          // unwind. In that case there can be no compiled code on the stack,
          // as it might break later operations (we can rewind ok now, but if
          // we unwind again, we would unwind through the extra compiled code
          // too).
          assert(!Asyncify.exportCallStack.length, 'waking up (starting to rewind) must be done from JS, without compiled code on the stack');
#endif
#if ASYNCIFY_DEBUG
          dbg(`ASYNCIFY: start rewind ${Asyncify.currData}`);
#endif
          Asyncify.state = Asyncify.State.Rewinding;
          runAndAbortIfError(() => _asyncify_start_rewind(Asyncify.currData));
          if (typeof MainLoop != 'undefined' && MainLoop.func) {
            MainLoop.resume();
          }
          var asyncWasmReturnValue, isError = false;
          try {
            asyncWasmReturnValue = Asyncify.doRewind(Asyncify.currData);
          } catch (err) {
            asyncWasmReturnValue = err;
            isError = true;
          }
          // Track whether the return value was handled by any promise handlers.
          var handled = false;
          if (!Asyncify.currData) {
            // All asynchronous execution has finished.
            // `asyncWasmReturnValue` now contains the final
            // return value of the exported async WASM function.
            //
            // Note: `asyncWasmReturnValue` is distinct from
            // `Asyncify.handleSleepReturnValue`.
            // `Asyncify.handleSleepReturnValue` contains the return
            // value of the last C function to have executed
            // `Asyncify.handleSleep()`, whereas `asyncWasmReturnValue`
            // contains the return value of the exported WASM function
            // that may have called C functions that
            // call `Asyncify.handleSleep()`.
            var asyncPromiseHandlers = Asyncify.asyncPromiseHandlers;
            if (asyncPromiseHandlers) {
              Asyncify.asyncPromiseHandlers = null;
              (isError ? asyncPromiseHandlers.reject : asyncPromiseHandlers.resolve)(asyncWasmReturnValue);
              handled = true;
            }
          }
          if (isError && !handled) {
            // If there was an error and it was not handled by now, we have no choice but to
            // rethrow that error into the global scope where it can be caught only by
            // `onerror` or `onunhandledpromiserejection`.
            throw asyncWasmReturnValue;
          }
        });
        reachedAfterCallback = true;
        if (!reachedCallback) {
          // A true async operation was begun; start a sleep.
          Asyncify.state = Asyncify.State.Unwinding;
          // TODO: reuse, don't alloc/free every sleep
          Asyncify.currData = Asyncify.allocateData();
#if ASYNCIFY_DEBUG
          dbg(`ASYNCIFY: start unwind ${Asyncify.currData}`);
#endif
          if (typeof MainLoop != 'undefined' && MainLoop.func) {
            MainLoop.pause();
          }
          runAndAbortIfError(() => _asyncify_start_unwind(Asyncify.currData));
        }
      } else if (Asyncify.state === Asyncify.State.Rewinding) {
        // Stop a resume.
#if ASYNCIFY_DEBUG
        dbg('ASYNCIFY: stop rewind');
#endif
        Asyncify.state = Asyncify.State.Normal;
        runAndAbortIfError(_asyncify_stop_rewind);
        _free(Asyncify.currData);
        Asyncify.currData = null;
        // Call all sleep callbacks now that the sleep-resume is all done.
        Asyncify.sleepCallbacks.forEach(callUserCallback);
      } else {
        abort(`invalid state: ${Asyncify.state}`);
      }
      return Asyncify.handleSleepReturnValue;
    },

    // Unlike `handleSleep`, accepts a function returning a `Promise`
    // and uses the fulfilled value instead of passing in a separate callback.
    //
    // This is particularly useful for native JS `async` functions where the
    // returned value will "just work" and be passed back to C++.
    handleAsync: (startAsync) => Asyncify.handleSleep(async (wakeUp) => {
      // TODO: add error handling as a second param when handleSleep implements it.
      wakeUp(await startAsync());
    }),

#elif ASYNCIFY == 2
    //
    // JSPI implementation of Asyncify.
    //

    // Stores all the exported raw Wasm functions that are wrapped with async
    // WebAssembly.Functions.
    asyncExports: null,
    isAsyncExport(func) {
      return Asyncify.asyncExports?.has(func);
    },
    handleAsync: async (startAsync) => {
      {{{ runtimeKeepalivePush(); }}}
      try {
        return await startAsync();
      } finally {
        {{{ runtimeKeepalivePop(); }}}
      }
    },
    handleSleep: (startAsync) => Asyncify.handleAsync(() => new Promise(startAsync)),
    makeAsyncFunction(original) {
#if ASYNCIFY_DEBUG
      dbg('asyncify: makeAsyncFunction for', original);
#endif
      return WebAssembly.promising(original);
    },
#endif
  },

  emscripten_sleep__async: 'auto',
  emscripten_sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),

  emscripten_wget_data__deps: ['$asyncLoad', 'malloc'],
  emscripten_wget_data__async: 'auto',
  emscripten_wget_data: async (url, pbuffer, pnum, perror) => {
    /* no need for run dependency, this is async but will not do any prepare etc. step */
    try {
      const byteArray = await asyncLoad(UTF8ToString(url));
      // can only allocate the buffer after the wakeUp, not during an asyncing
      var buffer = _malloc(byteArray.length); // must be freed by caller!
      HEAPU8.set(byteArray, buffer);
      {{{ makeSetValue('pbuffer', 0, 'buffer', '*') }}};
      {{{ makeSetValue('pnum', 0, 'byteArray.length', 'i32') }}};
      {{{ makeSetValue('perror', 0, '0', 'i32') }}};
    } catch (err) {
      {{{ makeSetValue('perror', 0, '1', 'i32') }}};
    }
  },

  emscripten_scan_registers__deps: ['$safeSetTimeout'],
  emscripten_scan_registers__async: true,
  emscripten_scan_registers: (func) => {
    return Asyncify.handleSleep((wakeUp) => {
      // We must first unwind, so things are spilled to the stack. Then while
      // we are pausing we do the actual scan. After that we can resume. Note
      // how using a timeout here avoids unbounded call stack growth, which
      // could happen if we tried to scan the stack immediately after unwinding.
      safeSetTimeout(() => {
        var stackBegin = Asyncify.currData + {{{ C_STRUCTS.asyncify_data_s.__size__ }}};
        var stackEnd = {{{ makeGetValue('Asyncify.currData', 0, '*') }}};
        {{{ makeDynCall('vpp', 'func') }}}(stackBegin, stackEnd);
        wakeUp();
      }, 0);
    });
  },

  $Fibers__deps: ['$Asyncify', 'emscripten_stack_set_limits', '$stackRestore'],
  $Fibers: {
    nextFiber: 0,
    trampolineRunning: false,
    trampoline() {
      if (!Fibers.trampolineRunning && Fibers.nextFiber) {
        Fibers.trampolineRunning = true;
        do {
          var fiber = Fibers.nextFiber;
          Fibers.nextFiber = 0;
#if ASYNCIFY_DEBUG >= 2
          dbg('ASYNCIFY/FIBER: trampoline jump into fiber', fiber, new Error().stack);
#endif
          Fibers.finishContextSwitch(fiber);
        } while (Fibers.nextFiber);
        Fibers.trampolineRunning = false;
      }
    },
    /*
     * NOTE: This function is the asynchronous part of emscripten_fiber_swap.
     */
    finishContextSwitch(newFiber) {
      var stack_base = {{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.stack_base,  '*') }}};
      var stack_max =  {{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.stack_limit, '*') }}};
      _emscripten_stack_set_limits(stack_base, stack_max);

#if STACK_OVERFLOW_CHECK >= 2
      ___set_stack_limits(stack_base, stack_max);
#endif

      stackRestore({{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.stack_ptr,   '*') }}});

      var entryPoint = {{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.entry, '*') }}};

      if (entryPoint !== 0) {
#if STACK_OVERFLOW_CHECK
        writeStackCookie();
#endif
#if ASYNCIFY_DEBUG
        dbg('ASYNCIFY/FIBER: entering fiber', newFiber, 'for the first time');
#endif
        Asyncify.currData = null;
        {{{ makeSetValue('newFiber', C_STRUCTS.emscripten_fiber_s.entry, 0, '*') }}};

        var userData = {{{ makeGetValue('newFiber', C_STRUCTS.emscripten_fiber_s.user_data, '*') }}};
        {{{ makeDynCall('vp', 'entryPoint') }}}(userData);
      } else {
        var asyncifyData = newFiber + {{{ C_STRUCTS.emscripten_fiber_s.asyncify_data }}};
        Asyncify.currData = asyncifyData;

#if ASYNCIFY_DEBUG
        dbg('ASYNCIFY/FIBER: start rewind', asyncifyData, '(resuming fiber', newFiber, ')');
#endif
        Asyncify.state = Asyncify.State.Rewinding;
        _asyncify_start_rewind(asyncifyData);
        Asyncify.doRewind(asyncifyData);
      }
    },
  },

  emscripten_fiber_swap__deps: ['$Asyncify', '$Fibers', '$stackSave'],
  emscripten_fiber_swap__async: true,
  emscripten_fiber_swap: (oldFiber, newFiber) => {
    if (ABORT) return;
#if ASYNCIFY_DEBUG
    dbg('ASYNCIFY/FIBER: swap', oldFiber, '->', newFiber, 'state:', Asyncify.state);
#endif
    if (Asyncify.state === Asyncify.State.Normal) {
      Asyncify.state = Asyncify.State.Unwinding;

      var asyncifyData = oldFiber + {{{ C_STRUCTS.emscripten_fiber_s.asyncify_data }}};
      Asyncify.setDataRewindFunc(asyncifyData);
      Asyncify.currData = asyncifyData;

#if ASYNCIFY_DEBUG
      dbg('ASYNCIFY/FIBER: start unwind', asyncifyData);
#endif
      _asyncify_start_unwind(asyncifyData);

      var stackTop = stackSave();
      {{{ makeSetValue('oldFiber', C_STRUCTS.emscripten_fiber_s.stack_ptr, 'stackTop', '*') }}};

      Fibers.nextFiber = newFiber;
    } else {
#if ASSERTIONS
      assert(Asyncify.state === Asyncify.State.Rewinding);
#endif
#if ASYNCIFY_DEBUG
      dbg('ASYNCIFY/FIBER: stop rewind');
#endif
      Asyncify.state = Asyncify.State.Normal;
      _asyncify_stop_rewind();
      Asyncify.currData = null;
    }
  },
#else // ASYNCIFY
  emscripten_sleep: () => {
    abort('Please compile your program with async support in order to use asynchronous operations like emscripten_sleep');
  },
  emscripten_wget: (url, file) => {
    abort('Please compile your program with async support in order to use asynchronous operations like emscripten_wget');
  },
  emscripten_wget_data: (url, pbuffer, pnum, perror) => {
    abort('Please compile your program with async support in order to use asynchronous operations like emscripten_wget_data');
  },
  emscripten_scan_registers: (func) => {
    abort('Please compile your program with async support in order to use asynchronous operations like emscripten_scan_registers');
  },
  emscripten_fiber_swap: (oldFiber, newFiber) => {
    abort('Please compile your program with async support in order to use asynchronous operations like emscripten_fiber_swap');
  },
#endif // ASYNCIFY
});

if (ASYNCIFY) {
  extraLibraryFuncs.push('$Asyncify');
}
