/*
  Hatari - rs232.c

  This file is distributed under the GNU General Public License, version 2
  or at your option any later version. Read the file gpl.txt for details.

  RS-232 Communications

  This is similar to the printing functions, we open a direct file
  (e.g. /dev/ttyS0) and send bytes over it.
  Using such method mimicks the ST exactly, and even allows us to connect
  to an actual ST! To wait for incoming data, we create a thread which copies
  the bytes into an input buffer. This method fits in with the internet code
  which also reads data into a buffer.
*/
const char RS232_fileid[] = "Hatari rs232.c : " __DATE__ " " __TIME__;

#include <config.h>

#if HAVE_TERMIOS_H
# include <termios.h>
# include <unistd.h>
#endif

#include <SDL.h>
#include <SDL_thread.h>
#include <errno.h>

#include "main.h"
#include "configuration.h"
#include "ioMem.h"
#include "m68000.h"
#include "mfp.h"
#include "rs232.h"


#define RS232_DEBUG 0

#if RS232_DEBUG
#define Dprintf(a) printf a
#else
#define Dprintf(a)
#endif


static FILE *hComIn = NULL;        /* Handle to file for reading */
static FILE *hComOut = NULL;       /* Handle to file for writing */

static unsigned char InputBuffer_RS232[MAX_RS232INPUT_BUFFER];
static int InputBuffer_Head=0, InputBuffer_Tail=0;
static volatile bool bQuitThread = false;

#if HAVE_TERMIOS_H

#if !HAVE_CFMAKERAW
static inline void cfmakeraw(struct termios *termios_p)
{
	termios_p->c_iflag &= ~(IGNBRK|BRKINT|PARMRK|ISTRIP|INLCR|IGNCR|ICRNL|IXON);
	termios_p->c_oflag &= ~OPOST;
	termios_p->c_lflag &= ~(ECHO|ECHONL|ICANON|ISIG|IEXTEN);
	termios_p->c_cflag &= ~(CSIZE|PARENB);
	termios_p->c_cflag |= CS8;
}
#endif


#if defined(__AMIGAOS4__)

// dummy functions. REMOVE THEM LATER

int tcgetattr(int file_descriptor,struct termios *tios_p)
{
	return -1;
}

int tcsetattr(int file_descriptor,int action,struct termios *tios_p)
{
	return -1;
}

int cfsetospeed(struct termios *tios, speed_t ospeed)
{

	tios->c_ospeed = ospeed;

	return 0;
}


int cfsetispeed(struct termios *tios,speed_t ispeed)
{
	tios->c_ispeed = ispeed;

	return 0;
}

#endif  /* __AMIGAOS4__ */


/*-----------------------------------------------------------------------*/
/**
 * Set serial line parameters to "raw" mode.
 */
static bool RS232_SetRawMode(FILE *fhndl)
{
	struct termios termmode;
	int fd;

	memset (&termmode, 0, sizeof(termmode));    /* Init with zeroes */
	fd = fileno(fhndl);                         /* Get file descriptor */

	if (isatty(fd))
	{
		if (tcgetattr(fd, &termmode) != 0)
			return false;

		/* Set "raw" mode: */
		termmode.c_cc[VMIN] = 1;
		termmode.c_cc[VTIME] = 0;
		cfmakeraw(&termmode);
		if (tcsetattr(fd, TCSADRAIN, &termmode) != 0)
			return false;
	}

	return true;
}

/*-----------------------------------------------------------------------*/
/**
 * Set hardware configuration of RS-232:
 * - Bits per character
 * - Parity
 * - Start/stop bits
 */
