//
// Created by Allen on 24/08/26.
//

#include "JsiCore.h"

JavaVM *gJvm = nullptr;

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
    gJvm = vm;
    JNIEnv *env = nullptr;
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }
    return JNI_VERSION_1_6;
}

namespace jsiConversion::core {

void jPointToJsObject(Runtime &rt, JNIEnv *env, jobject jPoint, Object &jsPoint) {
    if (!jPoint) {
        return;
    }
    jclass jclsPoint = env->FindClass("android/graphics/Point");
    jfieldID x = env->GetFieldID(jclsPoint, "x", "I");
    jfieldID y = env->GetFieldID(jclsPoint, "y", "I");
    jsPoint.setProperty(rt, "x", env->GetIntField(jPoint, x));
    jsPoint.setProperty(rt, "y", env->GetIntField(jPoint, y));
    env->DeleteLocalRef(jclsPoint);
}

void jQuadToJsObject(Runtime &rt, JNIEnv *env, jobject jQuad, Object &jsQuad) {
    if (!jQuad) {
        return;
    }
    jclass jclsQuad = env->FindClass("com/dynamsoft/core/basic_structures/Quadrilateral");
    jfieldID points = env->GetFieldID(jclsQuad, "points", "[Landroid/graphics/Point;");
    auto jPoints = (jobjectArray) env->GetObjectField(jQuad, points);
    if (!jPoints) {
        jsQuad.setProperty(rt, "points", Value::undefined());
        env->DeleteLocalRef(jclsQuad);
        return;
    }
    int count = env->GetArrayLength(jPoints);
    Array pointArr(rt, count);
    int sumX = 0, minX = INT32_MAX, maxX = INT32_MIN;
    int sumY = 0, minY = INT32_MAX, maxY = INT32_MIN;
    for (int i = 0; i < count; ++i) {
        Object jsPoint(rt);
        jobject jPoint = env->GetObjectArrayElement(jPoints, i);
        jPointToJsObject(rt, env, jPoint, jsPoint);
        int x = jsPoint.getProperty(rt, "x").asNumber();
        int y = jsPoint.getProperty(rt, "y").asNumber();
        sumX+=x;
        sumY+=y;
        minX = min(minX, x);
        minY = min(minY, y);
        maxX = max(maxX, x);
        maxY = max(maxY, y);
        pointArr.setValueAtIndex(rt, i, std::move(jsPoint));
        env->DeleteLocalRef(jPoint);
    }
    jsQuad.setProperty(rt, "points", std::move(pointArr));

    jmethodID getArea = env->GetMethodID(jclsQuad, "getArea", "()I");
    jsQuad.setProperty(rt, "area", env->CallIntMethod(jQuad, getArea));
    Object jsCentrePoint(rt);
    jsCentrePoint.setProperty(rt, "x", (int) (sumX / count));
    jsCentrePoint.setProperty(rt, "y", (int) (sumY / count));
    jsQuad.setProperty(rt,  "centrePoint", std::move(jsCentrePoint));

    Object jsBoundingRect(rt);
    jsBoundingRect.setProperty(rt, "x", minX);
    jsBoundingRect.setProperty(rt, "y", minY);
    jsBoundingRect.setProperty(rt, "width", maxX - minX);
    jsBoundingRect.setProperty(rt, "height", maxY - minY);
    jsQuad.setProperty(rt, "boundingRect", std::move(jsBoundingRect));

    env->DeleteLocalRef(jclsQuad);
    env->DeleteLocalRef(jPoints);
}

void jCapturedItemToJsObject(Runtime &rt, JNIEnv *env, jobject jItem, int &outType, Object &jsItem) {
    if (jItem == nullptr) {
        return;
    }
    jclass clsCapturedResultItem = env->FindClass("com/dynamsoft/core/basic_structures/CapturedResultItem");
    jmethodID getType = env->GetMethodID(clsCapturedResultItem, "getType", "()I");
    jmethodID getTaskName = env->GetMethodID(clsCapturedResultItem, "getTaskName", "()Ljava/lang/String;");
    jmethodID getTargetROIDefName = env->GetMethodID(clsCapturedResultItem, "getTargetROIDefName", "()Ljava/lang/String;");

    outType = env->CallIntMethod(jItem, getType);
    jsItem.setProperty(rt, "type", outType);
    auto jTaskName = (jstring) env->CallObjectMethod(jItem, getTaskName);
    auto jTargetROIDefName = (jstring) env->CallObjectMethod(jItem, getTargetROIDefName);
    auto pTaskName = jTaskName ? env->GetStringUTFChars(jTaskName, nullptr) : "";
    auto pTargetROIDefName = jTargetROIDefName ? env->GetStringUTFChars(jTargetROIDefName, nullptr) : "";
    jsItem.setProperty(rt, "taskName", pTaskName);
    jsItem.setProperty(rt, "targetROIDefName", pTargetROIDefName);
    if (jTaskName) {
        env->ReleaseStringUTFChars(jTaskName, pTaskName);
        env->DeleteLocalRef(jTaskName);
    }
    if (jTargetROIDefName) {
        env->ReleaseStringUTFChars(jTargetROIDefName, pTargetROIDefName);
        env->DeleteLocalRef(jTargetROIDefName);
    }
    env->DeleteLocalRef(clsCapturedResultItem);

}

void jCapturedItemArrayToJsArray(Runtime &rt, JNIEnv *env, jobjectArray jItemArray, int *&outTypes, Array &jsItemArray) {
    if (jItemArray == nullptr) {
        return;
    }
    int count = env->GetArrayLength(jItemArray);
    if (jsItemArray.size(rt) != count) {
        jsItemArray = Array(rt, count);
    }
    outTypes = new int[count];
    int outType = 0;
    for (int i = 0; i < count; ++i) {
        Object jsItem = Object(rt);
        jobject jItem = env->GetObjectArrayElement(jItemArray, i);
        jCapturedItemToJsObject(rt, env, jItem, outType, jsItem);
        outTypes[i] = outType;
        jsItemArray.setValueAtIndex(rt, i, std::move(jsItem));
        env->DeleteLocalRef(jItem);
    }
}

jobject getJImageDataFromHO(JNIEnv *env, const ImageDataHostObject &imageDataHO) {
    env->PushLocalFrame(8);
    jbyteArray jByteArr = nullptr;
    if (imageDataHO.buffer != nullptr) {
        jByteArr = env->NewByteArray((int) imageDataHO.bufferSize);
        env->SetByteArrayRegion(jByteArr, 0, (int) imageDataHO.bufferSize, (jbyte *) imageDataHO.buffer);
    }
    jclass jclsImageData = env->FindClass("com/dynamsoft/core/basic_structures/ImageData");
    jmethodID initImageData = env->GetMethodID(jclsImageData, "<init>", "([BIIIIILcom/dynamsoft/core/basic_structures/ImageTag;)V");
    return env->PopLocalFrame(
            env->NewObject(jclsImageData, initImageData,
                           jByteArr, imageDataHO.width, imageDataHO.height, imageDataHO.stride,
                           imageDataHO.format, imageDataHO.orientation, (jobject)nullptr)
    );
}

void getHOFromJImageData(JNIEnv *env, jobject jImageData, ImageDataHostObject &imageHO) {
    if (jImageData == nullptr) {
        return;
    }
    env->PushLocalFrame(8);
    jclass jclsImageData = env->FindClass("com/dynamsoft/core/basic_structures/ImageData");
    jfieldID bytes = env->GetFieldID(jclsImageData, "bytes", "[B");
    jfieldID width = env->GetFieldID(jclsImageData, "width", "I");
    jfieldID height = env->GetFieldID(jclsImageData, "height", "I");
    jfieldID stride = env->GetFieldID(jclsImageData, "stride", "I");
    jfieldID orientation = env->GetFieldID(jclsImageData, "orientation", "I");
    jfieldID format = env->GetFieldID(jclsImageData, "format", "I");

    auto jbytes = (jbyteArray) env->GetObjectField(jImageData, bytes);
    int bytesLength = 0;
    if (jbytes != nullptr) {
        bytesLength = env->GetArrayLength(jbytes);

        //Don't delete this buffer.
        //Put the responsibility of release to the user(Js-End).
        imageHO.buffer = (uint8_t *)malloc(bytesLength * sizeof(uint8_t));
        env->GetByteArrayRegion(jbytes, 0, bytesLength, (jbyte*)imageHO.buffer);
    }
    imageHO.bufferSize = bytesLength;
    imageHO.width = (int) env->GetIntField(jImageData, width);
    imageHO.height = (int) env->GetIntField(jImageData, height);
    imageHO.stride = (int) env->GetIntField(jImageData, stride);
    imageHO.format = (int) env->GetIntField(jImageData, format);
    imageHO.orientation = (int) env->GetIntField(jImageData, orientation);
    env->PopLocalFrame(nullptr);
}

} // namespace jsiConversion::core


