// Signal to dynamically load the library
//#define ICSNEOC_DYNAMICLOAD

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <ctype.h>

// Get the PRIu64 macro for timestamps
#define __STDC_FORMAT_MACROS
#include <inttypes.h>

// Include icsneo/icsneoc.h to access library functions
#include "icsneo/icsneoc.h"

#ifdef _WIN32
	#define SLEEP(msecs) Sleep(msecs)
#elif defined (__unix__) || (defined (__APPLE__) && defined (__MACH__))
	#include <time.h>
	#define SLEEP(msecs) do { \
		struct timespec ts; \
		ts.tv_sec = msecs/1000; \
		ts.tv_nsec = msecs%1000*1000; \
		nanosleep(&ts, NULL); \
	} while (0)
#else
	#error "Platform unknown"
#endif

size_t msgLimit = 50000;
size_t numDevices = 0;
neodevice_t devices[99];
const neodevice_t* selectedDevice = NULL;

/**
 * \brief Prints all current known devices to output in the following format:
 * [num] DeviceType SerialNum    Connected: Yes/No    Online: Yes/No    Msg Polling: On/Off
 *
 * If any devices could not be described due to an error, they will appear in the following format:
 * Description for device num not available!
 */
void printAllDevices() {
	if(numDevices == 0) {
		printf("No devices found! Please scan for new devices.\n");
	}
	for(size_t i = 0; i < numDevices; i++) {
		char productDescription[ICSNEO_DEVICETYPE_LONGEST_DESCRIPTION] = { 0 };
		size_t descriptionLength = ICSNEO_DEVICETYPE_LONGEST_DESCRIPTION;

		// Updates productDescription and descriptionLength for each device
		if(icsneo_describeDevice(devices + i, productDescription, &descriptionLength)) {
			printf("[%zd] %s\tConnected: ", i + 1, productDescription);
			if(icsneo_isOpen(devices + i)) {
				printf("Yes\t");
			} else printf("No\t");

			printf("Online: ");
			if(icsneo_isOnline(devices + i)) {
				printf("Yes\t");
			} else printf("No\t");

			printf("Msg Polling: ");
			if(icsneo_isMessagePollingEnabled(devices + i)) {
				printf("On\n");
			} else printf("Off\n");

		} else {
			printf("Description for device %zd not available!\n", i + 1);
		}
	}
}

/**
 * \brief Scans for any new devices, adding them to devices and updating numDevices accordingly
 * A total of 99 devices may be stored at once
 */
size_t scanNewDevices() {
	neodevice_t newDevices[99];
	size_t numNewDevices = 99;
	icsneo_findAllDevices(newDevices, &numNewDevices);

	for(size_t i = 0; i < numNewDevices; ++i) {
		devices[numDevices + i] = newDevices[i];
	}
	numDevices += numNewDevices;
	return numNewDevices;
}
void printLastError() {
	neoevent_t error;
	if(icsneo_getLastError(&error))
		printf("Error 0x%u: %s\n", error.eventNumber, error.description);
	else
		printf("No errors found!\n");
}

/**
 * \brief Gets all current API events and prints them to output
 * Flushes the API event cache, meaning future calls (barring any new events) will not detect any further API events
 */
void printAPIEvents() {
	neoevent_t events[99];
	size_t eventCount = 99;
	if(icsneo_getEvents(events, &eventCount)) {
		if(eventCount == 1) {
			printf("1 API event found!\n");
			printf("Event 0x%u: %s\n", events[0].eventNumber, events[0].description);
		} else {
			printf("%d API events found!\n", (int) eventCount);
			for(size_t i = 0; i < eventCount; ++i) {
				printf("Event 0x%u: %s\n", events[i].eventNumber, events[i].description);
			}
		}
	} else {
		printf("Failed to get API events!\n");
	}
}

/**
 * \brief Gets all current device events and prints them to output. If no device events were found, printAPIEvents() is called
 * Flushes the device event cache, meaning future calls (barring any new events) will not detect any further device events for this device
 */
void printDeviceEvents(neodevice_t* device) {
	neoevent_t events[99];
	size_t eventCount = 99;
	if(icsneo_getDeviceEvents(device, events, &eventCount)) {
		if(eventCount == 1) {
			printf("1 device event found!\n");
			printf("Event 0x%x: %s\n", events[0].eventNumber, events[0].description);
		} else {
			printf("%d device events found!\n", (int) eventCount);
			for(size_t i = 0; i < eventCount; ++i) {
				printf("Event 0x%x: %s\n", events[i].eventNumber, events[i].description);
			}
		}
	}
}

