/*
 * linphone.hh
 * Copyright (c) 2010-2022 Belledonne Communications SARL.
 *
 * This file is part of Liblinphone 
 * (see https://gitlab.linphone.org/BC/public/liblinphone).
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

#ifndef _MSC_VER
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-parameter"
#endif // _MSC_VER

#include <jni.h>
#include <cpu-features.h>

#include "bctoolbox/logging.h"
#include "belle-sip/object.h"
#include "mediastreamer2/mediastream.h"
#include "mediastreamer2/mscommon.h"
#include "mediastreamer2/msmediaplayer.h"
#include "mediastreamer2/msutils.h"
#include "mediastreamer2/devices.h"
#include "mediastreamer2/msjava.h"
#include "linphone/core_utils.h"
#include "linphone/core.h"
#include "linphone/tunnel.h"
#include "linphone/account_creator.h"
#include "linphone/wrapper_utils.h"
#include "linphone/lpconfig.h"

#ifdef __ANDROID__
#include <android/log.h>
#endif /* __ANDROID__ */

static JavaVM *jvm = nullptr;
static const char* LogDomain = "Linphone";

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *ajvm, void *reserved) {
#ifdef __ANDROID__
	ms_set_jvm(ajvm);
#endif /* __ANDROID__ */
	jvm = ajvm;
	return JNI_VERSION_1_2;
}

#define belle_sip_java_user_data_key "java_object"
#define belle_sip_const_java_user_data_key "const_java_object"
#define belle_sip_java_listener_user_data_key "java_listener_object"

static const char* GetStringUTFChars(JNIEnv* env, jstring string) {
	const char *cstring = string ? env->GetStringUTFChars(string, nullptr) : nullptr;
	return cstring;
}

static void ReleaseStringUTFChars(JNIEnv* env, jstring string, const char *cstring) {
	if (string) env->ReleaseStringUTFChars(string, cstring);
}

static jlong GetObjectNativePtr(JNIEnv *env, jobject object) {
	jclass objClass = env->GetObjectClass(object);
	jfieldID nativePtrId = env->GetFieldID(objClass, "nativePtr", "J");
	jlong nativePtr = env->GetLongField(object, nativePtrId);
	return nativePtr;
}

static void LogJavaObjectForNativePtr(JNIEnv *env, jobject obj, jlong cptr) {
	jclass javaObject = env->FindClass("java/lang/Object");
	jmethodID javaObjectToString = env->GetMethodID(javaObject, "toString", "()Ljava/lang/String;");
	jstring javaObjectName = (jstring)env->CallObjectMethod(obj, javaObjectToString);
	const char *name = GetStringUTFChars(env, javaObjectName);
	bctbx_debug("Java object name is %s for native pointer %p", name, (void *)cptr);
	ReleaseStringUTFChars(env, javaObjectName, name);
}

