/*
 * sercp - serial copy for transfering file to/from ZX Spectrum 128 AY's RS232
 * Copyright (c) 2018-2019 Pavel Vymetalek <pavel@vym.cz>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * any later version (GPL-3.0-or-later).
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

#ifdef __WIN32
	#include <stdio.h>
	#include <stdint.h>
	#include <io.h>
	#include <fcntl.h>
	#include <windows.h>
	#include <getopt.h>
	#include <signal.h>
	#include <fcntl.h>
	#include <errno.h>
	#include <unistd.h>
	#include <libgen.h>
	#include <sys/stat.h>
	#include <error.h>

#else

	#include <stdio.h>
	#include <unistd.h>
	#include <stdlib.h>
	#include <string.h>
	#include <errno.h>
	#include <stdint.h>
	#include <libgen.h>
	#include <getopt.h>
	#include <termios.h>
	#include <err.h>
	#include <sys/stat.h>
	#include <sys/ioctl.h>
	#include <sys/signal.h>
	#include <signal.h>
	#include <fcntl.h>
	#include <poll.h>
#endif


#define false 0
#define FALSE 0
#define true 1
#define TRUE 1

const char* _version = "v0.2";
// SERIAL
FILE *tapout_fd = NULL;
int is_outfile = 0;
int baud_rate = 0;
int wait_us = 200;
int wait_ms	= 800;
char *path;
char sercp_file[FILENAME_MAX];
unsigned char buff[32768];
unsigned char *p_buff;
unsigned int is_continue = 1;
unsigned int is_binary = 0;

static int inp_indx = 0;

size_t out_indx;
float width;		// width of terminal
long pos = 0;
int scp = 0;
int is_scp_read = 0;

char MODEMDEVICE[64] = {
#ifdef __WIN32
	"\\\\.\\COM1"
#else
	"/dev/ttyUSB0"
#endif
};

#ifdef __WIN32
	static HANDLE serial_fd;
#else
	int serial_fd;
	struct sigaction saterm;           /* definition of signal action */
	struct pollfd spolfd_serial[1];	  // pole descriptoru pro poll
#endif



typedef struct {
	uint16_t block_len;
	uint8_t  block_xor;
	uint8_t  block_sum;
} __attribute__((packed))  fi_sum;

typedef struct {
	uint16_t length;		// delka dat ve fileinfo
	uint8_t h_xor;			// xor bajtu fileinfa od fi_numblocks
	uint8_t h_sum;			// sum bajtu fileinfa od fi_numblocks
	uint8_t fi_numblocks;	// pocet bloku
	uint8_t fi_name[64];	// jmeno souboru - prozatim musi byt ve fromatu 8.3 - kvuli esxdosu
	uint8_t fi_date[8];		// nepouzito
	uint8_t fi_time[8];		// nepouzito
	fi_sum fi_blocks[256];	// delky a soucty bloku
} __attribute__((packed)) FILEINFO;

FILEINFO fileinfo;

void usage(void) {
	printf ("sercp %s (c)2018-2019 Pavel Vymetalek <pavel@vym.cz>\n", _version);
	printf ("serial copy for transfering file to/from ZX Spectrum 128 AY's RS232\n");
	printf ("Uses 1109bytes of fileinfo - blocks sums, filename, etc.\n");
	printf ("More info at https://vym.cz/sercp/\n");
	printf ("Usage:\nsercp [-v] [-h] -d /dev/serial [-b baud_rate] [-w time] [-r] <filename>\n");
	printf ("\t-v, --version\tShow version info\n");
	printf ("\t-h, --help\tShow this text\n");
#ifdef __WIN32
	printf ("\t-d, --device\tSerial com port\n");
#else
	printf ("\t-d, --device\tSerial communication device\n");
#endif
	printf ("\t-b, --baud\tSet the communication speed. Default 38400Bd\n");
	printf ("\t-w, --wait\tWaiting in milliseconds between transmitted blocks.\n\t\t\tDefault is -w 800 ms\n");
	printf ("\t-r, --read\tRead file from serial port\n");
}