/**
 * \brief Used to check character inputs for correctness (if they are found in an expected list)
 * \param[in] numArgs the number of possible options for the expected character
 * \param[in] ... the possible options for the expected character
 * \returns the entered character
 *
 * This function repeatedly prompts the user for input until a matching input is entered
 * Example usage: char input = getCharInput(5, 'F', 'u', 'b', 'a', 'r');
 */
char getCharInput(int numArgs, ...) {
	// 99 chars shold be more than enough to catch any typos
	char input[99];
	bool found = false;

	va_list vaList;
	va_start(vaList, numArgs);

	char* list = (char*) calloc(numArgs, sizeof(char));
	for(int i = 0; i < numArgs; ++i) {
		*(list + i) = (char)va_arg(vaList, int);
	}

	va_end(vaList);

	while(!found) {
		if(fgets(input, 99, stdin) == NULL) {
			break;
		}
		if(strlen(input) == 2) {
			for(int i = 0; i < numArgs; ++i) {
				if(input[0] == *(list + i)) {
					found = true;
					break;
				}
			}
		}

		if(!found) {
			printf("Input did not match expected options. Please try again.\n");
		}
	}

	free(list);

	return input[0];
}

/**
 * \brief Prompts the user to select a device from the list of currently known devices
 * \returns a pointer to the device in devices[] selected by the user
 * Requires an input from 1-9, so a maximum of 9 devices are supported
 */
const neodevice_t* selectDevice() {
	printf("Please select a device:\n");
	printAllDevices();
	printf("\n");

	size_t selectedDeviceNum = 10;

	while(selectedDeviceNum > numDevices) {
		char deviceSelection = getCharInput(9, '1', '2', '3', '4', '5', '6', '7', '8', '9');
		if(deviceSelection < '0') {
			printf("Selected device out of range!\n");
			continue;
		}
		selectedDeviceNum = deviceSelection - '0';
		if(selectedDeviceNum > numDevices) {
			printf("Selected device out of range!\n");
		}
	}

	printf("\n");

	return devices + selectedDeviceNum - 1;
}

