/*
 *	RadAngel gamma spectrometer USB HID device control and readout program
 *
 *	gcc -std=gnu99 -Wall radangel.c -o radangel -pthread -lm -lhidapi-libusb
 *
 *      strip -s radangel
 *
 *	echo 'SUBSYSTEM=="usb", ATTR{idVendor}=="04d8", ATTR{idProduct}=="0100", MODE="0666"' | sudo tee /etc/udev/rules.d/99-kromek-radangel.rules
 *
 */


#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include <pthread.h>
#include <string.h>
#include <getopt.h>
#include <sys/time.h>
#include <hidapi/hidapi.h>
#include <unistd.h> // usleep()


#define MAX_STR 255
#define MAX_CHANNEL 4096
#define QUEUESIZE 1024
#define DEV_VID 0x4d8
#define DEV_PID 0x100
#define LINE_MAX 4096

// non blocking
#define HID_TIMEOUT -1
// 500 ms timeout
//#define HID_TIMEOUT 500


typedef struct {
	struct timespec start_time;
} config;

typedef struct {
	FILE* file;
	char* name;
} output;

typedef struct {
	hid_device* handle;
	config config;
	output event;
	output stats;
} context;

typedef struct {
	struct timespec event_time;
	unsigned short channel;
} data;

typedef struct {
	data data[QUEUESIZE];
	long head, tail;
	int full, empty;
	pthread_mutex_t *mutex;
	pthread_cond_t *not_full, *not_empty;
	context context;
} queue;

void usage(char* name);
void info(const char* fmt, ...);
void timespec_diff(struct timespec *start, struct timespec *stop, struct timespec *result);
queue *queue_init(void);
void queue_free(queue *q);
void queue_add(queue *q, data src);
void queue_del(queue *q, data *dst);
void *event_source(void *args);
void *event_sink(void *args);
//void *update_context(void *args);


static int verbose_flag; // Flag set by -v | --verbose


void usage(char* name) {
        info("%s [<arguments>]\n\n", name);
	info("\t-h          | --help             Usage message.\n");
        info("\t-v          | --verbose          Verbose output.\n");
	info("\t-e <string> | --event  <string>  Event data file path.\n");
	info("\t-s <string> | --stats  <string>  Stats data file path.\n");
	info("\n");
}


int main(int argc, char **argv) {
	queue *fifo = NULL;
	pthread_t pro, con;

	struct timespec start_time = { 0, 0 };
	int c;
	int option_index = 0;

	fifo = queue_init();
	if (fifo ==  NULL) {
		info("Error: main queue_init() failed.\n");
		exit(1);
	}
	fifo->context.event.name = "-";
	fifo->context.event.file = stdout;
	fifo->context.stats.name = NULL;
	fifo->context.stats.file = NULL;

	while (1) {
		static struct option long_options[] = {
			{"verbose",	no_argument, 		&verbose_flag,	1  },
			{"help", 	no_argument,		NULL, 		'h'},
			{"event",	required_argument, 	NULL,		'e'},
			{"stats",	required_argument, 	NULL,		's'},
			{0, 		0, 			0, 		0  } // end of options
		};
		option_index = 0;
		c = getopt_long(argc, argv, "he:s:", long_options, &option_index);
		if (c == -1) { // end of options
			break;
		}
		switch (c) {
			case 0:
				if (long_options[option_index].flag != 0) { // option sets a flag
					break;
				}
				info("option %s", long_options[option_index].name);
				if (optarg) {
					info(" with arg %s", optarg);
				}
				info("\n");
			break;
			case 'e':
				fifo->context.event.name = optarg;
			break;
			case 's':
				fifo->context.stats.name = optarg;
			break;
			case 'h':
				usage(argv[0]);
				queue_free(fifo);
				exit(1);
			break;
			case '?': // getopt_long already printed an error message.
			break;
			default:
				abort();
		}
	}
	if (optind < argc) { // remaining command line arguments (not options).
		info("unknown argument: ");
		while (optind < argc) {
			info("%s ", argv[optind++]);
		}
		info("\n");
		queue_free(fifo);
		exit(1);
	}


	// ensure input
 	if (hid_init()) {
		info("Error: unable to initialize hidapi.\n");
		queue_free(fifo);
		exit(1);
	}
	// open device later in read thread, might not be connected yet
	// measurement start
	clock_gettime(CLOCK_MONOTONIC, &start_time);
	fifo->context.config.start_time = start_time;

	// start the producer and consumer threads
	pthread_create(&pro, NULL, event_source, fifo);
	pthread_create(&con, NULL, event_sink, fifo);
	pthread_join(pro, NULL);
	pthread_join(con, NULL);

	if (fifo->context.event.file != NULL) {
		fclose(fifo->context.event.file);
	}
	if (fifo->context.stats.file != NULL) {
		fclose(fifo->context.stats.file);
	}
	if (fifo->context.handle != NULL) {
		hid_close(fifo->context.handle);
	}
	hid_exit(); // Free static HIDAPI objects
	queue_free(fifo);

	return 0;
}


