#include "HypertubeNodejsRuntimeAddon.h"

void InitializeTransmitter(const FunctionCallbackInfo<Value>& args) {
    isolate = args.GetIsolate();
    permanentContext.Reset(isolate, isolate->GetCurrentContext());

    if (args.Length() != 1 || !args[0]->IsString()) {
        isolate->ThrowException(Exception::TypeError(
            String::NewFromUtf8(isolate, "HypertubeAddon InitializeTransmitter: Wrong number of arguments or wrong argument types.",
                NewStringType::kNormal).ToLocalChecked()));
        return;
    }
    v8::String::Utf8Value str(isolate, args[0]);
    std::string nodeJsAddonPath(*str);
    std::string arch = "";
#ifdef _WIN32
#if defined(_WIN64) && defined(_M_ARM64)
    arch = "ARM64";
#elif defined(_WIN64)
    arch = "X64";
#elif defined(_M_ARM)
    arch = "ARM";
#else
    arch = "X86";
#endif
    nativeLibFullPath = nodeJsAddonPath + "\\Binaries\\Native\\Windows\\" + arch + "\\HypertubeNodejsRuntimeNative.dll";
    HypertubeNodejsRuntimeNativeLibInstance = LoadLibraryA(nativeLibFullPath.c_str());
    if (!HypertubeNodejsRuntimeNativeLibInstance) {
        char errorMessage[1024];
        sprintf_s(errorMessage, "%s %s", "HypertubeAddon InitializeTransmitter: Native library not found in: ", nativeLibFullPath.c_str());
        isolate->ThrowException(Exception::Error(
            String::NewFromUtf8(isolate, errorMessage,
                NewStringType::kNormal).ToLocalChecked()));
        return;
    }
#endif // _WIN32

#if defined(__linux__) || defined(__APPLE__)
#if __aarch64__
    arch = "ARM64";
#elif __x86_64__ || __ppc64__
    arch = "X64";
#elif __arm__
    arch = "ARM";
#else
    arch = "X86";
#endif
#if defined(__linux__)
    std::string sharedLibName = "libHypertubeNodejsRuntimeNative.so";
    nativeLibFullPath = nodeJsAddonPath + "/Binaries/Native/Linux/" + arch + "/" + sharedLibName;
#endif // __linux__
#if defined(__APPLE__)
    std::string sharedLibName = "libHypertubeNodejsRuntimeNative.dylib";
    nativeLibFullPath = nodeJsAddonPath + "/Binaries/Native/MacOs/" + arch + "/" + sharedLibName;
#endif // __APPLE__

    HypertubeNodejsRuntimeNativeLibInstance = dlopen(nativeLibFullPath.c_str(), RTLD_LAZY);
    char* error = dlerror();
    if (HypertubeNodejsRuntimeNativeLibInstance == NULL || error != NULL) {
        char errorMessage[300];
        sprintf(errorMessage, "%s %s \n%s %s", "HypertubeAddon InitializeTransmitter: Native library not found in: ", nativeLibFullPath.c_str(), "Error: ", error);
        isolate->ThrowException(Exception::Error(
            String::NewFromUtf8(isolate, errorMessage,
                NewStringType::kNormal).ToLocalChecked()));
        return;
    }
#endif // __linux__ || __APPLE__

    SendCommandNative_Fn = (SendCommandNative)getProccessAddress(HypertubeNodejsRuntimeNativeLibInstance, "SendCommand");
    ReadResponseNative_Fn = (ReadResponseNative)getProccessAddress(HypertubeNodejsRuntimeNativeLibInstance, "ReadResponse");
    ActivateNative_Fn = (ActivateNative)getProccessAddress(HypertubeNodejsRuntimeNativeLibInstance, "Activate");
    GetNativeErrorNative_Fn = (GetNativeErrorNative)getProccessAddress(HypertubeNodejsRuntimeNativeLibInstance, "GetNativeError");
    SetConfigSourceNative_Fn = (SetConfigSourceNative)getProccessAddress(HypertubeNodejsRuntimeNativeLibInstance, "SetConfigSource");
    SetWorkingDirectoryNative_Fn = (SetWorkingDirectoryNative)getProccessAddress(HypertubeNodejsRuntimeNativeLibInstance, "SetWorkingDirectory");
    SetCallbackNative_Fn = (SetCallbackNative)getProccessAddress(HypertubeNodejsRuntimeNativeLibInstance, "SetCallback");

    if (SendCommandNative_Fn == nullptr ||
        ReadResponseNative_Fn == nullptr ||
        ActivateNative_Fn == nullptr ||
        GetNativeErrorNative_Fn == nullptr ||
        SetConfigSourceNative_Fn == nullptr ||
        SetWorkingDirectoryNative_Fn == nullptr ||
        SetCallbackNative_Fn == nullptr) {
        isolate->ThrowException(Exception::Error(
            String::NewFromUtf8(isolate, "HypertubeAddon InitializeTransmitter: Exported functions not found in native library",
                NewStringType::kNormal).ToLocalChecked()));
    }

    bool result = false;
    args.GetReturnValue().Set(result);
    return;
}

