// ----------------------------------------------------------------------------
// serial.cxx - Serial I/O class
//
// Copyright (C) 2007-2010
//		Dave Freese, W1HKJ
//
// This file is part of fldigi.
//
// Fldigi 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
// (at your option) any later version.
//
// Fldigi 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 fldigi.  If not, see <http://www.gnu.org/licenses/>.
// ----------------------------------------------------------------------------

#include <config.h>

#include <string>

#include "serial.h"
#include "debug.h"

LOG_FILE_SOURCE(debug::LOG_RIGCONTROL);


#ifndef __MINGW32__
#include <cstdio>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <sys/time.h>

#include <memory>

using namespace std;

Cserial::Cserial() {
	device = "/dev/ttyS0";
	baud = 1200;
	timeout = 50; //msec
	retries = 5;
	rts = dtr = false;
	rtsptt = dtrptt = false;
	rtscts = false;
	status = 0;
	stopbits = 2;
	fd = -1;
}

Cserial::~Cserial() {
	ClosePort();
}

///////////////////////////////////////////////////////
// Function name	: Cserial::OpenPort
// Description		: Opens the port specified by strPortName
// Return type		: BOOL
// Argument		 : c_string strPortName
///////////////////////////////////////////////////////
bool Cserial::OpenPort()  {

#ifdef __CYGWIN__
	com_to_tty(device);
#endif
	if ((fd = open( device.c_str(), O_RDWR | O_NOCTTY | O_NDELAY)) < 0)
		return false;
// save current port settings
	tcflush (fd, TCIFLUSH);

	tcgetattr (fd, &oldtio);
	newtio = oldtio;

	// 8 data bits
	newtio.c_cflag &= ~CSIZE;
	newtio.c_cflag |= CS8;
	// enable receiver, set local mode
	newtio.c_cflag |= (CLOCAL | CREAD);
	// no parity
	newtio.c_cflag &= ~PARENB;

	if (stopbits == 1)
		// 1 stop bit
		newtio.c_cflag &= ~CSTOPB;
	else
		// 2 stop bit
		newtio.c_cflag |= CSTOPB;

	if (rtscts)
		// h/w handshake
		newtio.c_cflag |= CRTSCTS;
	else
		// no h/w handshake
		newtio.c_cflag &= ~CRTSCTS;

	// raw input
	newtio.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
	// raw output
	newtio.c_oflag &= ~OPOST;
	// software flow control disabled
	newtio.c_iflag &= ~IXON;
	// do not translate CR to NL
	newtio.c_iflag &= ~ICRNL;

 	switch(baud) {
 		case 300:
			speed = B300;
			break;
  		case 1200:
			speed = B1200;
			break;
  		case 2400:
			speed = B2400;
			break;
  		case 4800:
			speed = B4800;
			break;
  		case 9600:
			speed = B9600;
			break;
  		case 19200:
			speed = B19200;
			break;
  		case 38400:
			speed = B38400;
			break;
		case 57600:
			speed = B57600;
			break;
  		case 115200:
			speed = B115200;
			break;
  		default:
  			speed = B1200;
  	}
	cfsetispeed(&newtio, speed);
	cfsetospeed(&newtio, speed);

	tcsetattr (fd, TCSANOW, &newtio);

	ioctl(fd, TIOCMGET, &status);
	origstatus = status;

	if (dtr)
		status |= TIOCM_DTR; 		// set the DTR bit
	else
		status &= ~TIOCM_DTR;		// clear the DTR bit

	if (rtscts == false) {			// rts OK for ptt if RTSCTS not used
		if (rts)
			status |= TIOCM_RTS;		// set the RTS bit
		else
			status &= ~TIOCM_RTS;		// clear the RTS bit
	}
	ioctl(fd, TIOCMSET, &status);

	return true;
}