void info(const char* fmt, ...) {
	va_list args;
	va_start(args, fmt);
	if (verbose_flag) {
		fprintf(stderr, fmt, args);
	}
	va_end(args);
}


queue *queue_init(void) {
	queue *q;

	q = (queue *) malloc (sizeof (queue));
	if (q == NULL) return (NULL);

	q->empty = 1;
	q->full = 0;
	q->head = 0;
	q->tail = 0;

	q->mutex = (pthread_mutex_t *) malloc (sizeof (pthread_mutex_t));
	pthread_mutex_init(q->mutex, NULL);

	q->not_full = (pthread_cond_t *) malloc (sizeof (pthread_cond_t));
	pthread_cond_init(q->not_full, NULL);

	q->not_empty = (pthread_cond_t *) malloc (sizeof (pthread_cond_t));
	pthread_cond_init(q->not_empty, NULL);

	return (q);
}


void queue_free(queue *q) {
	pthread_mutex_destroy(q->mutex);
	free(q->mutex);

	pthread_cond_destroy(q->not_full);
	free(q->not_full);

	pthread_cond_destroy(q->not_empty);
	free(q->not_empty);

	free(q);
}


void queue_add(queue *q, data src) {
	q->data[q->tail] = src;
	q->tail++;

	if (q->tail == QUEUESIZE) {
		q->tail = 0;
	}

	if (q->tail == q->head) {
		q->full = 1;
	}

	q->empty = 0;

	return;
}


void queue_del(queue *q, data *dst) {
	*dst = q->data[q->head];
	q->head++;

	if (q->head == QUEUESIZE) {
		q->head = 0;
	}

	if (q->head == q->tail) {
		q->empty = 1;
	}

	q->full = 0;

	return;
}


void *event_source(void *q) {
	queue *fifo;
	fifo = (queue *)q;
	data data;

	hid_device* handle = fifo->context.handle;
	unsigned char buffer[64];
	int result;

	struct timespec event_time = { 0, 0L };
	struct timespec sleep_time = { 0, 500000000L}; // retry time

	do { // read events loop
		while (handle == NULL) { // try to aquire handle
			handle = hid_open(DEV_VID, DEV_PID, NULL);
			if (handle != NULL) {
				fifo->context.handle = handle;
			} else {
				nanosleep(&sleep_time, NULL); // wait to retry
			}
		}
		while (handle != NULL) { // try to read data
			memset(buffer, 0, sizeof(buffer));
			result = hid_read_timeout(handle, buffer, sizeof(buffer), HID_TIMEOUT);
			if (result > 0) { // got some bytes
				break;
			} else if (result < 0) { // something went wrong: disconnect?
				hid_close(handle);
				handle = NULL; // invalidate handle for reaquire
				fifo->context.handle = handle;
			}
		}

		clock_gettime(CLOCK_MONOTONIC, &event_time);

		if (buffer[0] == 4) { // got an event
			data.channel = (((buffer[1] << 8) | buffer[2]) >> 4);
			data.event_time = event_time;

			// lock the queue, wait for space, push
			pthread_mutex_lock(fifo->mutex);
			while (fifo->full) { // queue is full, wait for consumer
				pthread_cond_wait(fifo->not_full, fifo->mutex); // unlocks, block, relock
			}
			queue_add(fifo, data);
			pthread_mutex_unlock(fifo->mutex);
			pthread_cond_signal(fifo->not_empty);
		}

	} while (1); // read events loop

	return (NULL);
}


void timespec_diff(struct timespec *start, struct timespec *stop, struct timespec *result) {
    if ((stop->tv_nsec - start->tv_nsec) < 0) {
        result->tv_sec = stop->tv_sec - start->tv_sec - 1;
        result->tv_nsec = stop->tv_nsec - start->tv_nsec + 1000000000;
    } else {
        result->tv_sec = stop->tv_sec - start->tv_sec;
        result->tv_nsec = stop->tv_nsec - start->tv_nsec;
    }
    return;
}