void SendCommand(const FunctionCallbackInfo<Value>& args)
{
    v8::Local<v8::Context> context = v8::Local<v8::Context>::New(isolate, permanentContext);

    if (args.Length() != 1 || !args[0]->IsUint8Array()) {
        isolate->ThrowException(Exception::TypeError(
            String::NewFromUtf8(isolate, "HypertubeAddon SendCommand: expected a single Uint8Array argument",
                NewStringType::kNormal).ToLocalChecked()));
        return;
    }

    // Input: borrow pointer from passed Uint8Array (no copy).
    Local<Uint8Array> input = args[0].As<Uint8Array>();
    size_t byteLength = input->ByteLength();
    Local<ArrayBuffer> inBuffer = input->Buffer();
    size_t inOffset = input->ByteOffset();
    std::shared_ptr<v8::BackingStore> inStore = inBuffer->GetBackingStore();
    byte* inPtr = static_cast<byte*>(inStore->Data()) + inOffset;

    int responseArrayLen = SendCommandNative_Fn(inPtr, static_cast<int>(byteLength));

    if (responseArrayLen > 0) {
        // Allocate output ArrayBuffer backing store and fill it from native.
        std::shared_ptr<v8::BackingStore> outStore = v8::ArrayBuffer::NewBackingStore(isolate, responseArrayLen);
        byte* outPtr = static_cast<byte*>(outStore->Data());
		outPtr[0] = inPtr[0];  
        // Temporary: read directly into the backing store memory.
        // If your native guarantees writing exactly responseArrayLen bytes, this is safe.
        ReadResponseNative_Fn(outPtr, responseArrayLen);

        Local<ArrayBuffer> outBuffer = ArrayBuffer::New(isolate, outStore);
        Local<Uint8Array> outView = Uint8Array::New(outBuffer, 0, responseArrayLen);
        args.GetReturnValue().Set(outView);
        return;
    }
    else if (responseArrayLen == 0) {
        isolate->ThrowException(Exception::Error(
            String::NewFromUtf8(isolate, "HypertubeAddon SendCommand: Response is empty",
                NewStringType::kNormal).ToLocalChecked()));
        args.GetReturnValue().Set(Number::New(isolate, responseArrayLen));
        return;
    }
    else {
        const char* errorMessage = GetNativeErrorNative_Fn();
        std::string errorMessage2 = std::string("Hypertube native error code: ") + std::to_string(responseArrayLen) + ". " + std::string(errorMessage);
        isolate->ThrowException(Exception::Error(
            String::NewFromUtf8(isolate, errorMessage2.c_str(),
                NewStringType::kNormal).ToLocalChecked()));
        args.GetReturnValue().Set(Number::New(isolate, responseArrayLen));
        return;
    }
}

