#include "napi.h"
#include "test_helper.h"

using namespace Napi;

// Native wrappers for testing Object::Get()
Value GetPropertyWithUint32(const CallbackInfo& info);
Value GetPropertyWithNapiValue(const CallbackInfo& info);
Value GetPropertyWithNapiWrapperValue(const CallbackInfo& info);
Value GetPropertyWithCStyleString(const CallbackInfo& info);
Value GetPropertyWithCppStyleString(const CallbackInfo& info);

// Native wrappers for testing Object::Set()
Value SetPropertyWithUint32(const CallbackInfo& info);
Value SetPropertyWithNapiValue(const CallbackInfo& info);
Value SetPropertyWithNapiWrapperValue(const CallbackInfo& info);
Value SetPropertyWithCStyleString(const CallbackInfo& info);
Value SetPropertyWithCppStyleString(const CallbackInfo& info);

// Native wrappers for testing Object::Delete()
Value DeletePropertyWithUint32(const CallbackInfo& info);
Value DeletePropertyWithNapiValue(const CallbackInfo& info);
Value DeletePropertyWithNapiWrapperValue(const CallbackInfo& info);
Value DeletePropertyWithCStyleString(const CallbackInfo& info);
Value DeletePropertyWithCppStyleString(const CallbackInfo& info);

// Native wrappers for testing Object::HasOwnProperty()
Value HasOwnPropertyWithNapiValue(const CallbackInfo& info);
Value HasOwnPropertyWithNapiWrapperValue(const CallbackInfo& info);
Value HasOwnPropertyWithCStyleString(const CallbackInfo& info);
Value HasOwnPropertyWithCppStyleString(const CallbackInfo& info);

// Native wrappers for testing Object::Has()
Value HasPropertyWithUint32(const CallbackInfo& info);
Value HasPropertyWithNapiValue(const CallbackInfo& info);
Value HasPropertyWithNapiWrapperValue(const CallbackInfo& info);
Value HasPropertyWithCStyleString(const CallbackInfo& info);
Value HasPropertyWithCppStyleString(const CallbackInfo& info);

// Native wrappers for testing Object::AddFinalizer()
Value AddFinalizer(const CallbackInfo& info);
Value AddFinalizerWithHint(const CallbackInfo& info);

// Native wrappers for testing Object::operator []
Value SubscriptGetWithCStyleString(const CallbackInfo& info);
Value SubscriptGetWithCppStyleString(const CallbackInfo& info);
Value SubscriptGetAtIndex(const CallbackInfo& info);
void SubscriptSetWithCStyleString(const CallbackInfo& info);
void SubscriptSetWithCppStyleString(const CallbackInfo& info);
void SubscriptSetAtIndex(const CallbackInfo& info);

static bool testValue = true;
// Used to test void* Data() integrity
struct UserDataHolder {
  int32_t value;
};

Value TestGetter(const CallbackInfo& info) {
  return Boolean::New(info.Env(), testValue);
}

void TestSetter(const CallbackInfo& info) {
  testValue = info[0].As<Boolean>();
}

Value TestGetterWithUserData(const CallbackInfo& info) {
  const UserDataHolder* holder = reinterpret_cast<UserDataHolder*>(info.Data());
  return Number::New(info.Env(), holder->value);
}

void TestSetterWithUserData(const CallbackInfo& info) {
  UserDataHolder* holder = reinterpret_cast<UserDataHolder*>(info.Data());
  holder->value = info[0].As<Number>().Int32Value();
}

Value TestFunction(const CallbackInfo& info) {
  return Boolean::New(info.Env(), true);
}

Value TestFunctionWithUserData(const CallbackInfo& info) {
  UserDataHolder* holder = reinterpret_cast<UserDataHolder*>(info.Data());
  return Number::New(info.Env(), holder->value);
}

Value EmptyConstructor(const CallbackInfo& info) {
  auto env = info.Env();
  bool isEmpty = info[0].As<Boolean>();
  Object object = isEmpty ? Object() : Object(env, Object::New(env));
  return Boolean::New(env, object.IsEmpty());
}

