#include <StoreObserver.h>

#include "Arc.hpp"
#include "Utils.h"
#include <memory>

namespace sharedjsi
{

DEFINE_RETAIN_RELEASE_FOR(Function, v_retain, v_release);

extern "C" {
void v_store_observation_handler(void *ctx, dittoffi_query_result_t *query_result, ArcDynFn0_void_t ffi_signal_next)
{
  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);

  auto release = ffi_signal_next.release;
  auto call = ffi_signal_next.call;
  auto cpp_owned_ffi_signal_next_ptr =
      std::shared_ptr<void>(ffi_signal_next.env_ptr, release);

  jsi_enqueue_call([=](Runtime &runtime) {
    auto queryResultPtr = cPointerToJSPointerObject(runtime, query_result);

    // Create a JS function for the signal_next callback
    Function signalNextFn = Function::createFromHostFunction(runtime,
                                                              PropNameID::forAscii(runtime, "signalNext"),
                                                              0,
                                                              [cpp_owned_ffi_signal_next_ptr, call](Runtime &runtime,
                                                                           const Value &thisValue,
                                                                           const Value *arguments,
                                                                           size_t count) -> Value
                                                              {
      call(cpp_owned_ffi_signal_next_ptr.get());
      return {};
    });

    jsCallback->call(runtime, queryResultPtr, signalNextFn);
  });
}
}

Function dittoffi_store_register_observer_throws(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 *ditto = jsPointerObjectToCPointer<CDitto_t>(runtime, arguments[0]);
    std::string query_str = jsTypedArrayToCString(runtime, arguments[1]);
    const char *query = query_str.c_str();

    std::vector<uint8_t> query_args_cbor_vec = jsTypedArrayToCVector(runtime, arguments[2]);
    slice_ref_uint8_t query_args_cbor = borrowVecAsOptRefSlice(query_args_cbor_vec);

    Function jsCallback = arguments[3].getObject(runtime).getFunction(runtime);
    Arc<Function> wrappedCallback(std::move(jsCallback));

    dittoffi_store_observation_handler_with_signal_next_t handler = {
      .env_ptr = Arc<Function>::into_raw(std::move(wrappedCallback)),
      .call = v_store_observation_handler,
      .free = v_release,
    };

    dittoffi_result_dittoffi_store_observer_ptr_t res = ::dittoffi_store_register_observer_throws(ditto, query, query_args_cbor, handler);

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

Function dittoffi_store_observers(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 *ditto = jsPointerObjectToCPointer<CDitto_t>(runtime, arguments[0]);
    Vec_dittoffi_store_observer_ptr_t res = ::dittoffi_store_observers(ditto);

    jsi::Array jsArray(runtime, res.len);

    for (size_t i = 0; i < res.len; ++i) {
      dittoffi_store_observer_t *ffi_observer = res.ptr[i];
      auto jsObj = cPointerToJSPointerObject<dittoffi_store_observer_t>(runtime, ffi_observer);
      jsArray.setValueAtIndex(runtime, i, jsObj);
    }

    ::dittoffi_store_observers_free_sparse(res);

    return jsArray;
  });
}

Function dittoffi_store_observer_id(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 *ffi_store_observer = jsPointerObjectToCPointer<dittoffi_store_observer_t>(runtime, arguments[0]);
    slice_boxed_uint8_t id_cbor = ::dittoffi_store_observer_id(ffi_store_observer);
    return cSlicedBoxToJSSlicedBox(runtime, id_cbor);
  });
}

Function dittoffi_store_observer_query_string(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 *ffi_store_observer = jsPointerObjectToCPointer<dittoffi_store_observer_t>(runtime, arguments[0]);
    char *res = ::dittoffi_store_observer_query_string(ffi_store_observer);
    return cPointerToJSPointerObject<char>(runtime, res);
  });
}

Function dittoffi_store_observer_query_arguments_cbor(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 *ffi_store_observer = jsPointerObjectToCPointer<dittoffi_store_observer_t>(runtime, arguments[0]);
    slice_boxed_uint8_t args_cbor = ::dittoffi_store_observer_query_arguments_cbor(ffi_store_observer);

    return cSlicedBoxToJSSlicedBox(runtime, args_cbor);
  });
}

Function dittoffi_store_observer_query_arguments_json(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 *ffi_store_observer = jsPointerObjectToCPointer<dittoffi_store_observer_t>(runtime, arguments[0]);
    slice_boxed_uint8_t args_json_bytes = ::dittoffi_store_observer_query_arguments_json(ffi_store_observer);
    return cSlicedBoxToJSSlicedBox(runtime, args_json_bytes);
  });
}

Function dittoffi_store_observer_cancel(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 *ffi_store_observer = jsPointerObjectToCPointer<dittoffi_store_observer_t>(runtime, arguments[0]);
    ::dittoffi_store_observer_cancel(ffi_store_observer);
    return {};
  });
}

Function dittoffi_store_observer_is_cancelled(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 *ffi_store_observer = jsPointerObjectToCPointer<dittoffi_store_observer_t>(runtime, arguments[0]);
    return ::dittoffi_store_observer_is_cancelled(ffi_store_observer);
  });
}

Function dittoffi_store_observer_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
                                          {
    auto *ffi_store_observer = jsPointerObjectToCPointer<dittoffi_store_observer_t>(runtime, arguments[0]);
    ::dittoffi_store_observer_free(ffi_store_observer);
    return {};
  });
}

}