extern "C" {

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

void linphone_android_log_handler(int prio, char *str) {
	char *current;
	char *next;

	if (strlen(str) < 512) {
		__android_log_write(prio, LogDomain, str);
	} else {
		current = str;
		while ((next = strchr(current, '\n')) != nullptr) {

			*next = '\0';
			if (next != str && next[-1] == '\r')
				next[-1] = '\0';
			__android_log_write(prio, LogDomain, current);
			current = next + 1;
		}
		__android_log_write(prio, LogDomain, current);
	}
}

static void linphone_android_ortp_log_handler(const char *domain, OrtpLogLevel lev, const char *fmt, va_list args) {
	char* str = bctbx_strdup_vprintf(fmt, args);
	if (str == nullptr) return;

	int prio;
	switch(lev) {
		case ORTP_DEBUG:	prio = ANDROID_LOG_DEBUG; break;
		case ORTP_MESSAGE:	prio = ANDROID_LOG_INFO; break;
		case ORTP_WARNING:	prio = ANDROID_LOG_WARN; break;
		case ORTP_ERROR:	prio = ANDROID_LOG_ERROR; break;
		case ORTP_FATAL:	prio = ANDROID_LOG_FATAL; break;
		default:			prio = ANDROID_LOG_DEFAULT;	break;
	}

	linphone_android_log_handler(prio, str);
	bctbx_free(str);
}

JNIEXPORT void JNICALL Java_org_linphone_core_FactoryImpl_setDebugMode(JNIEnv* env, jobject thiz, jboolean isDebug, jstring jdebugTag) {
	if (isDebug) {
		LogDomain = GetStringUTFChars(env, jdebugTag);
		linphone_core_enable_logs_with_cb(linphone_android_ortp_log_handler);
	} else {
		linphone_core_disable_logs();
	}
}

JNIEXPORT void JNICALL Java_org_linphone_core_FactoryImpl_setLoggerDomain(JNIEnv* env, jobject thiz, jstring jdebugTag) {
	LogDomain = GetStringUTFChars(env, jdebugTag);
}

JNIEXPORT void JNICALL Java_org_linphone_core_FactoryImpl_enableLogcatLogs(JNIEnv* env, jobject thiz, jboolean enableLogcat) {
	if (enableLogcat) {
		linphone_core_set_log_handler(linphone_android_ortp_log_handler);
	} else {
		linphone_core_set_log_handler(NULL);
	}
}

static jstring get_jstring_from_char(JNIEnv *env, const char* cString) {
	int len;
	jmethodID constructorString;
	jbyteArray bytesArray = nullptr;
	jstring javaString = nullptr;
	jclass classString = env->FindClass("java/lang/String");
	if (classString == nullptr) {
		bctbx_error("Cannot find java.lang.String class.\n");
		goto error;
	}

	constructorString = env->GetMethodID(classString, "<init>", "([BLjava/lang/String;)V");
	if (constructorString == nullptr) {
		bctbx_error("Cannot find String <init> method.\n");
		goto error;
	}

	len = (int)strlen(cString);
	bytesArray = env->NewByteArray(len);

	if (bytesArray) {
		env->SetByteArrayRegion(bytesArray, 0, len, (jbyte *)cString);
		jstring UTF8 = env->NewStringUTF("UTF8");
		javaString = (jstring)env->NewObject(classString, constructorString, bytesArray, UTF8);
		env->DeleteLocalRef(bytesArray);
		env->DeleteLocalRef(UTF8);
	}

	error:
	if (classString) env->DeleteLocalRef(classString);

	return javaString;
}

#ifdef __ANDROID__
int dumbMethodForAllowingUsageOfCpuFeaturesFromStaticLibMediastream() {
	return (android_getCpuFamily() == ANDROID_CPU_FAMILY_ARM && (android_getCpuFeatures() & ANDROID_CPU_ARM_FEATURE_NEON) != 0);
}

int dumbMethodForAllowingUsageOfMsAudioDiffFromStaticLibMediastream() {
	return ms_audio_diff(nullptr, nullptr, nullptr, 0, nullptr, nullptr);
}

JNIEXPORT void JNICALL setAndroidLogHandler() {
	linphone_core_enable_logs_with_cb(linphone_android_ortp_log_handler);
}

JNIEXPORT void JNICALL setMediastreamerAndroidContext(JNIEnv *env, void *context) {
	jclass ms_class = env->FindClass("org/linphone/mediastream/MediastreamerAndroidContext");
	jmethodID set_context = env->GetStaticMethodID(ms_class, "setContext", "(Ljava/lang/Object;)V");
	env->CallStaticVoidMethod(ms_class, set_context, (jobject)context);
}
#endif

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Logger
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

static void java_log(JNIEnv* env, BctbxLogLevel loglevel, jstring jdomain, jstring jmsg) {
	const char* domain = jdomain ? env->GetStringUTFChars(jdomain, NULL) : NULL;
	const char* msg = jmsg ? env->GetStringUTFChars(jmsg, NULL) : NULL;
	BCTBX_SLOG(domain, loglevel) << msg;
	if (msg) env->ReleaseStringUTFChars(jmsg, msg);
	if (domain) env->ReleaseStringUTFChars(jdomain, domain);
}

JNIEXPORT void JNICALL Java_org_linphone_core_tools_Log_d(JNIEnv* env, jobject thiz, jstring jdomain, jstring jmsg) {
	java_log(env, BCTBX_LOG_DEBUG, jdomain, jmsg);
}

JNIEXPORT void JNICALL Java_org_linphone_core_tools_Log_i(JNIEnv* env, jobject thiz, jstring jdomain, jstring jmsg) {
	java_log(env, BCTBX_LOG_MESSAGE, jdomain, jmsg);
}

JNIEXPORT void JNICALL Java_org_linphone_core_tools_Log_w(JNIEnv* env, jobject thiz, jstring jdomain, jstring jmsg) {
	java_log(env, BCTBX_LOG_WARNING, jdomain, jmsg);
}

JNIEXPORT void JNICALL Java_org_linphone_core_tools_Log_e(JNIEnv* env, jobject thiz, jstring jdomain, jstring jmsg) {
	java_log(env, BCTBX_LOG_ERROR, jdomain, jmsg);
}

JNIEXPORT void JNICALL Java_org_linphone_core_tools_Log_f(JNIEnv* env, jobject thiz, jstring jdomain, jstring jmsg) {
	java_log(env, BCTBX_LOG_FATAL, jdomain, jmsg);
}

JNIEXPORT void JNICALL Java_org_linphone_core_tools_Log_setDomain(JNIEnv* env, jobject thiz, jstring jdomain) {
	const char* domain = jdomain ? env->GetStringUTFChars(jdomain, NULL) : NULL;
	bctbx_set_log_level_mask(domain, (int)bctbx_get_log_level_mask(BCTBX_LOG_DOMAIN));
	if (domain) env->ReleaseStringUTFChars(jdomain, domain);
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

class LinphoneJavaBindings {
public:
	LinphoneJavaBindings(JNIEnv *env) {
		ms_factory_class = (jclass)env->NewGlobalRef(env->FindClass("org/linphone/mediastream/Factory"));
		ms_factory_class_constructor = env->GetMethodID(ms_factory_class, "<init>", "(J)V");

		throwable_class = (jclass)env->NewGlobalRef(env->FindClass("java/lang/Throwable"));
		throwable_to_string = env->GetMethodID(throwable_class, "toString", "()Ljava/lang/String;");

		{{#objects}}
		{{cPrefix}}_class = (jclass)env->NewGlobalRef(env->FindClass("{{jniPath}}{{classImplName}}"));
		{{cPrefix}}_class_constructor = env->GetMethodID({{cPrefix}}_class, "<init>", "(JZ)V");
		{{/objects}}

		{{#enums}}
		{{cPrefix}}_class = (jclass)env->NewGlobalRef(env->FindClass("{{jniPath}}{{jniName}}"));
		{{cPrefix}}_class_constructor_from_int = env->GetStaticMethodID({{cPrefix}}_class, "fromInt", "(I)L{{jniPath}}{{jniName}};");
		{{/enums}}
	}

	~LinphoneJavaBindings() {
		JNIEnv *env = ms_get_jni_env();
		if (!env) {
			bctbx_error("cannot attach VM");
		}

		env->DeleteGlobalRef(ms_factory_class);
		env->DeleteGlobalRef(throwable_class);

		{{#objects}}
		env->DeleteGlobalRef({{cPrefix}}_class);
		{{/objects}}

		{{#enums}}
		env->DeleteGlobalRef({{cPrefix}}_class);
		{{/enums}}
	}

	jclass ms_factory_class;
	jmethodID ms_factory_class_constructor;

	jclass throwable_class;
	jmethodID throwable_to_string;

	{{#objects}}
	jclass {{cPrefix}}_class;
	jmethodID {{cPrefix}}_class_constructor;
	{{/objects}}

	{{#enums}}
	jclass {{cPrefix}}_class;
	jmethodID {{cPrefix}}_class_constructor_from_int;
	{{/enums}}
};

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

{{#objects}}
JNIEXPORT jobject JNICALL get{{className}}(JNIEnv *env, {{classCName}} *cptr, bool_t takeref, bool_t is_const) {
	jobject jobj = nullptr;
	const char *key = belle_sip_java_user_data_key;
	if (is_const) key = belle_sip_const_java_user_data_key;

	if (cptr != nullptr) {
		void *up = belle_sip_object_data_get((belle_sip_object_t *)cptr, key);
		LinphoneJavaBindings *ljb = (LinphoneJavaBindings *)linphone_factory_get_user_data(linphone_factory_get());
		if (!ljb) {
			bctbx_message("get{{className}}(): First attempt at getting LinphoneJavaBindings object, creating it");
			ljb = new LinphoneJavaBindings(env);
			linphone_factory_set_user_data(linphone_factory_get(), ljb);
		}

		jclass {{cPrefix}}_class = ljb->{{cPrefix}}_class;
		jmethodID {{cPrefix}}_constructor = ljb->{{cPrefix}}_class_constructor;

		if (up == nullptr) {
			jobj = env->NewObject({{cPrefix}}_class, {{cPrefix}}_constructor, (jlong)cptr, (jboolean)is_const == TRUE);
			belle_sip_object_data_set((belle_sip_object_t *)cptr, key, (void*)env->NewWeakGlobalRef(jobj), nullptr);
			if (takeref) {
				{{#refCountable}}{{cPrefix}}_ref(cptr);{{/refCountable}}
			}
		} else {
			try {
				jobj = env->NewLocalRef((jobject)up);
				if (env->ExceptionCheck()) {
					env->ExceptionDescribe();
					env->ExceptionClear();
					jobj = nullptr;
				}
			} catch (...) {
				bctbx_warning("get{{className}}(): Exception trying to get local ref from pointer [%p], creating new object instead", up);
			}

			if (jobj == nullptr) {
				// Delete weak ref ?
				env->DeleteWeakGlobalRef((jobject)up);
				// takes implicit local ref
				jobj = env->NewObject({{cPrefix}}_class, {{cPrefix}}_constructor, (jlong)cptr, (jboolean)is_const == TRUE);
				belle_sip_object_data_set((belle_sip_object_t *)cptr, key, (void*)env->NewWeakGlobalRef(jobj), nullptr);
				if (takeref) {
					{{#refCountable}}{{cPrefix}}_ref(cptr);{{/refCountable}}
				}
			}
		}
	}
	return jobj;
}

JNIEXPORT jboolean JNICALL Java_{{jniPrefix}}{{classImplName}}_unref(JNIEnv* env, jobject thiz, jlong ptr, jboolean is_const) {
	{{classCName}} *cptr = ({{classCName}}*)ptr;
	if (cptr == 0) {
		bctbx_error("Java_{{jniPrefix}}{{classImplName}}_unref's {{classCName}} C ptr is null!");
		return TRUE;
	}

	if (is_const) {
		jobject wref = (jobject)belle_sip_object_data_get((belle_sip_object_t *)cptr, belle_sip_const_java_user_data_key);
		belle_sip_object_data_remove((belle_sip_object_t *)cptr, belle_sip_const_java_user_data_key);
		if (wref) {
			env->DeleteWeakGlobalRef(wref);
		}
	} else {
		jobject wref = (jobject)belle_sip_object_data_get((belle_sip_object_t *)cptr, belle_sip_java_user_data_key);
		belle_sip_object_data_remove((belle_sip_object_t *)cptr, belle_sip_java_user_data_key);
		if (wref) {
			env->DeleteWeakGlobalRef(wref);
		}
	}

	{{#refCountable}}return belle_sip_object_unref_2(cptr) == 1;{{/refCountable}}
	{{#notRefCountable}}return FALSE;{{/notRefCountable}}
}

{{/objects}}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

static inline void handle_possible_java_exception(JNIEnv *env, jobject listener, const char *listener_name)
{
	if (env->ExceptionCheck()) {
		env->ExceptionDescribe();
		jthrowable exception = env->ExceptionOccurred();
		env->ExceptionClear();

		LinphoneJavaBindings *ljb = (LinphoneJavaBindings *)linphone_factory_get_user_data(linphone_factory_get());
		if (!ljb) {
			bctbx_error("Failed to get LinphoneJavaBindings from factory user data!");
			return;
		}

		jstring toString = (jstring) env->CallObjectMethod(exception, ljb->throwable_to_string);
		const char * toStringChar = GetStringUTFChars(env, toString);
		bctbx_error("Listener %s [%p] raised an exception: %s", listener_name, listener, toStringChar);
		ReleaseStringUTFChars(env, toString, toStringChar);
	}
}

{{#callbacks}}
static {{return}} {{callbackName}}({{params}}) {
	JNIEnv *env = ms_get_jni_env();
	if (!env) {
		bctbx_error("cannot attach VM");
		return{{returnIfFail}};
	}

	jobject jlistener = nullptr;
	{{#isMultiListener}}
	{{classCName}} *multi_cbs = {{cPrefix}}_get_current_callbacks({{firstParam}});
	if (multi_cbs != nullptr) {
		jlistener = (jobject) belle_sip_object_data_get((belle_sip_object_t *)multi_cbs, belle_sip_java_listener_user_data_key);
	}
	{{/isMultiListener}}

	if (jlistener == nullptr) {
		bctbx_warning("{{callbackName}}() notification without listener");
		return{{returnIfFail}};
	}

	jobject up = env->NewLocalRef((jobject)jlistener);
	if (up == nullptr) {
		bctbx_debug("{{callbackName}}() listener in weak reference has been destroyed");
		return{{returnIfFail}};
	}

	jclass jlistenerClass = (jclass) env->GetObjectClass(up);
	jmethodID jcallback = env->GetMethodID(jlistenerClass, "{{jname}}", "{{jparams}}");
	env->DeleteLocalRef(jlistenerClass);
	env->DeleteLocalRef(up);

	{{#needLjb}}
	LinphoneJavaBindings *ljb = (LinphoneJavaBindings *)linphone_factory_get_user_data(linphone_factory_get());
	if (!ljb) {
		bctbx_error("{{callbackName}}(): failed to get LinphoneJavaBindings from factory user data!");
		return;
	}

	{{/needLjb}}
	{{#jobjects}}
	jobject j_{{objectName}} = get{{className}}(env, (Linphone{{className}} *){{objectName}}, TRUE, {{isObjectConst}});
	{{/jobjects}}
	{{#jenums}}
	jobject j_{{enumName}} = env->CallStaticObjectMethod(ljb->{{cEnumPrefix}}_class, ljb->{{cEnumPrefix}}_class_constructor_from_int, (jint){{enumName}});
	{{/jenums}}
	{{#jstrings}}
	jstring j_{{stringName}} = {{stringName}} ? get_jstring_from_char(env, {{stringName}}) : nullptr;
	{{/jstrings}}
	{{#jlists}}
	bctbx_list_t *list_it = (bctbx_list_t *){{list_name}};
	size_t count = bctbx_list_size({{list_name}});
	{{#isRealObjectArray}}
	jobjectArray j_{{list_name}} = env->NewObjectArray((int)count, ljb->{{objectCPrefix}}_class, nullptr);{{/isRealObjectArray}}
	{{#isStringObjectArray}}jobjectArray j_{{list_name}} = env->NewObjectArray((int)count, env->FindClass("java/lang/String"), env->NewStringUTF(""));{{/isStringObjectArray}}
	int i = 0;
	while (list_it != nullptr) {
		{{#isRealObjectArray}}
		{{objectClassCName}}* c_object = ({{objectClassCName}}*)list_it->data;
		jobject object = get{{objectClassName}}(env, c_object, TRUE, {{isObjectConst}});
		{{/isRealObjectArray}}
		{{#isStringObjectArray}}const char *cstring = (const char *)list_it->data;
		jstring object = cstring ? get_jstring_from_char(env, cstring) : 0;{{/isStringObjectArray}}
		if (object != nullptr) {
			env->SetObjectArrayElement(j_{{list_name}}, (int)i, object);
			{{#isRealObjectArray}}env->DeleteLocalRef(object);{{/isRealObjectArray}}
		}
		list_it = bctbx_list_next(list_it);
		i += 1;
	}
	{{/jlists}}

	{{#hasReturn}}{{jniUpcallType}} jni_upcall_result = {{/hasReturn}}env->{{jniUpcallMethod}}(jlistener, jcallback, {{params_impl}});
	{{#hasReturn}}
	{{#isJniUpcallObject}}
	{{return}} c_upcall_result = nullptr;
	if (jni_upcall_result) c_upcall_result = ({{return}})GetObjectNativePtr(env, jni_upcall_result);
	{{/isJniUpcallObject}}
	{{#isJniUpcallBasicType}}
	{{return}} c_upcall_result = ({{return}}) jni_upcall_result;
	{{/isJniUpcallBasicType}}
	{{/hasReturn}}

	{{#jobjects}}
	if (j_{{objectName}}) {
		env->DeleteLocalRef(j_{{objectName}});
	}
	{{/jobjects}}
	{{#jenums}}
	if (j_{{enumName}}) {
		env->DeleteLocalRef(j_{{enumName}});
	}
	{{/jenums}}
	{{#jstrings}}
	if (j_{{stringName}}) {
		env->DeleteLocalRef(j_{{stringName}});
	}
	{{/jstrings}}

	//handle_possible_java_exception(env, jlistener, "{{callbackName}}");
	{{#hasReturn}}
	return c_upcall_result;
	{{/hasReturn}}
}

{{/callbacks}}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

void belle_sip_java_listener_data_removed_callback(void *data) {
	JNIEnv *env = ms_get_jni_env();
	jobject listener = (jobject) data;
	env->DeleteGlobalRef(listener);
}

{{#interfaces}}
{{#isMultiListener}}
JNIEXPORT void JNICALL Java_{{jniPackage}}{{className}}Impl_addListener(JNIEnv* env, jobject thiz, jlong ptr, jobject jlistener) {
	if (jlistener == nullptr) return;
	{{classCName}} *cptr = ({{classCName}}*)ptr;
	if (cptr == 0) {
		bctbx_error("Java_{{jniPackage}}{{className}}Impl_addListener's {{classCName}} C ptr is null!");
		return;
	}
	{{classCName}}Cbs *cbs = linphone_factory_create_{{factoryName}}_cbs(linphone_factory_get());
	belle_sip_object_data_set((belle_sip_object_t *)cbs, belle_sip_java_listener_user_data_key, (void*)env->NewGlobalRef(jlistener), belle_sip_java_listener_data_removed_callback);
	{{#callbacksList}}
	{{cPrefix}}_cbs_set_{{callback}}(cbs, {{callbackName}});
	{{/callbacksList}}
	{{cPrefix}}_add_callbacks(cptr, cbs);
	{{cPrefix}}_cbs_unref(cbs);
}

JNIEXPORT void JNICALL Java_{{jniPackage}}{{className}}Impl_removeListener(JNIEnv* env, jobject thiz, jlong ptr, jobject jlistener) {
	{{classCName}} *cptr = ({{classCName}}*)ptr;
	if (cptr == 0) {
		bctbx_error("Java_{{jniPackage}}{{className}}Impl_removeListener's {{classCName}} C ptr is null!");
		return;
	}
	{{#isNotCore}}const {{/isNotCore}}bctbx_list_t *cbs_list = {{cPrefix}}_get_callbacks_list(cptr);
	bctbx_list_t *it;
	{{classCName}}Cbs *cbsToRemove = nullptr;

	for (it = (bctbx_list_t *)cbs_list; it != nullptr; it = it->next) {
		{{classCName}}Cbs *cbs = ({{classCName}}Cbs *)it->data;
		jobject listener = (jobject) belle_sip_object_data_get((belle_sip_object_t *)cbs, belle_sip_java_listener_user_data_key);
		if (env->IsSameObject(listener, jlistener)) {
			cbsToRemove = cbs;
			break;
		}
	}
	{{#isCore}}bctbx_list_free(cbs_list);{{/isCore}}

	if (cbsToRemove != nullptr) {
		belle_sip_object_data_remove((belle_sip_object_t *)cbsToRemove, belle_sip_java_listener_user_data_key); // Global ref will be deleted by belle_sip_java_listener_data_removed_callback
		{{cPrefix}}_remove_callbacks(cptr, cbsToRemove);
	} else {
		bctbx_error("Failed to find the native listener matching jobject [%p]", jlistener);
	}
}
{{/isMultiListener}}
{{/interfaces}}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

JNIEXPORT jobject JNICALL Java_{{jni_package}}CoreImpl_getMediastreamerFactory(JNIEnv *env, jobject thiz, jlong ptr) {
	LinphoneJavaBindings *ljb = (LinphoneJavaBindings *)linphone_factory_get_user_data(linphone_factory_get());
	MSFactory *factory = linphone_core_get_ms_factory((LinphoneCore*)ptr);
	return env->NewObject(ljb->ms_factory_class, ljb->ms_factory_class_constructor, (jlong)factory);
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

{{#methods}}
{{#notEmpty}}
JNIEXPORT {{return}} JNICALL {{name}}({{params}}) {
	{{#notStatic}}{{classCName}} *cptr = ({{classCName}}*)ptr;
	if (cptr == nullptr) {
		bctbx_error("{{name}}'s {{classCName}} C ptr is null!");
		{{#hasListReturn}}
		{{#isRealObjectArray}}
		LinphoneJavaBindings *ljb = (LinphoneJavaBindings *)linphone_factory_get_user_data(linphone_factory_get());
		if (!ljb) {
			bctbx_error("{{name}}(): failed to get LinphoneJavaBindings from factory user data!");
		}

		jobjectArray jni_list_result = env->NewObjectArray(0, ljb->{{objectCPrefix}}_class, NULL);{{/isRealObjectArray}}
		{{#isStringObjectArray}}jobjectArray jni_list_result = env->NewObjectArray(0, env->FindClass("java/lang/String"), env->NewStringUTF(""));{{/isStringObjectArray}}
		{{#isEnumObjectArray}}jintArray jni_list_result = env->NewIntArray(0);{{/isEnumObjectArray}}
		{{/hasListReturn}}
		return {{#hasReturn}}0{{/hasReturn}}{{#hasListReturn}}jni_list_result{{/hasListReturn}}{{#hasByteArrayReturn}}0{{/hasByteArrayReturn}};
	}
	{{/notStatic}}{{#strings}}
	const char* c_{{string}} = GetStringUTFChars(env, {{string}});
	{{/strings}}{{#bytes}}
	{{bytesargtype}} c_{{bytesargname}} = ({{bytesargtype}})env->GetByteArrayElements({{bytesargname}}, nullptr);
	{{/bytes}}{{#objects}}
	{{objectClassCName}}* c_{{object}} = nullptr;
	if ({{object}}) c_{{object}} = ({{objectClassCName}}*)GetObjectNativePtr(env, {{object}});
	{{/objects}}{{#lists}}
	bctbx_list_t *bctbx_list_{{list}} = nullptr;
	int {{list}}_count = env->GetArrayLength({{list}});
	{{#isEnumList}}jint *int_elems_{{list}} = env->GetIntArrayElements({{list}}, 0);{{/isEnumList}}
	for (int i=0; i < {{list}}_count; i++) {
		{{#isStringList}}
		jstring obj = (jstring) env->GetObjectArrayElement({{list}}, i);
		const char *str = GetStringUTFChars(env, obj);
		if (str) {
			bctbx_list_{{list}} = bctbx_list_append(bctbx_list_{{list}}, ms_strdup(str));
			ReleaseStringUTFChars(env, obj, str);
		}
		{{/isStringList}}
		{{#isObjList}}
		jobject obj = env->GetObjectArrayElement({{list}}, i);
		bctbx_list_{{list}} = bctbx_list_append(bctbx_list_{{list}}, ({{objectClassCName}} *)GetObjectNativePtr(env, obj));
		{{/isObjList}}
		{{#isEnumList}}
		bctbx_list_{{list}} = bctbx_list_append(bctbx_list_{{list}}, (void*)(intptr_t)int_elems_{{list}}[i]);
		{{/isEnumList}}
	}
	{{/lists}}{{#hasListReturn}}
	{{#isConst}}const {{/isConst}}bctbx_list_t *list = {{c_name}}({{#notStatic}}cptr{{/notStatic}}{{params_impl}});
	bctbx_list_t *list_it = (bctbx_list_t *)list;
	size_t count = bctbx_list_size(list);
	{{#isRealObjectArray}}
	LinphoneJavaBindings *ljb = (LinphoneJavaBindings *)linphone_factory_get_user_data(linphone_factory_get());
	if (!ljb) {
		bctbx_error("{{name}}(): failed to get LinphoneJavaBindings from factory user data!");
	}
	jobjectArray jni_list_result = env->NewObjectArray((int)count, ljb->{{objectCPrefix}}_class, nullptr);{{/isRealObjectArray}}
	{{#isStringObjectArray}}jobjectArray jni_list_result = env->NewObjectArray((int)count, env->FindClass("java/lang/String"), env->NewStringUTF(""));{{/isStringObjectArray}}
	{{#isEnumObjectArray}}jintArray jni_list_result = env->NewIntArray((int)count);
	int carray[(int)count];{{/isEnumObjectArray}}
	int i = 0;
	while (list_it != nullptr) {
		{{#isRealObjectArray}}
		{{objectClassCName}}* c_object = ({{objectClassCName}}*)list_it->data;
		jobject object = get{{objectClassName}}(env, c_object, {{takeRefOnReturnedObject}}, {{isRealObjectConst}});
		if (object != nullptr) {
			env->SetObjectArrayElement(jni_list_result, (int)i, object);
			env->DeleteLocalRef(object);
		}
		{{/isRealObjectArray}}
		{{#isStringObjectArray}}const char *cstring = (const char *)list_it->data;
		jstring object = cstring ? get_jstring_from_char(env, cstring) : 0;
		if (object != nullptr) {
			env->SetObjectArrayElement(jni_list_result, (int)i, object);
		}{{/isStringObjectArray}}
		{{#isEnumObjectArray}}
		carray[i] = (int)(intptr_t)list_it->data;
		{{/isEnumObjectArray}}
		list_it = bctbx_list_next(list_it);
		i += 1;
	}
	{{#isEnumObjectArray}}
	env->SetIntArrayRegion(jni_list_result, 0, (int)count, carray);
	{{/isEnumObjectArray}}
	{{#isNotConst}}bctbx_list_free(list);{{/isNotConst}}
	{{/hasListReturn}}{{#hasByteArrayReturn}}
	{{c_type_return}} jni_result = {{c_name}}({{#notStatic}}cptr{{/notStatic}}{{params_impl}});
	if (!jni_result) return nullptr;
	size_t jni_result_length = strlen((const char *)jni_result);
	jbyteArray array = env->NewByteArray((int)jni_result_length);
	env->SetByteArrayRegion(array, 0, (int)jni_result_length, (const jbyte*)jni_result);
	return array;
	{{/hasByteArrayReturn}}{{#hasStringReturn}}
	{{#isConst}}const {{/isConst}}char *c_string = {{c_name}}({{#notStatic}}cptr{{/notStatic}}{{params_impl}}){{#returnObject}}){{/returnObject}};
	jstring jni_result = (c_string != nullptr) ? get_jstring_from_char(env, c_string) : nullptr;
	{{#isNotConst}}bctbx_free(c_string);{{/isNotConst}}
	{{/hasStringReturn}}{{#hasNormalReturn}}
	{{#hasReturn}}{{return}} jni_result = ({{return}}){{#returnObject}}get{{returnClassName}}(env, (Linphone{{returnClassName}} *){{/returnObject}}{{/hasReturn}}{{c_name}}({{#notStatic}}cptr{{/notStatic}}{{params_impl}}){{#returnObject}}, {{takeRefOnReturnedObject}}, {{returnedObjectIsConst}}){{/returnObject}};
	{{/hasNormalReturn}}{{#strings}}
	ReleaseStringUTFChars(env, {{string}}, c_{{string}});
	{{/strings}}{{#bytes}}
	env->ReleaseByteArrayElements({{bytesargname}}, (jbyte*)c_{{bytesargname}}, JNI_ABORT);
	{{/bytes}}{{#hasReturn}}return jni_result;{{/hasReturn}}{{#hasListReturn}}return jni_list_result;{{/hasListReturn}}
}
{{/notEmpty}}
{{/methods}}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Manually wrapped
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

JNIEXPORT void JNICALL Java_org_linphone_core_CallImpl_setNativeVideoWindowId(JNIEnv *env, jobject thiz, jlong ptr, jobject id) {
	LinphoneCall *cptr = (LinphoneCall*)ptr;
	if (cptr == 0) {
		bctbx_error("Java_org_linphone_core_CallImpl_setNativeVideoWindowId's LinphoneCall C ptr is null!");
		return;
	}
	LinphoneCore *lc = linphone_call_get_core(cptr);
	linphone_core_set_native_video_window_id(lc, (void *)id);
}

JNIEXPORT void JNICALL Java_org_linphone_core_CoreImpl_setNativePreviewWindowId(JNIEnv *env, jobject thiz, jlong ptr, jobject id) {
	LinphoneCore *cptr = (LinphoneCore*)ptr;
	if (cptr == 0) {
		bctbx_error("Java_org_linphone_core_CoreImpl_setNativePreviewWindowId's LinphoneCore C ptr is null!");
		return;
	}
	linphone_core_set_native_preview_window_id(cptr, (void *)id);
}

JNIEXPORT void JNICALL Java_org_linphone_core_CoreImpl_setNativeVideoWindowId(JNIEnv *env, jobject thiz, jlong ptr, jobject id) {
	LinphoneCore *cptr = (LinphoneCore*)ptr;
	if (cptr == 0) {
		bctbx_error("Java_org_linphone_core_CoreImpl_setNativeVideoWindowId's LinphoneCore C ptr is null!");
		return;
	}
	linphone_core_set_native_video_window_id(cptr, (void *)id);
}

JNIEXPORT void JNICALL Java_org_linphone_core_ParticipantDeviceImpl_setNativeVideoWindowId(JNIEnv *env, jobject thiz, jlong ptr, jobject id) {
	LinphoneParticipantDevice *cptr = (LinphoneParticipantDevice*)ptr;
	if (cptr == 0) {
		bctbx_error("Java_org_linphone_core_ParticipantDeviceImpl_setNativeVideoWindowId's LinphoneCore C ptr is null!");
		return;
	}
	linphone_participant_device_set_native_video_window_id(cptr, (void *)id);
}

JNIEXPORT jobject JNICALL Java_org_linphone_core_FactoryImpl_getCore(JNIEnv *env, jobject thiz, jlong ptr, jlong lcPtr) {
	return getCore(env, (LinphoneCore*)lcPtr, TRUE, FALSE);
}

JNIEXPORT jlong JNICALL Java_org_linphone_core_FactoryImpl_init(JNIEnv *env, jobject thiz, jlong ptr) {
	return (jlong)linphone_factory_get();
}

} // End of extern C
#ifndef _MSC_VER
#pragma GCC diagnostic pop
#endif // _MSC_VER