int main() {
	neoversion_t ver = icsneo_getVersion();
	printf("ICS icsneoc version %u.%u.%u\n\n", ver.major, ver.minor, ver.patch);
	// Find and attempt to open device
	size_t numNewDevices = scanNewDevices();
	if(numNewDevices == 1) {
		printf("1 new device found!\n");
	} else {
		printf("%d new devices found!\n", (int) numNewDevices);
	}
	printAllDevices();
	printf("\n");

	// Select a device and get its description
	if(numDevices == 0) {
		printf("No devices found! Please scan for new devices.\n\n");
		return 1;
	}
	selectedDevice = &devices[0];

	// Get the product description for the device
	char productDescription[ICSNEO_DEVICETYPE_LONGEST_DESCRIPTION] = { 0 };
	size_t descriptionLength = ICSNEO_DEVICETYPE_LONGEST_DESCRIPTION;
	icsneo_describeDevice(selectedDevice, productDescription, &descriptionLength);

	// Attempt to open the selected device
	{
		if(icsneo_openDevice(selectedDevice)) {
			printf("%s successfully opened!\n\n", productDescription);
		} else {
			printf("%s failed to open!\n\n", productDescription);
			printLastError();
			printf("\n");
		}
	}
	// Attempt to go online
	{
		if(icsneo_goOnline(selectedDevice)) {
			printf("%s successfully went online!\n\n", productDescription);
		} else {
			printf("%s failed to go online!\n\n", productDescription);
			printLastError();
			printf("\n");
		}
	}
	// Attempt to enable message polling
	{
		if(icsneo_enableMessagePolling(selectedDevice)) {
			printf("Successfully enabled message polling for %s!\n\n", productDescription);
		} else {
			printf("Failed to enable message polling for %s!\n\n", productDescription);
			printLastError();
			printf("\n");
		}
	}
	// Send message LIN
	{
		// Start generating sample msg
		uint8_t sendMessageData[8];
		sendMessageData[0] = 0x33;
		sendMessageData[1] = 0x44;
		sendMessageData[2] = 0x55;
		sendMessageData[3] = 0x66;
		sendMessageData[4] = 0x77;
		sendMessageData[5] = 0x88;
		sendMessageData[6] = 0x88;

		neomessage_lin_t msg = {0};

		msg.header[0] = 0x11; //protected ID
		msg.header[1] = 0x11;
		msg.header[2] = 0x22;
		msg.length = 10;
		msg.netid = ICSNEO_NETID_LIN_01;
		msg.data = sendMessageData;
		msg.linStatus.txCommander = 1;
		msg.linStatus.txChecksumEnhanced = 1;
		msg.type = ICSNEO_NETWORK_TYPE_LIN;
		msg.checksum = 0x88;

		// Attempt to transmit the sample msg
		if(icsneo_transmit(selectedDevice, (const neomessage_t*) &msg)) {
			printf("Message transmit successful!\n\n");
		} else {
			printf("Failed to transmit message to %s!\n\n", productDescription);
			printLastError();
			printf("\n");
		}
	}
	// Wait for a moment
	SLEEP(1000);
	// Get messages
	{
		// Prepare the array of neomessage_t ptrs for reading in the messages
		neomessage_t* msgs = (neomessage_t*) malloc(msgLimit * sizeof(neomessage_t));

		// Get messages
		size_t msgCount = msgLimit;

		// Attempt to get messages
		if(!icsneo_getMessages(selectedDevice, msgs, &msgCount, (uint64_t) 0)) {
			printf("Failed to get messages for %s!\n\n", productDescription);
			printLastError();
			free(msgs);
			printf("\n");
		}

		if(msgCount == 1) {
			printf("1 message received from %s!\n", productDescription);
		} else {
			printf("%d messages received from %s!\n", (int) msgCount, productDescription);
		}

		// Print out the received messages
		for(size_t i = 0; i < msgCount; i++) {
			const neomessage_t* msg = &msgs[i];
			switch(msg->messageType) {
				case ICSNEO_MESSAGE_TYPE_FRAME: {
					const neomessage_frame_t* frame = (neomessage_frame_t*)msg;
					switch(frame->type) {
						case ICSNEO_NETWORK_TYPE_CAN: {
							neomessage_can_t* canMsg = (neomessage_can_t*)frame;
							printf("\t0x%03x [%zu] ", canMsg->arbid, canMsg->length);
							for(size_t j = 0; i < canMsg->length; j++) {
								printf("%02x ", canMsg->data[j]);
							}
							if(canMsg->status.transmitMessage)
								printf("TX%s %04x ", canMsg->status.globalError ? " ERR" : "", canMsg->description);
							printf("(%"PRIu64")\n", canMsg->timestamp);
							break;
						}
						case ICSNEO_NETWORK_TYPE_LIN: {
							neomessage_lin_t* linMsg = (neomessage_lin_t*)frame;
							size_t frameLen = linMsg->length;
							size_t dataLen = (frameLen > 2) ? (frameLen - 2) : 0;
							if(linMsg->netid == ICSNEO_NETID_LIN_01) {
								printf("LIN 1 | ID: 0x%02x [%zu] ", linMsg->header[0], dataLen);
							}
							else if (linMsg->netid == ICSNEO_NETID_LIN_02) {
								printf("LIN 2 | ID: 0x%02x [%zu] ", linMsg->header[0], dataLen);
							}

							for(size_t j = 0; j < dataLen; ++j) {
								if (j < 2) {
									printf("%02x ", linMsg->header[j+1]);
								} else {
									printf("%02x ", linMsg->data[j-2]);
								}
							}
							printf("| Checksum: 0x%02x\n", linMsg->checksum);
							break;
						}
					}
					break;
				}
				case ICSNEO_MESSAGE_TYPE_CAN_ERROR_COUNT: {
					const neomessage_can_error_t* cec = (neomessage_can_error_t*)msg;
					printf("\tCAN error counts changed, TEC=%d, REC=%d%s", cec->transmitErrorCount, cec->receiveErrorCount,
						cec->status.canBusOff ? " (Bus Off)" : "");
					break;
				}
			}
		}
		printf("\n");

		free(msgs);
	}
	// Attempt to disable message polling
	{
		if(icsneo_disableMessagePolling(selectedDevice)) {
			printf("Successfully disabled message polling for %s!\n\n", productDescription);
		} else {
			printf("Failed to disable message polling limit for %s!\n\n", productDescription);
			printLastError();
			printf("\n");
		}
	}
	// Attempt to go offline
	{
		if(icsneo_goOffline(selectedDevice)) {
			printf("%s successfully went offline!\n\n", productDescription);
		} else {
			printf("%s failed to go offline!\n\n", productDescription);
			printLastError();
			printf("\n");
		}
	}
	// Attempt to close the device
	{
		if(icsneo_closeDevice(selectedDevice)) {
			numDevices--;
			printf("Successfully closed %s!\n\n", productDescription);

			// Shifts everything after the removed device 1 index to the left
			bool startResizing = false;
			for(size_t i = 0; i < numDevices; ++i) {
				if(selectedDevice == devices + i)
					startResizing = true;
				if(startResizing)
					devices[i] = devices[i + 1];
			}

			selectedDevice = NULL;
		} else {
			printf("Failed to close %s!\n\n", productDescription);
			printLastError();
			printf("\n");
		}
	}
	//exit
	printf("Exiting program\n");
	return 0;
}
