#include <string.h>
#include <assert.h>
#include <android/log.h>

#include "vn_client.h"
#include "vn_malloc.h"
#include "VN_JClient.h"

#define DEBUG_TAG "VN_JClient"
extern JavaVM *g_jvm;

static jclass asynchResultClass = 0;
static jclass resultClass       = 0;
static jclass entityClass       = 0;

static void set_java_asynch_result(JNIEnv *jenv, jobject java_ar, int error_code, const char *error_string, jobject value)
{
    // Create com.videonext.Result
    jmethodID init = jenv->GetMethodID(resultClass, "<init>", "(ILjava/lang/String;Ljava/lang/Object;)V");
    jstring jstr = NULL;
    if (error_string)
        jstr = jenv->NewStringUTF(error_string);
    jobject result = jenv->NewObject(resultClass, init, error_code, jstr, value);
        
    // Invoke AsynchResult::set(Result<T> result)
    jmethodID set = jenv->GetMethodID(asynchResultClass, "set", "(Lcom/videonext/Result;)V");
    jenv->CallVoidMethod(java_ar, set, result);

    jenv->DeleteGlobalRef(java_ar);
    if (jstr)
        jenv->DeleteLocalRef(jstr);
}

static void process_login(vn_asynch_result_t *ar, void *user_data)
{
    jobject java_ar = (jobject)user_data;

    JNIEnv *jenv;
    g_jvm->AttachCurrentThread(&jenv, NULL);
    
    vn_result_t *r = vn_asynch_result_get(ar, 0);
    assert(r);

    jobject hashMap = NULL;
    if (r->kvl)
    {
        // Create hashmap
        jclass hashMapClass = jenv->FindClass("java/util/HashMap");
        jmethodID initMap = jenv->GetMethodID(hashMapClass, "<init>", "()V");
        jmethodID putMap = jenv->GetMethodID(hashMapClass, "put",
                                             "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");

        hashMap = jenv->NewObject(hashMapClass, initMap);
        
        //jclass integerClass = jenv->FindClass("java/lang/Integer");
        //jmethodID initInt = jenv->GetMethodID(integerClass, "<init>", "(I)V");
        
        jstring name = jenv->NewStringUTF("DEFAULT_ROLE");
        jstring id   = NULL;
        //jobject id   = NULL;
        if (r->kvl[0]->value)
        {
            id = jenv->NewStringUTF((char*)r->kvl[0]->value);
            //id = jenv->NewObject(integerClass, initInt, *(int*)(r->kvl[0]->value));
        }

        jenv->CallObjectMethod(hashMap, putMap, name, id);

        if (id)
            jenv->DeleteLocalRef(id);
        jenv->DeleteLocalRef(name);
        jenv->DeleteLocalRef(hashMapClass);
        //jenv->DeleteLocalRef(integerClass);
    }

    set_java_asynch_result(jenv, java_ar, r->error_code, r->error_string, hashMap);

    vn_asynch_result_destroy(ar);

    g_jvm->DetachCurrentThread();
}

static void process_get_roles(vn_asynch_result_t *ar, void *user_data)
{
    jobject java_ar = (jobject)user_data;

    JNIEnv *jenv;
    g_jvm->AttachCurrentThread(&jenv, NULL);

    vn_result_t *r = vn_asynch_result_get(ar, 0);
    assert(r);
    
    jobject hashMap = NULL;
    if (r->kvl)
    {
        // Create hashmap
        jclass hashMapClass = jenv->FindClass("java/util/HashMap");
        jmethodID initMap = jenv->GetMethodID(hashMapClass, "<init>", "()V");
        jmethodID putMap = jenv->GetMethodID(hashMapClass, "put",
                                             "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");

        hashMap = jenv->NewObject(hashMapClass, initMap);
        
        //jclass integerClass = jenv->FindClass("java/lang/Integer");
        //jmethodID initInt = jenv->GetMethodID(integerClass, "<init>", "(I)V");
        
        for (vn_kv_t **p = r->kvl; *p != 0; ++p)
        {
            vn_kv_t *role = *p;
            jstring id = jenv->NewStringUTF((char*)role->key);
            //jobject id   = jenv->NewObject(integerClass, initInt, *(int*)(role->key));
            jstring name = jenv->NewStringUTF((char*)role->value);            

            jenv->CallObjectMethod(hashMap, putMap, id, name);

            jenv->DeleteLocalRef(id);
            jenv->DeleteLocalRef(name);
        }

        jenv->DeleteLocalRef(hashMapClass);
        //jenv->DeleteLocalRef(integerClass);
    }

    set_java_asynch_result(jenv, java_ar, r->error_code, r->error_string, hashMap);

    vn_asynch_result_destroy(ar);

    g_jvm->DetachCurrentThread();
}