// fileinfo 1109bytes
/************************************************************************/
void TestArgs (int argc, char *argv[])
{
	int c;
	while (1) {
		int option_index = 0;
		static struct option long_options[] = {
			{"device", required_argument, NULL, 'd'},
			{"baud", required_argument, NULL, 'b'},
			{"wait", required_argument, NULL, 'w'},
			{"read", no_argument, NULL, 'r'},
			{"version", no_argument, NULL, 'v'},
			{"help", no_argument, NULL, 'h'},
			{0, 0, 0, 0}
		};
		c = getopt_long (argc, argv, "d:b:w:rvh", long_options, &option_index);
		if (c == -1) {
			// konec parametru
			break;
		}
		switch (c) {
			case 'd':
#ifdef __WIN32
				sprintf(MODEMDEVICE, "\\\\.\\%s", optarg);
#else
				sprintf(MODEMDEVICE, "%s", optarg);
#endif
								printf ("Serial port: %s\n", MODEMDEVICE);
				break;
			case 'b':
				baud_rate = atoi(optarg);
				break;
			case 'w':
				wait_us = atoi(optarg);
				wait_ms = atoi(optarg);
				break;
			case 'r':
				is_scp_read = 1;
				break;
			case 'v':
				printf ("%s\n", _version);
				exit(1);
				break;
			case 'h':
				usage();
				exit(1);
				break;
			default:
				break;
		}
	}
	if (optind < argc) {
		while (optind < argc){
			strncpy (&sercp_file[0], argv[optind++], 63);		// input file name or path - without option switch
		}
	}
}



//*****************************************************************************
//
// uSleep win32 implementation

void uSleep(int waitTime) {
	#ifdef __WIN32
	__int64 time1 = 0, time2 = 0, freq = 0;

	QueryPerformanceCounter((LARGE_INTEGER *) &time1);
	QueryPerformanceFrequency((LARGE_INTEGER *)&freq);

	do {
		QueryPerformanceCounter((LARGE_INTEGER *) &time2);
	} while ((time2 - time1) < waitTime);
	#else
	usleep(waitTime);
	#endif
}

void sleep_ms(int milliseconds) {
#ifdef WIN32
	Sleep(milliseconds);
#else
	usleep(milliseconds * 1000);
#endif
}


/************************************************************************/
void signal_handler_sigterm (int status) {
	// CTRL+C pressed
	is_continue = 0; 	// do not continue
}

/************************************************************************/
int GetTerminalWidth(void) {
#ifdef __WIN32
	CONSOLE_SCREEN_BUFFER_INFO csbi;
	int columns;
// 	int rows;

	GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi);
	columns = csbi.srWindow.Right - csbi.srWindow.Left + 1;
// 	rows = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
	return (columns);
#else
	struct winsize termsize;
	ioctl (STDOUT_FILENO, TIOCGWINSZ, &termsize);
	return (termsize.ws_col);
#endif
}

#define PROGRESS_PERCENT	0
#define PROGRESS_COMPLETTE	1
#define PROGRESS_ERROR_CSUM	2

/************************************************************************/
void DoProgress(size_t pos, size_t max, unsigned char csum_ok) {
	width = GetTerminalWidth();
	float percent, p, m;
	int imax = width - 40;
	int ipercent;
	int px;
	char progress_char;
	p = pos;
	m = max;
	percent = 100 / m * p;
	ipercent = percent / 100 * imax;
	if (is_binary) progress_char = '#';
	else  progress_char = '=';
	printf ("Proceed bytes: %6d/%6d [", (int) pos, (int)max);
	for (px = 0; px < imax; px++) {
		if (px < ipercent) printf ("%c", progress_char);
		else printf (" ");
	}
	if (csum_ok == 0) {
		printf ("] %3d %%\r", (int)percent);
	} else if (csum_ok == 1) {
		printf ("]  OK  \r");
	} else {
		printf ("]  ERR \r");
	}
	fflush (stdout);
}

/************************************************************************/