///////////////////////////////////////////////////////
// Function name	: Cserial::setPTT
// Return type	  : void
///////////////////////////////////////////////////////
void Cserial::SetPTT(bool b)
{
	if (fd < 0) {
		LOG_DEBUG("PTT fd < 0");
		return;
	}
	if (dtrptt || rtsptt) {
		ioctl(fd, TIOCMGET, &status);
		LOG_DEBUG("H/W PTT %d, status %X", b, status);
		if (b == true) {					  // ptt enabled
			if (dtrptt && dtr)  status &= ~TIOCM_DTR;	 // toggle low
			if (dtrptt && !dtr) status |= TIOCM_DTR;	  // toggle high
			if (rtscts == false) {
				if (rtsptt && rts)  status &= ~TIOCM_RTS; // toggle low
				if (rtsptt && !rts) status |= TIOCM_RTS;  // toggle high
			}
		} else {						  // ptt disabled
			if (dtrptt && dtr)  status |= TIOCM_DTR;	  // toggle high
			if (dtrptt && !dtr) status &= ~TIOCM_DTR;	 // toggle low
			if (rtscts == false) {
				if (rtsptt && rts)  status |= TIOCM_RTS;  // toggle high
				if (rtsptt && !rts) status &= ~TIOCM_RTS; // toggle low
			}
		}
		LOG_DEBUG("Status %02X, %s", status & 0xFF, uint2bin(status, 8));
		ioctl(fd, TIOCMSET, &status);
	}
	LOG_DEBUG("No PTT specified");
}

///////////////////////////////////////////////////////
// Function name	: Cserial::ClosePort
// Description		: Closes the Port
// Return type		: void
///////////////////////////////////////////////////////
void Cserial::ClosePort()
{
	if (fd < 0) return;
	LOG_DEBUG("Serial port closed, fd = %d", fd);
	ioctl(fd, TIOCMSET, &origstatus);
	tcsetattr (fd, TCSANOW, &oldtio);
	close(fd);
	fd = -1;
	return;
}

bool  Cserial::IOselect ()
{
	fd_set rfds;
	struct timeval tv;
	int retval;

	FD_ZERO (&rfds);
	FD_SET (fd, &rfds);
	tv.tv_sec = timeout/1000;
	tv.tv_usec = (timeout % 1000) * 1000;
	retval = select (FD_SETSIZE, &rfds, (fd_set *)0, (fd_set *)0, &tv);
	if (retval <= 0) // no response from serial port or error returned
		return false;
	return true;
}


///////////////////////////////////////////////////////
// Function name	: Cserial::ReadBuffer
// Description		: Reads upto nchars from the selected port
// Return type		: # characters received
// Argument		 : pointer to buffer; # chars to read
///////////////////////////////////////////////////////
int  Cserial::ReadBuffer (unsigned char *buf, int nchars)
{
	if (fd < 0) return 0;
	int retnum, nread = 0;
	while (nchars > 0) {
		if (!IOselect()) {
			return nread;
		}
		retnum = read (fd, (char *)(buf + nread), nchars);
		if (retnum < 0)
			return 0;//nread;
		if (retnum == 0)
			return nread;
		nread += retnum;
		nchars -= retnum;
	}
	return nread;
}

///////////////////////////////////////////////////////
// Function name	: Cserial::WriteBuffer
// Description		: Writes a string to the selected port
// Return type		: BOOL
// Argument		 : BYTE by
///////////////////////////////////////////////////////
int Cserial::WriteBuffer(unsigned char *buff, int n)
{
	if (fd < 0) return 0;
	int ret = write (fd, buff, n);
	return ret;
}

///////////////////////////////////////////////////////
// Function name : Cserial::FlushBuffer
// Description   : flushes the pending rx chars
// Return type   : void
///////////////////////////////////////////////////////
void Cserial::FlushBuffer()
{
	if (fd < 0)
		return;
	tcflush (fd, TCIFLUSH);
}

#else // __MINGW32__

using namespace std;

///////////////////////////////////////////////////////
// Function name	: Cserial::OpenPort
// Description		: Opens the port specified by strPortName
// Return type		: BOOL
// Argument		 : CString strPortName
///////////////////////////////////////////////////////
BOOL Cserial::OpenPort()
{
	string COMportname = "//./";

	tty_to_com(device);
	COMportname += device;

	hComm = CreateFile(COMportname.c_str(),
			  GENERIC_READ | GENERIC_WRITE,
			  0,
			  0,
			  OPEN_EXISTING,
			  0,
			  0);

	if(hComm == INVALID_HANDLE_VALUE) {
		LOG_ERROR("Invalid handle");
		return FALSE;
	}

	if (!ConfigurePort( baud, 8, FALSE, NOPARITY, stopbits)) {
		CloseHandle(hComm);
		hComm = INVALID_HANDLE_VALUE;
		LOG_ERROR("Could not configure port");
		return FALSE;
	}

	FlushBuffer();

	return TRUE;
}


