#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <dirent.h>
#include <pthread.h>

#include "cecilie_reg.h"

#define DISCARD_STDIN	{for(char x; (x = getchar()) != EOF && x != '\n';){}}

#define MAX_FILE_NAME_SIZE	300
#define DEVICE_CONFIG_DEV_NAME	"/dev/xdma0_bypass"
#define DEVICE_CH0_DEV_NAME	"/dev/xdma0_c2h_0"
#define DEVICE_CH1_DEV_NAME	"/dev/xdma0_c2h_1"


#define MAP_SIZE		(4*1024)
#define RW_MAX_SIZE		0x7ffff000
#define DRV_MAX_TRANSFER_SIZE	(255*4096)
#define DISCARD_STDIN	{for(char x; (x = getchar()) != EOF && x != '\n';){}}
#define TRANSFER_BASIC_UNIT	1024

struct settings_t {
	uint32_t trigger_level;
	uint32_t trigger_on;
	uint32_t trigger_channel;
	uint32_t trigger_edge;
};


void usage()
{
	printf(	"usage: pcie_app [device_base_name <\"xdma0\">]\n");
}

void help()
{
	printf("Application Usage\n"
		"-----------------\n"
		"\t'h' - Show this help\n"
		"\t's' - Show registers\n"
		"\t'w' - Write to register\n"
		"\t'r' - Read from register\n"
		"\t't' - Transfer data\n"
		"\t'i' - Trigger settings\n"
		"\t'e' - Exit\n");
}

void print_trigger_settings(struct settings_t *set)
{
	if(!set)
		return;

	printf("Trigger settings:\n");
	printf("\tenable: %d\n", set->trigger_on);
	printf("\tchannel: %d\n", set->trigger_channel);
	printf("\tedge: %s\n", set->trigger_edge == 0 ? "rising" : "falling");
	printf("\tlevel: %d\n", set->trigger_level);

}

char get_opt()
{
	char opt;
	opt = getchar();
	while(opt == '\n'){
		opt = getchar();
	}
	while(getchar() != '\n'){}
	return opt;
}

int rw_register(uint32_t reg_addr, uint32_t write_val, uint32_t *read_val)
{
	int conf_fd = -1;
	void *reg_base = NULL;
	uint32_t *reg_ptr = NULL;
	if(!read_val)
		printf("%s reg_addr: %08x write_val: %08x read_val: %p\n", __func__, reg_addr, write_val, read_val);

	if(reg_addr % 0x4){
		printf("Unaligned address\n");
		return 1;
	}

	/* Open device */
//	printf("Device name: %s\n", DEVICE_CONFIG_DEV_NAME);
	conf_fd = open(DEVICE_CONFIG_DEV_NAME, O_RDWR | O_SYNC);
	if(conf_fd < 0){
		printf("Unable to open device - %s\n", strerror(errno));
		return 1;
	}

	/* Map physical address of BAR to process virtual address */
	reg_base = mmap(0, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, conf_fd, 0);
	if(!reg_base){
		printf("Couldn't get base address\n");
		close(conf_fd);
		return 1;
	}

	/* Get target address */
	reg_ptr = (uint32_t*)(reg_base + reg_addr);

	/* Read or write value */
	if(read_val == NULL){
		*reg_ptr = write_val;
	}else{
		*read_val = *reg_ptr;
		printf("%s reg_addr: %08x write_val: %08x read_val: %p\n", __func__, reg_addr, write_val, read_val);
	}

	/* Unmap physical address */
	if(munmap(reg_base, MAP_SIZE)){
		printf("Couldn't unmap address\n");
		return 1;
	}

	return 0;
}

#define TRANSFER_STATE_FAILED	-1
#define TRANSFER_STATE_SUCCESS	0

#define PAGE_SIZE		4096

struct transfer_t {
	void *buf;
	char *file_name;
	uint64_t size;
	int channel;
	int state;
};

