/* ObjectReference can be used to create references to Values that
are not Objects by creating a blank Object and setting Values to
it. Subclasses of Objects can only be set using an ObjectReference
by first casting it as an Object. */
#include "assert.h"
#include "napi.h"
#include "test_helper.h"

using namespace Napi;

ObjectReference weak;
ObjectReference persistent;
ObjectReference reference;

ObjectReference casted_weak;
ObjectReference casted_persistent;
ObjectReference casted_reference;

// Set keys can be one of:
// C style string, std::string& utf8, and const char *

// Set values can be one of:
//  Napi::Value
//  napi_value (req static_cast)
// const char* (c style string)
// boolean
// double

enum VAL_TYPES { JS = 0, C_STR, CPP_STR, BOOL, INT, DOUBLE, JS_CAST };

// Test that Set() with std::string key and value accepts temporaries (rvalues).
// This verifies that the parameter is `const std::string&` rather than
// `std::string&`.
void SetWithTempString(const Napi::CallbackInfo& info) {
  Env env = info.Env();
  HandleScope scope(env);

  Napi::ObjectReference ref = Persistent(Object::New(env));
  ref.SuppressDestruct();

  ref.Set(std::string("tempKey"), std::string("tempValue"));
  ref.Set(std::string("anotherKey"), info[0].As<Napi::String>().Utf8Value());

  assert(MaybeUnwrap(ref.Get("tempKey")).As<Napi::String>().Utf8Value() ==
         "tempValue");
  assert(MaybeUnwrap(ref.Get("anotherKey")).As<Napi::String>().Utf8Value() ==
         info[0].As<Napi::String>().Utf8Value());
}

void MoveOperatorsTest(const Napi::CallbackInfo& info) {
  Napi::ObjectReference existingRef;
  Napi::ObjectReference existingRef2;
  Napi::Object testObject = Napi::Object::New(info.Env());
  testObject.Set("testProp", "tProp");

  // ObjectReference(Reference<Object>&& other);
  Napi::Reference<Napi::Object> refObj =
      Napi::Reference<Napi::Object>::New(testObject);
  Napi::ObjectReference objRef = std::move(refObj);
  std::string prop = MaybeUnwrap(objRef.Get("testProp")).As<Napi::String>();
  assert(prop == "tProp");

  // ObjectReference& operator=(Reference<Object>&& other);
  Napi::Reference<Napi::Object> refObj2 =
      Napi::Reference<Napi::Object>::New(testObject);
  existingRef = std::move(refObj2);
  prop = MaybeUnwrap(existingRef.Get("testProp")).As<Napi::String>();
  assert(prop == "tProp");

  // ObjectReference(ObjectReference&& other);
  Napi::ObjectReference objRef3 = std::move(existingRef);
  prop = MaybeUnwrap(objRef3.Get("testProp")).As<Napi::String>();
  assert(prop == "tProp");

  // ObjectReference& operator=(ObjectReference&& other);
  existingRef2 = std::move(objRef3);
  prop = MaybeUnwrap(objRef.Get("testProp")).As<Napi::String>();
  assert(prop == "tProp");
}

void SetObjectWithCStringKey(Napi::ObjectReference& obj,
                             Napi::Value key,
                             Napi::Value val,
                             int valType) {
  std::string c_key = key.As<Napi::String>().Utf8Value();
  switch (valType) {
    case JS:
      obj.Set(c_key.c_str(), val);
      break;

    case JS_CAST:
      obj.Set(c_key.c_str(), static_cast<napi_value>(val));
      break;

    case C_STR: {
      std::string c_val = val.As<Napi::String>().Utf8Value();
      obj.Set(c_key.c_str(), c_val.c_str());
      break;
    }

    case BOOL:
      obj.Set(c_key.c_str(), val.As<Napi::Boolean>().Value());
      break;

    case DOUBLE:
      obj.Set(c_key.c_str(), val.As<Napi::Number>().DoubleValue());
      break;
  }
}

void SetObjectWithCppStringKey(Napi::ObjectReference& obj,
                               Napi::Value key,
                               Napi::Value val,
                               int valType) {
  std::string c_key = key.As<Napi::String>();
  switch (valType) {
    case JS:
      obj.Set(c_key, val);
      break;

    case JS_CAST:
      obj.Set(c_key, static_cast<napi_value>(val));
      break;

    case CPP_STR: {
      std::string c_val = val.As<Napi::String>();
      obj.Set(c_key, c_val);
      break;
    }

    case BOOL:
      obj.Set(c_key, val.As<Napi::Boolean>().Value());
      break;

    case DOUBLE:
      obj.Set(c_key, val.As<Napi::Number>().DoubleValue());
      break;
  }
}

