#include "Utils.h"
#include "Arc.hpp"
#include <Transports.h>
#include <map>

namespace sharedjsi
{

DEFINE_RETAIN_RELEASE_FOR(Function, v_retain, v_release);

extern "C" {
void v_transport_condition_change_cb(void *ctx, ConditionSource_t condition_source, TransportCondition_t transport_condition)
{
  Arc<Function> const& borrowedJsCallback = Arc<Function>::borrow_from_raw(ctx);
  // copy ctor / retain so as to keep the ArcPointee<Function> alive until the enqueued_call completes.
  Arc<Function> jsCallback /* = */ (borrowedJsCallback);
  jsi_enqueue_call([=](Runtime &runtime) {
    const std::map<ConditionSource_t, std::string> conditionSourceToString = {
      {CONDITION_SOURCE_BLUETOOTH, "Bluetooth"},
      {CONDITION_SOURCE_TCP, "Tcp"},
      {CONDITION_SOURCE_AWDL, "Awdl"},
      {CONDITION_SOURCE_MDNS, "Mdns"},
      {CONDITION_SOURCE_WIFI_AWARE, "WiFiAware"}
    };
    
    
    std::string condition_source_str;
    auto it = conditionSourceToString.find(condition_source);
    if (it != conditionSourceToString.end()) {
      condition_source_str = it->second;
    } else {
      throw std::invalid_argument("Invalid ConditionSource_t value. A new value might've been introduced that is not handled.");
    }
    
    std::string transport_condition_str;
    const std::unordered_map<TransportCondition_t, std::string> transportConditionMap = {
      {TRANSPORT_CONDITION_UNKNOWN, "Unknown"},
      {TRANSPORT_CONDITION_OK, "Ok"},
      {TRANSPORT_CONDITION_GENERIC_FAILURE, "GenericFailure"},
      {TRANSPORT_CONDITION_APP_IN_BACKGROUND, "AppInBackground"},
      {TRANSPORT_CONDITION_MDNS_FAILURE, "MDNSFailure"},
      {TRANSPORT_CONDITION_TCP_LISTEN_FAILURE, "TCPListenFailure"},
      {TRANSPORT_CONDITION_NO_BLE_CENTRAL_PERMISSION, "NoBLECentralPermission"},
      {TRANSPORT_CONDITION_NO_BLE_PERIPHERAL_PERMISSION, "NoBLEPeripheralPermission"},
      {TRANSPORT_CONDITION_CANNOT_ESTABLISH_CONNECTION, "CannotEstablishConnection"},
      {TRANSPORT_CONDITION_BLE_DISABLED, "BLEDisabled"},
      {TRANSPORT_CONDITION_NO_BLE_HARDWARE, "NoBLEHardware"},
      {TRANSPORT_CONDITION_WIFI_DISABLED, "WiFiDisabled"},
      {TRANSPORT_CONDITION_TEMPORARILY_UNAVAILABLE, "TemporarilyUnavailable"},
    };
    
    auto it2 = transportConditionMap.find(transport_condition);
    if (it2 != transportConditionMap.end()) {
      transport_condition_str = it2->second;
    } else {
      throw std::invalid_argument("Invalid TransportCondition_t value. A new value might've been introduced that is not handled.");
    }
    
    jsCallback->call(runtime, condition_source_str, transport_condition_str);
  });
}
}

Function ditto_sdk_transports_error_new(Runtime &runtime)
{
  return Function::createFromHostFunction(runtime,
                                          PropNameID::forAscii(runtime,
                                                               STRINGIFIED_FUNC_NAME()),
                                          0,
                                          [](Runtime &runtime,
                                             const Value &thisValue,
                                             const Value *arguments,
                                             size_t count) -> Value
                                          {
    DittoSdkTransportsError_t *outError = ::ditto_sdk_transports_error_new();
    return cPointerToJSPointerObject<DittoSdkTransportsError_t>(runtime, outError);
  });
}

Function ditto_sdk_transports_init(Runtime &runtime)
{
  return Function::createFromHostFunction(runtime,
                                          PropNameID::forAscii(runtime,
                                                               STRINGIFIED_FUNC_NAME()),
                                          0,
                                          [](Runtime &runtime,
                                             const Value &thisValue,
                                             const Value *arguments,
                                             size_t count) -> Value
                                          {
    auto *outError = jsPointerObjectToCPointer<DittoSdkTransportsError_t>(runtime, arguments[0]);
    return ::ditto_sdk_transports_init(outError);
  });
}

Function ditto_sdk_transports_error_value(Runtime &runtime)
{
  return Function::createFromHostFunction(runtime,
                                          PropNameID::forAscii(runtime,
                                                               STRINGIFIED_FUNC_NAME()),
                                          0,
                                          [](Runtime &runtime,
                                             const Value &thisValue,
                                             const Value *arguments,
                                             size_t count) -> Value
                                          {
    auto *outError = jsPointerObjectToCPointer<DittoSdkTransportsError_t>(runtime, arguments[0]);
    DittoSdkTransportsError_t error = ::ditto_sdk_transports_error_value(outError);

    std::string error_string;
    switch (error) {
      case DITTO_SDK_TRANSPORTS_ERROR_NONE:
        error_string = "None";
        break;
      case DITTO_SDK_TRANSPORTS_ERROR_GENERIC:
        error_string = "Generic";
        break;
      case DITTO_SDK_TRANSPORTS_ERROR_UNAVAILABLE:
        error_string = "Unavailable";
        break;
      default:
        throw "Unimplemented error value";
    }
    return String::createFromUtf8(runtime, error_string);

  });
}

Function ditto_sdk_transports_error_free(Runtime &runtime)
{
  return Function::createFromHostFunction(runtime,
                                          PropNameID::forAscii(runtime,
                                                               STRINGIFIED_FUNC_NAME()),
                                          0,
                                          [](Runtime &runtime,
                                             const Value &thisValue,
                                             const Value *arguments,
                                             size_t count) -> Value
                                          {
    ::ditto_sdk_transports_error_free(jsPointerObjectToCPointer<DittoSdkTransportsError_t>(runtime, arguments[0]));
    return {};
  });
}

Function dittoffi_ditto_try_set_transport_config(Runtime &runtime)
{
  return Function::createFromHostFunction(runtime,
                                          PropNameID::forAscii(runtime,
                                                               STRINGIFIED_FUNC_NAME()),
                                          0,
                                          [](Runtime &runtime,
                                             const Value &thisValue,
                                             const Value *arguments,
                                             size_t count) -> Value
                                          {
    CDitto_t *ditto = jsPointerObjectToCPointer<CDitto_t>(runtime, arguments[0]);
    std::vector<uint8_t> id_vec = jsTypedArrayToCVector(runtime, arguments[1]);
    slice_ref_uint8_t transport_config_cbor = borrowVecAsRefSlice(id_vec);
    bool should_validate = arguments[2].asBool();

    dittoffi_result_void_t res = ::dittoffi_ditto_try_set_transport_config(ditto, transport_config_cbor, should_validate);

    Object obj(runtime);
    obj.setProperty(runtime, "error", cPointerToJSPointerObject(runtime, res.error));
    return obj;
  });
}

Function dittoffi_ditto_transport_config(Runtime &runtime)
{
  return Function::createFromHostFunction(runtime,
                                          PropNameID::forAscii(runtime,
                                                               STRINGIFIED_FUNC_NAME()),
                                          0,
                                          [](Runtime &runtime,
                                             const Value &thisValue,
                                             const Value *arguments,
                                             size_t count) -> Value
                                          {

    slice_boxed_uint8_t transport_config_cbor = ::dittoffi_ditto_transport_config(jsPointerObjectToCPointer<CDitto_t>(runtime, arguments[0]));
    return cSlicedBoxToJSSlicedBox(runtime, transport_config_cbor);
  });
}

Function dittoffi_ditto_set_cloud_sync_enabled(Runtime &runtime)
{
  return Function::createFromHostFunction(runtime,
                                          PropNameID::forAscii(runtime,
                                                               STRINGIFIED_FUNC_NAME()),
                                          0,
                                          [](Runtime &runtime,
                                             const Value &thisValue,
                                             const Value *arguments,
                                             size_t count) -> Value
                                          {
    CDitto_t *ditto = jsPointerObjectToCPointer<CDitto_t>(runtime, arguments[0]);
    bool enable_ditto_cloud_sync = arguments[1].asBool();

    ::dittoffi_ditto_set_cloud_sync_enabled(ditto, enable_ditto_cloud_sync);
    return {};
  });
}

Function ditto_register_transport_condition_changed_callback(Runtime &runtime)
{
  return Function::createFromHostFunction(runtime,
                                          PropNameID::forAscii(runtime,
                                                               STRINGIFIED_FUNC_NAME()),
                                          0,
                                          [](Runtime &runtime,
                                             const Value &thisValue,
                                             const Value *arguments,
                                             size_t count) -> Value
                                          {
    CDitto_t *ditto = jsPointerObjectToCPointer<CDitto_t>(runtime, arguments[0]);
    Function jsCallback = arguments[1].getObject(runtime).getFunction(runtime);
    Arc<Function> wrappedStateFfiWrapper(std::move(jsCallback));
    
    ::ditto_register_transport_condition_changed_callback(ditto, Arc<Function>::as_raw(wrappedStateFfiWrapper), v_retain, v_release, v_transport_condition_change_cb);
    return {};
  });
}
}