void *event_sink(void *q) {
	queue *fifo;
	fifo = (queue *)q;
	data data;

	char *events_filename = fifo->context.event.name;
	char *stats_filename  = fifo->context.stats.name;
	char *events_filemode = "a+";
	char *stats_filemode  = "w";
	FILE *events_file = events_filename ? ((strncmp(events_filename, "-", 1) == 0) ? stdout : fopen(events_filename, events_filemode)) : NULL;
	FILE *stats_file  = stats_filename  ? ((strncmp(stats_filename,  "-", 1) == 0) ? stdout : fopen(stats_filename,  stats_filemode )) : NULL;
	fifo->context.event.file = events_file;
	fifo->context.stats.file = stats_file;

	struct timespec start_time = fifo->context.config.start_time;
	struct timespec event_time = { 0, 0L };
	struct timespec sleep_time = { 0, 500000000L}; // retry time
	long int tv_sec;
	unsigned long int tv_nsec;

	unsigned int  channel = 0;
	unsigned long histogram[MAX_CHANNEL];
	memset(histogram, 0, MAX_CHANNEL * sizeof(unsigned long));

	const char event_header[] = "time\tchannel\n";
	const char event_format[] = "%ld.%03ld\t%d\n";
	const char stats_header[] = "channel\n";
	const char stats_format[] = "%ld\n";
	char line[LINE_MAX];

	int i, result = 1;

	// reconstruct event_time and histogram
	if (events_filename && events_file && (events_file != stdout)) {
		rewind(events_file);
		while (fgets(line, LINE_MAX, events_file) != NULL) {
			if(sscanf(line, event_format, &tv_sec, &tv_nsec, &channel) == 3) {
				event_time.tv_sec = tv_sec;
				event_time.tv_nsec = tv_nsec;
				histogram[channel % MAX_CHANNEL] += 1; // update histogram
			}
		}
	}
	if (events_filename && events_file && ((events_file == stdout) || (ftell(events_file) == 0))) {
		fprintf(events_file, event_header);
	}

	// readjust start_time
	timespec_diff(&event_time, &start_time, &start_time);
	fifo->context.config.start_time = start_time;


	if (stats_file && stats_filename) {
		stats_file  = (strncmp(stats_filename,  "-", 1) == 0) ? stdout : freopen(stats_filename,  stats_filemode, stats_file);
		fifo->context.stats.file = stats_file;
		result = !!stats_file;
		if (stats_file) {
			result = fprintf(stats_file, stats_header);
			for (i = 0; i < MAX_CHANNEL; i++) {
				result = result && (fprintf(stats_file, stats_format,
					histogram[i]
				) > 0);
			}
			fflush(stats_file);
		}
	}


	do { // output loop
		// lock the queue, wait for data, pull
		pthread_mutex_lock(fifo->mutex);
		while (fifo->empty) { // queue is empty, wait for producer
			pthread_cond_wait(fifo->not_empty, fifo->mutex); // unlock, block, relock
		}
		queue_del(fifo, &data);
		pthread_mutex_unlock (fifo->mutex);
		pthread_cond_signal(fifo->not_full);

		// event time since measurement start
		event_time = data.event_time;
		channel = data.channel;
		timespec_diff(&start_time, &event_time, &event_time);

		// update histogram
		histogram[channel % MAX_CHANNEL] += 1;

		if (events_file) {
			result = (fprintf(events_file, event_format,
				event_time.tv_sec,
				event_time.tv_nsec / 1000000,
				channel
			) > 0);
			fflush(events_file);
		} else if (events_filename) {// try reopen
			nanosleep(&sleep_time, NULL); // wait to retry
			events_file = (strncmp(events_filename, "-", 1) == 0) ? stdout : freopen(events_filename, events_filemode, events_file);
			fifo->context.event.file = events_file;
			result = !!events_file;
		}

		if (stats_file && stats_filename) {
			stats_file  = (strncmp(stats_filename,  "-", 1) == 0) ? stdout : freopen(stats_filename,  stats_filemode, stats_file);
			fifo->context.stats.file = stats_file;
			result = !!stats_file;
			if (stats_file) {
				for (i = 0; i < MAX_CHANNEL; i++) {
					result = result && (fprintf(stats_file, stats_format,
						histogram[i]
					) > 0);
				}
				fflush(stats_file);
			}
		}

	} while (result); // output loop

	return (NULL);
}


/*
void *update_context (void *q) {
	queue *fifo;
	fifo = (queue *)q;

	return (NULL);
}
*/