static bool RS232_SetBitsConfig(FILE *fhndl, int nCharSize, int nStopBits, bool bUseParity, bool bEvenParity)
{
	struct termios termmode;
	int fd;

	memset (&termmode, 0, sizeof(termmode));    /* Init with zeroes */
	fd = fileno(fhndl);

	if (isatty(fd))
	{
		if (tcgetattr(fd, &termmode) != 0)
		{
			Dprintf(("RS232_SetBitsConfig: tcgetattr failed.\n"));
			return false;
		}

		/* Set the character size: */
		termmode.c_cflag &= ~CSIZE;
		switch (nCharSize)
		{
		 case 8: termmode.c_cflag |= CS8; break;
		 case 7: termmode.c_cflag |= CS7; break;
		 case 6: termmode.c_cflag |= CS6; break;
		 case 5: termmode.c_cflag |= CS5; break;
		}

		/* Set stop bits: */
		if (nStopBits >= 2)
			termmode.c_oflag |= CSTOPB;
		else
			termmode.c_oflag &= ~CSTOPB;

		/* Parity bit: */
		if (bUseParity)
			termmode.c_cflag |= PARENB;
		else
			termmode.c_cflag &= ~PARENB;

		if (bEvenParity)
			termmode.c_cflag &= ~PARODD;
		else
			termmode.c_cflag |= PARODD;

		/* Now store the configuration: */
		if (tcsetattr(fd, TCSADRAIN, &termmode) != 0)
		{
			Dprintf(("RS232_SetBitsConfig: tcsetattr failed.\n"));
			return false;
		}
	}

	return true;
}

#endif /* HAVE_TERMIOS_H */


/*-----------------------------------------------------------------------*/
/**
 * Open file on COM port.
 */
static bool RS232_OpenCOMPort(void)
{
	bool ok = true;

	if (!hComOut && ConfigureParams.RS232.szOutFileName[0])
	{
		/* Create our COM file for output */
		hComOut = fopen(ConfigureParams.RS232.szOutFileName, "wb");
		if (hComOut)
		{
			setvbuf(hComOut, NULL, _IONBF, 0);
#if HAVE_TERMIOS_H
			/* First set the output parameters to "raw" mode */
			if (!RS232_SetRawMode(hComOut))
			{
				Log_Printf(LOG_WARN, "Can't set raw mode for %s\n",
					   ConfigureParams.RS232.szOutFileName);
			}
#endif
			Dprintf(("Successfully opened RS232 output file.\n"));
		}
		else
		{
			Log_Printf(LOG_WARN, "RS232: Failed to open output file %s\n",
				   ConfigureParams.RS232.szOutFileName);
			ok = false;
		}
	}

	if (!hComIn && ConfigureParams.RS232.szInFileName[0])
	{
		/* Create our COM file for input */
		hComIn = fopen(ConfigureParams.RS232.szInFileName, "rb");
		if (hComIn)
		{
			setvbuf(hComIn, NULL, _IONBF, 0);
#if HAVE_TERMIOS_H
			/* Now set the input parameters to "raw" mode */
			if (!RS232_SetRawMode(hComIn))
			{
				Log_Printf(LOG_WARN, "Can't set raw mode for %s\n",
					   ConfigureParams.RS232.szInFileName);
			}
#endif
			Dprintf(("Successfully opened RS232 input file.\n"));
		}
		else
		{
			Log_Printf(LOG_WARN, "RS232: Failed to open input file %s\n",
				   ConfigureParams.RS232.szInFileName);
			ok = false;
		}
	}
	return ok;
}


/*-----------------------------------------------------------------------*/
/**
 * Close file on COM port
 */
static void RS232_CloseCOMPort(void)
{
	if (hComIn)
	{
		/* Close */
		fclose(hComIn);
		hComIn = NULL;
	}
	if (hComOut)
	{
		fclose(hComOut);
		hComOut = NULL;
	}
	Dprintf(("Closed RS232 files.\n"));
}


/* thread stuff */
static SDL_sem* pSemFreeBuf;       /* Semaphore to sync free space in InputBuffer_RS232 */
static SDL_Thread *RS232Thread = NULL; /* Thread handle for reading incoming data */


/*-----------------------------------------------------------------------*/
/**
 * Add incoming bytes from other machine into our input buffer
 */
static void RS232_AddBytesToInputBuffer(unsigned char *pBytes, int nBytes)
{
	int i;

	/* Copy bytes into input buffer */
	for (i=0; i<nBytes; i++)
	{
		SDL_SemWait(pSemFreeBuf);    /* Wait for free space in buffer */
		InputBuffer_RS232[InputBuffer_Tail] = *pBytes++;
		InputBuffer_Tail = (InputBuffer_Tail+1) % MAX_RS232INPUT_BUFFER;
	}
}