Value ConstructorFromObject(const CallbackInfo& info) {
  auto env = info.Env();
  Object object = info[0].As<Object>();
  return Object(env, object);
}

Array GetPropertyNames(const CallbackInfo& info) {
  Object obj = info[0].As<Object>();
  Array arr = MaybeUnwrap(obj.GetPropertyNames());
  return arr;
}

void DefineProperties(const CallbackInfo& info) {
  Object obj = info[0].As<Object>();
  String nameType = info[1].As<String>();
  Env env = info.Env();

  Boolean trueValue = Boolean::New(env, true);
  UserDataHolder* holder = new UserDataHolder();
  holder->value = 1234;

  if (nameType.Utf8Value() == "literal") {
    obj.DefineProperties({
        PropertyDescriptor::Accessor(env, obj, "readonlyAccessor", TestGetter),
        PropertyDescriptor::Accessor(
            env, obj, "readwriteAccessor", TestGetter, TestSetter),
        PropertyDescriptor::Accessor(env,
                                     obj,
                                     "readonlyAccessorWithUserData",
                                     TestGetterWithUserData,
                                     napi_property_attributes::napi_default,
                                     reinterpret_cast<void*>(holder)),
        PropertyDescriptor::Accessor(env,
                                     obj,
                                     "readwriteAccessorWithUserData",
                                     TestGetterWithUserData,
                                     TestSetterWithUserData,
                                     napi_property_attributes::napi_default,
                                     reinterpret_cast<void*>(holder)),

        PropertyDescriptor::Accessor<TestGetter>("readonlyAccessorT"),
        PropertyDescriptor::Accessor<TestGetter, TestSetter>(
            "readwriteAccessorT"),
        PropertyDescriptor::Accessor<TestGetterWithUserData>(
            "readonlyAccessorWithUserDataT",
            napi_property_attributes::napi_default,
            reinterpret_cast<void*>(holder)),
        PropertyDescriptor::Accessor<TestGetterWithUserData,
                                     TestSetterWithUserData>(
            "readwriteAccessorWithUserDataT",
            napi_property_attributes::napi_default,
            reinterpret_cast<void*>(holder)),

        PropertyDescriptor::Value("readonlyValue", trueValue),
        PropertyDescriptor::Value("readwriteValue", trueValue, napi_writable),
        PropertyDescriptor::Value(
            "enumerableValue", trueValue, napi_enumerable),
        PropertyDescriptor::Value(
            "configurableValue", trueValue, napi_configurable),
        PropertyDescriptor::Function(env, obj, "function", TestFunction),
        PropertyDescriptor::Function(env,
                                     obj,
                                     "functionWithUserData",
                                     TestFunctionWithUserData,
                                     napi_property_attributes::napi_default,
                                     reinterpret_cast<void*>(holder)),
    });
  } else if (nameType.Utf8Value() == "string") {
    // VS2013 has lifetime issues when passing temporary objects into the
    // constructor of another object. It generates code to destruct the object
    // as soon as the constructor call returns. Since this isn't a common case
    // for using std::string objects, I'm refactoring the test to work around
    // the issue.
    std::string str1("readonlyAccessor");
    std::string str2("readwriteAccessor");
    std::string str1a("readonlyAccessorWithUserData");
    std::string str2a("readwriteAccessorWithUserData");

    std::string str1t("readonlyAccessorT");
    std::string str2t("readwriteAccessorT");
    std::string str1at("readonlyAccessorWithUserDataT");
    std::string str2at("readwriteAccessorWithUserDataT");

    std::string str3("readonlyValue");
    std::string str4("readwriteValue");
    std::string str5("enumerableValue");
    std::string str6("configurableValue");
    std::string str7("function");
    std::string str8("functionWithUserData");

    obj.DefineProperties({
        PropertyDescriptor::Accessor(env, obj, str1, TestGetter),
        PropertyDescriptor::Accessor(env, obj, str2, TestGetter, TestSetter),
        PropertyDescriptor::Accessor(env,
                                     obj,
                                     str1a,
                                     TestGetterWithUserData,
                                     napi_property_attributes::napi_default,
                                     reinterpret_cast<void*>(holder)),
        PropertyDescriptor::Accessor(env,
                                     obj,
                                     str2a,
                                     TestGetterWithUserData,
                                     TestSetterWithUserData,
                                     napi_property_attributes::napi_default,
                                     reinterpret_cast<void*>(holder)),

        PropertyDescriptor::Accessor<TestGetter>(str1t),
        PropertyDescriptor::Accessor<TestGetter, TestSetter>(str2t),
        PropertyDescriptor::Accessor<TestGetterWithUserData>(
            str1at,
            napi_property_attributes::napi_default,
            reinterpret_cast<void*>(holder)),
        PropertyDescriptor::Accessor<TestGetterWithUserData,
                                     TestSetterWithUserData>(
            str2at,
            napi_property_attributes::napi_default,
            reinterpret_cast<void*>(holder)),

        PropertyDescriptor::Value(str3, trueValue),
        PropertyDescriptor::Value(str4, trueValue, napi_writable),
        PropertyDescriptor::Value(str5, trueValue, napi_enumerable),
        PropertyDescriptor::Value(str6, trueValue, napi_configurable),
        PropertyDescriptor::Function(env, obj, str7, TestFunction),
        PropertyDescriptor::Function(env,
                                     obj,
                                     str8,
                                     TestFunctionWithUserData,
                                     napi_property_attributes::napi_default,
                                     reinterpret_cast<void*>(holder)),
    });
  } else if (nameType.Utf8Value() == "value") {
    obj.DefineProperties({
        PropertyDescriptor::Accessor(
            env, obj, Napi::String::New(env, "readonlyAccessor"), TestGetter),
        PropertyDescriptor::Accessor(
            env,
            obj,
            Napi::String::New(env, "readwriteAccessor"),
            TestGetter,
            TestSetter),
        PropertyDescriptor::Accessor(
            env,
            obj,
            Napi::String::New(env, "readonlyAccessorWithUserData"),
            TestGetterWithUserData,
            napi_property_attributes::napi_default,
            reinterpret_cast<void*>(holder)),
        PropertyDescriptor::Accessor(
            env,
            obj,
            Napi::String::New(env, "readwriteAccessorWithUserData"),
            TestGetterWithUserData,
            TestSetterWithUserData,
            napi_property_attributes::napi_default,
            reinterpret_cast<void*>(holder)),

        PropertyDescriptor::Accessor<TestGetter>(
            Napi::String::New(env, "readonlyAccessorT")),
        PropertyDescriptor::Accessor<TestGetter, TestSetter>(
            Napi::String::New(env, "readwriteAccessorT")),
        PropertyDescriptor::Accessor<TestGetterWithUserData>(
            Napi::String::New(env, "readonlyAccessorWithUserDataT"),
            napi_property_attributes::napi_default,
            reinterpret_cast<void*>(holder)),
        PropertyDescriptor::Accessor<TestGetterWithUserData,
                                     TestSetterWithUserData>(
            Napi::String::New(env, "readwriteAccessorWithUserDataT"),
            napi_property_attributes::napi_default,
            reinterpret_cast<void*>(holder)),

        PropertyDescriptor::Value(Napi::String::New(env, "readonlyValue"),
                                  trueValue),
        PropertyDescriptor::Value(
            Napi::String::New(env, "readwriteValue"), trueValue, napi_writable),
        PropertyDescriptor::Value(Napi::String::New(env, "enumerableValue"),
                                  trueValue,
                                  napi_enumerable),
        PropertyDescriptor::Value(Napi::String::New(env, "configurableValue"),
                                  trueValue,
                                  napi_configurable),
        PropertyDescriptor::Function(
            env, obj, Napi::String::New(env, "function"), TestFunction),
        PropertyDescriptor::Function(
            env,
            obj,
            Napi::String::New(env, "functionWithUserData"),
            TestFunctionWithUserData,
            napi_property_attributes::napi_default,
            reinterpret_cast<void*>(holder)),
    });
  }
}

