/*
* For C-standard/POSIX/OS specific/Custom/JNI errors, this function is called. It sets a pointer which is checked
* by java method when native function returns. If the pointer is set exception of class as set by this function is
* thrown.
*
* The type 1 indicates standard (C-standard/POSIX) error, 2 indicate custom (defined by this library)
* error, 3 indicates custom error with message string, 4 indicates error number specific to Windows OS.
*/
void throw_serialcom_exception(JNIEnv *env, int type, int error_code, const char *msg) {

	DWORD ret = -1;
	errno_t err_code = 0;
	char buffer[256];
	jclass serialComExceptionClass = NULL;

	(*env)->ExceptionClear(env);
	serialComExceptionClass = (*env)->FindClass(env, SCOMEXPCLASS);
	if ((serialComExceptionClass == NULL) || ((*env)->ExceptionOccurred(env) != NULL)) {
		(*env)->ExceptionClear(env);
		LOGE(E_FINDCLASSSCOMEXPSTR, FAILTHOWEXP);
		return;
	}

	switch (type) {
	case 4:
		/* Caller has given Windows error code */
		memset(buffer, '\0', sizeof(buffer));
		ret = FormatMessageA(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
			NULL, error_code, MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US),
			(LPSTR)&buffer, sizeof(buffer), NULL);
		if (ret == 0) {
			LOGEN(FAILTHOWEXP, "FormatMessageA()", GetLastError());
		}
		ret = (*env)->ThrowNew(env, serialComExceptionClass, buffer);
		if (ret < 0) {
			LOGE(FAILTHOWEXP, buffer);
		}
		break;
	case 3:
		/* Caller has given exception message explicitly */
		ret = (*env)->ThrowNew(env, serialComExceptionClass, msg);
		if (ret < 0) {
			LOGE(FAILTHOWEXP, msg);
		}
		break;
	case 2:
		/* Caller has given custom error code, need to get exception message corresponding to this code. */
		switch (error_code) {
		default: _snprintf_s(buffer, 256, 256, "%s\0", E_UNKNOWN);
		}
		ret = (*env)->ThrowNew(env, serialComExceptionClass, buffer);
		if (ret < 0) {
			LOGE(FAILTHOWEXP, buffer);
		}
		break;
	case 1:
		/* Caller has given posix error code, get error message corresponding to this code. */
		return;
	case 5:
		/* Caller has given <errno.h> error code for windows, get error message corresponding to this code. */
		memset(buffer, '\0', sizeof(buffer));
		err_code = strerror_s(buffer, sizeof(buffer), error_code);
		if (err_code != 0) {
			LOGEN(FAILTHOWEXP, "strerror_s()", err_code);
		}
	}
}
/* This thread wait for both data and control event both to occur on the specified port. When data is received on port or a control event has
 * occurred, it enqueue this to data or event to corresponding queue. Separate blocking queue for data and events are managed by java layer. */