int CheckFileInfo(FILEINFO* p_fi) {
	unsigned char *p_fiinfo = (unsigned char*)p_fi;
	uint16_t fi_len;
	uint8_t fi_xor = 0;
	uint8_t fi_sum = 0;
	fi_len = p_fi->length;
	p_fiinfo += 4;			// suma se bude pocitat od offsetu 4
	for (uint16_t indx = 0; indx < (fi_len-4); indx++) {
		fi_xor ^= *p_fiinfo;
		fi_sum += *p_fiinfo;
		p_fiinfo++;
	}
	if (fi_xor == p_fi->h_xor && fi_sum == p_fi->h_sum) {
		return 0;
	}
	return -1;
}

/************************************************************************/
void CountFileInfoChecksum(FILEINFO* p_fi) {
	unsigned char *p_fiinfo = (unsigned char*)p_fi;
	uint16_t fi_len;
	uint8_t fi_xor = 0;
	uint8_t fi_sum = 0;
	fi_len = p_fi->length;
	p_fiinfo += 4;			// suma se bude pocitat od offsetu 4
	for (uint16_t indx = 0; indx < (fi_len-4); indx++) {
		fi_xor ^= *p_fiinfo;
		fi_sum += *p_fiinfo;
		p_fiinfo++;
	}
	p_fi->h_xor = fi_xor;
	p_fi->h_sum = fi_sum;
}

/************************************************************************/
int CheckSumBlock(FILEINFO* p_fi, uint8_t block_indx, uint8_t *p_buffer) {
	uint16_t block_len;
	uint8_t b_xor = 0;
	uint8_t b_sum = 0;
	uint8_t block_xor;
	uint8_t block_sum;
	block_len = p_fi->fi_blocks[block_indx].block_len;
	block_sum = p_fi->fi_blocks[block_indx].block_sum;
	block_xor = p_fi->fi_blocks[block_indx].block_xor;
	for (uint16_t indx = 0; indx < block_len; indx++) {
		b_xor ^= *p_buffer;
		b_sum += *p_buffer;
		p_buffer++;
	}
	if (b_xor == block_xor && b_sum == block_sum) {
		return 0;	// vrat se, je to v poradku
	}
	return -1;		// vrat se s chybou bloku
}

/************************************************************************/
void CountSumBlock(FILEINFO* p_fi, uint8_t block_indx, uint8_t *p_buffer, uint16_t block_len) {
	uint8_t block_xor = 0;
	uint8_t block_sum = 0;
	for (uint16_t indx = 0; indx < block_len; indx++) {
		block_xor ^= *p_buffer;
		block_sum += *p_buffer;
		p_buffer++;
	}
	p_fi->fi_blocks[block_indx].block_len = block_len;
	p_fi->fi_blocks[block_indx].block_sum = block_sum;
	p_fi->fi_blocks[block_indx].block_xor = block_xor;
}


/************************************************************************/
uint32_t GetOverallLen(FILEINFO *p_fi) {
	uint32_t overall_len = 0;
	uint8_t block_num = p_fi->fi_numblocks;
	for (uint8_t indx = 0; indx < block_num; indx++) {
		overall_len += p_fi->fi_blocks[indx].block_len;
	}
	return overall_len;
}