void* transfer_thread(void *arg)
{
	struct transfer_t *ctx = (struct transfer_t *)arg;
	int ch_fd = -1;
	FILE *file_fd;
	uint8_t *buf = NULL;
	int tmp = 0;
	uint64_t cnt = 0;
	uint64_t read_cnt = 0;
	uint64_t write_cnt = 0;

	/* Check file name */
	if(ctx->file_name == NULL){
		printf("%s empty file name\n", __func__);
		ctx->state = TRANSFER_STATE_FAILED;
		return ctx;
	}

	/* Check channel */
	if(ctx->channel != 0 && ctx->channel != 1){
		printf("%s wrong channel %d (Should be 0 or 1)\n", __func__, ctx->channel);
		ctx->state = TRANSFER_STATE_FAILED;
		return ctx;
	}
	
	/* Check size */
	if(ctx->size <= 0 || ctx->size % TRANSFER_BASIC_UNIT){
		printf("%s wrong size %d (Should be modulo %d)\n", __func__, ctx->size, TRANSFER_BASIC_UNIT);
		ctx->state = TRANSFER_STATE_FAILED;
		return ctx;
	}

	/* Alocate memory buffer and check if it was successful */
	posix_memalign((void*)&buf, PAGE_SIZE, ctx->size);
	if(!buf){
		printf("%s Couldn't alocate memory buffer\n", __func__);
		ctx->state = TRANSFER_STATE_FAILED;
		return ctx;
	}
	
	/* Open device */
	switch(ctx->channel){
		case 0:
			ch_fd = open(DEVICE_CH0_DEV_NAME, O_RDWR);
			if(ch_fd < 0){
				printf("%d Couldn't open file device %s\n", DEVICE_CH0_DEV_NAME);
				ctx->state = TRANSFER_STATE_FAILED;
				goto memory;
			}
			break;
		case 1:
			ch_fd = open(DEVICE_CH1_DEV_NAME, O_RDWR);
			if(ch_fd < 0){
				printf("%d Couldn't open file device %s\n", DEVICE_CH1_DEV_NAME);
				ctx->state = TRANSFER_STATE_FAILED;
				goto memory;
			}
			break;
		default: 
			ctx->state = TRANSFER_STATE_FAILED;
			printf("%s unsupported channel\n", __func__);
			goto memory;
	}

	/* Open file */
	file_fd = fopen(ctx->file_name, "wb");
	if(file_fd < 0){
		printf("%s Couldn't open file for writing %s\n", __func__, ctx->file_name);
		ctx->state = TRANSFER_STATE_FAILED;
		goto device;
	}

	/* Initiate read */
	while(cnt < ctx->size){
		/* Determine nuber of bytes to read */
		read_cnt = ctx->size - cnt;
		if(read_cnt > RW_MAX_SIZE)
			read_cnt = RW_MAX_SIZE;

		/* Read */
		tmp = read(ch_fd, buf + cnt, (size_t)read_cnt);
		if(tmp < 0){
			printf("%s Read error %s\n", __func__, strerror(errno));
			ctx->state = TRANSFER_STATE_FAILED;
			goto file;
		}
		/* Update read cnt */
		cnt += (uint64_t)tmp;
	}
	
	if(cnt != ctx->size)
		goto file;
	cnt = 0;
	/* Write received data to file */
	while(cnt < ctx->size){
		/* Determine number of bytes to write */
		write_cnt = ctx->size - cnt;
		if(write_cnt > RW_MAX_SIZE)
			write_cnt = RW_MAX_SIZE;

		/* Write */
		tmp = fwrite(buf + cnt, sizeof(uint8_t), (size_t)write_cnt, file_fd);
		if(tmp < 0){
			printf("%s Failed to write to file\n", __func__);
			goto file;
		}
		cnt += tmp;
	}

	ctx->state = TRANSFER_STATE_SUCCESS;
	fclose(file_fd);
	close(ch_fd);
	free(buf);

	return ctx;

file: 
	fclose(file_fd);

device:
	close(ch_fd);

memory:
	free(buf);
	
	return ctx;
}