///////////////////////////////////////////////////////
// Function name	: Cserial::ClosePort
// Description		: Closes the Port
// Return type		: void
///////////////////////////////////////////////////////
void Cserial::ClosePort()
{
	if (hComm) {
		bPortReady = SetCommTimeouts (hComm, &CommTimeoutsSaved);
		CloseHandle(hComm);
		hComm = INVALID_HANDLE_VALUE;
	}
	return;
}

int  Cserial::ReadData (unsigned char *buf, int nchars)
{
	if (!hComm)
	return FALSE;

	DWORD dwRead = 0;
	ReadFile(hComm, buf, nchars, &dwRead, NULL);
	return (int) dwRead;
}

BOOL Cserial::ReadByte(unsigned char & by)
{
static	BYTE byResByte[2];
	if (ReadData(byResByte, 1) == 1) {
		by = byResByte[0];
		return true;
	}
	return false;
}

void Cserial::FlushBuffer()
{
	unsigned char c;
	while (ReadByte(c) == true);
}

///////////////////////////////////////////////////////
// Function name	: Cserial::WriteByte
// Description		: Writes a Byte to teh selected port
// Return type		: BOOL
// Argument		 : BYTE by
///////////////////////////////////////////////////////
BOOL Cserial::WriteByte(UCHAR by)
{
	if (!hComm)
		return FALSE;

	nBytesWritten = 0;
	if (WriteFile(hComm,&by,1,&nBytesWritten,NULL)==0)
		return FALSE;
	return TRUE;
}

///////////////////////////////////////////////////////
// Function name	: Cserial::WriteBuffer
// Description		: Writes a string to the selected port
// Return type		: BOOL
// Argument		 : BYTE by
///////////////////////////////////////////////////////
int Cserial::WriteBuffer(unsigned char *buff, int n)
{
	if (!hComm)
		return -1;

	WriteFile (hComm, buff, n, &nBytesWritten, NULL);
	if (!nBytesWritten) {
		LOG_DEBUG("Reopening comm port");
		ClosePort();
		OpenPort();
		WriteFile (hComm, buff, n, &nBytesWritten, NULL);
	}

	return nBytesWritten;
}


///////////////////////////////////////////////////////
// Function name	: Cserial::SetCommunicationTimeouts
// Description		: Sets the timeout for the selected port
// Return type		: BOOL
// Argument		 : DWORD ReadIntervalTimeout
// Argument		 : DWORD ReadTotalTimeoutMultiplier
// Argument		 : DWORD ReadTotalTimeoutConstant
// Argument		 : DWORD WriteTotalTimeoutMultiplier
// Argument		 : DWORD WriteTotalTimeoutConstant
///////////////////////////////////////////////////////
BOOL Cserial::SetCommunicationTimeouts(
	DWORD ReadIntervalTimeout, // msec
	DWORD ReadTotalTimeoutMultiplier,
	DWORD ReadTotalTimeoutConstant,
	DWORD WriteTotalTimeoutMultiplier,
	DWORD WriteTotalTimeoutConstant
)
{
	CommTimeouts.ReadIntervalTimeout = ReadIntervalTimeout;
	CommTimeouts.ReadTotalTimeoutMultiplier = ReadTotalTimeoutMultiplier;
	CommTimeouts.ReadTotalTimeoutConstant = ReadTotalTimeoutConstant;
	CommTimeouts.WriteTotalTimeoutConstant = WriteTotalTimeoutConstant;
	CommTimeouts.WriteTotalTimeoutMultiplier = WriteTotalTimeoutMultiplier;

	LOG_DEBUG("\n\
Read Interval Timeout............... %8ld %8ld\n\
Read Total Timeout Multiplier....... %8ld %8ld\n\
Read Total Timeout Constant Timeout. %8ld %8ld\n\
Write Total Timeout Constant........ %8ld %8ld\n\
Write Total Timeout Multiplier...... %8ld %8ld",
		 CommTimeoutsSaved.ReadIntervalTimeout,
		 CommTimeouts.ReadIntervalTimeout,
		 CommTimeoutsSaved.ReadTotalTimeoutMultiplier,
		 CommTimeouts.ReadTotalTimeoutMultiplier,
		 CommTimeoutsSaved.ReadTotalTimeoutConstant,
		 CommTimeouts.ReadTotalTimeoutConstant,
		 CommTimeoutsSaved.WriteTotalTimeoutConstant,
		 CommTimeouts.WriteTotalTimeoutConstant,
		 CommTimeoutsSaved.WriteTotalTimeoutMultiplier,
		 CommTimeouts.WriteTotalTimeoutMultiplier);

	bPortReady = SetCommTimeouts (hComm, &CommTimeouts);

	if(bPortReady ==0) {
		CloseHandle(hComm);
		hComm = INVALID_HANDLE_VALUE;
		return FALSE;
	}

	return TRUE;
}