/************************************************************************/
void sercpRecv(void) {
	int recv_phase = 0;			// 0 - fileinfo, 1 - blok 16kiB, 2 - posledni blok
	uint8_t block_index = 0;
	uint16_t len;
	uint16_t length = 0;
	uint16_t expected_len = 0;
// 	uint8_t expected_xor;
// 	uint8_t expected_sum;
	uint32_t overall_length = 0;		// celkova delka souboru
	uint32_t recv_length = 0;			// zatim prijatych dat ze souboru
#ifndef __WIN32
	int result;
	int _err;
#endif
	FILEINFO *p_fileinfo = &fileinfo;
	unsigned char *p_buff = buff;
	tapout_fd = NULL;
#ifdef __WIN32
	unsigned long ulNumBytes;
#endif

	memset (p_fileinfo, 0, sizeof(fileinfo));
	while (is_continue) {
		#ifndef __WIN32
		result = poll (spolfd_serial, 1, 200);		// 200ms timeout
		if (result == 0) {
			// nic neprislo
			continue;
		}
		#endif
		switch (recv_phase) {
			case 0:
				// prijem fileinfo
// 				if (spolfd_serial[0].revents & POLLIN) {
#ifdef __WIN32
					sleep_ms(10);
					ReadFile(serial_fd, p_buff,  sizeof(fileinfo), &ulNumBytes, NULL);
					len = (uint16_t) ulNumBytes;
#else
					sleep_ms(10);
					len = read (serial_fd, p_buff, sizeof(fileinfo));
#endif
					p_buff += len;
					length += len;
					if (length == sizeof(fileinfo)) {
						memcpy((unsigned char*) p_fileinfo, buff, sizeof(fileinfo));
						if (CheckFileInfo(p_fileinfo) == 0) {
// 							printf("Fileinfo received\n");
							overall_length = GetOverallLen(p_fileinfo);
							printf("File: \"%s\" number of blocks:%d, length of file: %u\n", p_fileinfo->fi_name, p_fileinfo->fi_numblocks, overall_length);
							recv_phase++;			// priste se uz prijimaji bloky
							block_index = 0;		// zacina se prvnim blokem
							p_buff = buff;			// buffer na zacatek
							length = 0;
							expected_len = p_fileinfo->fi_blocks[block_index].block_len;
							tapout_fd = fopen ((char*)p_fileinfo->fi_name, "wb");
							if (tapout_fd == NULL) {
								#ifdef __WIN32
								printf ("can't open output file");
								exit (EXIT_FAILURE);

								#else
								_err = errno;
								err (1, "can't open output file");
								#endif
							}
						} else {
							printf("Fileinfo corrupted. End...\n");
							break;
						}
						break;
					} else if (length > sizeof(fileinfo)){
#ifdef __WIN32
						ReadFile(serial_fd, p_buff,  sizeof(fileinfo), &ulNumBytes, NULL);
						len = (uint16_t)ulNumBytes;
#else

						len = read (serial_fd, p_buff, sizeof(fileinfo));
#endif
						printf("Received unknown data. End...\n");
						exit (EXIT_FAILURE);
						break;
					}
// 				}
				break;
			case 1:
				// prijem datoveho bloku - max. delka 16kiB
// 				if (spolfd_serial[0].revents & POLLIN) {
#ifdef __WIN32
					ReadFile(serial_fd, p_buff, expected_len, &ulNumBytes, NULL);
					len = (uint16_t)ulNumBytes;
					sleep_ms(10);
#else
					len = read (serial_fd, p_buff, expected_len);
					sleep_ms(10);
#endif
// 					printf ("read len: %d\n", len);
					p_buff += len;
					expected_len -= len;
					length += len;
					recv_length += len;
					DoProgress(recv_length, overall_length, PROGRESS_PERCENT);
					if (length == p_fileinfo->fi_blocks[block_index].block_len) {
						// prijaty prvni block
// 						printf("Prijaty blok c.%d delky: %d\n", block_index, length);
						if (CheckSumBlock(p_fileinfo, block_index, buff) == 0) {
							// blok je v cajku - zapsat do souboru
							if (tapout_fd) fwrite(buff, length, 1, tapout_fd);
						}
						length = 0;
						p_buff = buff;
						block_index++;
						expected_len = p_fileinfo->fi_blocks[block_index].block_len;
					}
					if (expected_len == 0 || block_index == 255) {
						printf ("\nTransfer successful\n");
						recv_phase++;
						is_continue = 0;
						break;
					}
// 				}
			}
	}
	if (tapout_fd) {
		fflush (tapout_fd);
		fclose (tapout_fd);
	}
}

