#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <stdlib.h>
#include <string.h>
#include <sys/param.h>
#include <errno.h>
#include <unistd.h>
#include <sys/time.h>
#include <poll.h>
#include <jni.h>
#include "tipcc.h"
#include "tipcjni.h"

/*
 * Caching java classes and method Ids
 */
static jclass jclassInt = NULL;
static jfieldID jclassIntField = NULL;
static jmethodID jclassIntNewObject = NULL; /* constructor java.lang.Integer */

static jclass jclassTipcAddr = NULL;
static jfieldID jtipcAddrType = NULL;
static jfieldID jtipcAddrInstance = NULL;
static jfieldID jtipcAddrNode = NULL;

/*
 * Function to convert from C struct tipc_addr to Java TipcAddr instance
 */
static void convertTipcAddrToJNI(JNIEnv* env, const struct tipc_addr* addr, jobject jobjTipcAddr) {
	if (!jobjTipcAddr || !addr)
		return;

	(*env)->SetLongField(env, jobjTipcAddr, jtipcAddrType, (jlong)(unsigned long)addr->type);
	(*env)->SetLongField(env, jobjTipcAddr, jtipcAddrInstance, (jlong)(unsigned long)addr->instance);
	(*env)->SetLongField(env, jobjTipcAddr, jtipcAddrNode, (jlong)(unsigned long)addr->node);
}

/*
 * Function to convert from Java TipcAddr instance to C struct tipc_addr
 */
static void convertTipcAddrFromJNI(JNIEnv* env, struct tipc_addr* addr, jobject jobjTipcAddr) {
	if (!addr || !jobjTipcAddr)
		return;

	addr->type = (uint32_t)(*env)->GetLongField(env, jobjTipcAddr, jtipcAddrType);
	addr->instance = (uint32_t)(*env)->GetLongField(env, jobjTipcAddr, jtipcAddrInstance);
	addr->node = (uint32_t)(*env)->GetLongField(env, jobjTipcAddr, jtipcAddrNode);
}

jint throwOutOfMemoryError(JNIEnv *env)
{
	jclass localExClass;
	localExClass = (*env)->FindClass(env, "java/lang/OutOfMemoryError");
	if (localExClass) {
		return (*env)->ThrowNew( env, localExClass, "Out Of Memory");
	}
	return -1;
}

jint JNI_OnLoad(JavaVM* vm, void* reserved) {
	JNIEnv* env;
	if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) {
		return JNI_ERR;
	} else {
		jclass localIntClass = (*env)->FindClass(env, "java/lang/Integer");

		if (localIntClass) {
			jclassInt = (*env)->NewGlobalRef(env, localIntClass);
			/*
			 * Release local ref
			 */
			(*env)->DeleteLocalRef(env, localIntClass);
			if (jclassInt == NULL) {
				return JNI_ERR;
			}
		}

		jclassIntField = (*env)->GetFieldID(env, jclassInt, "value", "I");
		if (jclassIntField == NULL) {
			goto releaseGlobalClassInt;
		}

		jclassIntNewObject = (*env)->GetMethodID(env, jclassInt, "<init>", "(I)V");
		if (jclassIntNewObject == NULL) {
			goto releaseGlobalClassInt;
		}

		jclass localTipcAddrclass = (*env)->FindClass(env, "com/tipcj/TipcAddr");
		if (localTipcAddrclass == NULL) {
			goto releaseGlobalClassInt;
		} else {
			jclassTipcAddr = (*env)->NewGlobalRef(env, localTipcAddrclass);
			/*
			 * Release local ref
			 */
			(*env)->DeleteLocalRef(env, localTipcAddrclass);
		}
		jtipcAddrType = (*env)->GetFieldID(env, jclassTipcAddr, "Type", "J");
		jtipcAddrInstance = (*env)->GetFieldID(env, jclassTipcAddr, "Instance", "J");
		jtipcAddrNode = (*env)->GetFieldID(env, jclassTipcAddr, "Node", "J");
		if (!jtipcAddrType || !jtipcAddrInstance || !jtipcAddrNode)
			goto releaseGlobalClassTipcAddr;
	}

	return JNI_VERSION_1_6;

releaseGlobalClassTipcAddr:
	(*env)->DeleteGlobalRef(env, jclassTipcAddr);
releaseGlobalClassInt:
	(*env)->DeleteGlobalRef(env, jclassInt);
	return JNI_ERR;
}