/*
 * ReadIntervalTimeout
 *
 * The maximum time allowed to elapse between the arrival of two bytes on the
 * communications line, in milliseconds. During a ReadFile operation, the time
 * period begins when the first byte is received. If the interval between the
 * arrival of any two bytes exceeds this amount, the ReadFile operation is
 * completed and any buffered data is returned. A value of zero indicates that
 * interval time-outs are not used.
 *
 * A value of MAXDWORD, combined with zero values for both the
 * ReadTotalTimeoutConstant and ReadTotalTimeoutMultiplier members, specifies
 * that the read operation is to return immediately with the bytes that have
 * already been received, even if no bytes have been received.
 *
 * ReadTotalTimeoutMultiplier
 *
 * The multiplier used to calculate the total time-out period for read
 * operations, in milliseconds. For each read operation, this value is
 * multiplied by the requested number of bytes to be read.
 *
 * ReadTotalTimeoutConstant
 *
 * A constant used to calculate the total time-out period for read operations,
 * in milliseconds. For each read operation, this value is added to the product
 * of the ReadTotalTimeoutMultiplier member and the requested number of bytes.
 *
 * A value of zero for both the ReadTotalTimeoutMultiplier and
 * ReadTotalTimeoutConstant members indicates that total time-outs are not
 * used for read operations.
 *
 * WriteTotalTimeoutMultiplier
 *
 * The multiplier used to calculate the total time-out period for write
 * operations, in milliseconds. For each write operation, this value is
 * multiplied by the number of bytes to be written.
 *
 * WriteTotalTimeoutConstant
 *
 * A constant used to calculate the total time-out period for write operations,
 * in milliseconds. For each write operation, this value is added to the product
 * of the WriteTotalTimeoutMultiplier member and the number of bytes to be
 * written.
 *
 * A value of zero for both the WriteTotalTimeoutMultiplier and
 * WriteTotalTimeoutConstant members indicates that total time-outs are not
 * used for write operations.
 *
 * Remarks
 *
 * If an application sets ReadIntervalTimeout and ReadTotalTimeoutMultiplier to
 * MAXDWORD and sets ReadTotalTimeoutConstant to a value greater than zero and
 * less than MAXDWORD, one of the following occurs when the ReadFile function
 * is called:
 *
 * If there are any bytes in the input buffer, ReadFile returns immediately
 * with the bytes in the buffer.
 *
 * If there are no bytes in the input buffer, ReadFile waits until a byte
 * arrives and then returns immediately.
 *
 * If no bytes arrive within the time specified by ReadTotalTimeoutConstant,
 * ReadFile times out.
*/
BOOL Cserial::SetCommTimeout() {
	return SetCommunicationTimeouts (
		MAXDWORD, // Read Interval Timeout
		MAXDWORD, // Read Total Timeout Multiplier
		10,       // Read Total Timeout Constant
		50,       // Write Total Timeout Constant
		5         // Write Total Timeout Multiplier
	);
  }