/*-----------------------------------------------------------------------*/
/**
 * Thread to read incoming RS-232 data, and pass to emulator input buffer
 */
static int RS232_ThreadFunc(void *pData)
{
	int iInChar;
	unsigned char cInChar;

	/* Check for any RS-232 incoming data */
	while (!bQuitThread)
	{
		if (hComIn)
		{
			/* Read the bytes in, if we have any */
			iInChar = fgetc(hComIn);
			if (iInChar != EOF)
			{
				/* Copy into our internal queue */
				cInChar = iInChar;
				RS232_AddBytesToInputBuffer(&cInChar, 1);
				/* FIXME: Use semaphores to lock MFP variables? */
				MFP_InputOnChannel ( MFP_INT_RCV_BUF_FULL , 0 );
				Dprintf(("RS232: Read character $%x\n", iInChar));
				/* Sleep for a while */
				SDL_Delay(2);
			}
			else
			{
				/*Dprintf(("RS232: Reached end of input file!\n"));*/
				/* potential data race on hComIn modification */
				clearerr(hComIn);
				SDL_Delay(20);
			}
		}
		else
		{
			/* No RS-232 connection, sleep for 0.2s */
			SDL_Delay(200);
		}
	}

	return true;
}


/*-----------------------------------------------------------------------*/
/**
 * Initialize RS-232, start thread to wait for incoming data
 * (we will open a connection when first bytes are sent even
 *  if RS-232 isn't initialized for reading).
 */
void RS232_Init(void)
{
	if (ConfigureParams.RS232.bEnableRS232)
	{
		if (!RS232_OpenCOMPort())
		{
			RS232_CloseCOMPort();
			Log_AlertDlg(LOG_ERROR, "RS232 input or output file open failed. RS232 support disabled.");
			ConfigureParams.RS232.bEnableRS232 = false;
			return;
		}
	}
	if (hComIn)
	{
		/* Create semaphore */
		if (pSemFreeBuf == NULL)
			pSemFreeBuf = SDL_CreateSemaphore(MAX_RS232INPUT_BUFFER);
		if (pSemFreeBuf == NULL)
		{
			RS232_CloseCOMPort();
			Log_Printf(LOG_WARN, "RS232_Init: Can't create semaphore!\n");
			return;
		}

		/* Create thread to wait for incoming bytes over RS-232 */
		if (!RS232Thread)
		{
			bQuitThread = false;
#if WITH_SDL2
			RS232Thread = SDL_CreateThread(RS232_ThreadFunc, "rs232", NULL);
#else
			RS232Thread = SDL_CreateThread(RS232_ThreadFunc, NULL);
#endif
			Dprintf(("RS232 thread has been created.\n"));
		}
	}
}


/*-----------------------------------------------------------------------*/
/**
 * Close RS-232 connection and stop checking for incoming data.
 */
void RS232_UnInit(void)
{
	/* Close, kill thread and free resource */
	if (RS232Thread)
	{
		/* Instead of killing the thread directly, we should
		 * probably better inform it via IPC so that it can
		 * terminate gracefully... but then we would need to
		 * wait until it exits, otherwise there's a data race
		 * on accessing/modifying hComIn.
		 */
		Dprintf(("Killing RS232 thread...\n"));
		bQuitThread = true;
#if !WITH_SDL2
		SDL_KillThread(RS232Thread);
#endif
		RS232Thread = NULL;
	}
	RS232_CloseCOMPort();

	if (pSemFreeBuf)
	{
		SDL_DestroySemaphore(pSemFreeBuf);
		pSemFreeBuf = NULL;
	}
}