int transfer_one_channel(int channel, uint64_t transfer_size, char fname[], struct settings_t *trigger_settings)
{
	int conf_fd = -1;
	pthread_t ch_tid;
	struct transfer_t ch_transfer = {
		.file_name = fname,
		.size = transfer_size,
		.buf = NULL,
		.channel = channel,
	};
	uint32_t reg_val;
	/* Check if channel is available */
	if(channel != 0 && channel != 1){
		printf("%s Unsupported channel %d\n", __func__, channel);
		return 1;
	}
	
	/* Check size */
	if(transfer_size <= 0 || transfer_size % TRANSFER_BASIC_UNIT){
		printf("%s wrong size %d (Should be modulo %d)\n", __func__, transfer_size, TRANSFER_BASIC_UNIT);
		return 1;
	}

	/* Clear overflow on desired channel before starting transfer */
	if(channel == 0){
		if(rw_register(CEC_REG_CH0_CONTROL, 0x2, NULL)){
			printf("%s couldn't write to cecilie register\n", __func__);
			return 1;
		}	
		if(rw_register(CEC_REG_CH0_CONTROL, 0x0, NULL)){
			printf("%s couldn't write to cecilie register\n", __func__);
			return 1;
		}
	}else{
		if(rw_register(CEC_REG_CH1_CONTROL, 0x2, NULL)){
			printf("%s Couldn't write to Cecilie register\n", __func__);
			return 1;
		}
		if(rw_register(CEC_REG_CH1_CONTROL, 0x0, NULL)){
			printf("%s Couldn't write to Cecilie register\n", __func__);
			return 1;
		}
	}

	/* Setup trigger if enabled */
	if(trigger_settings->trigger_on){
		reg_val = CEC_TRIG_EN(reg_val);
		reg_val = CEC_TRIG_LEVEL(reg_val, trigger_settings->trigger_level);
		
		if(trigger_settings->trigger_channel == 0){
			reg_val = CEC_TRIG_CHANNEL0(reg_val);
		}else{
			reg_val = CEC_TRIG_CHANNEL1(reg_val);
		}

		if(trigger_settings->trigger_edge == 0){
			reg_val = CEC_TRIG_RISING_EDGE(reg_val);
		}else{
			reg_val = CEC_TRIG_FALLING_EDGE(reg_val);
		}
	}
	
	if(rw_register(CEC_REG_COMMON_CONTROL, reg_val, NULL)){
		printf("%s Couldn't write to Cecilie register\n", __func__);
		return 1;
	}

	/* Create transfer threads */
	if(pthread_create(&ch_tid, NULL, transfer_thread, &ch_transfer)){
		printf("%s Failed to create thread\n", __func__);
		return 1;
	}
	pthread_join(ch_tid, NULL);

	if(ch_transfer.state == TRANSFER_STATE_FAILED){
		printf("%s Transfer FAILED\n", __func__);
	}else{
		printf("%s Transfer SUCCEED\n", __func__);
	}

	/* Clear overflow on desired channel when transfer is done */
	if(channel == 0){
		if(rw_register(CEC_REG_CH0_CONTROL, 0x2, NULL)){
			printf("%s couldn't write to cecilie register\n", __func__);
			return 1;
		}	
		if(rw_register(CEC_REG_CH0_CONTROL, 0x0, NULL)){
			printf("%s couldn't write to cecilie register\n", __func__);
			return 1;
		}
	}else{
		if(rw_register(CEC_REG_CH1_CONTROL, 0x2, NULL)){
			printf("%s Couldn't write to Cecilie register\n", __func__);
			return 1;
		}
		if(rw_register(CEC_REG_CH1_CONTROL, 0x0, NULL)){
			printf("%s Couldn't write to Cecilie register\n", __func__);
			return 1;
		}
	}

	return 0;
}