void SetObjectWithIntKey(Napi::ObjectReference& obj,
                         Napi::Value key,
                         Napi::Value val,
                         int valType) {
  uint32_t c_key = key.As<Napi::Number>().Uint32Value();
  switch (valType) {
    case JS:
      obj.Set(c_key, val);
      break;

    case JS_CAST:
      obj.Set(c_key, static_cast<napi_value>(val));
      break;

    case C_STR: {
      std::string c_val = val.As<Napi::String>();
      obj.Set(c_key, c_val.c_str());
      break;
    }

    case CPP_STR: {
      std::string cpp_val = val.As<Napi::String>();
      obj.Set(c_key, cpp_val);
      break;
    }

    case BOOL:
      obj.Set(c_key, val.As<Napi::Boolean>().Value());
      break;

    case DOUBLE:
      obj.Set(c_key, val.As<Napi::Number>().DoubleValue());
      break;
  }
}

void SetObject(const Napi::CallbackInfo& info) {
  Env env = info.Env();
  HandleScope scope(env);

  weak = Weak(Object::New(env));
  weak.SuppressDestruct();

  persistent = Persistent(Object::New(env));
  persistent.SuppressDestruct();

  reference = Reference<Object>::New(Object::New(env), 2);
  reference.SuppressDestruct();

  Napi::Object configObject = info[0].As<Napi::Object>();

  int keyType =
      MaybeUnwrap(configObject.Get("keyType")).As<Napi::Number>().Uint32Value();
  int valType =
      MaybeUnwrap(configObject.Get("valType")).As<Napi::Number>().Uint32Value();
  Napi::Value key = MaybeUnwrap(configObject.Get("key"));
  Napi::Value val = MaybeUnwrap(configObject.Get("val"));

  switch (keyType) {
    case CPP_STR:
      SetObjectWithCppStringKey(weak, key, val, valType);
      SetObjectWithCppStringKey(persistent, key, val, valType);
      SetObjectWithCppStringKey(reference, key, val, valType);
      break;

    case C_STR:
      SetObjectWithCStringKey(weak, key, val, valType);
      SetObjectWithCStringKey(persistent, key, val, valType);
      SetObjectWithCStringKey(reference, key, val, valType);
      break;

    case INT:
      SetObjectWithIntKey(weak, key, val, valType);
      SetObjectWithIntKey(persistent, key, val, valType);
      SetObjectWithIntKey(reference, key, val, valType);

    default:
      break;
  }
}

void SetCastedObjects(const CallbackInfo& info) {
  Env env = info.Env();
  HandleScope scope(env);

  Array ex = Array::New(env);
  ex.Set((uint32_t)0, String::New(env, "hello"));
  ex.Set(1, String::New(env, "world"));
  ex.Set(2, String::New(env, "!"));

  casted_weak = Weak(ex.As<Object>());
  casted_weak.SuppressDestruct();

  casted_persistent = Persistent(ex.As<Object>());
  casted_persistent.SuppressDestruct();

  casted_reference = Reference<Object>::New(ex.As<Object>(), 2);
  casted_reference.SuppressDestruct();
}

// info[0] is a flag to determine if the weak, persistent, or
// multiple reference ObjectReference is being requested.
Value GetFromValue(const CallbackInfo& info) {
  Env env = info.Env();

  if (info[0] == String::New(env, "weak")) {
    if (weak.IsEmpty()) {
      return String::New(env, "No Referenced Value");
    } else {
      return weak.Value();
    }
  } else if (info[0] == String::New(env, "persistent")) {
    return persistent.Value();
  } else {
    return reference.Value();
  }
}

Value GetHelper(ObjectReference& ref,
                Object& configObject,
                const Napi::Env& env) {
  int keyType =
      MaybeUnwrap(configObject.Get("keyType")).As<Napi::Number>().Uint32Value();
  if (ref.IsEmpty()) {
    return String::New(env, "No referenced Value");
  }

  switch (keyType) {
    case C_STR: {
      std::string c_key =
          MaybeUnwrap(configObject.Get("key")).As<String>().Utf8Value();
      return MaybeUnwrap(ref.Get(c_key.c_str()));
      break;
    }
    case CPP_STR: {
      std::string cpp_key =
          MaybeUnwrap(configObject.Get("key")).As<String>().Utf8Value();
      return MaybeUnwrap(ref.Get(cpp_key));
      break;
    }
    case INT: {
      uint32_t key =
          MaybeUnwrap(configObject.Get("key")).As<Number>().Uint32Value();
      return MaybeUnwrap(ref.Get(key));
      break;
    }

    default:
      return String::New(env, "Error: Reached end of getter");
      break;
  }
}

Value GetFromGetters(const CallbackInfo& info) {
  std::string object_req = info[0].As<String>();
  Object configObject = info[1].As<Object>();
  if (object_req == "weak") {
    return GetHelper(weak, configObject, info.Env());
  } else if (object_req == "persistent") {
    return GetHelper(persistent, configObject, info.Env());
  }

  return GetHelper(reference, configObject, info.Env());
}