void Activate(const FunctionCallbackInfo<Value>& args)
{
    if (args.Length() != 1 || !args[0]->IsString()) {
        isolate->ThrowException(Exception::TypeError(
            String::NewFromUtf8(isolate, "HypertubeAddon Activate:  Wrong number of arguments or wrong argument types.",
                NewStringType::kNormal).ToLocalChecked()));
        return;
    }

    v8::String::Utf8Value licenseKeystr(isolate, args[0]);
    std::string licenseKey(*licenseKeystr);

    auto activationResult = ActivateNative_Fn(licenseKey.c_str());
    args.GetReturnValue().Set(activationResult);
    if (activationResult < 0) {
        const char* errorMessage = GetNativeErrorNative_Fn();
        std::string errorMessage2 = std::string("Hypertube activation result: ") + std::to_string(activationResult) + ". Native error message: " + std::string(errorMessage);
        isolate->ThrowException(Exception::Error(
            String::NewFromUtf8(isolate, errorMessage2.c_str(),
                NewStringType::kNormal).ToLocalChecked()));
    }
}

void SetConfigSource(const FunctionCallbackInfo<Value>& args)
{
    if (args.Length() != 1 || !args[0]->IsString()) {
        isolate->ThrowException(Exception::TypeError(
            String::NewFromUtf8(isolate, "HypertubeAddon SetConfigSource: Wrong number of arguments or wrong argument types.",
                NewStringType::kNormal).ToLocalChecked()));
        return;
    }
    v8::String::Utf8Value str(isolate, args[0]);
    std::string configSource(*str);
    int result = SetConfigSourceNative_Fn(configSource.c_str());
    args.GetReturnValue().Set(result);
    if (result < 0) {
        const char* errorMessage = GetNativeErrorNative_Fn();
        std::string errorMessage2 = std::string("Hypertube set config source result: ") + std::to_string(result) + ". Native error message: " + std::string(errorMessage);
        isolate->ThrowException(Exception::Error(
            String::NewFromUtf8(isolate, errorMessage2.c_str(),
                NewStringType::kNormal).ToLocalChecked()));
    }
}

void SetWorkingDirectory(const FunctionCallbackInfo<Value>& args)
{
    if (args.Length() != 1 || !args[0]->IsString()) {
        isolate->ThrowException(Exception::TypeError(
            String::NewFromUtf8(isolate, "HypertubeAddon SetWorkingDirectory: Wrong number of arguments or wrong argument types.",
                NewStringType::kNormal).ToLocalChecked()));
        return;
    }
    v8::String::Utf8Value str(isolate, args[0]);
    std::string workingDirectory(*str);
    int result = SetWorkingDirectoryNative_Fn(workingDirectory.c_str());
    args.GetReturnValue().Set(result);
    if (result < 0) {
        const char* errorMessage = GetNativeErrorNative_Fn();
        std::string errorMessage2 = std::string("Hypertube set working directory result: ") + std::to_string(result) + ". Native error message: " + std::string(errorMessage);
        isolate->ThrowException(Exception::Error(
            String::NewFromUtf8(isolate, errorMessage2.c_str(),
                NewStringType::kNormal).ToLocalChecked()));
    }
}