static void process_get_objects(vn_asynch_result_t *ar, void *user_data)
{
    jobject java_ar = (jobject)user_data;

    JNIEnv *jenv;
    g_jvm->AttachCurrentThread(&jenv, NULL);
    
    vn_result_t *r = vn_asynch_result_get(ar, 0);
    assert(r);

    jobject hashMap = NULL;
    if (r->kvl)
    {
        // Create hashmap
        jclass hashMapClass = jenv->FindClass("java/util/HashMap");
        jmethodID initMap = jenv->GetMethodID(hashMapClass, "<init>", "()V");
        jmethodID putMap = jenv->GetMethodID(hashMapClass, "put",
                                             "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");

        // Create linked hashmap to preserve insertation order
        jclass linkedHashMapClass = jenv->FindClass("java/util/LinkedHashMap");
        jmethodID initLinkedMap = jenv->GetMethodID(linkedHashMapClass, "<init>", "()V");
        jmethodID putLinkedMap = jenv->GetMethodID(linkedHashMapClass, "put",
                                                   "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");

        hashMap = jenv->NewObject(linkedHashMapClass, initLinkedMap);

        //jclass integerClass = jenv->FindClass("java/lang/Integer");
        //jmethodID initInt = jenv->GetMethodID(integerClass, "<init>", "(I)V");
        
        for (vn_kv_t **p = r->kvl; *p != 0; ++p)
        {
            vn_kv_t *obj = *p;
            const char* Key = (char*)obj->key;
            jstring id = jenv->NewStringUTF((char*)obj->key);
            //jobject id = jenv->NewObject(integerClass, initInt, *(int*)(obj->key));
            jobject attrsHashMap = jenv->NewObject(hashMapClass, initMap);
            
            // iterate over attributes
            vn_kv_t **attrs = (vn_kv_t **)obj->value;
            for (vn_kv_t **a = attrs; *a != 0; ++a)
            {
                jstring key = jenv->NewStringUTF((char*)(*a)->key);
                jstring val = jenv->NewStringUTF((char*)(*a)->value);            

                jenv->CallObjectMethod(attrsHashMap, putMap, key, val);

                jenv->DeleteLocalRef(key);
                jenv->DeleteLocalRef(val);
            }

            // create com.videonext.Entity
            jmethodID initEntity = jenv->GetMethodID(entityClass, "<init>", "(ILjava/util/Map;)V");
            jobject entity = jenv->NewObject(entityClass, initEntity, (char*)obj->key, attrsHashMap);

            jenv->CallObjectMethod(hashMap, putLinkedMap, id, entity);

            jenv->DeleteLocalRef(id);
            jenv->DeleteLocalRef(attrsHashMap);
        }

        jenv->DeleteLocalRef(linkedHashMapClass);
        jenv->DeleteLocalRef(hashMapClass);
        //jenv->DeleteLocalRef(integerClass);
    }

    set_java_asynch_result(jenv, java_ar, r->error_code, r->error_string, hashMap);

    vn_asynch_result_destroy(ar);

    g_jvm->DetachCurrentThread();
}