Value imageDataHOToBase64(ImageDataHostObject &imageDataHO, Runtime &rt) {
    JNIEnv *env;
    bool needDetach = false;
    int state = gJvm->GetEnv((void **) &env, JNI_VERSION_1_6);
    if (state == JNI_EDETACHED || env == nullptr) {
        gJvm->AttachCurrentThread(&env, nullptr);
        needDetach = true;
    }

    auto cls = env->FindClass("com/dynamsoft/reactnativelib/basicutils/ImageUtilKt");
    auto imageData2base64 = env->GetStaticMethodID(cls, "imageData2base64", "(Lcom/dynamsoft/core/basic_structures/ImageData;)Ljava/lang/String;");

    auto jImageData = jsiConversion::core::getJImageDataFromHO(env, imageDataHO);
    auto jBase64 = (jstring) env->CallStaticObjectMethod(cls, imageData2base64, jImageData);
    std::string base64Str = "";
    if (jBase64 != nullptr) {
        auto pBase64 = env->GetStringUTFChars(jBase64, nullptr);
        base64Str = std::string(pBase64);
        env->ReleaseStringUTFChars(jBase64, pBase64);
        env->DeleteLocalRef(jBase64);
    }

    env->DeleteLocalRef(cls);
    env->DeleteLocalRef(jImageData);
    if (needDetach)
        gJvm->DetachCurrentThread();
    return String::createFromUtf8(rt, base64Str);
}

