#include <jni.h>
#include <jsi/jsi.h>
#include <pthread.h>
#include <sstream>
#include <string>
#include <vector>
#include <android/log.h>
#include <ReactCommon/CallInvokerHolder.h>

#include "dittoffi.h"
#include "main.h"


using namespace facebook;

JavaVM *java_vm;
jobject java_object;
jobject java_context;

/**
 * A callback function that detaches the current JNI Environment when the thread exits.
 * See https://stackoverflow.com/a/30026231 for detailed explanation.
 */
void DeferThreadDetach(JNIEnv *env) {
    static pthread_key_t thread_key;

    static auto run_once = [] {
        const auto err = pthread_key_create(&thread_key, [](void *ts_env) {
            if (ts_env) {
                java_vm->DetachCurrentThread();
            }
        });
        if (err) {
            // Failed to create TSD key. Handle error if necessary.
        }
        return 0;
    };

    run_once();

    const auto ts_env = pthread_getspecific(thread_key);
    if (!ts_env) {
        if (pthread_setspecific(thread_key, env)) {
            // Failed to set thread-specific value for key. Handle error if necessary.
        }
    }
}

/**
 * Gets a JNIEnv* valid for the current thread, attaching to the JVM if necessary.
 * See https://stackoverflow.com/a/30026231 for detailed explanation.
 */
JNIEnv *GetJniEnv() {
    JNIEnv *env = nullptr;
    auto get_env_result = java_vm->GetEnv((void **)&env, JNI_VERSION_1_6);
    if (get_env_result == JNI_EDETACHED) {
        if (java_vm->AttachCurrentThread(&env, NULL) == JNI_OK) {
            DeferThreadDetach(env);
        } else {
            // Failed to attach thread. Handle error if necessary.
        }
    } else if (get_env_result == JNI_EVERSION) {
        // Unsupported JNI version. Handle error if necessary.
    }
    return env;
}

void install(Runtime &jsiRuntime) {
    auto defaultDeviceName = Function::createFromHostFunction(
            jsiRuntime,
            PropNameID::forAscii(jsiRuntime, "defaultDeviceName"),
            0,
            [](Runtime &runtime, const Value &thisValue, const Value *arguments, size_t count) -> Value {
                JNIEnv *jniEnv = GetJniEnv();

                jclass local_java_class = jniEnv->GetObjectClass(java_object);
                jmethodID defaultDeviceNameMethod = jniEnv->GetMethodID(
                        local_java_class, "defaultDeviceName", "()Ljava/lang/String;");
                jobject result = jniEnv->CallObjectMethod(java_object, defaultDeviceNameMethod);

                const char *str = jniEnv->GetStringUTFChars((jstring)result, NULL);
                Value returnValue = Value(runtime, String::createFromUtf8(runtime, str));

                jniEnv->ReleaseStringUTFChars((jstring)result, str);
                jniEnv->DeleteLocalRef(result);
                jniEnv->DeleteLocalRef(local_java_class);

                return returnValue;
            });

    jsiRuntime.global().setProperty(jsiRuntime, "defaultDeviceName", std::move(defaultDeviceName));

    auto ditto_sdk_transports_set_android_context = Function::createFromHostFunction(
            jsiRuntime,
            PropNameID::forUtf8(jsiRuntime, "ditto_sdk_transports_set_android_context"),
            1,
            [](Runtime &runtime, const Value &thisArg, const Value *args, size_t count) -> Value {
                int result = ::ditto_sdk_transports_set_android_context(GetJniEnv(), java_context);
                return Value(result);
            });

    jsiRuntime.global().setProperty(jsiRuntime, "ditto_sdk_transports_set_android_context",
                                    std::move(ditto_sdk_transports_set_android_context));

    auto ditto_sdk_transports_android_missing_permissions = Function::createFromHostFunction(
            jsiRuntime,
            PropNameID::forUtf8(jsiRuntime, "ditto_sdk_transports_android_missing_permissions"),
            1,
            [](Runtime &runtime, const Value &thisArg, const Value *args, size_t count) -> Value {
                const char *result = ::ditto_sdk_transports_android_missing_permissions();
                std::vector<std::string> lines;
                std::stringstream ss(result);
                std::string line;
                while (std::getline(ss, line, '\n')) {
                    lines.push_back(line);
                }

                Array jsiArray(runtime, lines.size());
                for (size_t i = 0; i < lines.size(); ++i) {
                    jsiArray.setValueAtIndex(runtime, i, String::createFromUtf8(runtime, lines[i]));
                }

                free(const_cast<char*>(result));

                return jsiArray;
            });

    jsiRuntime.global().setProperty(jsiRuntime, "ditto_sdk_transports_android_missing_permissions",
                                    std::move(ditto_sdk_transports_android_missing_permissions));
}

extern "C"
JNIEXPORT void JNICALL
Java_com_dittolive_rnsdk_DittoRNSDKModule_nativeInstall(JNIEnv *env, jobject thiz, jobject context,
                                                        jobject call_invoker, jlong jsi_ptr,
                                                        jstring default_dir) {
    // Save context, module, java_vm objects globally (thread-safe, as opposed to JNIEnv)
    java_context = env->NewGlobalRef(context);
    java_object = env->NewGlobalRef(thiz);
    env->GetJavaVM(&java_vm);

    Runtime &runtime = *(reinterpret_cast<Runtime *>(jsi_ptr));

    // We knew we need to pass the callInvoker from Java to JNI and eventually
    // access the cthis() method to get the actual CallInvoker object, just like
    // in the video: https://youtu.be/oLmGInjKU2U?feature=shared&t=1220
    // The details on how to do this were not documented, though.
    // We have to figure a way to get the actual CallInvoker object using the fbjni library.
    //
    // jni::alias_ref<> is a wrapper provided by the React Native JNI layer.
    // It creates an “alias” reference type which helps in managing the
    // lifecycle of JNI references more safely. The alias_ref ensures that the
    // reference is valid within the current scope and automatically deletes
    // the local reference when going out of scope to prevent memory leaks.
    auto callInvokerHolderObjRef = reinterpret_cast<react::CallInvokerHolder::javaobject>(call_invoker);
    auto jsCallInvokerHolder = jni::alias_ref<react::CallInvokerHolder::javaobject>(callInvokerHolderObjRef)->cthis();
    auto jsCallInvoker = jsCallInvokerHolder->getCallInvoker();

    Object parameters(runtime);
    const char *str = env->GetStringUTFChars(default_dir, NULL);
    String defaultDirStr = String::createFromUtf8(runtime, str);
    parameters.setProperty(runtime, "defaultDirectory", defaultDirStr);
    env->ReleaseStringUTFChars(default_dir, str);

    sharedjsi::install(runtime, jsCallInvoker, parameters);
    install(runtime);
}