static void process_get_snapshot(vn_asynch_result_t *ar, void *user_data)
{
    jobject java_ar = (jobject)user_data;

    JNIEnv *jenv;
    g_jvm->AttachCurrentThread(&jenv, NULL);
    
    vn_result_t *r = vn_asynch_result_get(ar, 0);
    assert(r);

    jbyteArray jpg = NULL;
    if (r->kvl)
    {
        unsigned size = *((unsigned*)r->kvl[0]->key);
        jbyte *data = (jbyte *)r->kvl[0]->value;
    
        jpg = jenv->NewByteArray(size);
        jenv->SetByteArrayRegion(jpg, 0, size, data);
    }

    set_java_asynch_result(jenv, java_ar, r->error_code, r->error_string, jpg);

    vn_asynch_result_destroy(ar);

    g_jvm->DetachCurrentThread();
}

static void process_get_media_url(vn_asynch_result_t *ar, void *user_data)
{
    jobject java_ar = (jobject)user_data;

    JNIEnv *jenv;
    g_jvm->AttachCurrentThread(&jenv, NULL);
    
    vn_result_t *r = vn_asynch_result_get(ar, 0);
    assert(r);

    jstring url = NULL;
    if(r->kvl)
       url = jenv->NewStringUTF((char*)r->kvl[0]->value);

    set_java_asynch_result(jenv, java_ar, r->error_code, r->error_string, url);

    vn_asynch_result_destroy(ar);

    if(r->kvl)
        jenv->DeleteLocalRef(url);

    g_jvm->DetachCurrentThread();
}

static void process_add_object(vn_asynch_result_t *ar, void *user_data)
{
    jobject java_ar = (jobject)user_data;

    JNIEnv *jenv;
    g_jvm->AttachCurrentThread(&jenv, NULL);

    vn_result_t *r = vn_asynch_result_get(ar, 0);
    assert(r);

    jobject objid = NULL;
    if (r->kvl)
    {
        jclass integerClass = jenv->FindClass("java/lang/Integer");
        jmethodID initInt = jenv->GetMethodID(integerClass, "<init>", "(I)V");
        objid = jenv->NewObject(integerClass, initInt, *(int*)(r->kvl[0]->value));

        jenv->DeleteLocalRef(integerClass);
    }

    set_java_asynch_result(jenv, java_ar, r->error_code, r->error_string, objid);
    if(objid)
        jenv->DeleteLocalRef(objid);

    vn_asynch_result_destroy(ar);

    g_jvm->DetachCurrentThread();
}

static void process_ptz_answer(vn_asynch_result_t *ar, void *user_data)
{
    jobject java_ar = (jobject)user_data;

    JNIEnv *jenv;
    g_jvm->AttachCurrentThread(&jenv, NULL);

    vn_result_t *r = vn_asynch_result_get(ar, 0);
    assert(r);

    jobject objid = NULL;
    if (r->kvl)
    {
        jclass integerClass = jenv->FindClass("java/lang/Integer");
        jmethodID initInt = jenv->GetMethodID(integerClass, "<init>", "(I)V");
        objid = jenv->NewObject(integerClass, initInt, *(int*)(r->kvl[0]->value));

        jenv->DeleteLocalRef(integerClass);
    }

    set_java_asynch_result(jenv, java_ar, r->error_code, r->error_string, objid);
    if(objid)
        jenv->DeleteLocalRef(objid);

    vn_asynch_result_destroy(ar);

    g_jvm->DetachCurrentThread();
}