// info[0] is a flag to determine if the weak, persistent, or
// multiple reference ObjectReference is being requested.
// info[1] is the key, and it be either a String or a Number.
Value GetFromGetter(const CallbackInfo& info) {
  Env env = info.Env();

  if (info[0] == String::New(env, "weak")) {
    if (weak.IsEmpty()) {
      return String::New(env, "No Referenced Value");
    } else {
      if (info[1].IsString()) {
        return MaybeUnwrap(weak.Get(info[1].As<String>().Utf8Value()));
      } else if (info[1].IsNumber()) {
        return MaybeUnwrap(weak.Get(info[1].As<Number>().Uint32Value()));
      }
    }
  } else if (info[0] == String::New(env, "persistent")) {
    if (info[1].IsString()) {
      return MaybeUnwrap(persistent.Get(info[1].As<String>().Utf8Value()));
    } else if (info[1].IsNumber()) {
      return MaybeUnwrap(persistent.Get(info[1].As<Number>().Uint32Value()));
    }
  } else {
    if (info[0].IsString()) {
      return MaybeUnwrap(reference.Get(info[0].As<String>().Utf8Value()));
    } else if (info[0].IsNumber()) {
      return MaybeUnwrap(reference.Get(info[0].As<Number>().Uint32Value()));
    }
  }

  return String::New(env, "Error: Reached end of getter");
}

// info[0] is a flag to determine if the weak, persistent, or
// multiple reference ObjectReference is being requested.
Value GetCastedFromValue(const CallbackInfo& info) {
  Env env = info.Env();

  if (info[0] == String::New(env, "weak")) {
    if (casted_weak.IsEmpty()) {
      return String::New(env, "No Referenced Value");
    } else {
      return casted_weak.Value();
    }
  } else if (info[0] == String::New(env, "persistent")) {
    return casted_persistent.Value();
  } else {
    return casted_reference.Value();
  }
}

// info[0] is a flag to determine if the weak, persistent, or
// multiple reference ObjectReference is being requested.
// info[1] is the key and it must be a Number.
Value GetCastedFromGetter(const CallbackInfo& info) {
  Env env = info.Env();

  if (info[0] == String::New(env, "weak")) {
    if (casted_weak.IsEmpty()) {
      return String::New(env, "No Referenced Value");
    } else {
      return MaybeUnwrap(casted_weak.Get(info[1].As<Number>()));
    }
  } else if (info[0] == String::New(env, "persistent")) {
    return MaybeUnwrap(casted_persistent.Get(info[1].As<Number>()));
  } else {
    return MaybeUnwrap(casted_reference.Get(info[1].As<Number>()));
  }
}

// info[0] is a flag to determine if the weak, persistent, or
// multiple reference ObjectReference is being requested.
Number UnrefObjects(const CallbackInfo& info) {
  Env env = info.Env();
  uint32_t num;

  if (info[0] == String::New(env, "weak")) {
    num = weak.Unref();
  } else if (info[0] == String::New(env, "persistent")) {
    num = persistent.Unref();
  } else if (info[0] == String::New(env, "references")) {
    num = reference.Unref();
  } else if (info[0] == String::New(env, "casted weak")) {
    num = casted_weak.Unref();
  } else if (info[0] == String::New(env, "casted persistent")) {
    num = casted_persistent.Unref();
  } else {
    num = casted_reference.Unref();
  }

  return Number::New(env, num);
}

// info[0] is a flag to determine if the weak, persistent, or
// multiple reference ObjectReference is being requested.
Number RefObjects(const CallbackInfo& info) {
  Env env = info.Env();
  uint32_t num;

  if (info[0] == String::New(env, "weak")) {
    num = weak.Ref();
  } else if (info[0] == String::New(env, "persistent")) {
    num = persistent.Ref();
  } else if (info[0] == String::New(env, "references")) {
    num = reference.Ref();
  } else if (info[0] == String::New(env, "casted weak")) {
    num = casted_weak.Ref();
  } else if (info[0] == String::New(env, "casted persistent")) {
    num = casted_persistent.Ref();
  } else {
    num = casted_reference.Ref();
  }

  return Number::New(env, num);
}

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

  exports["setCastedObjects"] = Function::New(env, SetCastedObjects);
  exports["setObject"] = Function::New(env, SetObject);
  exports["getCastedFromValue"] = Function::New(env, GetCastedFromValue);
  exports["getFromGetters"] = Function::New(env, GetFromGetters);
  exports["getCastedFromGetter"] = Function::New(env, GetCastedFromGetter);
  exports["getFromValue"] = Function::New(env, GetFromValue);
  exports["unrefObjects"] = Function::New(env, UnrefObjects);
  exports["refObjects"] = Function::New(env, RefObjects);
  exports["moveOpTest"] = Function::New(env, MoveOperatorsTest);
  exports["setWithTempString"] = Function::New(env, SetWithTempString);

  return exports;
}