void JNI_OnUnload(JavaVM *vm, void *reserved) {
	JNIEnv* env;
	if ((*vm)->GetEnv(vm, (void **) &env, JNI_VERSION_1_6) != JNI_OK) {
		return;
	} else {
		(*env)->DeleteGlobalRef(env, jclassInt);
		(*env)->DeleteGlobalRef(env, jclassTipcAddr);
	}
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_own_node
 * Signature: ()I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1own_1node
  (JNIEnv *env, jobject jobj) {
	return tipc_own_node();
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_linkname
 * Signature: (JJ)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_tipcj_TipcBaseApi_tipc_1linkname
  (JNIEnv *env, jobject jobj, jlong peer, jlong bearerid) {
#define BUF_SZ 40
	char buf[BUF_SZ] = {0};
	tipc_linkname(buf, BUF_SZ, peer, bearerid);
	return (*env)->NewStringUTF(env, buf);
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_socket
 * Signature: (I)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1socket
  (JNIEnv *jenv, jobject jobj, jint sk_type) {
	int fd;
	fd = tipc_socket(sk_type);
	if (fd < 0 ) {
		printf("Err: [%s]\n", strerror(errno));
		return 0;
	}
	return fd;
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_sock_non_block
 * Signature: (I)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1sock_1non_1block
  (JNIEnv *env, jobject obj, jint jfd) {
	return tipc_sock_non_block(jfd);
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_sock_rejectable
 * Signature: (I)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1sock_1rejectable
(JNIEnv *env, jobject obj, jint jfd) {
	return tipc_sock_rejectable(jfd);
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_close
 * Signature: (I)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1close
  (JNIEnv *env, jobject jobj, jint jfd) {
	  return tipc_close(jfd);
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_sockaddr
 * Signature: (ILcom/tipcj/TipcAddr;)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1sockaddr
  (JNIEnv *env, jobject obj, jint jfd, jobject jtipcAddr) {
	struct tipc_addr addr = {0, 0, 0};
	int ret = 0;

	ret = tipc_sockaddr(jfd, &addr);
	if (!ret && jtipcAddr) {
		convertTipcAddrToJNI(env, &addr, jtipcAddr);
	}

	return ret;
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_bind
 * Signature: (IJJJJ)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1bind
  (JNIEnv *env, jobject jobj, jint sd, jlong type, jlong lower, jlong upper, jlong scope) {
	  return tipc_bind(sd, type, lower, upper, scope);
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_unbind
 * Signature: (IJJJ)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1unbind
  (JNIEnv *env, jobject jobj, jint sd, jlong type, jlong lower, jlong upper) {
	return tipc_unbind(sd, type, lower, upper);
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_connect
 * Signature: (ILcom/tipcj/TipcAddr;)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1connect
  (JNIEnv *env, jobject jobj, jint sd, jobject jtipcAddr) {
	struct tipc_addr addr = {0, 0, 0};

	if (jtipcAddr == NULL) {
		return -1;
	}
	convertTipcAddrFromJNI(env, &addr, jtipcAddr);
	return tipc_connect(sd, &addr);
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_listen
 * Signature: (IJ)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1listen
  (JNIEnv *env, jobject jobj, jint sd, jlong backlog) {
	return tipc_listen(sd, backlog);
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_accept
 * Signature: (ILcom/tipcj/TipcAddr;)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1accept
  (JNIEnv *env, jobject obj, jint sd, jobject jtipcAddr) {
	struct tipc_addr addr = {0, 0, 0};
	int ret = 0;

	if (jtipcAddr != NULL) {
		ret = tipc_accept(sd, &addr);
		if (ret) {
			convertTipcAddrToJNI(env, &addr, jtipcAddr);
		}
		return ret;
	}
	return tipc_accept(sd, NULL);
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_join
 * Signature: (ILcom/tipcj/TipcAddr;ZZ)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1join
  (JNIEnv *env, jobject obj, jint sd, jobject jtipcAddr, jboolean jevents, jboolean jloopback) {
	struct tipc_addr addr = {0, 0, 0};
	bool events = 0, loopback = 0;

	if (jevents == JNI_TRUE) {
		events = 1;
	}

	if (jloopback == JNI_TRUE) {
		loopback = 1;
	}

	if (jtipcAddr != NULL) {
		convertTipcAddrFromJNI(env, &addr, jtipcAddr);
	}

	return tipc_join(sd, &addr, events, loopback);
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_leave
 * Signature: (I)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1leave
  (JNIEnv *env, jobject jobj, jint sd) {
	return tipc_leave(sd);
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_recvfrom
 * Signature: (ILjava/nio/ByteBuffer;ILcom/tipcj/TipcAddr;Lcom/tipcj/TipcAddr;Ljava/lang/Integer;)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1recvfrom
  (JNIEnv *env, jobject jobj, jint jfd, jobject jbuf, jint len, jobject jsocket, jobject jmemberid, jobject jerr) {

	struct tipc_addr socket = {0, 0, 0};
	struct tipc_addr memberid = {0, 0, 0};
	void *buf = NULL;
	int err = 0;
	int ret = 0;

	buf = (void *) (*env)->GetDirectBufferAddress (env, jbuf);
	if (buf == NULL) {
		return throwOutOfMemoryError(env);
	}

	/*
	 * Calling TIPC C Api
	 */
	ret = tipc_recvfrom(jfd, buf, len, &socket, &memberid, &err);
	/**
	 * Convert c int to jInteger
	 */
	(*env)->SetIntField(env, jerr, jclassIntField, err);
	if (jmemberid != NULL) {
		convertTipcAddrToJNI(env, &memberid, jmemberid);
	}

	if (jsocket != NULL) {
		convertTipcAddrToJNI(env, &socket, jsocket);
	}

	return ret;
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_recv
 * Signature: (ILjava/nio/ByteBuffer;IZ)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1recv
  (JNIEnv *env, jobject jobj, jint jfd, jobject jbuf, jint len, jboolean jwaitall) {
	int waitall = 0;
	void *buf = NULL;

	if (jwaitall == JNI_TRUE)
		waitall = 1;

	buf = (void *) (*env)->GetDirectBufferAddress (env, jbuf);
	if (buf == NULL) {
		return throwOutOfMemoryError(env);
	}

	return tipc_recv(jfd, buf, len, waitall);
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_sendmsg
 * Signature: (ILjava/nio/ByteBuffer;I)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1sendmsg
  (JNIEnv *env, jobject obj, jint jfd, jobject jbuf, jint len) {
	void *buf = NULL;

	buf = (void *) (*env)->GetDirectBufferAddress (env, jbuf);
	if (buf == NULL) {
		return throwOutOfMemoryError(env);
	}

	return tipc_send(jfd, buf, len);
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_sendto
 * Signature: (ILjava/nio/ByteBuffer;ILcom/tipcj/TipcAddr;)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1sendto
  (JNIEnv *env, jobject jobj, jint jfd, jobject jbuf, jint len, jobject jtipcAddr) {
	struct tipc_addr dst = {0,0,0};
	void *buf = NULL;
	int ret = 0;

	buf = (void *) (*env)->GetDirectBufferAddress (env, jbuf);
	if (buf == NULL || jtipcAddr == NULL) {
		return throwOutOfMemoryError(env);
	}
	convertTipcAddrFromJNI(env, &dst, jtipcAddr);
	/*
	 * Calling TIPC C Api
	 */
	ret = tipc_sendto(jfd, buf, len, &dst);

	return ret;
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_send
 * Signature: (ILjava/nio/ByteBuffer;I)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1send
  (JNIEnv *env, jobject jobj, jint jfd, jobject jbuf, jint len) {
	/**
	 * TODO
	 */
	return Java_com_tipcj_TipcBaseApi_tipc_1sendmsg(env, jobj, jfd, jbuf, len);
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_mcast
 * Signature: (ILjava/nio/ByteBuffer;ILcom/tipcj/TipcAddr;)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1mcast
  (JNIEnv *env, jobject obj, jint sd, jobject jbuf, jint len, jobject jtipcAddr) {
	struct tipc_addr dst = {0,0,0};
	void *buf = NULL;

	buf = (void *) (*env)->GetDirectBufferAddress (env, jbuf);
	if (buf == NULL || jtipcAddr == NULL) {
		return throwOutOfMemoryError(env);
	}
	convertTipcAddrFromJNI(env, &dst, jtipcAddr);
	return tipc_mcast(sd, buf, len, &dst);
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_topsrv_conn
 * Signature: (J)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1topsrv_1conn
  (JNIEnv *env, jobject obj, jlong node) {
	return tipc_topsrv_conn((uint32_t)node);
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_srv_subscr
 * Signature: (IJJJZI)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1srv_1subscr
  (JNIEnv *env, jobject obj, jint sd, jlong type, jlong lower, jlong upper, jboolean jall, jint expire) {
	bool all = false;

	if (jall == JNI_TRUE) {
		all = true;
	}
	return tipc_srv_subscr(sd, (uint32_t)type, (uint32_t)lower, (uint32_t)upper, all, expire);
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_srv_evt
 * Signature: (ILcom/tipcj/TipcAddr;Lcom/tipcj/TipcAddr;Ljava/lang/Integer;Ljava/lang/Integer;)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1srv_1evt
  (JNIEnv *env, jobject obj, jint sd, jobject jsrv, jobject jid, jobject javailable, jobject jexpired) {
	struct tipc_addr srv = {0, 0, 0}, id = {0, 0, 0};
	int ret = 0;
	bool available = false, expired = false;

	ret = tipc_srv_evt(sd, &srv, &id, &available, &expired);
	if (!ret && !expired) {
		if (jsrv != NULL) {
			convertTipcAddrToJNI(env, &srv, jsrv);
		}

		if (jid != NULL) {
			convertTipcAddrToJNI(env, &id, jid);
		}
	}

	if (javailable) {
		(*env)->SetIntField(env, javailable, jclassIntField, available);
	}
	if (jexpired) {
		(*env)->SetIntField(env, jexpired, jclassIntField, expired);
	}

	return ret;
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_srv_wait
 * Signature: (Lcom/tipcj/TipcAddr;I)Z
 */
JNIEXPORT jboolean JNICALL Java_com_tipcj_TipcBaseApi_tipc_1srv_1wait
  (JNIEnv *env, jobject jobj, jobject jtipcAddr, jint wait) {
	struct tipc_addr srv = {0,0,0};
	int up = 0;

	convertTipcAddrFromJNI(env, &srv, jtipcAddr);
	up = tipc_srv_wait(&srv, wait);
	if (up)
		return JNI_TRUE;

	return JNI_FALSE;
}
/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_poll
 * Signature: (ISLjava/lang/Integer;I)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1poll
  (JNIEnv *env, jobject obj, jint jfd, jshort events, jobject revents, jint timeout) {
	struct pollfd pfd;
	int ret = 0;

	pfd.fd = jfd;
	pfd.events = events;

	ret = poll(&pfd, 1, timeout);
	if (ret && revents) {
		(*env)->SetIntField(env, revents, jclassIntField, pfd.revents);
	}
	return ret;
}

/*
 * Class:     com_tipcj_TipcBaseApi
 * Method:    tipc_pollset
 * Signature: ([I[S[Ljava/lang/Integer;I)I
 */
JNIEXPORT jint JNICALL Java_com_tipcj_TipcBaseApi_tipc_1pollset
  (JNIEnv *env, jobject obj, jintArray jfds, jshortArray jevents, jobjectArray revents, jint timeout) {
	jsize lenFds = (*env)->GetArrayLength(env, jfds);
	jsize lenEvtArr = (*env)->GetArrayLength(env, jevents);
	struct pollfd *pfd = NULL;
	int ret = 0;
	int i = 0;
	jint *jintArr = (*env)->GetIntArrayElements(env, jfds, 0);
	jshort *jshortArr = (*env)->GetShortArrayElements(env, jevents, 0);

	// Validate parameters
	if (lenEvtArr != lenFds) {
		goto releaseArr;
	}

	if (jintArr && jshortArr && lenFds >= 1) {
		pfd = malloc(lenFds * sizeof(struct pollfd));
		if (pfd == NULL) {
			goto releaseArr;
		}
		memset(pfd, 0, lenFds * sizeof(struct pollfd));
	}

	for (i = 0; i<lenFds; i++) {
		pfd[i].fd = jintArr[i];
		pfd[i].events = jshortArr[i];
		pfd[i].revents = 0;
	}

	ret = poll(pfd, lenFds, timeout);
	for (i = 0; i<lenFds; i++) {
		// return revents from pfd revents
		jobject obj = (*env)->GetObjectArrayElement(env, revents, i);
		if (obj == NULL) {
			obj = (*env)->NewObject(env, jclassInt, jclassIntNewObject, pfd[i].revents);
			if (obj != NULL) {
				(*env)->SetObjectArrayElement(env, revents, i, obj);
			}
		} else {
			(*env)->SetIntField(env, obj, jclassIntField, pfd[i].revents);
		}
	}
	free(pfd);

releaseArr:
	(*env)->ReleaseIntArrayElements(env, jfds, jintArr, 0);
	(*env)->ReleaseShortArrayElements(env, jevents, jshortArr, 0);

	return ret;
}
