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

addToLibrary({
  // Returns the C function with a specified identifier (for C++, you need to do manual name mangling)
#if MODULARIZE == 'instance' && !INCLUDE_FULL_LIBRARY
  $getCFunc__deps: [() => error('ccall is not yet compatible with MODULARIZE=instance')],
#endif
  $getCFunc__internal: true,
  $getCFunc: (ident) => {
    var func = Module['_' + ident]; // closure exported function
#if ASSERTIONS
    assert(func, `Cannot call unknown function ${ident}, make sure it is exported`);
#endif
    return func;
  },

  // C calling interface.
  $ccall__deps: ['$getCFunc', '$writeArrayToMemory', '$stringToUTF8OnStack', '$stackSave', '$stackRestore', '$stackAlloc'],
  $ccall__docs: `
  /**
   * @param {string|null=} returnType
   * @param {Array=} argTypes
   * @param {Array=} args
   * @param {Object=} opts
   */`,
  $ccall: (ident, returnType, argTypes, args, opts) => {
    // For fast lookup of conversion functions
    var toC = {
#if MEMORY64
      'pointer': (p) => {{{ to64('p') }}},
#endif
      'string': (str) => {
        var ret = 0;
        if (str !== null && str !== undefined && str !== 0) { // null string
          ret = stringToUTF8OnStack(str);
        }
        return {{{ to64('ret') }}};
      },
      'array': (arr) => {
        var ret = stackAlloc(arr.length);
        writeArrayToMemory(arr, ret);
        return {{{ to64('ret') }}};
      }
    };

    function convertReturnValue(ret) {
      if (returnType === 'string') {
        return UTF8ToString({{{ from64Expr('ret') }}});
      }
#if MEMORY64
      if (returnType === 'pointer') return Number(ret);
#elif CAN_ADDRESS_2GB
      if (returnType === 'pointer') return ret >>> 0;
#endif
      if (returnType === 'boolean') return Boolean(ret);
      return ret;
    }

    var func = getCFunc(ident);
    var cArgs = [];
    var stack = 0;
#if ASSERTIONS
    assert(returnType !== 'array', 'return type should not be "array"');
#endif
    if (args) {
      for (var i = 0; i < args.length; i++) {
        var converter = toC[argTypes[i]];
        if (converter) {
          if (stack === 0) stack = stackSave();
          cArgs[i] = converter(args[i]);
        } else {
          cArgs[i] = args[i];
        }
      }
    }
#if ASYNCIFY == 1
    // Data for a previous async operation that was in flight before us.
    var previousAsync = Asyncify.currData;
#endif
    var ret = func(...cArgs);
    function onDone(ret) {
#if ASYNCIFY == 1
      runtimeKeepalivePop();
#endif
      if (stack !== 0) stackRestore(stack);
      return convertReturnValue(ret);
    }
#if ASYNCIFY
  var asyncMode = opts?.async;
#endif

#if ASYNCIFY == 1
    // Keep the runtime alive through all calls. Note that this call might not be
    // async, but for simplicity we push and pop in all calls.
    runtimeKeepalivePush();
    if (Asyncify.currData != previousAsync) {
#if ASSERTIONS
      // A change in async operation happened. If there was already an async
      // operation in flight before us, that is an error: we should not start
      // another async operation while one is active, and we should not stop one
      // either. The only valid combination is to have no change in the async
      // data (so we either had one in flight and left it alone, or we didn't have
      // one), or to have nothing in flight and to start one.
      assert(!(previousAsync && Asyncify.currData), 'We cannot start an async operation when one is already in flight');
      assert(!(previousAsync && !Asyncify.currData), 'We cannot stop an async operation in flight');
#endif
      // This is a new async operation. The wasm is paused and has unwound its stack.
      // We need to return a Promise that resolves the return value
      // once the stack is rewound and execution finishes.
#if ASSERTIONS
      assert(asyncMode, `The call to ${ident} is running asynchronously. If this was intended, add the async option to the ccall/cwrap call.`);
#endif
      return Asyncify.whenDone().then(onDone);
    }
#endif

#if ASYNCIFY == 2
    if (asyncMode) return ret.then(onDone);
#endif

    ret = onDone(ret);
#if ASYNCIFY == 1
    // If this is an async ccall, ensure we return a promise
    if (asyncMode) return Promise.resolve(ret);
#endif
    return ret;
  },

  $cwrap__docs: `
  /**
   * @param {string=} returnType
   * @param {Array=} argTypes
   * @param {Object=} opts
   */`,
  $cwrap__deps: [ '$ccall',
#if !ASSERTIONS
    '$getCFunc',
#endif
  ],
  $cwrap: (ident, returnType, argTypes, opts) => {
#if !ASSERTIONS
    // When the function takes numbers and returns a number, we can just return
    // the original function
    var numericArgs = !argTypes || argTypes.every((type) => type === 'number' || type === 'boolean');
    var numericRet = returnType !== 'string';
    if (numericRet && numericArgs && !opts) {
      return getCFunc(ident);
    }
#endif
    return (...args) => ccall(ident, returnType, argTypes, args, opts);
  },
});