void addImageToBuffer(ImageDataHostObject &imageDataHO, string &nativeIsaId) {
    JNIEnv *env;
    bool needDetach = false;
    int state = gJvm->GetEnv((void **) &env, JNI_VERSION_1_6);
    if (state == JNI_EDETACHED || env == nullptr) {
        gJvm->AttachCurrentThread(&env, nullptr);
        needDetach = true;
    }

    env->PushLocalFrame(8);
    jclass clsIsaModuleImpl = env->FindClass("com/dynamsoft/reactnativelib/core/ImageSourceAdapterModuleImpl");
    jmethodID getISA =env->GetStaticMethodID(clsIsaModuleImpl, "getISA", "(Ljava/lang/String;)Lcom/dynamsoft/core/basic_structures/ImageSourceAdapter;");
    jobject jISA = env->CallStaticObjectMethod(clsIsaModuleImpl, getISA, env->NewStringUTF(nativeIsaId.c_str()));

    if(!env->IsSameObject(jISA, nullptr)) {
        auto clsISA = env->FindClass("com/dynamsoft/core/basic_structures/ImageSourceAdapter");
        auto addImageToBuffer = env->GetMethodID(clsISA, "addImageToBuffer", "(Lcom/dynamsoft/core/basic_structures/ImageData;)V");
        env->CallVoidMethod(jISA, addImageToBuffer, jsiConversion::core::getJImageDataFromHO(env, imageDataHO));
    }
    env->PopLocalFrame(nullptr);

    if (needDetach) {
        gJvm->DetachCurrentThread();
    }
}