///////////////////////////////////////////////////////
// Function name	: ConfigurePort
// Description		: Configures the Port
// Return type		: BOOL
// Argument		 : DWORD BaudRate
// Argument		 : BYTE ByteSize
// Argument		 : DWORD fParity
// Argument		 : BYTE  Parity
// Argument		 : BYTE StopBits
///////////////////////////////////////////////////////
BOOL Cserial::ConfigurePort(DWORD	BaudRate,
				BYTE	ByteSize,
				DWORD	dwParity,
				BYTE	Parity,
				BYTE	StopBits)
{
	if((bPortReady = GetCommState(hComm, &dcb))==0) {
		LOG_ERROR("GetCommState error on %s", device.c_str());
		CloseHandle(hComm);
		hComm = INVALID_HANDLE_VALUE;
		return FALSE;
	}

	dcb.BaudRate			= BaudRate;
	dcb.ByteSize			= ByteSize;
	dcb.Parity				= Parity ;
	dcb.StopBits			= (StopBits == 1 ? ONESTOPBIT : TWOSTOPBITS);
	dcb.fBinary				= TRUE;
	dcb.fDsrSensitivity		= FALSE;
	dcb.fParity				= dwParity;
	dcb.fOutX				= FALSE;
	dcb.fInX				= FALSE;
	dcb.fNull				= FALSE;
	dcb.fAbortOnError		= TRUE;
	dcb.fOutxCtsFlow		= FALSE;
	dcb.fOutxDsrFlow		= FALSE;

	if (dtr)
		dcb.fDtrControl = DTR_CONTROL_ENABLE;
	else
		dcb.fDtrControl = DTR_CONTROL_DISABLE;

	dcb.fDsrSensitivity = FALSE;

	if (rtscts)
		dcb.fRtsControl = RTS_CONTROL_ENABLE;
	else {
		if (rts)
			dcb.fRtsControl = RTS_CONTROL_ENABLE;
		else
			dcb.fRtsControl = RTS_CONTROL_DISABLE;
	}

	bPortReady = SetCommState(hComm, &dcb);
	if(bPortReady == 0) {
		CloseHandle(hComm);
		hComm = INVALID_HANDLE_VALUE;
		LOG_ERROR("Port not available");
		return FALSE;
	}

	if ( (bPortReady = GetCommTimeouts (hComm, &CommTimeoutsSaved) ) == 0)
		return FALSE;

	return SetCommTimeout();
}

///////////////////////////////////////////////////////
// Function name	: Cserial::setPTT
// Return type	  : void
///////////////////////////////////////////////////////
void Cserial::SetPTT(bool b)
{
	if ( !(dtrptt || rtsptt) )
		return;
	if(hComm == INVALID_HANDLE_VALUE) {
		LOG_ERROR("Invalid handle");
		return;
	}
	LOG_DEBUG("PTT = %d, DTRptt = %d, DTR = %d, RTSptt = %d, RTS = %d",
		  b, dtrptt, dtr, rtsptt, rts);

	if (b == true) {				// ptt enabled
		if (dtrptt && dtr)
			dcb.fDtrControl = DTR_CONTROL_DISABLE;
		if (dtrptt && !dtr)
			dcb.fDtrControl = DTR_CONTROL_ENABLE;
		if (rtscts == false) {
			if (rtsptt && rts)
				dcb.fRtsControl = RTS_CONTROL_DISABLE;
			if (rtsptt && !rts)
				dcb.fRtsControl = RTS_CONTROL_ENABLE;
		}
	} else {						// ptt disabled
		if (dtrptt && dtr)
			dcb.fDtrControl = DTR_CONTROL_ENABLE;
		if (dtrptt && !dtr)
			dcb.fDtrControl = DTR_CONTROL_DISABLE;
		if (rtscts == false) {
			if (rtsptt && rts)
				dcb.fRtsControl = RTS_CONTROL_ENABLE;
			if (rtsptt && !rts)
				dcb.fRtsControl = RTS_CONTROL_DISABLE;
		}
	}
	SetCommState(hComm, &dcb);
}

#endif //__MINGW32__


#ifdef __WOE32__
#include <sstream>
#include "re.h"

// convert COMx to /dev/ttySy with y = x - 1
void com_to_tty(string& port)
{
	re_t re("com([0-9]+)", REG_EXTENDED | REG_ICASE);
	if (!(re.match(port.c_str()) && re.nsub() == 2))
		return;
	stringstream ss;
	int n;
	ss << re.submatch(1);
	ss >> n;
	if (--n < 0)
		n = 0;
	ss.clear(); ss.str("");
	ss << "/dev/ttyS" << n;
	ss.seekp(0);
	port = ss.str();
}

// convert  /dev/ttySx to COMy with y = x + 1
void tty_to_com(string& port)
{
	re_t re("/dev/tty.([0-9]+)", REG_EXTENDED | REG_ICASE);
	if (!(re.match(port.c_str()) && re.nsub() == 2))
		return;
	stringstream ss;
	int n;
	ss << re.submatch(1);
	ss >> n;
	ss.clear(); ss.str("");
	ss << "COM" << n + 1;
	ss.seekp(0);
	port = ss.str();
}
#endif
