/*
 * Tencent is pleased to support the open source community by making ScriptX available.
 * Copyright (C) 2021 THL A29 Limited, a Tencent company.  All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include "WasmHelper.h"
#include "../../src/Native.hpp"
#include "../../src/Scope.h"
#include "WasmEngine.hpp"
#include "WasmScope.hpp"

namespace script::wasm_backend {

static struct {
  // if there is any js exception thrown, and we should rethrow to cpp
  // the js exception is on stack top
  bool hasPendingJsException_ = false;

  // if there is any cpp exception thrown, and we should rethrow to js
  // the cpp exception is converted to js Error and pushed onto stack top
  bool hasPendingCppException_ = false;
} kExceptionStore;

WasmEngine &currentEngine() { return EngineScope::currentEngineCheckedAs<WasmEngine>(); }

// ========== stack ===========
// emscripten's val.h is not fully developed, we use our own implementation based on lua stack
// concepts.

void invokeFunctionHelper(Stack::RawFunctionType func, void *ptr0, void *ptr1, int stackBase,
                          WasmEngine *engine) {
  try {
    // stack[0] == this
    // stack[...] -> args
    auto top = Stack::top();

    auto args = wasm_backend::WasmEngine::make<Arguments>(
        ArgumentsData{stackBase, stackBase + 1, top - stackBase, engine});

    int retIndex = Stack::pushNull();

    // this function may be directly called from JS...
    // in which case we don't have a EngineScope

    // plus: EngineScope can't escape any local object... we have to hack it
    {
      EngineScope scope(engine);
      auto ret = (*func)(args, ptr0, ptr1);
      Stack::set(retIndex, ret);
    }
  } catch (const Exception &e) {
    rethrowException(e);
  }
}

void deleteScriptClassHelper(ScriptClass *scriptClass) {
  wasm_backend::WasmEngine::doDeleteScriptClass(scriptClass);
}

extern "C" {

// exported helper function called by js
// return has return value or not
// this function is so simple, we don't need to use bind.h

// the function name is using C mangling convention by adding '_' prefix to function name.
EMSCRIPTEN_KEEPALIVE
void ScriptX_Bridge_invokeFunction(intptr_t func, intptr_t ptr0, intptr_t ptr1, int stackBase,
                                   intptr_t engine) {
  invokeFunctionHelper(reinterpret_cast<Stack::RawFunctionType>(func),
                       reinterpret_cast<void *>(ptr0), reinterpret_cast<void *>(ptr1), stackBase,
                       reinterpret_cast<WasmEngine *>(engine));
}

EMSCRIPTEN_KEEPALIVE
void ScriptX_Stack_finalizeFunction(intptr_t func) {
  delete reinterpret_cast<script::FunctionCallback *>(func);
}

EMSCRIPTEN_KEEPALIVE
void ScriptX_Native_deleteScriptClass(intptr_t scriptClass) {
  deleteScriptClassHelper(reinterpret_cast<ScriptClass *>(scriptClass));
}

EMSCRIPTEN_KEEPALIVE
intptr_t ScriptX_NativeBuffer_newSharedPtr(intptr_t ptr) {
  return reinterpret_cast<intptr_t>(
      new std::shared_ptr<void>(reinterpret_cast<void *>(ptr), [](void *ptr) { std::free(ptr); }));
}

EMSCRIPTEN_KEEPALIVE
void ScriptX_NativeBuffer_deleteSharedPtr(intptr_t sharedPtr) {
  delete reinterpret_cast<std::shared_ptr<void> *>(sharedPtr);
}
}

void rethrowException(const Exception &e) {
  kExceptionStore.hasPendingCppException_ = true;
  Stack::pushValue(e.exception());
}

void checkException() {
  if (kExceptionStore.hasPendingJsException_) {
    kExceptionStore.hasPendingJsException_ = false;
    throw Exception(Stack::topValue());
  }
}

// wrap a exception handling around EM_JS
#define CHECKED_EM_JS(ret, name, params, ...)     \
  EM_JS(ret, name, params, {                      \
    try {                                         \
      __VA_ARGS__                                 \
    } catch (e) {                                 \
      return Module._ScriptX_rethrowException(e); \
    }                                             \
  })

#define CHECKED_JS_CALL(call)               \
  auto &&ret = call;                        \
  ::script::wasm_backend::checkException(); \
  return std::move(ret)

#define CHECKED_VOID_JS_CALL(call) \
  call;                            \
  ::script::wasm_backend::checkException();

// <editor-fold desc="Emscripten JS Library">
// clang-format off

CHECKED_EM_JS(void, _ScriptX_setInternalStatePointer,
    (intptr_t hasPendingJsException, intptr_t hasPendingCppException), {
  Object.defineProperty(Module, 'SCRIPTX_HAS_PENDING_JS_EXCEPTION', {
    get() { return Module.HEAPU8[hasPendingJsException] !== 0;},
    set(has) { Module.HEAPU8[hasPendingJsException] = (has ? 1 : 0); },
    enumerable: false,
    configurable: false
  });

  Object.defineProperty(Module, 'SCRIPTX_HAS_PENDING_CPP_EXCEPTION', {
      get() { return Module.HEAPU8[hasPendingCppException] !== 0;},
      set(has) { Module.HEAPU8[hasPendingCppException] = (has ? 1 : 0); },
      enumerable: false,
      configurable: false
  });
});

EM_JS(void, _ScriptX_initJavaScriptLibrary, (), {
  // declare how js stack class should be

  class ScriptStack {
    constructor() {
      this.values = [];
    }

    push(val) {
      return this.values.push(val) - 1;
    }

    setTop(newTop) { this.values.length = newTop + 1; }

    getTop() { return this.values.length - 1; }

    getTopValue() { return this.values[this.values.length - 1]; }

    pop(count) {
      this.setTop(this.getTop() - count);
      return this.getTop();
    }

    set(atIndex, valueIndex) {
      this.values[atIndex] = this.values[valueIndex];
    }
  };
  Module.SCRIPTX_STACK = new ScriptStack();

  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis
  Module._ScriptX_getGlobalObject = function () {
    const ret =  globalThis || self || window || global;

    if (typeof ret !== 'object') {
      ret = (new Function("return this;"))();
    }

    if (typeof ret !== 'object') {
      throw TypeError("can't find global");
    }
    return ret;
  };

  Module._ScriptX_rethrowException = function(e) {
    Module.SCRIPTX_HAS_PENDING_JS_EXCEPTION = true;
    Module.SCRIPTX_STACK.push(e);
    return -1;
  };

  Module._ScriptX_checkException = function() {
    if (Module.SCRIPTX_HAS_PENDING_CPP_EXCEPTION) {
      Module.SCRIPTX_HAS_PENDING_CPP_EXCEPTION = false;
      throw Module.SCRIPTX_STACK.getTopValue();
    }
  };

  Module._ScriptX_TypedArray_memcpy = function(from, to) {
    function makeInt8Array(what) {
      if (what instanceof Int8Array) {
        return what;
      } else if (what instanceof ArrayBuffer) {
        return new Int8Array(what);
      } else  {
        // TypedArray || DataView || ScriptX.SharedByteBuffer
        return new Int8Array(what.buffer, what.byteOffset, what.byteLength);
      }
    }
    makeInt8Array(to).set(makeInt8Array(from));
  };
  Module._ScriptX_SharedByteBuffer = function(byteOffset, byteLength, sharedPtr) {
    // ctor1:  ScriptX.SharedByteBuffer(bufferSize)
    // ctor2:  ScriptX.SharedByteBuffer(...) internally used

    if (!(this instanceof Module._ScriptX_SharedByteBuffer)) {
      throw Error("must call as constructor, with new operator!");
    }

    if (arguments.length == 1 && typeof arguments[0] === 'number') {
      // ctor 1
      byteLength = arguments[0];
      byteOffset = Module._malloc(byteLength);
      sharedPtr = Module._ScriptX_NativeBuffer_newSharedPtr(byteOffset);
    }

    // ctor 2
    if (!(typeof byteOffset === 'number' &&
          typeof byteLength === 'number' &&
          typeof sharedPtr === 'number')) {
      throw TypeError("use factory method to create SharedByteBuffer");
    }

    const sym = Module._ScriptX_Native_Symbol_SharedByteBuffer_ptr;
    Object.defineProperty(this, 'byteOffset', {
        value: byteOffset,
        enumerable: true
    });
    Object.defineProperty(this, 'byteLength', {
        value: byteLength,
        enumerable: true
    });
    Object.defineProperty(this, sym, {
        value: sharedPtr
    });
    Object.defineProperty(this, 'buffer', {
        get: function() { return Module.HEAP8.buffer; },
        enumerable: true,
        configurable: false
    });

    this.destroy = function() {
      const sharedPtrValue = this[sym];

      // sentence this object DEATH
      this[sym] = 0;
      this.death = true;

      Module._ScriptX_NativeBuffer_deleteSharedPtr(sharedPtrValue);
    };
  };

  Module._ScriptX_Native_destroyInstanceInternal = function(instance) {
    if (typeof instance !== 'object') return;
    const ptr = instance[Module._ScriptX_Native_Symbol_instance];
    if (ptr) {
      _ScriptX_Native_deleteScriptClass(ptr);

      // sentence this object DEATH
      instance[Module._ScriptX_Native_Symbol_InternalStore] = undefined;
      instance[Module._ScriptX_Native_Symbol_classDefine] = undefined;
      instance[Module._ScriptX_Native_Symbol_instance] = undefined;
      Object.setPrototypeOf(instance, {});
      instance.death = true;
    }
  };

  // export to global namespace
  const exports = {};
  Module._ScriptX_getGlobalObject().ScriptX = exports;
  exports.SharedByteBuffer = Module._ScriptX_SharedByteBuffer;
  exports.destroyScriptClass = Module._ScriptX_Native_destroyInstanceInternal;
  exports.destroySharedByteBuffer = function (sbb) {
    if (sbb instanceof Module._ScriptX_SharedByteBuffer) sbb.destroy();
  };

  Module._ScriptX_Global_ref = {};
  Module._ScriptX_Global_ref_key = 0;
  Module._ScriptX_Weak_ref = {};
  Module._ScriptX_Weak_ref_key = 0;
  Module._ScriptX_Global_getMap = function(isWeak) {
    if (isWeak) {
      return Module._ScriptX_Weak_ref;
    } else {
      return Module._ScriptX_Global_ref;
    }
  };
  Module._ScriptX_Global_newKey = function(isWeak) {
    const INT_MAX = 2147483647;
    let ret = 0;
    if (isWeak) {
      ret = Module._ScriptX_Weak_ref_key++;
      if (ret < 0 || ret >= INT_MAX) {
        Module._ScriptX_Weak_ref_key = 0;
        ret =  0;
      }
    } else {
      ret = Module._ScriptX_Global_ref_key++;
      if (ret < 0 || ret >= INT_MAX) {
        Module._ScriptX_Global_ref_key = 0;
        ret =  0;
      }
    }
    return ret;
  };

  Module._ScriptX_Native_Symbol_instance =
      Symbol('ScriptX_Native_Symbol_instance');
  Module._ScriptX_Native_Symbol_classDefine =
      Symbol('ScriptX_Native_Symbol_classDefine');
  Module._ScriptX_Native_Symbol_CppNewMark =
      Symbol('ScriptX_Native_Symbol_CppNewMark');
  Module._ScriptX_Native_Symbol_InternalStore =
      Symbol('ScriptX_Native_Symbol_InternalStore');
  Module._ScriptX_Native_Symbol_SharedByteBuffer_ptr =
      Symbol('ScriptX_Native_Symbol_SharedByteBuffer_ptr');
});

CHECKED_EM_JS(int, _ScriptX_Stack_top, (),
      { return Module.SCRIPTX_STACK.getTop(); });

CHECKED_EM_JS(int, _ScriptX_Stack_setTop, (int top),
      { return Module.SCRIPTX_STACK.setTop(top); });

CHECKED_EM_JS(void, _ScriptX_Stack_set, (int atIndex, int valueIndex),
      { Module.SCRIPTX_STACK.set(atIndex, valueIndex); });

CHECKED_EM_JS(int, _ScriptX_Stack_newObject, (),
      { return Module.SCRIPTX_STACK.push({}); });

CHECKED_EM_JS(int, _ScriptX_Stack_newObjectArgs, (int type, int stackBase), {
  const stack = Module.SCRIPTX_STACK;

  const Type = stack.values[type];
  const args = [];
  for (let i = stackBase; i <= stack.getTop(); i++) {
    args.push(stack.values[i]);
  }

  // clear args
  stack.setTop(stackBase - 1);

  // use spread operator
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
  const ret = new Type(...args);
  return stack.push(ret);
});

CHECKED_EM_JS(int, _ScriptX_Stack_newArray, (int size), {
    const arr = [];
    arr.length = size;
    return Module.SCRIPTX_STACK.push(arr);
});

CHECKED_EM_JS(int, _ScriptX_Stack_newStringLen, (const char* string, int len), {
  return Module.SCRIPTX_STACK.push(UTF8ToString(
              string,
              len,
              len));
});

CHECKED_EM_JS(int, _ScriptX_Stack_newNumInt, (int num),
      { return Module.SCRIPTX_STACK.push(num); });

CHECKED_EM_JS(int, _ScriptX_Stack_newNumDouble, (double num),
      { return Module.SCRIPTX_STACK.push(num); });

// Local<Value> (*func)(const Arguments &, void *, void *)
CHECKED_EM_JS(int, _ScriptX_Stack_newRawFunction,
    (intptr_t func, intptr_t ptr0, intptr_t ptr1,
        WasmEngine* engine, bool checkForConstructorCall), {
  // use IIFE to erase Function.name for wrapper
  let wrapper = (function() { return function() {
    // check is constructor call
    if (checkForConstructorCall && !(this instanceof wrapper)) {
        throw Error("constructor can't be call as normal function");
    }
    const stack = Module.SCRIPTX_STACK;
    const stackBase = stack.getTop();
    try {
      const newBase = stack.push(this);
      for (let arg of arguments) {
        stack.push(arg);
      }

      // stack[0] == this
      // stack[...] -> args
      Module._ScriptX_Bridge_invokeFunction(func, ptr0, ptr1, newBase, engine);
      Module._ScriptX_checkException();

      return stack.getTopValue();
    } finally {
      // restore stack
      stack.setTop(stackBase);
    }
  }})();

  return Module.SCRIPTX_STACK.push(wrapper);
});

CHECKED_EM_JS(int, _ScriptX_callFunction, (int stackBase), {
  const stack = Module.SCRIPTX_STACK;
  const func = stack.values[stackBase];
  const thiz = stack.values[stackBase + 1];
  const args = [];
  for (let i = stackBase + 2; i <= stack.getTop(); i++) {
    args.push(stack.values[i]);
  }
  // pop args
  stack.setTop(stackBase - 1);

  const ret = func.apply(thiz, args);
  return stack.push(ret);
});

CHECKED_EM_JS(int, _ScriptX_Stack_newBoolean, (int boolean),
      { return Module.SCRIPTX_STACK.push(boolean !== 0); });

CHECKED_EM_JS(int, _ScriptX_Stack_newError, (int message), {
  const stack = Module.SCRIPTX_STACK;
  const err = new Error(stack.values[message] || "unknown error");
  return stack.push(err);
});

CHECKED_EM_JS(int, _ScriptX_Stack_pushArrayBuffer, (size_t length), {
  const stack = Module.SCRIPTX_STACK;
  return stack.push(new ArrayBuffer(length));
});

CHECKED_EM_JS(int, _ScriptX_Stack_pushValue, (int index), {
  const stack = Module.SCRIPTX_STACK;
  return stack.push(stack.values[index]);
});

CHECKED_EM_JS(int, _ScriptX_Stack_pop, (int count), {
  const stack = Module.SCRIPTX_STACK;
  return stack.pop(count);
});

CHECKED_EM_JS(bool, _ScriptX_Stack_isObject, (int index), {
  const stack = Module.SCRIPTX_STACK;
  return stack.values[index] instanceof Object;
});

CHECKED_EM_JS(bool, _ScriptX_Stack_isArray, (int index), {
  const stack = Module.SCRIPTX_STACK;
  return Array.isArray(stack.values[index]);
});

CHECKED_EM_JS(bool, _ScriptX_Stack_isString, (int index), {
  const stack = Module.SCRIPTX_STACK;
  return typeof stack.values[index] == "string";
});

CHECKED_EM_JS(bool, _ScriptX_Stack_isNum, (int index), {
  const stack = Module.SCRIPTX_STACK;
  return typeof stack.values[index] == "number";
});

CHECKED_EM_JS(bool, _ScriptX_Stack_isBoolean, (int index), {
  const stack = Module.SCRIPTX_STACK;
  return typeof stack.values[index] == "boolean";
});

CHECKED_EM_JS(bool, _ScriptX_Stack_isFunction, (int index), {
  const stack = Module.SCRIPTX_STACK;
  return stack.values[index] instanceof Function;
});

CHECKED_EM_JS(bool, _ScriptX_Stack_isByteBuffer, (int index), {
  const stack = Module.SCRIPTX_STACK;
  const val = stack.values[index];
  return val instanceof ArrayBuffer ||
            ArrayBuffer.isView(val) ||
            val instanceof Module._ScriptX_SharedByteBuffer;
});

CHECKED_EM_JS(bool, _ScriptX_Stack_isNullOrUndefined, (int index), {
  const stack = Module.SCRIPTX_STACK;
  const value = stack.values[index];
  return value === null || value === undefined;
});

CHECKED_EM_JS(bool, _ScriptX_Stack_isUnsupported, (int index), {
  const stack = Module.SCRIPTX_STACK;
  const val = stack.values[index];
  return !(
            val === null ||
            val === undefined ||
            val instanceof Object ||
            Array.isArray(val) ||
            typeof val === 'string' ||
            typeof val === 'number' ||
            typeof val === 'boolean' ||
            val instanceof Function ||
            val instanceof ArrayBuffer ||
            ArrayBuffer.isView(val));
});

CHECKED_EM_JS(int, _ScriptX_Stack_toString, (int index), {
  const stack = Module.SCRIPTX_STACK;
  const val = stack.values[index];
  let str;
  if (val === undefined) {
    str = typeof val;
  } else if (val === null) {
    str = 'null';
  } else if (val.toString) {
    str = val.toString();
  }

  return stack.push(str);
});

CHECKED_EM_JS(bool, _ScriptX_Stack_equals, (int lhs, int rhs), {
  const stack = Module.SCRIPTX_STACK;
  return stack.values[lhs] === stack.values[rhs];
});

CHECKED_EM_JS(int, _ScriptX_Stack_objectGet, (int obj, int key), {
  const stack = Module.SCRIPTX_STACK;
  obj = stack.values[obj];
  const ret = obj[stack.values[key]];
  return stack.push(ret);
});

CHECKED_EM_JS(void, _ScriptX_Stack_objectSet, (int obj, int key, int value), {
  const stack = Module.SCRIPTX_STACK;
  obj = stack.values[obj];
  key = stack.values[key];
  value = stack.values[value];
  obj[key] = value;
});

CHECKED_EM_JS(void, _ScriptX_Stack_objectRemove, (int obj, int key), {
  const stack = Module.SCRIPTX_STACK;
  obj = stack.values[obj];
  key = stack.values[key];
  delete obj[key];
});

CHECKED_EM_JS(int, _ScriptX_Stack_objectHas, (int obj, int key), {
  const stack = Module.SCRIPTX_STACK;
  obj = stack.values[obj];
  key = stack.values[key];
  return typeof obj == "object" && obj.hasOwnProperty(key);
});

CHECKED_EM_JS(int, _ScriptX_Stack_objectInstanceOf, (int obj, int who), {
  const stack = Module.SCRIPTX_STACK;
  obj = stack.values[obj];
  who = stack.values[who];
  return obj instanceof who;
});

CHECKED_EM_JS(int, _ScriptX_Stack_objectGetKeys, (int obj), {
  const stack = Module.SCRIPTX_STACK;
  obj = stack.values[obj];
  const keys = Object.getOwnPropertyNames(obj);
  return stack.push(keys);
});

CHECKED_EM_JS(int, _ScriptX_Stack_arrayGet, (int array, int index), {
  const stack = Module.SCRIPTX_STACK;
  array = stack.values[array];
  const ret = array[index];
  return stack.push(ret);
});

CHECKED_EM_JS(void, _ScriptX_Stack_arraySet, (int array, int index, int valueIndex), {
  const stack = Module.SCRIPTX_STACK;
  array = stack.values[array];
  array[index] = stack.values[valueIndex];
});

CHECKED_EM_JS(int, _ScriptX_Stack_arrayLength, (int array), {
  const stack = Module.SCRIPTX_STACK;
  return stack.values[array].length;
});

CHECKED_EM_JS(void, _ScriptX_Stack_arrayClear, (int array), {
  const stack = Module.SCRIPTX_STACK;
  stack.values[array].length = 0;
});

CHECKED_EM_JS(int32_t, _ScriptX_Stack_toNumInt32, (int num), {
  const stack = Module.SCRIPTX_STACK;
  num = stack.values[num];
  return num
});

CHECKED_EM_JS(int64_t, _ScriptX_Stack_toNumInt64, (int num), {
  const stack = Module.SCRIPTX_STACK;
  num = stack.values[num];
  return num
});

CHECKED_EM_JS(float, _ScriptX_Stack_toNumFloat, (int num), {
  const stack = Module.SCRIPTX_STACK;
  num = stack.values[num];
  return num
});

CHECKED_EM_JS(double, _ScriptX_Stack_toNumDouble, (int num), {
  const stack = Module.SCRIPTX_STACK;
  num = stack.values[num];
  return num
});

CHECKED_EM_JS(bool, _ScriptX_Stack_toBoolean, (int index), {
  const stack = Module.SCRIPTX_STACK;
  return stack.values[index];
});

CHECKED_EM_JS(intptr_t, _ScriptX_Stack_toCString, (int index), {
  const stack = Module.SCRIPTX_STACK;
  let str =  stack.values[index];

  if (str === undefined) {
    str = typeof val;
  } else if (str === null) {
    str = 'null';
  } else if (str.toString) {
    str = str.toString();
  }

  const buffer = allocateUTF8(str);
  return buffer;
});

CHECKED_EM_JS(int, _ScriptX_evaluateJavaScriptCode, (int code, int source), {
  const stack = Module.SCRIPTX_STACK;
  code = stack.values[code];
  source = stack.values[source];

  const useFunction = false;
  if (useFunction) {
    let func = new Function("'use strict';" + code);
    if (typeof source === 'string') {
      const real = func;
      const wrapper = {
        [source]: function() { return real(); }
      };
      // the wrapper function has a name [source]
      func = wrapper[source];
    }
    const ret = func();
    return stack.push(ret);
  } else {
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval
    // If you use the eval function indirectly, by invoking it via a reference other than eval,
    // as of ECMAScript 5 it works in the global scope rather than the local scope.
    const geval = eval;
    const ret = geval(code);
    return stack.push(ret);
  }
});

CHECKED_EM_JS(int, _ScriptX_Stack_getGlobal, (), {
  const globalValue = Module._ScriptX_getGlobalObject();
  return Module.SCRIPTX_STACK.push(globalValue);
});

CHECKED_EM_JS(int, _ScriptX_Stack_globalGet, (int key), {
  const globalValue = Module._ScriptX_getGlobalObject();
  const stack = Module.SCRIPTX_STACK;
  return stack.push(globalValue[stack.values[key]]);
});

CHECKED_EM_JS(void, _ScriptX_Stack_globalSet, (int key, int value), {
  const stack = Module.SCRIPTX_STACK;
  const globalValue = Module._ScriptX_getGlobalObject();
  globalValue[stack.values[key]] = stack.values[value];
});

CHECKED_EM_JS(int, _ScriptX_Global_new, (int localValueIndex, bool isWeak), {
  const stack = Module.SCRIPTX_STACK;
  const key = Module._ScriptX_Global_newKey(isWeak);
  const map = Module._ScriptX_Global_getMap(isWeak);
  map[key] = stack.values[localValueIndex];
  return key;
});

CHECKED_EM_JS(int, _ScriptX_Global_dup, (int key, bool isFromWeak, bool isToWeak), {
  const fromMap = Module._ScriptX_Global_getMap(isFromWeak);
  const toMap = Module._ScriptX_Global_getMap(isToWeak);
  const newKey = Module._ScriptX_Global_newKey(isToWeak);

  toMap[newKey] = fromMap[key];
  return newKey;
});

CHECKED_EM_JS(void, _ScriptX_Global_delete, (int key, bool isWeak), {
  const map = Module._ScriptX_Global_getMap(isWeak);
  delete map[key];
});

CHECKED_EM_JS(int, _ScriptX_Global_get, (int key, bool isWeak), {
  const stack = Module.SCRIPTX_STACK;
  const map = Module._ScriptX_Global_getMap(isWeak);
  const value = map[key];
  return stack.push(value);
});

CHECKED_EM_JS(void, _ScriptX_Native_defineProperty,
    (int obj, int name, int getter, int setter), {
  const stack = Module.SCRIPTX_STACK;
  obj = stack.values[obj];
  name = stack.values[name];
  getter = stack.values[getter] || function() {};
  setter = stack.values[setter] || function() {};

  Object.defineProperty(obj, name, {
      get: getter,
      set: setter,
      enumerable: true,
      configurable: true
  });
});

CHECKED_EM_JS(int, _ScriptX_Native_getNamespaceForRegister, (int nameSpace), {
  try {
    const globalValue = Module._ScriptX_getGlobalObject();
    const stack = Module.SCRIPTX_STACK;
    nameSpace = stack.values[nameSpace];

    let obj = globalValue;
    if (nameSpace && nameSpace !== "") {
      let segs = nameSpace.split('.');
      for (let s of segs) {
        let n = obj[s];
        if (!(n instanceof Object)) {
          if (n === undefined || n === null) {
            n = {};
            obj[s] = n;
          } else {
            throw Error("already has a non object value");
          }
        }
        obj = n;
      }
    }
    return stack.push(obj);
  } catch (e) {
    console.log("failed to getNamespaceForRegister [" + nameSpace + "] " + e);
    return -1;
  }
});

CHECKED_EM_JS(void, _ScriptX_Native_setInternalState,
    (int obj, intptr_t classDefine, intptr_t instance), {
  const stack = Module.SCRIPTX_STACK;
  obj = stack.values[obj];
  obj[Module._ScriptX_Native_Symbol_instance] = instance;
  obj[Module._ScriptX_Native_Symbol_classDefine] = classDefine;
});

CHECKED_EM_JS(intptr_t, _ScriptX_Native_getInternalStateInstance, (int obj), {
  const stack = Module.SCRIPTX_STACK;
  obj = stack.values[obj];
  if (!(obj instanceof Object)) {
    return 0;
  }
  return obj[Module._ScriptX_Native_Symbol_instance];
});

CHECKED_EM_JS(intptr_t, _ScriptX_Native_getInternalStateClassDefine, (int obj), {
  const stack = Module.SCRIPTX_STACK;
  obj = stack.values[obj];
  if (!(obj instanceof Object)) {
    return 0;
  }
  return obj[Module._ScriptX_Native_Symbol_classDefine];
});

CHECKED_EM_JS(int, _ScriptX_Native_pushCppNewMark, (), {
  const stack = Module.SCRIPTX_STACK;
  return stack.push(Module._ScriptX_Native_Symbol_CppNewMark);
});

CHECKED_EM_JS(int, _ScriptX_Native_isCppNewMark, (int mark), {
  const stack = Module.SCRIPTX_STACK;
  return stack.values[mark] === Module._ScriptX_Native_Symbol_CppNewMark;
});

CHECKED_EM_JS(int, _ScriptX_Native_getInternalStore, (int weakThis), {
  const stack = Module.SCRIPTX_STACK;
  const sym = Module._ScriptX_Native_Symbol_InternalStore;
  const weakMap = Module._ScriptX_Global_getMap(true);
  const thiz = weakMap[weakThis];
  if (!thiz) {
    return -1;
  }
  if (typeof thiz[sym] !== 'object') {
      thiz[sym] = [];
  }
  return stack.push(thiz[sym]);
});

CHECKED_EM_JS(void, _ScriptX_Native_destroyInstance, (int scriptClassInstance), {
  const stack = Module.SCRIPTX_STACK;
  scriptClassInstance = stack.values[scriptClassInstance];

  Module._ScriptX_Native_destroyInstanceInternal(scriptClassInstance);
});

CHECKED_EM_JS(void, _ScriptX_ByteBuffer_fillBackingStore,
      (int val, intptr_t sharedPtr, intptr_t buffer), {
  const stack = Module.SCRIPTX_STACK;
  val = stack.values[val];

  if (val instanceof Module._ScriptX_SharedByteBuffer) {
    const sym = Module._ScriptX_Native_Symbol_SharedByteBuffer_ptr;
    setValue(sharedPtr, val[sym], "void*");
  } else {
    // val should be ArrayBuffer, DataView, TypedArray
    let length = val.byteLength;
    let backingBuffer = _malloc(length);
    let backingI8 = new Int8Array(Module.HEAPU8.buffer, backingBuffer, length);
    Module._ScriptX_TypedArray_memcpy(val, backingI8);
    setValue(buffer, backingBuffer, "void*");
  }
});

CHECKED_EM_JS(void, _ScriptX_ByteBuffer_fillTypeAndSize,
    (int val, intptr_t pType, intptr_t pSize), {
  const stack = Module.SCRIPTX_STACK;
  val = stack.values[val];

  let length = val.byteLength;
  setValue(pSize, length, "i32");

  // NOTE: KEEP SYNC WITH CPP
  const kUnspecified = 0x1;
  const kInt8 = 0x101;
  const kUint8 = 0x201;
  const kInt16 = 0x302;
  const kUint16 = 0x402;
  const kInt32 = 0x504;
  const kUint32 = 0x604;
  const kInt64 = 0x708;
  const kUint64 = 0x808;
  const KFloat32 = 0x904;
  const kFloat64 = 0xa08;

  let type = kUnspecified;
  if (val instanceof Int8Array) {
    type = kInt8;
  } else if (val instanceof Uint8Array || val instanceof Uint8ClampedArray) {
    type = kUint8;
  } else if (val instanceof Int16Array) {
    type = kInt16;
  } else if (val instanceof Uint16Array) {
    type = kUint16;
  } else if (val instanceof Int16Array) {
    type = kUint16;
  } else if (val instanceof Int32Array) {
    type = kInt32;
  } else if (val instanceof Uint32Array) {
    type = kUint32;
  } else if (val instanceof Float32Array) {
    type = KFloat32;
  } else if (val instanceof Float64Array) {
    type = kFloat64;
  } else if (val instanceof BigInt64Array) {
    type = kInt64;
  } else if (val instanceof BigUint64Array) {
    type = kUint64;
  }

  setValue(pType, type, "i16");
});

CHECKED_EM_JS(int, _ScriptX_ByteBuffer_fillWrapper,
      (intptr_t pointer, intptr_t sharedPtr, size_t size), {
  const stack = Module.SCRIPTX_STACK;
  const sharedByteBuffer = new Module._ScriptX_SharedByteBuffer(pointer, size, sharedPtr);
  return stack.push(sharedByteBuffer);
});

CHECKED_EM_JS(bool, _ScriptX_ByteBuffer_freeSharedByteBuffer, (int val), {
  const stack = Module.SCRIPTX_STACK;
  val = stack.values[val];
  if (val instanceof Module._ScriptX_SharedByteBuffer) {
    val.destroy();
    return true;
  }
  return false;
});

CHECKED_EM_JS(void, _ScriptX_ByteBuffer_syncData,
              (int val, intptr_t buffer, size_t length, bool isCommitOrSync), {
  const stack = Module.SCRIPTX_STACK;
  val = stack.values[val];

  // only if not shared
  if (!(val instanceof Module._ScriptX_SharedByteBuffer)) {
    let backingI8 = new Int8Array(Module.HEAPU8.buffer, buffer, length);
    if (isCommitOrSync) {
      Module._ScriptX_TypedArray_memcpy(backingI8, val);
    } else {
      Module._ScriptX_TypedArray_memcpy(val, backingI8);
    }
  }
});

CHECKED_EM_JS(bool, _ScriptX_ByteBuffer_isSharedByteBuffer, (int val), {
  const stack = Module.SCRIPTX_STACK;
  val = stack.values[val];
  return val instanceof Module._ScriptX_SharedByteBuffer;
});

// clang-format on
// </editor-fold>

namespace {

struct StaticInit {
  StaticInit() noexcept;
} dummy;

StaticInit::StaticInit() noexcept {
  _ScriptX_setInternalStatePointer(
      reinterpret_cast<intptr_t>(&kExceptionStore.hasPendingJsException_),
      reinterpret_cast<intptr_t>(&kExceptionStore.hasPendingCppException_));

  _ScriptX_initJavaScriptLibrary();
}

}  // namespace

int Stack::top() noexcept { CHECKED_JS_CALL(_ScriptX_Stack_top()); }

Local<Value> Stack::topValue() noexcept {
  CHECKED_JS_CALL(wasm_backend::WasmEngine::make<Local<Value>>(top()));
}

int Stack::setTop(int top) noexcept { CHECKED_JS_CALL(_ScriptX_Stack_setTop(top)); }

int Stack::set(int atIndex, int valueIndex) noexcept {
  _ScriptX_Stack_set(atIndex, valueIndex);
  return atIndex;
}

int Stack::set(int atIndex, const Local<Value> &value) noexcept {
  CHECKED_JS_CALL(set(atIndex, WasmEngine::refIndex(value)));
}

int Stack::newObject() { CHECKED_JS_CALL(_ScriptX_Stack_newObject()); }

int Stack::newObject(int type, int argsBase) {
  CHECKED_JS_CALL(_ScriptX_Stack_newObjectArgs(type, argsBase));
}

int Stack::newArray(size_t size) { CHECKED_JS_CALL(_ScriptX_Stack_newArray(size)); }

int Stack::newString(const char *str) { return newString(str, std::strlen(str)); }

int Stack::newString(const char *str, size_t length) {
  CHECKED_JS_CALL(_ScriptX_Stack_newStringLen(str, length));
}

int Stack::newNumber(int32_t num) { CHECKED_JS_CALL(_ScriptX_Stack_newNumInt(num)); }

int Stack::newNumber(double num) { CHECKED_JS_CALL(_ScriptX_Stack_newNumDouble(num)); }

int Stack::newBoolean(bool boolean) { CHECKED_JS_CALL(_ScriptX_Stack_newBoolean(boolean ? 1 : 0)); }

int Stack::newFunction(RawFunctionType callback, const void *ptr0, const void *ptr1,
                       bool checkForConstructorCall) {
  CHECKED_JS_CALL(_ScriptX_Stack_newRawFunction(
      reinterpret_cast<intptr_t>(callback), reinterpret_cast<intptr_t>(ptr0),
      reinterpret_cast<intptr_t>(ptr1), &currentEngine(), checkForConstructorCall));
}

int Stack::pushArrayBuffer(size_t length) {
  CHECKED_JS_CALL(_ScriptX_Stack_pushArrayBuffer(length));
}

int Stack::newError(int messageIndex) { CHECKED_JS_CALL(_ScriptX_Stack_newError(messageIndex)); }

int Stack::pushValue(int index) { CHECKED_JS_CALL(_ScriptX_Stack_pushValue(index)); }

int Stack::pushValue(const Local<Value> &ref) {
  CHECKED_JS_CALL(pushValue(WasmEngine::refIndex(ref)));
}

int Stack::pop(int count) noexcept { CHECKED_JS_CALL(_ScriptX_Stack_pop(count)); }

bool Stack::isObject(int index) noexcept { CHECKED_JS_CALL(_ScriptX_Stack_isObject(index)); }

bool Stack::isArray(int index) noexcept { CHECKED_JS_CALL(_ScriptX_Stack_isArray(index)); }

bool Stack::isString(int index) noexcept { CHECKED_JS_CALL(_ScriptX_Stack_isString(index)); }

bool Stack::isNumber(int index) noexcept { CHECKED_JS_CALL(_ScriptX_Stack_isNum(index)); }

bool Stack::isBoolean(int index) noexcept { CHECKED_JS_CALL(_ScriptX_Stack_isBoolean(index)); }

bool Stack::isFunction(int index) noexcept { CHECKED_JS_CALL(_ScriptX_Stack_isFunction(index)); }

bool Stack::isByteBuffer(int index) noexcept {
  CHECKED_JS_CALL(_ScriptX_Stack_isByteBuffer(index));
}

bool Stack::isUnsupported(int index) noexcept {
  CHECKED_JS_CALL(_ScriptX_Stack_isUnsupported(index));
}

bool Stack::isNullOrUndefined(int index) noexcept {
  CHECKED_JS_CALL(_ScriptX_Stack_isNullOrUndefined(index));
}

int Stack::toString(int index) { CHECKED_JS_CALL(_ScriptX_Stack_toString(index)); }

bool Stack::equals(int lhs, int rhs) { CHECKED_JS_CALL(_ScriptX_Stack_equals(lhs, rhs)); }

int Stack::objectGet(int obj, int key) { CHECKED_JS_CALL(_ScriptX_Stack_objectGet(obj, key)); }

void Stack::objectSet(int obj, int key, int value) {
  CHECKED_VOID_JS_CALL(_ScriptX_Stack_objectSet(obj, key, value));
}

void Stack::objectRemove(int obj, int key) {
  CHECKED_VOID_JS_CALL(_ScriptX_Stack_objectRemove(obj, key));
}

int Stack::objectHas(int obj, int key) { CHECKED_JS_CALL(_ScriptX_Stack_objectHas(obj, key)); }

int Stack::objectInstanceOf(int obj, int who) {
  CHECKED_JS_CALL(_ScriptX_Stack_objectInstanceOf(obj, who));
}

int Stack::objectGetKeys(int obj) { CHECKED_JS_CALL(_ScriptX_Stack_objectGetKeys(obj)); }

int Stack::arrayGet(int arrayIndex, int index) {
  CHECKED_JS_CALL(_ScriptX_Stack_arrayGet(arrayIndex, index));
}

void Stack::arraySet(int arrayIndex, int index, int valueIndex) {
  CHECKED_VOID_JS_CALL(_ScriptX_Stack_arraySet(arrayIndex, index, valueIndex));
}

int Stack::arrayLength(int arrayIndex) { CHECKED_JS_CALL(_ScriptX_Stack_arrayLength(arrayIndex)); }

void Stack::arrayClear(int arrayIndex) { _ScriptX_Stack_arrayClear(arrayIndex); }

float Stack::toNumberFloat(int num) { CHECKED_JS_CALL(_ScriptX_Stack_toNumFloat(num)); }
double Stack::toNumberDouble(int num) { CHECKED_JS_CALL(_ScriptX_Stack_toNumDouble(num)); }
int32_t Stack::toNumberInt32(int num) { CHECKED_JS_CALL(_ScriptX_Stack_toNumInt32(num)); }
int64_t Stack::toNumberInt64(int num) { CHECKED_JS_CALL(_ScriptX_Stack_toNumInt64(num)); }
bool Stack::toBoolean(int index) { CHECKED_JS_CALL(_ScriptX_Stack_toBoolean(index)); }
const char *Stack::toCString(int index) {
  CHECKED_JS_CALL(reinterpret_cast<const char *>(_ScriptX_Stack_toCString(index)));
}

int Stack::getGlobal() { CHECKED_JS_CALL(_ScriptX_Stack_getGlobal()); }

int Stack::globalGet(int key) { CHECKED_JS_CALL(_ScriptX_Stack_globalGet(key)); }

void Stack::globalSet(int key, int value) {
  CHECKED_VOID_JS_CALL(_ScriptX_Stack_globalSet(key, value));
}

int evaluateJavaScriptCode(int code, int source) {
  CHECKED_JS_CALL(_ScriptX_evaluateJavaScriptCode(code, source));
}

Local<Value> callFunction(const Local<Function> &func, const Local<Value> &thiz, size_t size,
                          const Local<Value> *args) {
  /*
   * call function
   * args are [stackBase, stackTop],
   * stack[0] == func
   * stack[1] == this
   * ... -> args
   *
   * pop args from stack and push result onto it.
   */

  Local<Value> ret;
  {
    StackFrameScope frame;
    int base = Stack::pushValue(func);
    Stack::pushValue(thiz);
    for (size_t i = 0; i < size; ++i) {
      Stack::pushValue(args[i]);
    }
    auto retIndex = _ScriptX_callFunction(base);
    checkException();
    ret = frame.returnValue(wasm_backend::WasmEngine::make<Local<Value>>(retIndex));
  }
  return ret;
}