/*-----------------------------------------------------------------------*/
/**
 * Set hardware configuration of RS-232 according to the USART control register.
 *
 * ucr: USART Control Register
 *   Bit 0: unused
 *   Bit 1: 0-Odd Parity, 1-Even Parity
 *   Bit 2: 0-No Parity, 1-Parity
 *   Bits 3,4: Start/Stop bits
 *     0 0 : 0-Start, 0-Stop    Synchronous
 *     0 1 : 0-Start, 1-Stop    Asynchronous
 *     1 0 : 1-Start, 1.5-Stop  Asynchronous
 *     1 1 : 1-Start, 2-Stop    Asynchronous
 *   Bits 5,6: 'WordLength'
 *     0 0 : 8 Bits
 *     0 1 : 7 Bits
 *     1 0 : 6 Bits
 *     1 1 : 5 Bits
 *   Bit 7: Frequency from TC and RC
 */
void RS232_HandleUCR(Sint16 ucr)
{
#if HAVE_TERMIOS_H
	int nCharSize;                   /* Bits per character: 5, 6, 7 or 8 */
	int nStopBits;                   /* Stop bits: 0=0 bits, 1=1 bit, 2=1.5 bits, 3=2 bits */

	nCharSize = 8 - ((ucr >> 5) & 3);
	nStopBits = (ucr >> 3) & 3;

	Dprintf(("RS232_HandleUCR(%i) : character size=%i , stop bits=%i\n",
	         ucr, nCharSize, nStopBits));

	if (hComOut != NULL)
	{
		if (!RS232_SetBitsConfig(hComOut, nCharSize, nStopBits, ucr&4, ucr&2))
			Log_Printf(LOG_WARN, "RS232_HandleUCR: failed to set bits configuration for %s\n", ConfigureParams.RS232.szOutFileName);
	}

	if (hComIn != NULL)
	{
		if (!RS232_SetBitsConfig(hComIn, nCharSize, nStopBits, ucr&4, ucr&2))
			Log_Printf(LOG_WARN, "RS232_HandleUCR: failed to set bits configuration for %s\n", ConfigureParams.RS232.szInFileName);
	}
#endif /* HAVE_TERMIOS_H */
}


/*-----------------------------------------------------------------------*/
/**
 * Set baud rate configuration of RS-232.
 */
bool RS232_SetBaudRate(int nBaud)
{
#if HAVE_TERMIOS_H
	int i;
	int fd;
	speed_t baudtype;
	struct termios termmode;
	static const int baudtable[][2] =
	{
		{ 50, B50 },
		{ 75, B75 },
		{ 110, B110 },
		{ 134, B134 },
		{ 150, B150 },
		{ 200, B200 },
		{ 300, B300 },
		{ 600, B600 },
		{ 1200, B1200 },
		{ 1800, B1800 },
		{ 2400, B2400 },
		{ 4800, B4800 },
		{ 9600, B9600 },
		{ 19200, B19200 },
		{ 38400, B38400 },
		{ 57600, B57600 },
		{ 115200, B115200 },
#ifdef B230400                 /* B230400 is not defined on all systems */
		{ 230400, B230400 },
#endif
		{ -1, -1 }
	};

	Dprintf(("RS232_SetBaudRate(%i)\n", nBaud));

	/* Convert baud number to baud termios constant: */
	baudtype = -1;
	for (i = 0; baudtable[i][0] != -1; i++)
	{
		if (baudtable[i][0] == nBaud)
		{
			baudtype = baudtable[i][1];
			break;
		}
	}

	if (baudtype == (speed_t)-1)
	{
		Dprintf(("RS232_SetBaudRate: Unsupported baud rate %i.\n", nBaud));
		return false;
	}

	/* Set ouput speed: */
	if (hComOut != NULL)
	{
		memset (&termmode, 0, sizeof(termmode));    /* Init with zeroes */
		fd = fileno(hComOut);
		if (isatty(fd))
		{
			if (tcgetattr(fd, &termmode) != 0)
				return false;

			cfsetospeed(&termmode, baudtype);

			if (tcsetattr(fd, TCSADRAIN, &termmode) != 0)
				return false;
		}
	}

	/* Set input speed: */
	if (hComIn != NULL)
	{
		memset (&termmode, 0, sizeof(termmode));    /* Init with zeroes */
		fd = fileno(hComIn);
		if (isatty(fd))
		{
			if (tcgetattr(fd, &termmode) != 0)
				return false;

			cfsetispeed(&termmode, baudtype);

			if (tcsetattr(fd, TCSADRAIN, &termmode) != 0)
				return false;
		}
	}
#endif /* HAVE_TERMIOS_H */

	return true;
}