std::vector<byte> nodejsRuntimeCallback(byte* messageByteArray, int32_t messageByteArrayLen) {
    std::vector<byte> responseVector;
    v8::Local<v8::Context> context = v8::Local<v8::Context>::New(isolate, permanentContext);

    // Get the `sendCommand` method
    v8::Local<v8::Object> receiverNativeLocal = v8::Local<v8::Object>::New(isolate, receiverNative);

    v8::Local<v8::Value> sendCommandVal = receiverNativeLocal->Get(
        isolate->GetCurrentContext(),
        v8::String::NewFromUtf8(isolate, "sendCommand", v8::NewStringType::kNormal).ToLocalChecked()
    ).ToLocalChecked();

    if (!sendCommandVal->IsFunction()) {
        isolate->ThrowException(Exception::TypeError(
            String::NewFromUtf8(isolate, "HypertubeAddon nodejsRuntimeCallback:  Cannot find sendCommand in NativeReceiver",
                NewStringType::kNormal).ToLocalChecked()));
        return responseVector;
    }

    v8::Local<v8::Function> sendCommandFunc = sendCommandVal.As<v8::Function>();

    // Wrap messageByteArray into a Uint8Array without extra JS copies.
    std::shared_ptr<v8::BackingStore> inStore = v8::ArrayBuffer::NewBackingStore(isolate, messageByteArrayLen);
    std::memcpy(inStore->Data(), messageByteArray, static_cast<size_t>(messageByteArrayLen));
    v8::Local<v8::ArrayBuffer> inBuffer = v8::ArrayBuffer::New(isolate, inStore);
    v8::Local<v8::Uint8Array> uint8Array = v8::Uint8Array::New(inBuffer, 0, static_cast<size_t>(messageByteArrayLen));

    // Call `sendCommand` with Uint8Array
    v8::Local<v8::Value> argv[1] = { uint8Array };
    v8::Local<v8::Value> result;

    if (!sendCommandFunc->Call(context, receiverNativeLocal, 1, argv).ToLocal(&result)) {
        isolate->ThrowException(Exception::TypeError(
            String::NewFromUtf8(isolate, "HypertubeAddon nodejsRuntimeCallback:  Error during calling callback",
                NewStringType::kNormal).ToLocalChecked()));
        return responseVector;
    }

    // Expect Uint8Array back and copy into std::vector<byte>
    if (result->IsUint8Array()) {
        v8::Local<v8::Uint8Array> resultArray = result.As<v8::Uint8Array>();
        size_t length = resultArray->ByteLength();
        responseVector.resize(length);

        Local<ArrayBuffer> resBuf = resultArray->Buffer();
        size_t resOff = resultArray->ByteOffset();
        std::shared_ptr<v8::BackingStore> resStore = resBuf->GetBackingStore();
        const byte* resPtr = static_cast<const byte*>(resStore->Data()) + resOff;

        std::memcpy(responseVector.data(), resPtr, length);
    } else {
        isolate->ThrowException(Exception::TypeError(
            String::NewFromUtf8(isolate, "HypertubeAddon nodejsRuntimeCallback: Wrong return type (expected Uint8Array)",
                NewStringType::kNormal).ToLocalChecked()));
    }

    return responseVector;
}

void SetReceiverNative(const v8::FunctionCallbackInfo<v8::Value>& args) {
    v8::Local<v8::Context> context = v8::Local<v8::Context>::New(isolate, permanentContext);

    if (args.Length() != 1 || !args[0]->IsObject()) {
        isolate->ThrowException(v8::Exception::TypeError(
            v8::String::NewFromUtf8(isolate, "HypertubeAddon SetReceiverNative: Wrong number of arguments or wrong argument types", v8::NewStringType::kNormal).ToLocalChecked()
        ));
        return;
    }

    // store the receiver object
    receiverNative.Reset(isolate, args[0].As<v8::Object>());

    // set the callback in native
    SetCallbackNative_Fn(nodejsRuntimeCallback);
}

void Initialize(Local<Object> exports) {
    NODE_SET_METHOD(exports, "initializeTransmitter", InitializeTransmitter);
    NODE_SET_METHOD(exports, "sendCommand", SendCommand);
    NODE_SET_METHOD(exports, "activate", Activate);
    NODE_SET_METHOD(exports, "setConfigSource", SetConfigSource);
    NODE_SET_METHOD(exports, "setReceiverNative", SetReceiverNative);
    NODE_SET_METHOD(exports, "setWorkingDirectory", SetWorkingDirectory);
}

NODE_MODULE(NODE_GYP_MODULE_NAME, Initialize)