int GlobalHelper::newGlobal(int localValueIndex, bool isWeak) {
  // TODO(taylor): the weak is fake for now
  // https://v8.dev/features/weak-references
  // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef
  CHECKED_JS_CALL(_ScriptX_Global_new(localValueIndex, isWeak));
}

int GlobalHelper::dupGlobal(int key, bool isFromWeak, bool isToWeak) {
  if (key == -1) return -1;
  CHECKED_JS_CALL(_ScriptX_Global_dup(key, isFromWeak, isToWeak));
}

void GlobalHelper::deleteGlobal(int value, bool isWeak) {
  if (value != -1) {
    CHECKED_VOID_JS_CALL(_ScriptX_Global_delete(value, isWeak));
  }
}

int GlobalHelper::getGlobal(int key, bool isWeak) {
  if (key == -1) return -1;
  CHECKED_JS_CALL(_ScriptX_Global_get(key, isWeak));
}

void NativeHelper::setInternalState(int obj, const void *classDefine, void *instance) {
  CHECKED_VOID_JS_CALL(_ScriptX_Native_setInternalState(
      obj, reinterpret_cast<uintptr_t>(classDefine), reinterpret_cast<uintptr_t>(instance)));
}

void *NativeHelper::getInternalStateInstance(int obj) {
  CHECKED_JS_CALL(reinterpret_cast<void *>(_ScriptX_Native_getInternalStateInstance(obj)));
}