/************************************************************************/
void sercpSend(void) {
	FILE *tap_fd;
	char *bname, *basec;			// pinter na kopii nazvu souboru - basename
	char *shortfilename;
	size_t fn_len;
	unsigned int  no, len;			//err,
	struct stat st;
	FILEINFO *p_fileinfo = &fileinfo;
	unsigned char *p_buff = buff;
	uint32_t file_len;
	uint8_t num_blocks = 0;
	ssize_t odeslano;
	uint32_t len_sent;
	uint16_t sent_size;
	uint32_t overall_sent;
#ifdef __WIN32
	unsigned long ulNumBytes;
#endif



	no = stat(sercp_file, &st);
	if (no != 0) {
		printf ("can't stat input file\n");
		exit (EXIT_FAILURE);
	}

	tap_fd = fopen(sercp_file, "rb");
	if (tap_fd == NULL)	{
		printf ("can't open input file\n");
		exit (EXIT_FAILURE);
	}
	if (st.st_size == 0) {
		printf ("Zero length of file. End\n");
		return;
	}
	printf ("File %s, length: %ld\n", sercp_file, st.st_size);
	memset (p_fileinfo, 0, sizeof(fileinfo));		// smazat fileinfo

	basec = strdup (sercp_file);
	bname = basename (basec);
	if (strlen(bname) > 12) {
		printf ("Short filename: ");
		shortfilename = strdup(bname);
		fn_len = strlen(shortfilename);
		memcpy(shortfilename+4, shortfilename+fn_len-8, 8);
		*(shortfilename+12) = 0;
		printf ("%s\n", shortfilename);
		bname = shortfilename;
	}

	memcpy(p_fileinfo->fi_name, bname, strlen(bname));
	file_len = (uint32_t) st.st_size;
	while (file_len) {
		len = fread(buff, 1, 16384, tap_fd);			// precti 16kiB dat
		CountSumBlock(p_fileinfo, num_blocks, buff, len);
// 		printf ("Blok c. %d, delka: %d\n", num_blocks, len);
		file_len -= len;
		num_blocks++;
	}
	p_fileinfo->fi_numblocks = num_blocks;
	p_fileinfo->length = num_blocks*4 + 85;
	CountFileInfoChecksum(p_fileinfo);
#ifdef __WIN32
	WriteFile(serial_fd, (void*)p_fileinfo, sizeof(fileinfo), &ulNumBytes, NULL);
#else
	odeslano = write (serial_fd, (void*)p_fileinfo, sizeof(fileinfo));
	tcdrain(serial_fd);
#endif
	printf("Fileinfo sent with filename: %s\n", bname);
	// TODO Cekat pauzu mezi bloky
	sleep_ms(wait_ms);

	rewind(tap_fd);				// prenaseny soubor na zacatek
	file_len = (uint32_t) st.st_size;
	overall_sent = 0;
	while (file_len && is_continue) {
		len = fread(buff, 1, 16384, tap_fd);			// precti 16kiB dat
		p_buff = buff;
		sent_size = 256;
		len_sent = len;
		while (len_sent && is_continue) {
#ifdef __WIN32
			WriteFile(serial_fd, (void*)p_buff, sent_size, &ulNumBytes, NULL);
			odeslano = (size_t)ulNumBytes;
#else
			odeslano = write (serial_fd, (void*)p_buff, sent_size);
			tcdrain(serial_fd);
#endif
			p_buff += odeslano;
			overall_sent += odeslano;
			len_sent -= odeslano;
			if (len_sent < 256) {
				sent_size = len_sent;
			}
			DoProgress(overall_sent, st.st_size, PROGRESS_PERCENT);
		}
		file_len -= len;
		if (file_len > 0) {
			sleep_ms(wait_ms);
		}
	}
	if (is_continue) {
		printf("\nFile sent...\n");
	} else {
		printf("\nend...\n");
	}

	fclose(tap_fd);
}