static void process_get_geo(vn_asynch_result_t *ar, void *user_data)
{
    jobject java_ar = (jobject)user_data;

    JNIEnv *jenv;
    g_jvm->AttachCurrentThread(&jenv, NULL);

    vn_result_t *r = vn_asynch_result_get(ar, 0);
    assert(r);

    jobject hashMap = NULL;
    if (r->kvl)
    {
        // Create hashmap
        jclass hashMapClass = jenv->FindClass("java/util/HashMap");
        jmethodID initMap = jenv->GetMethodID(hashMapClass, "<init>", "()V");
        jmethodID putMap = jenv->GetMethodID(hashMapClass, "put",
                                             "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");

        hashMap = jenv->NewObject(hashMapClass, initMap);

        jclass integerClass = jenv->FindClass("java/lang/Integer");
        jmethodID initInt = jenv->GetMethodID(integerClass, "<init>", "(I)V");

        for (vn_kv_t **p = r->kvl; *p != 0; ++p)
        {
            vn_kv_t *obj_pos = *p;
            jobject objid   = jenv->NewObject(integerClass, initInt, *(int*)(obj_pos->key));
            jstring pos = jenv->NewStringUTF((char*)obj_pos->value);

            jenv->CallObjectMethod(hashMap, putMap, objid, pos);

            jenv->DeleteLocalRef(objid);
            jenv->DeleteLocalRef(pos);
        }

        jenv->DeleteLocalRef(hashMapClass);
        jenv->DeleteLocalRef(integerClass);
    }

    set_java_asynch_result(jenv, java_ar, r->error_code, r->error_string, hashMap);

    vn_asynch_result_destroy(ar);

    g_jvm->DetachCurrentThread();
}

static void process_get_server_info(vn_asynch_result_t *ar, void *user_data)
{
    jobject java_ar = (jobject)user_data;

    JNIEnv *jenv;
    g_jvm->AttachCurrentThread(&jenv, NULL);

    vn_result_t *r = vn_asynch_result_get(ar, 0);
    assert(r);

    // HashMap
    jclass hashMapClass = jenv->FindClass("java/util/HashMap");
    jmethodID initMap = jenv->GetMethodID(hashMapClass, "<init>", "()V");
    jmethodID putMap = jenv->GetMethodID(hashMapClass, "put",
                                         "(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object;");

    // create result HashMap
    jobject hashMap = NULL;
    if (r->kvl)
    {
        hashMap = jenv->NewObject(hashMapClass, initMap);
        for (vn_kv_t **p = r->kvl; *p != 0; ++p) {
            jstring k = jenv->NewStringUTF((char*)(*p)->key);
            jstring v = jenv->NewStringUTF((char*)(*p)->value);

            jenv->CallObjectMethod(hashMap, putMap, k, v);

            jenv->DeleteLocalRef(k);
            jenv->DeleteLocalRef(v);
        }
    }

    jenv->DeleteLocalRef(hashMapClass);

    set_java_asynch_result(jenv, java_ar, r->error_code, r->error_string, hashMap);

    vn_asynch_result_destroy(ar);

    g_jvm->DetachCurrentThread();
}

JNIEXPORT jlong JNICALL Java_com_videonext_MediaClient_nativeInit(JNIEnv *jenv, jobject jobj, 
                                                                  jstring host, jshort port, 
                                                                  jboolean ssl, jlong timeout)
{
    const char *host_str = jenv->GetStringUTFChars(host, NULL);
    if (host_str == NULL ) 
        return 0; /* OutOfMemoryError already thrown */

    vn_client_config_t cfg = {};
    strcpy(cfg.remote_host, host_str);
    cfg.use_ssl = ssl;
    cfg.timeout = timeout;
        
    vn_client_context_t *ctx = vn_client_create(&cfg);
    
    jenv->ReleaseStringUTFChars(host, host_str);

    if (!asynchResultClass)
    {
        asynchResultClass = (jclass)jenv->NewGlobalRef(jenv->FindClass("com/videonext/AsynchResult"));
        if (asynchResultClass == NULL)
        {
            __android_log_print(ANDROID_LOG_ERROR, DEBUG_TAG, "Class not found: com/videonext/AsynchResult");
            return NULL;
        }
    }

    if (!resultClass)
    {
        resultClass = (jclass)jenv->NewGlobalRef(jenv->FindClass("com/videonext/Result"));
        if (resultClass == NULL)
        {
            __android_log_print(ANDROID_LOG_ERROR, DEBUG_TAG, "Class not found: com/videonext/Result");
            return NULL;
        }
    }

    if (!entityClass)
    {
        entityClass = (jclass)jenv->NewGlobalRef(jenv->FindClass("com/videonext/Entity"));
        if (entityClass == NULL)
        {
            __android_log_print(ANDROID_LOG_ERROR, DEBUG_TAG, "Class not found: com/videonext/Entity");
            return NULL;
        }        
    }

    return (jlong)ctx;

}