void *NativeHelper::getInternalStateClassDefine(int obj) {
  CHECKED_JS_CALL(reinterpret_cast<void *>(_ScriptX_Native_getInternalStateClassDefine(obj)));
}

void NativeHelper::defineProperty(int obj, int name, int getter, int setter) {
  CHECKED_VOID_JS_CALL(_ScriptX_Native_defineProperty(obj, name, getter, setter));
}

int NativeHelper::getNamespaceForRegister(int nameSpace) {
  CHECKED_JS_CALL(_ScriptX_Native_getNamespaceForRegister(nameSpace));
}

int NativeHelper::pushCppNewMark() { CHECKED_JS_CALL(_ScriptX_Native_pushCppNewMark()); }

bool NativeHelper::isCppNewMark(int mark) { CHECKED_JS_CALL(_ScriptX_Native_isCppNewMark(mark)); }

int NativeHelper::getInternalStore(int weakThis) {
  CHECKED_JS_CALL(_ScriptX_Native_getInternalStore(weakThis));
}

void ByteBufferHelper::fillBackingStore(int val, std::shared_ptr<void> **sharedPtr, void **buffer) {
  CHECKED_VOID_JS_CALL(_ScriptX_ByteBuffer_fillBackingStore(
      val, reinterpret_cast<intptr_t>(sharedPtr), reinterpret_cast<intptr_t>(buffer)));
}