/*-----------------------------------------------------------------------*/
/**
 * Set baud rate configuration of RS-232 according to the Timer-D hardware
 * registers.
 */
void RS232_SetBaudRateFromTimerD(void)
{
	int nTimerD_CR, nTimerD_DR, nBaudRate;

	nTimerD_CR = IoMem[0xfffa1d] & 0x07;
	nTimerD_DR = IoMem[0xfffa25];

	if (!nTimerD_CR)
		return;

	if ( nTimerD_DR == 0 )
		nTimerD_DR = 256;		/* In MFP, a data register=0 is in fact 256 */

	/* Calculate baud rate: (MFP/Timer-D is supplied with 2.4576 MHz) */
	nBaudRate = 2457600 / nTimerD_DR / 2;

	/*if (IoMem[0xfffa29] & 0x80)*/  /* We only support the by-16 prescaler */
	nBaudRate /= 16;

	switch (nTimerD_CR)
	{
		case 1:  nBaudRate /= 4;  break;
		case 2:  nBaudRate /= 10;  break;
		case 3:  nBaudRate /= 16;  break;
		case 4:  nBaudRate /= 50;  break;
		case 5:  nBaudRate /= 64;  break;
		case 6:  nBaudRate /= 100;  break;
		case 7:  nBaudRate /= 200;  break;
	}

	/* Adjust some ugly baud rates from TOS to more reasonable values: */
	switch (nBaudRate)
	{
		case 80:  nBaudRate = 75;  break;
		case 109:  nBaudRate = 110;  break;
		case 120:  nBaudRate = 110;  break;
		case 1745:  nBaudRate = 1800;  break;
		case 1920:  nBaudRate = 1800;  break;
	}

	RS232_SetBaudRate(nBaudRate);
}


/*-----------------------------------------------------------------------*/
/**
 * Set flow control configuration of RS-232.
 */
void RS232_SetFlowControl(Sint16 ctrl)
{
	Dprintf(("RS232_SetFlowControl(%i)\n", ctrl));

	/* Not yet written */
}


/*----------------------------------------------------------------------- */
/**
 * Pass bytes from emulator to RS-232
 */
bool RS232_TransferBytesTo(Uint8 *pBytes, int nBytes)
{
	/* Make sure there's a RS-232 connection if it's enabled */
	if (ConfigureParams.RS232.bEnableRS232)
		RS232_OpenCOMPort();

	/* Have we connected to the RS232? */
	if (hComOut)
	{
		/* Send bytes directly to the COM file */
		if (fwrite(pBytes, 1, nBytes, hComOut))
		{
			Dprintf(("RS232: Sent %i bytes ($%x ...)\n", nBytes, *pBytes));
			MFP_InputOnChannel ( MFP_INT_TRN_BUF_EMPTY , 0 );

			return true;   /* OK */
		}
	}

	return false;  /* Failed */
}


/*-----------------------------------------------------------------------*/
/**
 * Read characters from our internal input buffer (bytes from other machine)
 */
bool RS232_ReadBytes(Uint8 *pBytes, int nBytes)
{
	int i;

	/* Connected? */
	if (hComIn && InputBuffer_Head != InputBuffer_Tail)
	{
		/* Read bytes out of input buffer */
		for (i=0; i<nBytes; i++)
		{
			*pBytes++ = InputBuffer_RS232[InputBuffer_Head];
			InputBuffer_Head = (InputBuffer_Head+1) % MAX_RS232INPUT_BUFFER;
			SDL_SemPost(pSemFreeBuf);    /* Signal free space */
		}
		return true;
	}

	return false;
}


/*-----------------------------------------------------------------------*/
/**
 * Return true if bytes waiting!
 */