JNIEXPORT void JNICALL Java_com_videonext_MediaClient_nativeDestroy(JNIEnv *jenv, jobject jobj, jlong hndl)
{
    vn_client_context_t *ctx = (vn_client_context_t *)hndl;

    vn_client_destroy(ctx);
}

JNIEXPORT jobject JNICALL Java_com_videonext_MediaClient_nativeLogin(JNIEnv *jenv, jobject jobj, 
                                                                     jlong hndl, 
                                                                     jstring user_name, jstring password)
{
    vn_client_context_t *ctx = (vn_client_context_t *)hndl;
 
    const char *user_name_str = jenv->GetStringUTFChars(user_name, NULL);
    const char *password_str = jenv->GetStringUTFChars(password, NULL);

    jmethodID init = jenv->GetMethodID(asynchResultClass, "<init>", "()V");
    jobject asynchResult = jenv->NewObject(asynchResultClass, init);

    vn_asynch_result_t *ar = vn_client_login(ctx, user_name_str, password_str);

    vn_asynch_result_set_callback(ar, process_login, jenv->NewGlobalRef(asynchResult));

    jenv->ReleaseStringUTFChars(user_name, user_name_str);
    jenv->ReleaseStringUTFChars(password, password_str);

    return asynchResult;
}

JNIEXPORT jobject JNICALL Java_com_videonext_MediaClient_nativeGetRoles(JNIEnv *jenv, jobject jobj, 
                                                                        jlong hndl)
{
   vn_client_context_t *ctx = (vn_client_context_t *)hndl;

   jmethodID init = jenv->GetMethodID(asynchResultClass, "<init>", "()V");
   jobject asynchResult = jenv->NewObject(asynchResultClass, init);

   vn_asynch_result_t *ar = vn_client_get_roles(ctx);

   vn_asynch_result_set_callback(ar, process_get_roles, jenv->NewGlobalRef(asynchResult));

   return asynchResult;
}

JNIEXPORT jobject JNICALL Java_com_videonext_MediaClient_nativeGetSets(JNIEnv *jenv, jobject jobj, 
                                                                       jlong hndl, jstring role_id)
{
   vn_client_context_t *ctx = (vn_client_context_t *)hndl;
   const char *role_id_str = role_id ? jenv->GetStringUTFChars(role_id, NULL) : NULL;

   jmethodID init = jenv->GetMethodID(asynchResultClass, "<init>", "()V");
   jobject asynchResult = jenv->NewObject(asynchResultClass, init);

   vn_asynch_result_t *ar = vn_client_get_sets(ctx, role_id_str);

   vn_asynch_result_set_callback(ar, process_get_roles, jenv->NewGlobalRef(asynchResult));

   if(role_id)
       jenv->ReleaseStringUTFChars(role_id, role_id_str);

   return asynchResult;
}

JNIEXPORT jobject JNICALL Java_com_videonext_MediaClient_nativeGetObjects(JNIEnv *jenv, jobject jobj, 
                                                                          jlong hndl, jstring set_id)
{
   vn_client_context_t *ctx = (vn_client_context_t *)hndl;

   const char *set_id_str = set_id ? jenv->GetStringUTFChars(set_id, NULL) : NULL;

   jmethodID init = jenv->GetMethodID(asynchResultClass, "<init>", "()V");
   jobject asynchResult = jenv->NewObject(asynchResultClass, init);

   vn_asynch_result_t *ar = vn_client_get_objects(ctx, set_id_str, NULL);

   vn_asynch_result_set_callback(ar, process_get_objects, jenv->NewGlobalRef(asynchResult));

   if(set_id)
       jenv->ReleaseStringUTFChars(set_id, set_id_str);

   return asynchResult;
}