int ByteBufferHelper::fillWrapper(void *pVoid, std::shared_ptr<void> *sharedPtr, size_t size) {
  CHECKED_JS_CALL(_ScriptX_ByteBuffer_fillWrapper(reinterpret_cast<intptr_t>(pVoid),
                                                  reinterpret_cast<intptr_t>(sharedPtr), size));
}

void ByteBufferHelper::fillTypeAndSize(int val, uint16_t *pType, size_t *pSize) {
  CHECKED_VOID_JS_CALL(_ScriptX_ByteBuffer_fillTypeAndSize(val, reinterpret_cast<intptr_t>(pType),
                                                           reinterpret_cast<intptr_t>(pSize)));
}

bool ByteBufferHelper::freeSharedByteBuffer(int val) {
  CHECKED_JS_CALL(_ScriptX_ByteBuffer_freeSharedByteBuffer(val));
}

void ByteBufferHelper::commit(const Local<script::ByteBuffer> &buffer) {
  auto &&internalState = wasm_backend::WasmEngine::refIndex(buffer);
  auto jsBuffer = internalState.val_;
  auto ptr = internalState.backingStore_.get();
  // only do sync when we have 1) js array buffer 2) native buffer
  if (jsBuffer != -1 && ptr) {
    CHECKED_VOID_JS_CALL(wasm_backend::_ScriptX_ByteBuffer_syncData(
        jsBuffer, reinterpret_cast<intptr_t>(ptr), buffer.byteLength(), true));
  }
}

