#include "serial_dev.h"

int serial_dev_create(serial_dev_t *dev, const char* port, unsigned baud) {
	if (!dev) 
		return -1;
	dev->port = port;
	dev->baud = baud;
	#ifdef IS_WINDOWS
	dev->handle = INVALID_HANDLE_VALUE;
	#else
	dev->fd = -1;
	#endif
	return 0;
}

int serial_dev_open(serial_dev_t *dev) {
	fprintf(stderr, "Opening serial port %s at %u baud.\n", dev->port, dev->baud);
#ifdef IS_WINDOWS
	// Windows quirk: port = "COM10" is invalid, has to be encoded as "\\.\COM10".
	// This also works for COM below 9. So, let's give the user the ability to use
	// any "COMx" string and just prepend the "\\.\".
	char winPortName[64];
	if(dev->port[0] != '\\') {
		snprintf(winPortName, sizeof(winPortName), "\\\\.\\%s", dev->port);
	} else {
		// copy verbatim if string already starts with a '\'
		snprintf(winPortName, sizeof(winPortName), "%s", dev->port);
	}
	dev->handle = CreateFileA(winPortName, GENERIC_READ | GENERIC_WRITE, 0, 0, OPEN_EXISTING, 0,0);
	if (dev->handle == INVALID_HANDLE_VALUE) {
		if (GetLastError() == ERROR_FILE_NOT_FOUND) {
			fprintf(stderr, "Serial port %s not found.\n", dev->port);
			// weird: without this, errno = 0 (no error).
			_set_errno(ERROR_FILE_NOT_FOUND);
			return -1; // Device not found
		}
		// Error while opening the device
		return -1;
	}
	DCB dcbSerialParams;
	dcbSerialParams.DCBlength = sizeof(dcbSerialParams);
	if (!GetCommState(dev->handle, &dcbSerialParams)) {
		return -1;
	}
	// set baud and 8N1 serial formatting
	dcbSerialParams.BaudRate = dev->baud;
	dcbSerialParams.ByteSize = 8;
	dcbSerialParams.StopBits = ONESTOPBIT;
	dcbSerialParams.Parity = NOPARITY;
	// write back
	if (!SetCommState(dev->handle, &dcbSerialParams)){ 
		return -1;
	}
	// Set the timeout parameters to "no timeout" (blocking).
	// see https://learn.microsoft.com/en-us/windows/win32/api/winbase/ns-winbase-commtimeouts
	COMMTIMEOUTS timeouts;
	timeouts.ReadIntervalTimeout = 0;
	timeouts.ReadTotalTimeoutConstant = MAXDWORD;
	timeouts.ReadTotalTimeoutMultiplier = 0;
	timeouts.WriteTotalTimeoutConstant = MAXDWORD;
	timeouts.WriteTotalTimeoutMultiplier = 0;
	// Write the parameters
	if (!SetCommTimeouts(dev->handle, &timeouts)) {
		return -1;
	}
#else
	struct termios attr;
	if ((dev->fd = open(dev->port, O_RDWR | O_NOCTTY)) == -1) {
		perror("open");
		return -1;
	}

	if (tcgetattr(dev->fd, &attr) == -1) {
		perror("tcgetattr");
		return -1;
	}

	cfmakeraw(&attr);
	cfsetspeed(&attr, dev->baud);

	if (tcsetattr(dev->fd, TCSANOW, &attr) == -1) {
		perror("tcsetattr");
		return -1;
	}
#endif
	// all okay if we get here
	return 0;
}

int serial_dev_write(serial_dev_t *dev, const void* data, size_t len) {
#ifdef IS_WINDOWS
	DWORD dwBytesWritten;
	if (!WriteFile(dev->handle, data, len, &dwBytesWritten,NULL)) {
		return -1;
	}
	return (int) dwBytesWritten;
#else
	return write(dev->fd, data, len);
#endif
}

int serial_dev_read(serial_dev_t *dev, void* data, size_t len) {
#ifdef IS_WINDOWS
	DWORD dwBytesRead = 0;
	if (!ReadFile(dev->handle, data, len, &dwBytesRead, NULL)) {
		return -1;
	}
	return (int) dwBytesRead;
#else
	return read(dev->fd, data, len);
#endif
}

int serial_dev_do_dtr_reset(serial_dev_t *dev) {
#ifdef IS_WINDOWS
	// EscapeCommFunction returns 0 on fail
	if(EscapeCommFunction(dev->handle, SETDTR) == 0) {
		return -1;
	}
	if(EscapeCommFunction(dev->handle, CLRDTR) == 0) {
		return -1;
	}
#else
	int argp = TIOCM_DTR;
	// Arduino DTR reset.
	if (ioctl(dev->fd, TIOCMBIC, &argp) == -1) {
		perror("ioctl");
		return -1;
	}

	if (tcdrain(dev->fd) == -1) {
		perror("tcdrain");
		return -1;
	}

	if (ioctl(dev->fd, TIOCMBIS, &argp) == -1) {
		perror("ioctl");
		return -1;
	}
#endif
	return 0;
}

int serial_dev_flush_rx(serial_dev_t *dev) {
#ifdef IS_WINDOWS
	// PurgeComm returns 0 on fail
	if (PurgeComm(dev->handle, PURGE_RXCLEAR) == 0) {
		return -1;
	}
#else
	if (tcflush(dev->fd, TCIFLUSH) == -1) {
		perror("tcflush");
		return -1;
	}
#endif
	return 0;
}

int serial_dev_close(serial_dev_t *dev) {
#ifdef IS_WINDOWS
	if(!CloseHandle(dev->handle)) {
		return -1;
	}
	dev->handle = INVALID_HANDLE_VALUE;
#else
	int ret = 0;
	if((ret = close(dev->fd)) != 0) {
		return ret;
	}
	dev->fd = -1;
#endif
	return 0;
}