JNIEXPORT jobject JNICALL Java_com_videonext_MediaClient_nativeGetEvents(JNIEnv *jenv, jobject jobj, 
                                                                         jlong hndl, jstring roleid, jstring filter)
{
   vn_client_context_t *ctx = (vn_client_context_t *)hndl;

   const char *roleid_str = roleid ? jenv->GetStringUTFChars(roleid, NULL) : NULL;
   const char *filter_str = filter ? jenv->GetStringUTFChars(filter, NULL) : NULL;

   jmethodID init = jenv->GetMethodID(asynchResultClass, "<init>", "()V");
   jobject asynchResult = jenv->NewObject(asynchResultClass, init);

   vn_asynch_result_t *ar = vn_client_get_events(ctx, roleid_str, filter_str);

   vn_asynch_result_set_callback(ar, process_get_objects, jenv->NewGlobalRef(asynchResult));

   if(roleid)
       jenv->ReleaseStringUTFChars(roleid, roleid_str);
   if(filter)
       jenv->ReleaseStringUTFChars(filter, filter_str);

   return asynchResult;
}

JNIEXPORT jobject JNICALL Java_com_videonext_MediaClient_nativeGetSnapshot(JNIEnv *jenv, jobject jobj, 
                                                                           jlong hndl,
                                                                           jstring objid, jlong ts, jint stream_num, jboolean metadata)
{
   vn_client_context_t *ctx = (vn_client_context_t *)hndl;

   const char *objid_str = jenv->GetStringUTFChars(objid, NULL);

   jmethodID init = jenv->GetMethodID(asynchResultClass, "<init>", "()V");
   jobject asynchResult = jenv->NewObject(asynchResultClass, init);

   vn_asynch_result_t *ar = vn_client_get_snapshot(ctx, objid_str, ts, stream_num, metadata);

   vn_asynch_result_set_callback(ar, process_get_snapshot, jenv->NewGlobalRef(asynchResult));

   jenv->ReleaseStringUTFChars(objid, objid_str);

   return asynchResult;
}

JNIEXPORT jobject JNICALL Java_com_videonext_MediaClient_nativeGetMediaURL(JNIEnv *jenv, jobject jobj, 
                                                                           jlong hndl,
                                                                           jstring video_obj_id, jstring audio_obj_id,
                                                                           jlong start, jlong end, 
                                                                           jstring transcode_codec, jstring transcode_resolution,
                                                                           jint analytics)
{
   vn_client_context_t *ctx = (vn_client_context_t *)hndl;

   jmethodID init = jenv->GetMethodID(asynchResultClass, "<init>", "()V");
   jobject asynchResult = jenv->NewObject(asynchResultClass, init);

   const char *v_objid_str = video_obj_id ? jenv->GetStringUTFChars(video_obj_id, NULL) : NULL;
   const char *a_objid_str = audio_obj_id ? jenv->GetStringUTFChars(audio_obj_id, NULL) : NULL;
   const char *transcode_codec_str = transcode_codec ? jenv->GetStringUTFChars(transcode_codec, NULL) : NULL;
   const char *transcode_resolution_str = transcode_resolution ? jenv->GetStringUTFChars(transcode_resolution, NULL) : NULL;

   vn_asynch_result_t *ar = vn_client_get_media_url(ctx, v_objid_str, a_objid_str, start, end,
                                                    transcode_codec_str, transcode_resolution_str, analytics);

   vn_asynch_result_set_callback(ar, process_get_media_url, jenv->NewGlobalRef(asynchResult));

   if(video_obj_id)
       jenv->ReleaseStringUTFChars(video_obj_id, v_objid_str);
   if(audio_obj_id)
       jenv->ReleaseStringUTFChars(audio_obj_id, a_objid_str);
   if(transcode_codec)
       jenv->ReleaseStringUTFChars(transcode_codec, transcode_codec_str);
   if(transcode_resolution)
       jenv->ReleaseStringUTFChars(transcode_resolution, transcode_resolution_str);

   return asynchResult;
}