void ByteBufferHelper::sync(const Local<script::ByteBuffer> &buffer) {
  auto &&internalState = wasm_backend::WasmEngine::refIndex(buffer);
  auto jsBuffer = internalState.val_;
  auto ptr = internalState.backingStore_.get();
  // only do sync when we have 1) js array buffer 2) native buffer
  if (jsBuffer != -1 && ptr) {
    CHECKED_VOID_JS_CALL(wasm_backend::_ScriptX_ByteBuffer_syncData(
        jsBuffer, reinterpret_cast<intptr_t>(ptr), buffer.byteLength(), false));
  }
}

bool ByteBufferHelper::isSharedByteBuffer(const Local<ByteBuffer> &sharedByteBuffer) {
  auto &&internalState = wasm_backend::WasmEngine::refIndex(sharedByteBuffer);
  if (internalState.val_ == -1 && internalState.backingStore_) {
    // not realized SharedByteBuffer
    return true;
  }
  if (internalState.val_ != -1) {
    CHECKED_JS_CALL(wasm_backend::_ScriptX_ByteBuffer_isSharedByteBuffer(internalState.val_));
  }
  return false;
}

}  // namespace script::wasm_backend

namespace script {

Local<script::ByteBuffer> wasm_interop::newSharedByteBuffer(size_t size) {
  return ByteBuffer::newByteBuffer(
      std::shared_ptr<void>(std::malloc(size), [](void *ptr) { std::free(ptr); }), size);
}

void wasm_interop::destroySharedByteBuffer(const Local<ByteBuffer> &sharedByteBuffer) {
  auto &&internalState = wasm_backend::WasmEngine::refIndex(sharedByteBuffer);
  if (internalState.val_ != -1 &&
      wasm_backend::ByteBufferHelper::freeSharedByteBuffer(internalState.val_)) {
    internalState.val_ = -1;
  }
}

void wasm_interop::destroyScriptClass(const Local<Object> &scriptClass) {
  CHECKED_VOID_JS_CALL(wasm_backend::_ScriptX_Native_destroyInstance(
      wasm_backend::WasmEngine::refIndex(scriptClass)));
}

}  // namespace script