int transfer_both_channel(uint64_t transfer_size, char fname0[], char fname1[], struct settings_t *trigger_settings)
{
	int conf_fd = -1;
	pthread_t ch0_tid, ch1_tid;
	struct transfer_t ch0_transfer = {
		.file_name = fname0,
		.size = transfer_size,
		.buf = NULL,
		.channel = 0,
	};
	struct transfer_t ch1_transfer = {
		.file_name = fname1,
		.size = transfer_size,
		.buf = NULL,
		.channel = 1,
	};
	uint32_t reg_val = 0;

	/* Check size */
	if(transfer_size <= 0 || transfer_size % TRANSFER_BASIC_UNIT){
		printf("%s wrong size %d (Should be modulo %d)\n", __func__, transfer_size, TRANSFER_BASIC_UNIT);
		return 1;
	}

	/* Clear both channels */
	if(rw_register(CEC_REG_COMMON_CONTROL, 0x1 | 0x04, NULL)){
		printf("%s Couldn't write to Cecilie register\n", __func__);
		return 1;
	}

	/* Synchronize channels and setup trigger if enabled */
	if(trigger_settings->trigger_on){
		reg_val = CEC_SYNC_EN(reg_val);
		reg_val = CEC_TRIG_EN(reg_val);
		reg_val = CEC_TRIG_LEVEL(reg_val, trigger_settings->trigger_level);
	
		if(trigger_settings->trigger_channel == 0){
			reg_val = CEC_TRIG_CHANNEL0(reg_val);
		}else{
			reg_val = CEC_TRIG_CHANNEL1(reg_val);
		}

		if(trigger_settings->trigger_edge == 0){
			reg_val = CEC_TRIG_RISING_EDGE(reg_val);
		}else{
			reg_val = CEC_TRIG_FALLING_EDGE(reg_val);
		}

	}else{
		reg_val = CEC_SYNC_EN(reg_val);	
	}	

	if(rw_register(CEC_REG_COMMON_CONTROL, reg_val, NULL)){
		printf("%s Couldn't write to Cecilie register\n", __func__);
		return 1;
	}

	/* Create threads fot channel 0 */
	if(pthread_create(&ch0_tid, NULL, transfer_thread, &ch0_transfer)){
		printf("%s Failed to create thread\n");
		return 1;
	}

	/* Create threads fot channel 1 */
	if(pthread_create(&ch1_tid, NULL, transfer_thread, &ch1_transfer)){
		printf("%s Failed to create thread\n");
		return 1;
	}

	/* Waint until both channels are done */
	pthread_join(ch0_tid, NULL);
	pthread_join(ch1_tid, NULL);

	if(ch0_transfer.state == TRANSFER_STATE_FAILED){
		printf("%s Transfer on channel 0 FAILED\n", __func__);
	}else{
		printf("%s Transfer on channel 0 SUCCEED\n", __func__);
	}

	if(ch1_transfer.state == TRANSFER_STATE_FAILED){
		printf("%s Transfer on channel 1 FAILED\n", __func__);
	}else{
		printf("%s Transfer on channel 1 SUCCEED\n", __func__);
	}

	/* Clear common control register */
	if(rw_register(CEC_REG_COMMON_CONTROL, 0, NULL)){
		printf("%s Couldn't write to Cecilie register\n", __func__);
		return 1;
	}

	return 0;
}

int transfer_menu(struct settings_t *trigger_settings)
{
	char opt = '\n';
	uint64_t transfer_size = 0;
	char name[100];
	char fname_ch0[MAX_FILE_NAME_SIZE];
	char fname_ch1[MAX_FILE_NAME_SIZE];
	DIR* dir = NULL;

	if(!trigger_settings){
		printf("%s Trigger settings is NULL\n",  __func__);
		return 1;
	}

	memset(name, '\0', sizeof(name));
	memset(fname_ch0, '\0', sizeof(fname_ch0));
	memset(fname_ch1, '\0', sizeof(fname_ch1));

	printf("Transfer size (modulo %d, max %d): ", TRANSFER_BASIC_UNIT, DRV_MAX_TRANSFER_SIZE);
	if(scanf("%lu", &transfer_size) != 1){
		DISCARD_STDIN;
		printf("Wrong input\n");
		return 1;
	}
	
	/* Check size */
	if(transfer_size <= 0 || transfer_size % TRANSFER_BASIC_UNIT){
		printf("%s wrong size %d (Should be modulo %d)\n", __func__, transfer_size, TRANSFER_BASIC_UNIT);
		return 1;
	}
	
	if(transfer_size > DRV_MAX_TRANSFER_SIZE){
		printf("Transfer size larger that %d, setting to maximum\n", DRV_MAX_TRANSFER_SIZE);
		transfer_size = DRV_MAX_TRANSFER_SIZE;
	}


	/* Get result file name */
	printf("Results file name: ");
	if(scanf("%99s", name) != 1){
		DISCARD_STDIN;
		printf("Wrong input!\n");
		return 1;
	}

	dir = opendir("./results");
	if (dir) {
		closedir(dir);
	} else{
		printf("Directory ./results doesn't exist ... creating\n");
		mkdir("./results", ACCESSPERMS);
	}

	snprintf(fname_ch0, sizeof(fname_ch0), "./results/%s_ch0", name);
	snprintf(fname_ch1, sizeof(fname_ch1), "./results/%s_ch1", name);

	printf("Choose channel (0 - fist, 1 - second, b - both): ");
	opt = get_opt();
	switch(opt){
		case '0':
			if(transfer_one_channel(0, transfer_size, fname_ch0, trigger_settings))
				return 1;
			break;
		case '1':
			if(transfer_one_channel(1, transfer_size, fname_ch1, trigger_settings))
				return 1;
			break;
		case 'b':
			if(transfer_both_channel(transfer_size, fname_ch0, fname_ch1, trigger_settings))
				return 1;
			break;
		default:
			printf("Invalid option\n");
			return 1;
	}
	
	return 0;

}