int OpenUart() {
#ifdef __WIN32
	DCB sDCB;
	COMMTIMEOUTS sCommTimeouts;

	serial_fd = CreateFile(MODEMDEVICE, GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, 0, NULL);
	if (serial_fd == INVALID_HANDLE_VALUE) {
		return (-1);
	}
	if (GetCommState(serial_fd, &sDCB) == 0) {
		return (-1);
	}
	if (baud_rate == 0) baud_rate = 38400;
	sDCB.BaudRate = baud_rate;
	sDCB.ByteSize = 8;
	sDCB.Parity = NOPARITY;
	sDCB.fBinary = TRUE;
	sDCB.StopBits = ONESTOPBIT;
	sDCB.fAbortOnError = TRUE;
	sDCB.fOutxDsrFlow = FALSE;
	sDCB.fDtrControl = DTR_CONTROL_DISABLE;
	sDCB.fRtsControl = RTS_CONTROL_HANDSHAKE;
	sDCB.fOutxCtsFlow = TRUE;
	if (SetCommState(serial_fd, &sDCB) == 0) {
		return (-1);
	}

	if (GetCommTimeouts(serial_fd, &sCommTimeouts) == 0) {
		return (-1);
	}
	sCommTimeouts.ReadIntervalTimeout = MAXDWORD;	//80
	sCommTimeouts.ReadTotalTimeoutConstant = 00;
	sCommTimeouts.ReadTotalTimeoutMultiplier = 00;
	sCommTimeouts.WriteTotalTimeoutConstant = 80;
	sCommTimeouts.WriteTotalTimeoutMultiplier = 80;

	if (SetCommTimeouts(serial_fd, &sCommTimeouts) == 0) {
		return (-1);
	}
	return (0);
#else
	struct termios oldtio, newtio;
	/* open the device to be non-blocking (read will return immediatly) */
	serial_fd = open(MODEMDEVICE, O_RDWR | O_NOCTTY | O_NONBLOCK);
	if (serial_fd < 0) {
		perror(MODEMDEVICE);
		return(-1);
	}
	tcgetattr(serial_fd, &oldtio); /* save current port settings */
  bzero(&newtio, sizeof (newtio));
	switch (baud_rate){
		case 115200:
			newtio.c_cflag = B115200;
			break;
		case 57600:
			newtio.c_cflag = B57600;
			break;
		case 38400:
			newtio.c_cflag = B38400;
			break;
		case 19200:
			newtio.c_cflag = B19200;
			break;
		case 9600:
			newtio.c_cflag = B9600;
			break;
		case 4800:
			newtio.c_cflag = B4800;
			break;
		case 2400:
			newtio.c_cflag = B2400;
			break;
		case 1200:
			newtio.c_cflag = B1200;
			break;
		default:
			baud_rate = 38400;
      newtio.c_cflag = B38400;
      break;
	}
  cfmakeraw(&newtio);
  cfsetspeed(&newtio, baud_rate);
	newtio.c_cflag |=  CS8 | CLOCAL | CREAD | CRTSCTS;		// CSTOPB - dva stop bity NEE
	printf ("Serial device: %s, communication speed is: %d Bd\n", MODEMDEVICE, baud_rate);
	newtio.c_cc[VMIN] = 1;
	newtio.c_cc[VTIME] = 0;
	tcsetattr(serial_fd, TCSANOW, &newtio);
	tcflush(serial_fd, TCIOFLUSH);
	return 0;
#endif
}

void CloseUart() {
	if (serial_fd) {
#ifdef __WIN32
	CloseHandle(serial_fd);
#else
		close (serial_fd);
#endif
	}
}



/************************************************************************/
/************************************************************************/
int main(int argc, char** argv, char** env)
{
#ifndef __WIN32
	// osetreni breaku ^C v unixech
	saterm.sa_handler = signal_handler_sigterm;
	saterm.sa_flags = 0;
	sigaction (SIGINT, &saterm, NULL);
#endif
	width = GetTerminalWidth();

	// nastaveni serioveho portu
	inp_indx = 0;
	if (argc < 2) {
		printf("You must specify the Serial device and file\n");
		usage();
		exit(1);
	}
	TestArgs (argc, argv);

	if (OpenUart() == -1) {
		printf ("Can't open serial port\n");
		exit (EXIT_FAILURE);
	}

	#ifndef __WIN32
	spolfd_serial[0].fd = serial_fd;		// nastaveni hlidaneho descriptoru
	spolfd_serial[0].events = POLLIN;		// hlidaji se data na vstupu
	#endif
	// serial copy activated
	if (is_scp_read) {
		sercpRecv();
	} else {
		sercpSend();
	}
	CloseUart();
	return 0;
}