bool RS232_GetStatus(void)
{
	/* Connected? */
	if (hComIn)
	{
		/* Do we have bytes in the input buffer? */
		if (InputBuffer_Head != InputBuffer_Tail)
			return true;
	}

	/* No, none */
	return false;
}


/*-----------------------------------------------------------------------*/
/**
 * Read from the Syncronous Character Register.
 */
void RS232_SCR_ReadByte(void)
{
	M68000_WaitState(4);

	/* nothing */
}

/*-----------------------------------------------------------------------*/
/**
 * Write to the Syncronous Character Register.
 */
void RS232_SCR_WriteByte(void)
{
	M68000_WaitState(4);

	/*Dprintf(("RS232: Write to SCR: $%x\n", (int)IoMem[0xfffa27]));*/
}


/*-----------------------------------------------------------------------*/
/**
 * Read from the USART Control Register.
 */
void RS232_UCR_ReadByte(void)
{
	M68000_WaitState(4);

	Dprintf(("RS232: Read from UCR: $%x\n", (int)IoMem[0xfffa29]));
}

/*-----------------------------------------------------------------------*/
/**
 * Write to the USART Control Register.
 */
void RS232_UCR_WriteByte(void)
{
	M68000_WaitState(4);

	Dprintf(("RS232: Write to UCR: $%x\n", (int)IoMem[0xfffa29]));

	RS232_HandleUCR(IoMem[0xfffa29]);
}


/*-----------------------------------------------------------------------*/
/**
 * Read from the Receiver Status Register.
 */
void RS232_RSR_ReadByte(void)
{
	M68000_WaitState(4);

	if (RS232_GetStatus())
		IoMem[0xfffa2b] |= 0x80;        /* Buffer full */
	else
		IoMem[0xfffa2b] &= ~0x80;       /* Buffer not full */

	Dprintf(("RS232: Read from RSR: $%x\n", (int)IoMem[0xfffa2b]));
}

/*-----------------------------------------------------------------------*/
/**
 * Write to the Receiver Status Register.
 */
void RS232_RSR_WriteByte(void)
{
	M68000_WaitState(4);

	Dprintf(("RS232: Write to RSR: $%x\n", (int)IoMem[0xfffa2b]));
}


/*-----------------------------------------------------------------------*/
/**
 * Read from the Transmitter Status Register.
 * When RS232 emulation is not enabled, we still return 0x80 to allow
 * some games to work when they don't require send/receive on the RS232 port
 * (eg : 'Treasure Trap', 'The Deep' write some debug informations to RS232)
 */
void RS232_TSR_ReadByte(void)
{
	M68000_WaitState(4);

	IoMem[0xfffa2d] |= 0x80;        /* Buffer empty */

	Dprintf(("RS232: Read from TSR: $%x\n", (int)IoMem[0xfffa2d]));
}

/*-----------------------------------------------------------------------*/
/**
 * Write to the Transmitter Status Register.
 */
void RS232_TSR_WriteByte(void)
{
	M68000_WaitState(4);

	Dprintf(("RS232: Write to TSR: $%x\n", (int)IoMem[0xfffa2d]));
}


/*-----------------------------------------------------------------------*/
/**
 * Read from the USART Data Register.
 */
void RS232_UDR_ReadByte(void)
{
	Uint8 InByte = 0;

	M68000_WaitState(4);

	RS232_ReadBytes(&InByte, 1);
	IoMem[0xfffa2f] = InByte;
	Dprintf(("RS232: Read from UDR: $%x\n", (int)IoMem[0xfffa2f]));

	if (RS232_GetStatus())              /* More data waiting? */
	{
		/* Yes, generate another interrupt. */
		MFP_InputOnChannel ( MFP_INT_RCV_BUF_FULL , 0 );
	}
}

/*-----------------------------------------------------------------------*/
/**
 * Write to the USART Data Register.
 */
void RS232_UDR_WriteByte(void)
{
	Uint8 OutByte;

	M68000_WaitState(4);

	OutByte = IoMem[0xfffa2f];
	RS232_TransferBytesTo(&OutByte, 1);
	Dprintf(("RS232: Write to UDR: $%x\n", (int)IoMem[0xfffa2f]));
}