int check_devices_exists()
{
	int ret = 0;
	if(access(DEVICE_CONFIG_DEV_NAME, F_OK)){
		ret++;
		printf("Configuration device %s doesn't exist\n", DEVICE_CONFIG_DEV_NAME);
	}

	if(access(DEVICE_CH0_DEV_NAME, F_OK)){
		ret++;
		printf("Channel device %s doesn't exist\n", DEVICE_CH0_DEV_NAME);
	}

	if(access(DEVICE_CH1_DEV_NAME, F_OK)){
		ret++;
		printf("Channel device %s doesn't exist\n", DEVICE_CH1_DEV_NAME);
	}
}

int main(int argc, char *argv[])
{
	int nexit = 1;
	char opt = '\n';
	uint32_t reg_addr;
	uint32_t reg_val;
	uint64_t transfer_size; 
	uint32_t val =  0;
	struct settings_t trigger_settings = {0, 0, 0, 0};

	/* Let user know about this application */
	printf("\n*** Cecilie PCI Express application ***\n\n");
	registers(); printf("\n");
	help(); printf("\n");

	printf("Checking if devices exists ...\n");
	if(check_devices_exists()){
		printf("One or more devices doesn't exists, exiting ...\n\n");
		return 1;
	}
	printf("OK\n\n");

	while(nexit){
		printf("Choose option ('h' for help): ");
		opt = get_opt();
		switch(opt){
			case 'h':
				help();
				break;
			case 's':
				registers();
				break;
			case 'w':
				printf("Register address: ");
				if(scanf("%x", &reg_addr) != 1){
					DISCARD_STDIN;
					printf("Wrong input\n");
					break;
				}
				printf("Register value: ");
				if(scanf("%x", &reg_val) != 1){
					DISCARD_STDIN;
					printf("Wrong input\n");
					break;
				}
				if(rw_register(reg_addr, reg_val, NULL))
					printf("Successful write\n");
				break;
			case 'r':
				printf("Register address: ");
				if(scanf("%x", &reg_addr) != 1){
					DISCARD_STDIN;
					printf("Wrong input\n");
					break;
				}
				if(rw_register(reg_addr, 0, &reg_val) == 0)
					printf("Register: %08x, Value: %08x\n", reg_addr, reg_val);
				break;
			case 't':
				transfer_menu(&trigger_settings);
				break;
			case 'i':
				printf("Enable trigger? (0 off / 1 on): ");
				opt = get_opt();
				switch(opt){
					case '0':
						printf("Disabling trigger\n");
						trigger_settings.trigger_on = 0;
						break;
					case '1': 
						printf("Enabling trigger\n");
						trigger_settings.trigger_on = 1;
						break;
					default:
						printf("Invalid option - setting off\n");
						break;
				}
				
				if(trigger_settings.trigger_on == 0)
					break;
				
				printf("Trigger channel (0 or 1 - default 0): ");
				opt = get_opt();
				switch(opt){
					case '0':
						trigger_settings.trigger_channel = 0;
						break;
					case '1': 
						trigger_settings.trigger_channel = 1;
						break;
					default:
						printf("Channel %c doesn't exist - setting default\n", opt);
						break;
				}
					
				printf("Trigger edge ('r' or 'f' - default 'r'): ");
				opt = get_opt();
				switch(opt){
					case 'r':
						trigger_settings.trigger_edge = 0;
						break;
					case 'f': 
						trigger_settings.trigger_edge = 1;
						break;
					default:
						printf("Option %c doesn't exist - setting default\n", opt);
						break;
				}
					
				printf("Trigger level (0 to 16383 - default 8191): ");
				if(scanf("%d", &trigger_settings.trigger_level) != 1){
					DISCARD_STDIN;
					printf("Invalid option - setting default\n");
					trigger_settings.trigger_level = 8192;
				}
				
				if(trigger_settings.trigger_level > 16383){
					printf("Invalid option - setting default\n");
					trigger_settings.trigger_level = 8192;
				}

				print_trigger_settings(&trigger_settings);
				break;

			case 'e':
				printf("Exiting..\n");
				nexit = 0;
				break;
			default:
				printf("Invalid option\n");
				break;
		}
	
		printf("\n");
		
	}
	
	return 0;
}