ImageDataHostObject getImage(string &nativeIsaId) {
    JNIEnv *env;
    bool needDetach = false;
    int state = gJvm->GetEnv((void **) &env, JNI_VERSION_1_6);
    if (state == JNI_EDETACHED || env == nullptr) {
        gJvm->AttachCurrentThread(&env, nullptr);
        needDetach = true;
    }

    env->PushLocalFrame(8);
    jclass clsIsaModuleImpl = env->FindClass("com/dynamsoft/reactnativelib/core/ImageSourceAdapterModuleImpl");
    jmethodID getISA =env->GetStaticMethodID(clsIsaModuleImpl, "getISA", "(Ljava/lang/String;)Lcom/dynamsoft/core/basic_structures/ImageSourceAdapter;");
    jobject jISA = env->CallStaticObjectMethod(clsIsaModuleImpl, getISA, env->NewStringUTF(nativeIsaId.c_str()));

    ImageDataHostObject imageHO;
    if(!env->IsSameObject(jISA, nullptr)) {
        auto clsISA = env->FindClass("com/dynamsoft/core/basic_structures/ImageSourceAdapter");
        auto getImage = env->GetMethodID(clsISA, "getImage", "()Lcom/dynamsoft/core/basic_structures/ImageData;");
        auto jImageData = env->CallObjectMethod(jISA, getImage);
        jsiConversion::core::getHOFromJImageData(env, jImageData, imageHO);
    }
    env->PopLocalFrame(nullptr);

    if (needDetach) {
        gJvm->DetachCurrentThread();
    }
    return imageHO;
}

#include "android_log.h"
#define JSI_FUNC [](Runtime &runtime, const Value &thisValue, const Value *arguments, size_t count) -> Value

namespace jsi_install {
void install_imageDataToBase64(Runtime &jsiRuntime) {
    if (!jsiRuntime.global().hasProperty(jsiRuntime, "imageDataToBase64")) {
        auto nameId = PropNameID::forAscii(jsiRuntime, "imageDataToBase64");
        auto imageDataToBase64Func = Function::createFromHostFunction(jsiRuntime, nameId, 1, JSI_FUNC {
                auto obj = arguments[0].asObject(runtime);
                auto imageDataHostObject = ImageDataHostObject::getHOFromObject(runtime, std::move(obj));
                return imageDataHOToBase64(imageDataHostObject, runtime);
        });
        jsiRuntime.global().setProperty(jsiRuntime, nameId, imageDataToBase64Func);
    }
}
namespace isa {
void install_addImageToBuffer(Runtime &jsiRuntime) {
    if(!jsiRuntime.global().hasProperty(jsiRuntime, "isaAddImageToBuffer")) {
        auto nameID = PropNameID::forAscii(jsiRuntime, "isaAddImageToBuffer");
        auto func = Function::createFromHostFunction(jsiRuntime, nameID, 2, JSI_FUNC {
                string nativeId = arguments[0].asString(runtime).utf8(runtime);
                ImageDataHostObject imageDataHO = ImageDataHostObject::getHOFromObject(runtime, arguments[1].asObject(runtime));
                addImageToBuffer(imageDataHO, nativeId);
                return Value::undefined();
        });
        jsiRuntime.global().setProperty(jsiRuntime, nameID, func);
    }
}
void install_getImageData(Runtime &jsiRuntime) {
    if(!jsiRuntime.global().hasProperty(jsiRuntime, "isaGetImageData")) {
        auto nameID = PropNameID::forAscii(jsiRuntime, "isaGetImageData");
        auto func = Function::createFromHostFunction(jsiRuntime, nameID, 1, JSI_FUNC {
                string nativeId = arguments[0].asString(runtime).utf8(runtime);
                return Object::createFromHostObject(runtime, std::make_shared<ImageDataHostObject>(getImage(nativeId)));
        });
        jsiRuntime.global().setProperty(jsiRuntime, nameID, func);
    }
}
} // namespace isa
} // namespace jsi_install

void install_coreMethods(Runtime &jsiRuntime) {
    jsi_install::install_imageDataToBase64(jsiRuntime);
    jsi_install::isa::install_addImageToBuffer(jsiRuntime);
    jsi_install::isa::install_getImageData(jsiRuntime);
}

extern "C"
JNIEXPORT void JNICALL
Java_com_dynamsoft_reactnativelib_core_ImageSourceAdapterModuleImpl_nativeInstall(JNIEnv *env, jclass clz, jlong jsiPtr) {
    auto runtime = reinterpret_cast<Runtime *>(jsiPtr);
    if (runtime) {
        install_coreMethods(*runtime);
    }
}
