#include "Logger.h"
#include "Utils.h"
#include "Arc.hpp"

#include <unordered_map>

namespace sharedjsi
{

extern "C" {
void v_call(void *ctx, dittoffi_result_uint64 res)
{
  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) {
    Object jsi_ffi_result(runtime);
    jsi_ffi_result.setProperty(runtime, "success", uint64ToBigIntOrNumber(runtime, res.success));
    jsi_ffi_result.setProperty(runtime, "error", cPointerToJSPointerObject(runtime, res.error));
    jsCallback->call(runtime, jsi_ffi_result);
  });
}

void v_free(void *ctx)
{
  // Force destruction by reducing reference counts.
  Arc<Function> ownedJsCallback = Arc<Function>::from_raw(ctx);
}
}

CLogLevel_t jsLogLevelStringToCLogLevel(std::string log_level)
{
  static const std::unordered_map<std::string, CLogLevel_t> lookup = {
    {"Error", C_LOG_LEVEL_ERROR},
    {"Warning", C_LOG_LEVEL_WARNING},
    {"Info", C_LOG_LEVEL_INFO},
    {"Debug", C_LOG_LEVEL_DEBUG},
    {"Verbose", C_LOG_LEVEL_VERBOSE},
  };

  auto it = lookup.find(log_level.c_str());
  if (it != lookup.end())
  {
    return it->second;
  }
  else
  {
    throw std::invalid_argument("Invalid log level");
  }
};

std::string cLogLevelToJSLogLevelString(CLogLevel_t log_level)
{
  static const std::unordered_map<CLogLevel_t, std::string> lookup = {
    {C_LOG_LEVEL_ERROR, "Error"},
    {C_LOG_LEVEL_WARNING, "Warning"},
    {C_LOG_LEVEL_INFO, "Info"},
    {C_LOG_LEVEL_DEBUG, "Debug"},
    {C_LOG_LEVEL_VERBOSE, "Verbose"},
  };

  auto it = lookup.find(log_level);
  if (it != lookup.end())
  {
    return it->second;
  }
  else
  {
    throw std::invalid_argument("Invalid log level");
  }
};

Function ditto_logger_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
                                          {
    ::ditto_logger_init();
    return Value();
  });
}

Function ditto_log(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
                                          {
    std::string log_level_str = arguments[0].toString(runtime).utf8(runtime);
    CLogLevel_t log_level = jsLogLevelStringToCLogLevel(log_level_str);

    std::string message_str = jsTypedArrayToCString(runtime, arguments[1]);
    const char *message = message_str.c_str();

    ::ditto_log(log_level, message);
    return Value();
  });
}

Function ditto_logger_minimum_log_level_get(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
                                          {
    CLogLevel_t res = ::ditto_logger_minimum_log_level_get();
    std::string log_level = cLogLevelToJSLogLevelString(res);
    return String::createFromUtf8(runtime, log_level);
  });
}

Function ditto_logger_minimum_log_level(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
                                          {
    std::string log_level_str = arguments[0].toString(runtime).utf8(runtime);
    CLogLevel_t log_level = jsLogLevelStringToCLogLevel(log_level_str);

    ::ditto_logger_minimum_log_level(log_level);
    return Value();
  });
}

Function ditto_logger_set_log_file(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_logger_set_log_file(arguments[0].toString(runtime).utf8(runtime).c_str());
    return Value();
  });
}

Function ditto_logger_emoji_headings_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
                                          {
    ::ditto_logger_emoji_headings_enabled(arguments[0].asBool());
    return Value();
  });
}

Function ditto_logger_emoji_headings_enabled_get(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
                                          {
    return ::ditto_logger_emoji_headings_enabled_get();
  });
}

Function ditto_logger_enabled_get(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
                                          {
    return ::ditto_logger_enabled_get();
  });
}

Function ditto_logger_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
                                          {
    ::ditto_logger_enabled(arguments[0].asBool());
    return Value();
  });
}

Function dittoffi_logger_try_export_to_file_async(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
                                          {
    std::string dest_path_str = jsTypedArrayToCString(runtime, arguments[0]);
    Function callback = arguments[1].getObject(runtime).getFunction(runtime);
    Arc<Function> wrapped_callback(std::move(callback));

    ::dittoffi_logger_try_export_to_file_async(dest_path_str.c_str(), {
      .env_ptr = Arc<Function>::into_raw(std::move(wrapped_callback)),
      .call = v_call,
      .free = v_free,
    });
    return Value();
  });
}

// Stores a wrapped JS callback if any has been set. Log callbacks are global
// and shared across all instances of the logger.
std::shared_ptr<Function> global_custom_log_callback;

extern "C"
{
void custom_log_callback_shim(CLogLevel_t c_log_level, char *c_message)
{
    // Don't even enqueue the call if there's no JS callback set.
    if (global_custom_log_callback.get() == nullptr)
    {
        return;
    }

  jsi_enqueue_call([=](Runtime &runtime) {
        // Do another check in case the callback was unset while waiting for
        // the enqueued call. There is no room for TOCTOU problems here
        // since the js world (of which the `Runtime` is a witness) is
        // single-threaded.
        if (global_custom_log_callback.get() != nullptr)
        {
            global_custom_log_callback->call(
                                             runtime,
                                             cLogLevelToJSLogLevelString(c_log_level),
                                             cPointerToJSPointerObject(runtime, c_message));
        } });
}
}


Function ditto_logger_set_custom_log_cb(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
                                            {
        if (count > 0 && !arguments[0].isNull() && !arguments[0].isUndefined())
        {
            global_custom_log_callback = std::make_shared<Function>(arguments[0].getObject(runtime).getFunction(runtime));
            ::ditto_logger_set_custom_log_cb(custom_log_callback_shim);
        }
        else
        {
            ::ditto_logger_set_custom_log_cb(nullptr);
            global_custom_log_callback.reset();
        }
        return Value::undefined();
    });
}
}