JNIEXPORT jobject JNICALL Java_com_videonext_MediaClient_nativeAddObject(JNIEnv *jenv, jobject jobj,
                                                                         jlong hndl,
                                                                         jstring hwid,
                                                                         jstring name)
{
   vn_client_context_t *ctx = (vn_client_context_t *)hndl;

   jmethodID init = jenv->GetMethodID(asynchResultClass, "<init>", "()V");
   jobject asynchResult = jenv->NewObject(asynchResultClass, init);

   //std::string post_data = "type=camera&attributes={\"MEDIA_FORMAT\":\"" + mediaFormat + "\",\"CAMERAMODEL\":\"iStream\",\"NAME\":\""
   //        + name + "\",\"HWID\":\"" + hwid + "\",\"TELEPHONE_NUMBER\":\"" + telnum + "\",\"FRAMERATE\":\"30\",\"ARCFRAMERATE\":\"30\"}";

   const char *keys[] = {"CAMERAMODEL", "HWID", "NAME", "MEDIA_FORMAT", "FRAMERATE", "ARCFRAMERATE"};
   vn_kv_t **kvl = vn_kv_list_create(sizeof(keys)/sizeof(char*));
   for(size_t k = 0; k < sizeof(keys)/sizeof(char*); k++)
   {
       kvl[k]->key = vn_malloc(strlen(keys[k]) + 1, NULL);
       strcpy((char*)kvl[k]->key, keys[k]);
   }

   kvl[0]->value = vn_malloc(strlen("iStream")+1, NULL);
   strcpy((char*)kvl[0]->value, "iStream");

   const char *hwid_str = NULL;
   if (hwid) {
       hwid_str = jenv->GetStringUTFChars(hwid, NULL);
       kvl[1]->value = vn_malloc(strlen(hwid_str)+1, NULL);
       strcpy((char*)kvl[1]->value, hwid_str);
   } else kvl[1]->value = vn_malloc(1, NULL);

   const char *name_str = NULL;
   if (name) {
       name_str = jenv->GetStringUTFChars(name, NULL);
       kvl[2]->value = vn_malloc(strlen(name_str)+1, NULL);
       strcpy((char*)kvl[2]->value, name_str);
   } else kvl[2]->value = vn_malloc(1, NULL);

   kvl[3]->value = vn_malloc(strlen("h264")+1, NULL);
   strcpy((char*)kvl[3]->value, "h264");

   kvl[4]->value = vn_malloc(strlen("30")+1, NULL);
   strcpy((char*)kvl[4]->value, "30");

   kvl[5]->value = vn_malloc(strlen("30")+1, NULL);
   strcpy((char*)kvl[5]->value, "30");

   vn_asynch_result_t *ar = vn_client_add_object(ctx, (const vn_kv_t **)kvl);

   vn_asynch_result_set_callback(ar, process_add_object, jenv->NewGlobalRef(asynchResult));

   vn_kv_list_destroy(kvl);

   return asynchResult;
}