unsigned WINAPI event_data_looper(void* arg) {

	int ret = 0;
	int error_count = 0;
	BOOL result = FALSE;
	DWORD error_type = 0;
	DWORD errorVal = 0;
	DWORD events_mask = 0;
	DWORD mask_applied = 0;
	DWORD num_of_bytes_read = 0;
	OVERLAPPED overlapped;
	BOOL eventOccurred = FALSE;
	DWORD dwEvent;
	
	int CTS =  0x01;  // 0000001
	int DSR =  0x02;  // 0000010
	int DCD =  0x04;  // 0000100
	int RI  =  0x08;  // 0001000
	int lines_status = 0;
	int cts, dsr, dcd, ri = 0;
	int event = 0;
	
	void* env1;
	JNIEnv* env;
	jbyte data_buf[1024];
	jbyteArray data_read;
	jclass SerialComLooper = NULL;
	jmethodID data_mid = NULL;
	jmethodID mide = NULL;
	jmethodID event_mid = NULL;

	struct looper_thread_params* params = (struct looper_thread_params*) arg;
	JavaVM *jvm = (*params).jvm;
	HANDLE hComm = (*params).hComm;
	jobject looper = (*params).looper;
	int data_enabled = (*params).data_enabled;
	int event_enabled = (*params).event_enabled;

	/* The JNIEnv is valid only in the current thread. So, threads created should attach 
	 * itself to the VM and obtain a JNI interface pointer. */
	if((*jvm)->AttachCurrentThread(jvm, &env1, NULL) != JNI_OK) {
		((struct looper_thread_params*) arg)->custom_err_code = E_ATTACHCURRENTTHREAD;
		((struct looper_thread_params*) arg)->init_done = 2;
		return 0;
	}
	env = (JNIEnv*) env1;

	/* Local references are valid for the duration of a native method call.
	   They are freed automatically after the native method returns.
	   Local references are only valid in the thread in which they are created.
	   The native code must not pass local references from one thread to another if required. */
	SerialComLooper = (*env)->GetObjectClass(env, looper);
	if((SerialComLooper == NULL) || ((*env)->ExceptionOccurred(env) != NULL)) {
		((struct looper_thread_params*) arg)->custom_err_code = E_GETOBJECTCLASS;
		((struct looper_thread_params*) arg)->init_done = 2;
		(*jvm)->DetachCurrentThread(jvm);
		return 0;   /* For unrecoverable errors we would like to exit and try again. */
	}
	
	event_mid = (*env)->GetMethodID(env, SerialComLooper, "insertInEventQueue", "(I)V");
	if ((event_mid == NULL) || ((*env)->ExceptionOccurred(env) != NULL)) {
		((struct looper_thread_params*) arg)->custom_err_code = E_GETMETHODID;
		((struct looper_thread_params*) arg)->init_done = 2;
		(*jvm)->DetachCurrentThread(jvm);
		return 0;
	}
	
	data_mid = (*env)->GetMethodID(env, SerialComLooper, "insertInDataQueue", "([B)V");
	if ((data_mid == NULL) || ((*env)->ExceptionOccurred(env) != NULL)) {
		((struct looper_thread_params*) arg)->custom_err_code = E_GETMETHODID;
		((struct looper_thread_params*) arg)->init_done = 2;
		(*jvm)->DetachCurrentThread(jvm);
		return 0;
	}
	
	mide = (*env)->GetMethodID(env, SerialComLooper, "insertInDataErrorQueue", "(I)V");
	if((mide == NULL) || ((*env)->ExceptionOccurred(env) != NULL)) {
		((struct looper_thread_params*) arg)->custom_err_code = E_GETMETHODID;
		((struct looper_thread_params*) arg)->init_done = 2;
		(*jvm)->DetachCurrentThread(jvm);
		return 0;
	}

	/* Set the event mask this thread will wait for. */
	if(data_enabled == 1) {
		/* A character was received and placed in the input buffer. */
		mask_applied = mask_applied | EV_RXCHAR;
	}
	if(event_enabled == 1) {
		mask_applied = mask_applied | EV_BREAK | EV_CTS | EV_ERR | EV_RING | EV_RXFLAG | EV_DSR | EV_RLSD;
	}

	ret = SetCommMask(hComm, mask_applied);
	if(ret == 0) {
		((struct looper_thread_params*) arg)->standard_err_code = GetLastError();
		((struct looper_thread_params*) arg)->init_done = 2;
		(*jvm)->DetachCurrentThread(jvm);
		return 0;
	}

	((struct looper_thread_params*) arg)->wait_event_handles[0] = CreateEvent(NULL, FALSE, FALSE, NULL);
	if(((struct looper_thread_params*) arg)->wait_event_handles[0] == NULL) {
		((struct looper_thread_params*) arg)->standard_err_code = GetLastError();
		((struct looper_thread_params*) arg)->init_done = 2;
		(*jvm)->DetachCurrentThread(jvm);
		return 0;
	}

	/* indicate success to the caller so it can return success to java layer */
	((struct looper_thread_params*) arg)->init_done = 0;
	ret = SetEvent(((struct looper_thread_params*) arg)->init_done_event_handle);
	if (ret == 0) {
		((struct looper_thread_params*) arg)->standard_err_code = GetLastError();
		((struct looper_thread_params*) arg)->init_done = 2;
		(*jvm)->DetachCurrentThread(jvm);
		return 0;
	}

	/* This keep looping forever until listener is unregistered, waiting for data or 
	 * event and passing it to java layer which put it in the queue. */
	while(1) {
	
		eventOccurred = FALSE;

		/* The OVERLAPPED structure is used by the kernel to store progress of the operation. 
		 * Only hEvent member need to be initialled and others can be left 0. The OVERLAPPED 
		 * structure must contain a handle to a manual-reset event object.  */
		memset(&overlapped, 0, sizeof(overlapped));
		overlapped.hEvent = CreateEvent(NULL, FALSE, FALSE, NULL);   /* auto reset, unnamed event object */
		if(overlapped.hEvent == NULL) {
			LOGEN("CreateEvent()", "event_data_looper() failed creating overlapped event handle with error number : ", GetLastError());
			continue;
		}

		((struct looper_thread_params*) arg)->wait_event_handles[1] = overlapped.hEvent;

		/* If the overlapped operation cannot be completed immediately, the function returns FALSE 
		 * and the GetLastError function returns ERROR_IO_PENDING, indicating that the operation is 
		 * executing in the background. When this happens, the system sets the hEvent member of the 
		 * OVERLAPPED structure to the not-signaled state before WaitCommEvent returns, and then it 
		 * sets it to the signaled state when one of the specified events or an error occurs. */
		ret = WaitCommEvent(hComm, &events_mask, &overlapped);
		if(ret == 0) {
			errorVal = GetLastError();
			if(errorVal == ERROR_IO_PENDING) {
				error_count = 0;  /* reset error_count */
				dwEvent = WaitForMultipleObjects(2, ((struct looper_thread_params*) arg)->wait_event_handles, FALSE, INFINITE);
				switch (dwEvent) {
					case WAIT_OBJECT_0 + 0:
						/* Thread is asked to exit. */
						if(1 == ((struct looper_thread_params*) arg)->thread_exit) {							
							(*jvm)->DetachCurrentThread(jvm);
							CloseHandle(overlapped.hEvent);
							return 0;
						}
						break;
					case WAIT_OBJECT_0 + 1:
						/* Some event on serial port has happened. */
						eventOccurred = TRUE;
						break;
					case WAIT_FAILED:
						LOGEN("event_data_looper()", "Unexpected WAIT_FAILED in WaitForMultipleObjects() with error code : ", GetLastError());
						break;
					default:
						LOGEN("event_data_looper()", "Unexpected WAIT_FAILED in WaitForMultipleObjects() with error code : ", GetLastError());
				}
			}else {
				if(((struct looper_thread_params*) arg)->data_enabled == 1) {
					error_count++;
					if(error_count > 25) {
						(*env)->CallVoidMethod(env, looper, mide, errorVal);
						if((*env)->ExceptionOccurred(env)) {
							LOGEN("event_data_looper()", "WaitCommEvent() failed with error code : ", errorVal);
						}
						error_count = 0; /* reset error_count */
					}
				}
				continue;
			}
		}else {
			/* Thread is asked to exit. */
			if(((struct looper_thread_params*) arg)->thread_exit == 1) {
				(*jvm)->DetachCurrentThread(jvm);
				CloseHandle(overlapped.hEvent);
				return 0;
			}
			/* WaitCommEvent tells an event occured in one shot. */
			eventOccurred = TRUE;
		}

		CloseHandle(overlapped.hEvent);

		/* Check it is data or control event and enqueue in appropriate queue in java layer with the help of java method. */
		if(eventOccurred == TRUE) {
			if(events_mask & EV_RXCHAR) {
				/* A data event has occured and application has registered listener for data also, so send data to application. */
				memset(&overlapped, 0, sizeof(overlapped));
				overlapped.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
				if(overlapped.hEvent != NULL) {
					result = ReadFile(hComm, data_buf, sizeof(data_buf), &num_of_bytes_read, &overlapped);
					if(result == TRUE) {
						if(num_of_bytes_read > 0) {
							data_read = (*env)->NewByteArray(env, num_of_bytes_read);
							(*env)->SetByteArrayRegion(env, data_read, 0, num_of_bytes_read, data_buf);
							(*env)->CallVoidMethod(env, looper, data_mid, data_read);
							if((*env)->ExceptionOccurred(env)) {
								LOGE("event_data_looper()", "JNI call CallVoidMethod() failed");
							}
						}
					}else {
						errorVal = GetLastError();
						if(errorVal == ERROR_IO_PENDING) {
							if(WaitForSingleObject(overlapped.hEvent, INFINITE) == WAIT_OBJECT_0) {
								if(GetOverlappedResult(hComm, &overlapped, &num_of_bytes_read, FALSE)) {
									if(num_of_bytes_read > 0) {
										data_read = (*env)->NewByteArray(env, num_of_bytes_read);
										(*env)->SetByteArrayRegion(env, data_read, 0, num_of_bytes_read, data_buf);
										(*env)->CallVoidMethod(env, looper, data_mid, data_read);
										if((*env)->ExceptionOccurred(env)) {
											LOGE("event_data_looper()", "JNI call CallVoidMethod() failed");
										}
									}
								}
							}
						}else {
							LOGEN("event_data_looper()", "ReadFile() failed with error code : ", errorVal);
						}
					}

					CloseHandle(overlapped.hEvent);
				}
			}
			
			if((events_mask & EV_CTS) || (events_mask & EV_DSR) || (events_mask & EV_RLSD) || (events_mask & EV_RING)) {
					/* waitcommevent says control event has occured, so we get it. */
					lines_status = 0;
					cts = 0;
					dsr = 0;
					dcd = 0;
					ri = 0;
					event = 0;

					ret = GetCommModemStatus(hComm, &lines_status);
					if(ret == 0) {
						LOGEN("event_data_looper()", "GetCommModemStatus() failed with error code : ", GetLastError());
						continue;
					}

					cts = (lines_status & MS_CTS_ON)  ? 1 : 0;
					dsr = (lines_status & MS_DSR_ON)  ? 1 : 0;
					dcd = (lines_status & MS_RLSD_ON) ? 1 : 0;
					ri  = (lines_status & MS_RING_ON) ? 1 : 0;

					if(cts) {
						event = event | CTS;
					}
					if(dsr) {
						event = event | DSR;
					}
					if(dcd) {
						event = event | DCD;
					}
					if(ri) {
						event = event | RI;
					}

					if(cts || dsr || dcd || ri) {
						/* It is control event(s), so enqueue it in event queue. */
						/* if(DBG) fprintf(stderr, "%s %d\n", "NATIVE event_data_looper() sending bit mapped events ", event);
						   if(DBG) fflush(stderr); */
						(*env)->CallVoidMethod(env, looper, event_mid, event);
						if((*env)->ExceptionOccurred(env)) {
							LOGE("event_data_looper()", "JNI call CallVoidMethod() failed");
						}
					}
			}
		}
	} /* Go back to loop again waiting for a control event or data event to occur using WAITCOMMEVENT(). */

	return 0;
}
/* This thread keep polling for the physical existence of a port/file/device. When port removal is detected, this
* informs java listener and exit. Associate the handler with a class, that class with a window and register that
* window with notification system. */
unsigned __stdcall usb_device_hotplug_monitor(void *arg) {

	int i = 0;
	int ret = 0;
	BOOL result = FALSE;
	DWORD errorVal = 0;
	MSG msg;
	ATOM atom;
	HDEVNOTIFY notification_handle = NULL;
	DEV_BROADCAST_DEVICEINTERFACE dev_broadcast_iface;
	HWND window_handle;
	WNDCLASSEX wndClass = { 0 };
	HINSTANCE hInstance;
	TCHAR classname_buf[64];

	struct usb_dev_monitor_info* ptr = (struct usb_dev_monitor_info*) arg;
	jmethodID onUSBHotPlugEventMethodID = NULL;
	jclass usbHotPlugEventListenerClass = NULL;
	jobject usbHotPlugEventListenerObj = (*ptr).usbHotPlugEventListener;
	JavaVM *jvm = (*ptr).jvm;
	JNIEnv* env = NULL;
	void* env1 = NULL;

	EnterCriticalSection(((struct usb_dev_monitor_info*) arg)->csmutex);

	/* USB device (excluding hub and host controller) */
	const GUID GUID_DEVINTERFACE_USB_DEVICE = { 0xA5DCBF10, 0x6530, 0x11D2, 0x90, 0x1F, 0x00, 0xC0, 0x4F, 0xB9, 0x51, 0xED };

	if ((*jvm)->AttachCurrentThread(jvm, &env1, NULL) != JNI_OK) {
		((struct  usb_dev_monitor_info*) arg)->custom_err_code = E_ATTACHCURRENTTHREAD;
		((struct usb_dev_monitor_info*) arg)->init_done = 2;
		LeaveCriticalSection(((struct usb_dev_monitor_info*) arg)->csmutex);
		return 0;
	}
	env = (JNIEnv*)env1;

	usbHotPlugEventListenerClass = (*env)->GetObjectClass(env, usbHotPlugEventListenerObj);
	if ((usbHotPlugEventListenerClass == NULL) || ((*env)->ExceptionOccurred(env) != NULL)) {
		((struct usb_dev_monitor_info*) arg)->custom_err_code = E_GETOBJECTCLASS;
		((struct usb_dev_monitor_info*) arg)->init_done = 2;
		(*jvm)->DetachCurrentThread(jvm);
		LeaveCriticalSection(((struct usb_dev_monitor_info*) arg)->csmutex);
		/* For unrecoverable errors we would like to exit and try registering again. */
		return 0;
	}

	onUSBHotPlugEventMethodID = (*env)->GetMethodID(env, usbHotPlugEventListenerClass, "onUSBHotPlugEvent", "(I)V");
	if ((onUSBHotPlugEventMethodID == NULL) || ((*env)->ExceptionOccurred(env) != NULL)) {
		((struct usb_dev_monitor_info*) arg)->custom_err_code = E_GETMETHODID;
		((struct usb_dev_monitor_info*) arg)->init_done = 2;
		(*jvm)->DetachCurrentThread(jvm);
		LeaveCriticalSection(((struct usb_dev_monitor_info*) arg)->csmutex);
		return 0;
	}

	/* Registers a window class for subsequent use in calls to the CreateWindow or CreateWindowEx function.  */
	memset(classname_buf, '\0', 64);
	_stprintf_s(classname_buf, 64, TEXT("SCM USB : %p"), ((struct usb_dev_monitor_info*) arg)->thread_handle);
	hInstance = (HINSTANCE)GetModuleHandle(NULL);
	wndClass.cbSize = sizeof(WNDCLASSEX);
	wndClass.style = 0;
	wndClass.lpfnWndProc = (WNDPROC)usb_hotplug_event_handler;
	wndClass.cbClsExtra = 0;
	wndClass.cbWndExtra = 0;
	wndClass.hInstance = hInstance;
	wndClass.hIcon = NULL;
	wndClass.hCursor = NULL;
	wndClass.hbrBackground = NULL;
	wndClass.lpszMenuName = NULL;
	wndClass.lpszClassName = classname_buf;
	wndClass.hIconSm = NULL;
	atom = RegisterClassEx(&wndClass);
	if (atom == 0) {
		/* should not happen, just log it for later analysis */
		LOGEN("RegisterClassEx() failed in ", "usb_device_hotplug_monitor() with Windows error code : ", GetLastError());
	}

	/* Create message only window.  Windows will deliver messages to this window. */
	window_handle = CreateWindowEx(WS_EX_TOPMOST, classname_buf, TEXT("scm usb hot plug event thread window"), 0,
									0, 0, 0, 0, HWND_MESSAGE, 0, 0, 0);
	if (window_handle == NULL) {
		UnregisterClass(wndClass.lpszClassName, hInstance);
		((struct usb_dev_monitor_info*) arg)->standard_err_code = GetLastError();
		((struct usb_dev_monitor_info*) arg)->init_done = 2;
		(*jvm)->DetachCurrentThread(jvm);
		LeaveCriticalSection(((struct usb_dev_monitor_info*) arg)->csmutex);
		return 0;
	}

	/* Register with the system to receive device notifications. */
	SecureZeroMemory(&dev_broadcast_iface, sizeof(dev_broadcast_iface));
	dev_broadcast_iface.dbcc_size = sizeof(dev_broadcast_iface);
	dev_broadcast_iface.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
	dev_broadcast_iface.dbcc_classguid = GUID_DEVINTERFACE_USB_DEVICE;
	notification_handle = RegisterDeviceNotification(window_handle,                /* events recipient window */
													&dev_broadcast_iface,          /* type of device for which notification will be sent */
													DEVICE_NOTIFY_WINDOW_HANDLE);  /* type of recipient handle */
	if (notification_handle == NULL) {
		DestroyWindow(window_handle);
		UnregisterClass(wndClass.lpszClassName, hInstance);
		((struct usb_dev_monitor_info*) arg)->standard_err_code = GetLastError();
		((struct usb_dev_monitor_info*) arg)->init_done = 2;
		(*jvm)->DetachCurrentThread(jvm);
		LeaveCriticalSection(((struct usb_dev_monitor_info*) arg)->csmutex);
		return 0;
	}

	/* Save so that event callback function can use them. */
	usbport_monitor_info_ptr = ((struct usb_dev_monitor_info*) arg)->info;
	((struct usb_dev_monitor_info*) arg)->env = env;
	((struct usb_dev_monitor_info*) arg)->onUSBHotPlugEventMethodID = onUSBHotPlugEventMethodID;
	((struct usb_dev_monitor_info*) arg)->window_handle = window_handle;

	/* indicate success to the caller so it can return success to java layer */
	((struct usb_dev_monitor_info*) arg)->init_done = 0;
	LeaveCriticalSection(((struct usb_dev_monitor_info*) arg)->csmutex);

	/* message loop */
	while (1) {
		/* block until there is a message in the queue. */
		result = GetMessage(&msg, NULL, 0, 0);
		if (result > 0) {
			if (((struct usb_dev_monitor_info*) arg)->thread_exit == 1) {
				/* application wish to unregister usb hot plug listener, get out of loop and exit thread */
				break;
			}
			TranslateMessage(&msg);
			DispatchMessage(&msg);
		}else if (result == 0) {
			/* WM_QUIT received, get out of loop and exit thread */
			break;
		}else {
			/* should not happen, just log it for later analysis */
			LOGEN("GetMessage() failed in ", "usb_device_hotplug_monitor() with Windows error code : %d", GetLastError());
		}
	}

	UnregisterDeviceNotification(notification_handle);
	DestroyWindow(window_handle);
	UnregisterClass(wndClass.lpszClassName, hInstance);
	(*jvm)->DetachCurrentThread(jvm);
	return 0;
}