void DefineValueProperty(const CallbackInfo& info) {
  Object obj = info[0].As<Object>();
  Name name = info[1].As<Name>();
  Value value = info[2];

  obj.DefineProperty(PropertyDescriptor::Value(name, value));
}

Value CreateObjectUsingMagic(const CallbackInfo& info) {
  Env env = info.Env();
  Object obj = Object::New(env);
  obj["cp_false"] = false;
  obj["cp_true"] = true;
  obj[std::string("s_true")] = true;
  obj[std::string("s_false")] = false;
  obj["0"] = 0;
  obj[(uint32_t)42] = 120;
  obj["0.0f"] = 0.0f;
  obj["0.0"] = 0.0;
  obj["-1"] = -1;
  obj["foo2"] = u"foo";
  obj[std::string("foo4")] = NAPI_WIDE_TEXT("foo");
  obj[std::string("foo5")] = "foo";
  obj[std::string("foo6")] = std::u16string(NAPI_WIDE_TEXT("foo"));
  obj[std::string("foo7")] = std::string("foo");
  obj[std::string("circular")] = obj;
  obj["circular2"] = obj;
  return obj;
}

#ifdef NAPI_CPP_EXCEPTIONS
Value Sum(const CallbackInfo& info) {
  Object object = info[0].As<Object>();
  int64_t sum = 0;

  for (const auto& e : object) {
    sum += static_cast<Value>(e.second).As<Number>().Int64Value();
  }

  return Number::New(info.Env(), sum);
}

