// Copyright 2012 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.

// Number of handles reserved for non-use (0) or common values w/o refcount.
{{{
  const EMVAL_RESERVED_HANDLES = 5;
  const EMVAL_LAST_RESERVED_HANDLE = EMVAL_RESERVED_HANDLES * 2 - 1;
}}}
var LibraryEmVal = {
  // Stack of handles available for reuse.
  $emval_freelist: [],
#if !DISABLE_EXCEPTION_CATCHING || WASM_EXCEPTIONS
  $emval_exception_decrefs: [],
#endif
  // Array of alternating pairs (value, refcount).
  // reserve 0 and some special values. These never get de-allocated.
  $emval_handles: [
    0, 1,
    undefined, 1,
    null, 1,
    true, 1,
    false, 1,
  ],
#if ASSERTIONS
  $emval_handles__postset: 'assert(emval_handles.length === {{{ EMVAL_RESERVED_HANDLES }}} * 2)',
#endif
  $emval_symbols: {}, // address -> string

  $count_emval_handles__deps: ['$emval_freelist', '$emval_handles'],
  $count_emval_handles: () => {
    return emval_handles.length / 2 - {{{ EMVAL_RESERVED_HANDLES }}} - emval_freelist.length;
  },

  _emval_register_symbol__deps: ['$emval_symbols', '$AsciiToString'],
  _emval_register_symbol: (address) => {
    emval_symbols[address] = AsciiToString(address);
  },

  $getStringOrSymbol__deps: ['$emval_symbols', '$AsciiToString'],
  $getStringOrSymbol: (address) => {
    var symbol = emval_symbols[address];
    if (symbol === undefined) {
      return AsciiToString(address);
    }
    return symbol;
  },

  $Emval__deps: ['$emval_freelist', '$emval_handles', '$throwBindingError'],
  $Emval: {
    toValue: (handle) => {
      if (!handle) {
          throwBindingError(`Cannot use deleted val. handle = ${handle}`);
      }
  #if ASSERTIONS
      // handle 2 is supposed to be `undefined`.
      assert(handle === 2 || emval_handles[handle] !== undefined && handle % 2 === 0, `invalid handle: ${handle}`);
  #endif
      return emval_handles[handle];
    },

    toHandle: (value) => {
      switch (value) {
        case undefined: return 2;
        case null: return 4;
        case true: return 6;
        case false: return 8;
        default:{
          const handle = emval_freelist.pop() || emval_handles.length;
          emval_handles[handle] = value;
          emval_handles[handle + 1] = 1;
          return handle;
        }
      }
    }
  },

  _emval_incref__deps: ['$emval_handles'],
  _emval_incref: (handle) => {
    if (handle > {{{ EMVAL_LAST_RESERVED_HANDLE }}}) {
      emval_handles[handle + 1] += 1;
    }
  },

  _emval_decref__deps: ['$emval_freelist', '$emval_handles',
#if !DISABLE_EXCEPTION_CATCHING || WASM_EXCEPTIONS
    '$emval_exception_decrefs',
#endif
  ],
  _emval_decref: (handle) => {
    if (handle > {{{ EMVAL_LAST_RESERVED_HANDLE }}} && 0 === --emval_handles[handle + 1]) {
  #if ASSERTIONS
      assert(emval_handles[handle] !== undefined, `decref for unallocated handle`);
  #endif
      var value = emval_handles[handle];
      emval_handles[handle] = undefined;
#if !DISABLE_EXCEPTION_CATCHING || WASM_EXCEPTIONS
      // In case the value is a C++ exception, decrement the refcount, so the
      // memory can be freed correctly
      var destructor = emval_exception_decrefs[handle];
      if (destructor) {
        emval_exception_decrefs[handle] = undefined;
        destructor(value);
      }
#endif
      emval_freelist.push(handle);
    }
  },

  _emval_run_destructors__deps: ['_emval_decref', '$Emval', '$runDestructors'],
  _emval_run_destructors: (handle) => {
    var destructors = Emval.toValue(handle);
    runDestructors(destructors);
    __emval_decref(handle);
  },

  _emval_new_array__deps: ['$Emval'],
  _emval_new_array: () => Emval.toHandle([]),

#if !SUPPORT_BIG_ENDIAN
  _emval_new_array_from_memory_view__deps: ['$Emval'],
  _emval_new_array_from_memory_view: (view) => {
    view = Emval.toValue(view);
    // using for..loop is faster than Array.from
    var a = new Array(view.length);
    for (var i = 0; i < view.length; i++) a[i] = view[i];
    return Emval.toHandle(a);
  },
  _emval_array_to_memory_view__deps: ['$Emval'],
  _emval_array_to_memory_view: (dst, src) => {
    dst = Emval.toValue(dst);
    src = Emval.toValue(src);
    dst.set(src);
  },
#else
  _emval_new_array_from_memory_view__deps: ['$Emval'],
  _emval_new_array_from_memory_view: (view) => {
    view = Emval.toValue(view);
    const dv = new DataView(view.buffer, view.byteOffset);
    const reader = {
      Int8Array: dv.getInt8,
      Uint8Array: dv.getUint8,
      Int16Array: dv.getInt16,
      Uint16Array: dv.getUint16,
      Int32Array: dv.getInt32,
      Uint32Array: dv.getUint32,
      BigInt64Array: dv.getBigInt64,
      BigUint64Array: dv.getBigUint64,
      Float32Array: dv.getFloat32,
      Float64Array: dv.getFloat64,
    }[view[Symbol.toStringTag]];
    var a = new Array(view.length);
    for (var i = 0; i < view.length; i++) a[i] = reader.call(dv, i * view.BYTES_PER_ELEMENT, true);
    return Emval.toHandle(a);
  },
  _emval_array_to_memory_view__deps: ['$Emval'],
  _emval_array_to_memory_view: (dst, src) => {
    dst = Emval.toValue(dst);
    src = Emval.toValue(src);
    const dv = new DataView(dst.buffer, dst.byteOffset);
    const writer = {
      Int8Array: dv.setInt8,
      Uint8Array: dv.setUint8,
      Int16Array: dv.setInt16,
      Uint16Array: dv.setUint16,
      Int32Array: dv.setInt32,
      Uint32Array: dv.setUint32,
      BigInt64Array: dv.setBigInt64,
      BigUint64Array: dv.setBigUint64,
      Float32Array: dv.setFloat32,
      Float64Array: dv.setFloat64,
    }[dst[Symbol.toStringTag]];
    for (var i = 0; i < src.length; i++) writer.call(dv, i * dst.BYTES_PER_ELEMENT, src[i], true);
  },
#endif

  _emval_new_object__deps: ['$Emval'],
  _emval_new_object: () => Emval.toHandle({}),

  _emval_new_cstring__deps: ['$getStringOrSymbol', '$Emval'],
  _emval_new_cstring: (v) => Emval.toHandle(getStringOrSymbol(v)),

  _emval_new_u8string__deps: ['$Emval'],
  _emval_new_u8string: (v) => Emval.toHandle(UTF8ToString(v)),

  _emval_new_u16string__deps: ['$Emval'],
  _emval_new_u16string: (v) => Emval.toHandle(UTF16ToString(v)),

  _emval_get_global__deps: ['$Emval', '$getStringOrSymbol'],
  _emval_get_global: (name) => {
    if (!name) {
      return Emval.toHandle(globalThis);
    }
    name = getStringOrSymbol(name);
    return Emval.toHandle(globalThis[name]);
  },

  _emval_get_module_property__deps: ['$getStringOrSymbol', '$Emval'],
  _emval_get_module_property: (name) => {
    name = getStringOrSymbol(name);
    return Emval.toHandle(Module[name]);
  },

  _emval_get_property__deps: ['$Emval'],
  _emval_get_property: (handle, key) => {
    handle = Emval.toValue(handle);
    key = Emval.toValue(key);
    return Emval.toHandle(handle[key]);
  },

  _emval_set_property__deps: ['$Emval'],
  _emval_set_property: (handle, key, value) => {
    handle = Emval.toValue(handle);
    key = Emval.toValue(key);
    value = Emval.toValue(value);
    handle[key] = value;
  },

  $emval_returnValue__deps: ['$Emval'],
  $emval_returnValue: (toReturnWire, destructorsRef, handle) => {
    var destructors = [];
    var result = toReturnWire(destructors, handle);
    if (destructors.length) {
      // void, primitives and any other types w/o destructors don't need to allocate a handle
      {{{ makeSetValue('destructorsRef', '0', 'Emval.toHandle(destructors)', '*') }}};
    }
    return result;
  },

  _emval_equals__deps: ['$Emval'],
  _emval_equals: (first, second) => {
    first = Emval.toValue(first);
    second = Emval.toValue(second);
    return first == second;
  },

  _emval_strictly_equals__deps: ['$Emval'],
  _emval_strictly_equals: (first, second) => {
    first = Emval.toValue(first);
    second = Emval.toValue(second);
    return first === second;
  },

  _emval_greater_than__deps: ['$Emval'],
  _emval_greater_than: (first, second) => {
    first = Emval.toValue(first);
    second = Emval.toValue(second);
    return first > second;
  },

  _emval_less_than__deps: ['$Emval'],
  _emval_less_than: (first, second) => {
    first = Emval.toValue(first);
    second = Emval.toValue(second);
    return first < second;
  },

  _emval_not__deps: ['$Emval'],
  _emval_not: (object) => {
    object = Emval.toValue(object);
    return !object;
  },

  $emval_lookupTypes__deps: ['$requireRegisteredType'],
  $emval_lookupTypes: (argCount, argTypes) => {
    var a = new Array(argCount);
    for (var i = 0; i < argCount; ++i) {
      a[i] = requireRegisteredType({{{ makeGetValue('argTypes', `i*${POINTER_SIZE}`, '*') }}},
                                   `parameter ${i}`);
    }
    return a;
  },

  // Leave id 0 undefined.  It's not a big deal, but might be confusing
  // to have null be a valid method caller.
  $emval_methodCallers: [undefined],

  $emval_addMethodCaller__deps: ['$emval_methodCallers'],
  $emval_addMethodCaller: (caller) => {
    var id = emval_methodCallers.length;
    emval_methodCallers.push(caller);
    return id;
  },

  _emval_create_invoker__deps: [
    '$emval_addMethodCaller', '$emval_lookupTypes',
    '$createNamedFunction', '$emval_returnValue',
    '$Emval', '$getStringOrSymbol',
  ],
  _emval_create_invoker: (argCount, argTypesPtr, kind) => {
    var GenericWireTypeSize = {{{ 2 * POINTER_SIZE }}};

    var [retType, ...argTypes] = emval_lookupTypes(argCount, argTypesPtr);
    var toReturnWire = retType.toWireType.bind(retType);
    var argFromPtr = argTypes.map(type => type.readValueFromPointer.bind(type));
    argCount--; // remove the extracted return type

#if DYNAMIC_EXECUTION
    var captures = {'toValue': Emval.toValue};
    var args = argFromPtr.map((argFromPtr, i) => {
      var captureName = `argFromPtr${i}`;
      captures[captureName] = argFromPtr;
      return `${captureName}(args${i ? '+' + i * GenericWireTypeSize : ''})`;
    });
    var functionBody;
    switch (kind){
      case {{{ cDefs['internal::EM_INVOKER_KIND::FUNCTION'] }}}:
        functionBody = 'toValue(handle)';
        break;
      case {{{ cDefs['internal::EM_INVOKER_KIND::CONSTRUCTOR'] }}}:
        functionBody = 'new (toValue(handle))';
        break;
      case {{{ cDefs['internal::EM_INVOKER_KIND::CAST'] }}}:
        functionBody = '';
        break;
      case {{{ cDefs['internal::EM_INVOKER_KIND::METHOD'] }}}:
        captures['getStringOrSymbol'] = getStringOrSymbol;
        functionBody = 'toValue(handle)[getStringOrSymbol(methodName)]';
        break;
    }
    functionBody += `(${args})`;
    if (!retType.isVoid) {
      captures['toReturnWire'] = toReturnWire;
      captures['emval_returnValue'] = emval_returnValue;
      functionBody = `return emval_returnValue(toReturnWire, destructorsRef, ${functionBody})`;
    }
    functionBody = `return function (handle, methodName, destructorsRef, args) {
${functionBody}
}`;

    var invokerFunction = new Function(Object.keys(captures), functionBody)(...Object.values(captures));
#else
    var argN = new Array(argCount);
    var invokerFunction = (handle, methodName, destructorsRef, args) => {
      var offset = 0;
      for (var i = 0; i < argCount; ++i) {
        argN[i] = argFromPtr[i](args + offset);
        offset += GenericWireTypeSize;
      }
      var rv;
      switch (kind) {
        case {{{ cDefs['internal::EM_INVOKER_KIND::FUNCTION'] }}}:
          rv = Emval.toValue(handle).apply(null, argN);
          break;
        case {{{ cDefs['internal::EM_INVOKER_KIND::CONSTRUCTOR'] }}}:
          rv = Reflect.construct(Emval.toValue(handle), argN);
          break;
        case {{{ cDefs['internal::EM_INVOKER_KIND::CAST'] }}}:
          // no-op, just return the argument
          rv = argN[0];
          break;
        case {{{ cDefs['internal::EM_INVOKER_KIND::METHOD'] }}}:
          rv = Emval.toValue(handle)[getStringOrSymbol(methodName)](...argN);
          break;
      }
      return emval_returnValue(toReturnWire, destructorsRef, rv);
    };
#endif
    var functionName = `methodCaller<(${argTypes.map(t => t.name)}) => ${retType.name}>`;
    return emval_addMethodCaller(createNamedFunction(functionName, invokerFunction));
  },

  _emval_invoke__deps: ['$getStringOrSymbol', '$emval_methodCallers', '$Emval'],
  _emval_invoke: (caller, handle, methodName, destructorsRef, args) => {
    return emval_methodCallers[caller](handle, methodName, destructorsRef, args);
  },

  // Same as `_emval_invoke`, just imported into Wasm under a different return type.
  // TODO: remove this if/when https://github.com/emscripten-core/emscripten/issues/20478 is fixed.
  _emval_invoke_i64: '_emval_invoke',

  _emval_typeof__deps: ['$Emval'],
  _emval_typeof: (handle) => {
    handle = Emval.toValue(handle);
    return Emval.toHandle(typeof handle);
  },

  _emval_instanceof__deps: ['$Emval'],
  _emval_instanceof: (object, constructor) => {
    object = Emval.toValue(object);
    constructor = Emval.toValue(constructor);
    return object instanceof constructor;
  },

  _emval_is_number__deps: ['$Emval'],
  _emval_is_number: (handle) => {
    handle = Emval.toValue(handle);
    return typeof handle == 'number';
  },

  _emval_is_string__deps: ['$Emval'],
  _emval_is_string: (handle) => {
    handle = Emval.toValue(handle);
    return typeof handle == 'string';
  },

  _emval_in__deps: ['$Emval'],
  _emval_in: (item, object) => {
    item = Emval.toValue(item);
    object = Emval.toValue(object);
    return item in object;
  },

  _emval_delete__deps: ['$Emval'],
  _emval_delete: (object, property) => {
    object = Emval.toValue(object);
    property = Emval.toValue(property);
    return delete object[property];
  },

#if !DISABLE_EXCEPTION_CATCHING || WASM_EXCEPTIONS
  $isCppExceptionObject__deps: ['$Emval'],
  $isCppExceptionObject: (object) => {
#if !DISABLE_EXCEPTION_CATCHING
    return object instanceof CppException;
#else // WASM_EXCEPTIONS
    return object instanceof WebAssembly.Exception;
#endif
  },
#endif

  _emval_is_catchable_cpp_exception_object__deps: [
    '$Emval',
#if !DISABLE_EXCEPTION_CATCHING || WASM_EXCEPTIONS
    '$isCppExceptionObject',
#endif
  ],
  _emval_is_catchable_cpp_exception_object: (object) => {
#if !DISABLE_EXCEPTION_CATCHING || WASM_EXCEPTIONS
    return isCppExceptionObject(Emval.toValue(object));
#else
    return false;
#endif
  },

  _emval_throw__deps: ['$Emval',
#if !DISABLE_EXCEPTION_CATCHING || WASM_EXCEPTIONS
#if !DISABLE_EXCEPTION_CATCHING
    '$exceptionLast',
    '$ExceptionInfo',
#endif
    '$incrementExceptionRefcount',
    '$incrementUncaughtExceptionCount',
    '$isCppExceptionObject',
#endif
  ],
  _emval_throw: (object) => {
    object = Emval.toValue(object);
#if !DISABLE_EXCEPTION_CATCHING || WASM_EXCEPTIONS
    if (isCppExceptionObject(object)) {
#if !DISABLE_EXCEPTION_CATCHING
      var info = new ExceptionInfo(object.excPtr);
      info.set_caught(false);
      info.set_rethrown(false);
      exceptionLast = object;
#endif
      incrementUncaughtExceptionCount();
      incrementExceptionRefcount(object);
    }
#endif
    throw object;
  },

#if ASYNCIFY
  _emval_await__deps: ['$Emval', '$Asyncify'],
  _emval_await__async: 'auto',
  _emval_await: async (promise) => {
    var value = await Emval.toValue(promise);
    return Emval.toHandle(value);
  },
#endif

  _emval_iter_begin__deps: ['$Emval'],
  _emval_iter_begin: (iterable) => {
    iterable = Emval.toValue(iterable);
    return Emval.toHandle(iterable[Symbol.iterator]());
  },

  _emval_iter_next__deps: ['$Emval'],
  _emval_iter_next: (iterator) => {
    iterator = Emval.toValue(iterator);
    var result = iterator.next();
    return result.done ? 0 : Emval.toHandle(result.value);
  },

  _emval_coro_suspend__deps: ['$Emval', '_emval_coro_resume',  '_emval_coro_reject'],
  _emval_coro_suspend: (promiseHandle, awaiterPtr) => {
    Emval.toValue(promiseHandle)
      .then((result) => __emval_coro_resume(awaiterPtr, Emval.toHandle(result)),
            (error) => __emval_coro_reject(awaiterPtr, Emval.toHandle(error)));
  },

  _emval_coro_make_promise__deps: ['$Emval'],
  _emval_coro_make_promise: (resolveHandlePtr, rejectHandlePtr) => {
    return Emval.toHandle(new Promise((resolve, reject) => {
      {{{ makeSetValue('resolveHandlePtr', '0', 'Emval.toHandle(resolve)', '*') }}};
      {{{ makeSetValue('rejectHandlePtr', '0', 'Emval.toHandle(reject)', '*') }}};
    }));
  },

  _emval_from_current_cxa_exception__deps: ['$Emval', '__cxa_rethrow',
#if !DISABLE_EXCEPTION_THROWING || WASM_EXCEPTIONS
    '$decrementUncaughtExceptionCount',
#endif
#if !DISABLE_EXCEPTION_CATCHING || WASM_EXCEPTIONS
    '$decrementExceptionRefcount',
    '$emval_exception_decrefs',
#endif
  ],
  _emval_from_current_cxa_exception: () => {
    try {
      // Use __cxa_rethrow which already has mechanism for generating
      // user-friendly error message and stacktrace from C++ exception
      // if EXCEPTION_STACK_TRACES is enabled and numeric exception
      // with metadata optimised out otherwise.
      ___cxa_rethrow();
    } catch (e) {
#if !DISABLE_EXCEPTION_THROWING || WASM_EXCEPTIONS
      // ___cxa_rethrow incremented uncaughtExceptionCount.
      // Since we caught it in JS, we need to manually decrement it to balance.
      decrementUncaughtExceptionCount();
#endif
      var handle = Emval.toHandle(e);
#if !DISABLE_EXCEPTION_CATCHING || WASM_EXCEPTIONS
      emval_exception_decrefs[handle] = decrementExceptionRefcount;
#endif
      return handle;
    }
  },
};

addToLibrary(LibraryEmVal);