JNIEXPORT jobject JNICALL Java_com_videonext_MediaClient_nativePerformPTZCommand(JNIEnv *jenv, jobject jobj,
                                                                                 jlong hndl, jstring obj_id,
                                                                                 jstring mode, jstring cmd)
{
   vn_client_context_t *ctx = (vn_client_context_t *)hndl;

   const char *objid_str = jenv->GetStringUTFChars(obj_id, NULL);

   jmethodID init = jenv->GetMethodID(asynchResultClass, "<init>", "()V");
   jobject asynchResult = jenv->NewObject(asynchResultClass, init);

   const char *mode_str = NULL;
   if (mode)
       mode_str = jenv->GetStringUTFChars(mode, NULL);

   const char *cmd_str = NULL;
   if (cmd)
      cmd_str = jenv->GetStringUTFChars(cmd, NULL);

   vn_asynch_result_t *ar = vn_client_perform_ptz_command(ctx, objid_str, mode_str, cmd_str);

   vn_asynch_result_set_callback(ar, process_ptz_answer, jenv->NewGlobalRef(asynchResult));

   jenv->ReleaseStringUTFChars(obj_id, objid_str);

   return asynchResult;
}

JNIEXPORT jobject JNICALL Java_com_videonext_MediaClient_nativeGetGEO(JNIEnv *jenv, jobject jobj,
                                                                           jlong hndl, jobjectArray objs_arr)
{
    vn_client_context_t *ctx = (vn_client_context_t *)hndl;

    jsize len = jenv->GetArrayLength(objs_arr);
    char** obj_ids = new char*[len];
    for (int i=0; i<len; i++) {
        jstring obj = (jstring) (jenv->GetObjectArrayElement(objs_arr, i));
        obj_ids[i] = const_cast<char*>(jenv->GetStringUTFChars(obj, NULL));
    }

    jmethodID init = jenv->GetMethodID(asynchResultClass, "<init>", "()V");
    jobject asynchResult = jenv->NewObject(asynchResultClass, init);

    vn_asynch_result_t *ar = vn_client_get_geo(ctx, const_cast<const char**>(obj_ids), len);

    vn_asynch_result_set_callback(ar, process_get_geo, jenv->NewGlobalRef(asynchResult));

    for (int i=0; i<len; i++)
        jenv->ReleaseStringUTFChars((jstring) (jenv->GetObjectArrayElement(objs_arr, i)),
                                    obj_ids[i]);
    delete[] obj_ids;
    return asynchResult;
}

JNIEXPORT jobject JNICALL Java_com_videonext_MediaClient_nativeGetServerInfo(JNIEnv *jenv, jobject jobj,
                                                                             jlong hndl)
{
    vn_client_context_t *ctx = (vn_client_context_t *)hndl;

    jmethodID init = jenv->GetMethodID(asynchResultClass, "<init>", "()V");
    jobject asynchResult = jenv->NewObject(asynchResultClass, init);

    vn_asynch_result_t *ar = vn_client_get_server_info(ctx);

    vn_asynch_result_set_callback(ar, process_get_server_info, jenv->NewGlobalRef(asynchResult));

    return asynchResult;
}

JNIEXPORT jobject JNICALL Java_com_videonext_MediaClient_nativeSubmitAction(JNIEnv *jenv, jobject jobj,
                                                                            jlong hndl, jint eventid,
                                                                            jstring action, jstring roleid,
                                                                            jstring data)
{
    vn_client_context_t *ctx = (vn_client_context_t *)hndl;

    const char *action_str = action ? jenv->GetStringUTFChars(action, NULL) : NULL;
    const char *roleid_str = roleid ? jenv->GetStringUTFChars(roleid, NULL) : NULL;
    const char *data_str = data ? jenv->GetStringUTFChars(data, NULL) : NULL;

    jmethodID init = jenv->GetMethodID(asynchResultClass, "<init>", "()V");
    jobject asynchResult = jenv->NewObject(asynchResultClass, init);

    vn_asynch_result_t *ar = vn_client_submit_action(ctx, eventid, action_str, roleid_str, data_str);

    vn_asynch_result_set_callback(ar, process_ptz_answer, jenv->NewGlobalRef(asynchResult));

    if(action_str)
        jenv->ReleaseStringUTFChars(action, action_str);
    if(roleid_str)
        jenv->ReleaseStringUTFChars(roleid, roleid_str);
    if(data_str)
        jenv->ReleaseStringUTFChars(data, data_str);

    return asynchResult;
}