void Increment(const CallbackInfo& info) {
  Env env = info.Env();
  Object object = info[0].As<Object>();

  for (auto e : object) {
    int64_t value = static_cast<Value>(e.second).As<Number>().Int64Value();
    ++value;
    e.second = Napi::Number::New(env, value);
  }
}
#endif  // NAPI_CPP_EXCEPTIONS

Value InstanceOf(const CallbackInfo& info) {
  Object obj = info[0].UnsafeAs<Object>();
  Function constructor = info[1].As<Function>();
  return Boolean::New(info.Env(), MaybeUnwrap(obj.InstanceOf(constructor)));
}

Value GetPrototype(const CallbackInfo& info) {
  Object obj = info[0].UnsafeAs<Object>();
  return MaybeUnwrap(obj.GetPrototype());
}

#ifdef NODE_API_EXPERIMENTAL_HAS_SET_PROTOTYPE
Value SetPrototype(const CallbackInfo& info) {
  Object obj = info[0].UnsafeAs<Object>();
  Object prototype = info[1].UnsafeAs<Object>();
  return Boolean::New(info.Env(), MaybeUnwrap(obj.SetPrototype(prototype)));
}
#endif  // NODE_API_EXPERIMENTAL_HAS_SET_PROTOTYPE

Object InitObject(Env env) {
  Object exports = Object::New(env);

  exports["emptyConstructor"] = Function::New(env, EmptyConstructor);
  exports["constructorFromObject"] = Function::New(env, ConstructorFromObject);

  exports["GetPropertyNames"] = Function::New(env, GetPropertyNames);
  exports["defineProperties"] = Function::New(env, DefineProperties);
  exports["defineValueProperty"] = Function::New(env, DefineValueProperty);

  exports["getPropertyWithUint32"] = Function::New(env, GetPropertyWithUint32);
  exports["getPropertyWithNapiValue"] =
      Function::New(env, GetPropertyWithNapiValue);
  exports["getPropertyWithNapiWrapperValue"] =
      Function::New(env, GetPropertyWithNapiWrapperValue);
  exports["getPropertyWithCStyleString"] =
      Function::New(env, GetPropertyWithCStyleString);
  exports["getPropertyWithCppStyleString"] =
      Function::New(env, GetPropertyWithCppStyleString);

  exports["setPropertyWithUint32"] = Function::New(env, SetPropertyWithUint32);
  exports["setPropertyWithNapiValue"] =
      Function::New(env, SetPropertyWithNapiValue);
  exports["setPropertyWithNapiWrapperValue"] =
      Function::New(env, SetPropertyWithNapiWrapperValue);
  exports["setPropertyWithCStyleString"] =
      Function::New(env, SetPropertyWithCStyleString);
  exports["setPropertyWithCppStyleString"] =
      Function::New(env, SetPropertyWithCppStyleString);

  exports["deletePropertyWithUint32"] =
      Function::New(env, DeletePropertyWithUint32);
  exports["deletePropertyWithNapiValue"] =
      Function::New(env, DeletePropertyWithNapiValue);
  exports["deletePropertyWithNapiWrapperValue"] =
      Function::New(env, DeletePropertyWithNapiWrapperValue);
  exports["deletePropertyWithCStyleString"] =
      Function::New(env, DeletePropertyWithCStyleString);
  exports["deletePropertyWithCppStyleString"] =
      Function::New(env, DeletePropertyWithCppStyleString);

  exports["hasOwnPropertyWithNapiValue"] =
      Function::New(env, HasOwnPropertyWithNapiValue);
  exports["hasOwnPropertyWithNapiWrapperValue"] =
      Function::New(env, HasOwnPropertyWithNapiWrapperValue);
  exports["hasOwnPropertyWithCStyleString"] =
      Function::New(env, HasOwnPropertyWithCStyleString);
  exports["hasOwnPropertyWithCppStyleString"] =
      Function::New(env, HasOwnPropertyWithCppStyleString);

  exports["hasPropertyWithUint32"] = Function::New(env, HasPropertyWithUint32);
  exports["hasPropertyWithNapiValue"] =
      Function::New(env, HasPropertyWithNapiValue);
  exports["hasPropertyWithNapiWrapperValue"] =
      Function::New(env, HasPropertyWithNapiWrapperValue);
  exports["hasPropertyWithCStyleString"] =
      Function::New(env, HasPropertyWithCStyleString);
  exports["hasPropertyWithCppStyleString"] =
      Function::New(env, HasPropertyWithCppStyleString);

  exports["createObjectUsingMagic"] =
      Function::New(env, CreateObjectUsingMagic);
#ifdef NAPI_CPP_EXCEPTIONS
  exports["sum"] = Function::New(env, Sum);
  exports["increment"] = Function::New(env, Increment);
#endif  // NAPI_CPP_EXCEPTIONS

  exports["addFinalizer"] = Function::New(env, AddFinalizer);
  exports["addFinalizerWithHint"] = Function::New(env, AddFinalizerWithHint);

  exports["instanceOf"] = Function::New(env, InstanceOf);

  exports["subscriptGetWithCStyleString"] =
      Function::New(env, SubscriptGetWithCStyleString);
  exports["subscriptGetWithCppStyleString"] =
      Function::New(env, SubscriptGetWithCppStyleString);
  exports["subscriptGetAtIndex"] = Function::New(env, SubscriptGetAtIndex);
  exports["subscriptSetWithCStyleString"] =
      Function::New(env, SubscriptSetWithCStyleString);
  exports["subscriptSetWithCppStyleString"] =
      Function::New(env, SubscriptSetWithCppStyleString);
  exports["subscriptSetAtIndex"] = Function::New(env, SubscriptSetAtIndex);

  exports["getPrototype"] = Function::New(env, GetPrototype);
#ifdef NODE_API_EXPERIMENTAL_HAS_SET_PROTOTYPE
  exports["setPrototype"] = Function::New(env, SetPrototype);
#endif  // NODE_API_EXPERIMENTAL_HAS_SET_PROTOTYPE

  return exports;
}
