/*
 *   Serial.c
 *
 *   This file is part of Emu48
 *
 *   Copyright (C) 1998 Christoph Gießelink
 *
 */
#include "pch.h"
#include "Emu48.h"
#include "io.h"

#define INTERRUPT ((void)(Chipset.SoftInt=TRUE,bInterrupt=TRUE))

// state of USRQ
#define NINT2ERBZ ((Chipset.IORam[IOC] & (SON | ERBZ)) == (SON | ERBZ) && (Chipset.IORam[RCS] & RBZ) != 0)
#define	NINT2ERBF ((Chipset.IORam[IOC] & (SON | ERBF)) == (SON | ERBF) && (Chipset.IORam[RCS] & RBF) != 0)
#define NINT2ETBE ((Chipset.IORam[IOC] & (SON | ETBE)) == (SON | ETBE) && (Chipset.IORam[TCS] & TBF) == 0)

#define NINT2USRQ (NINT2ERBZ || NINT2ERBF || NINT2ETBE)

static HANDLE     hComm = NULL;

static HANDLE     hCThreadTxd;
static HANDLE     hCThreadEv;

static HANDLE     hEventTxd;
static BOOL       bWriting;
static BYTE       tbr;

static BOOL       bReading;
static BYTE       cBuffer[32];
static WORD       nRp;
static DWORD      dwBytesRead;

static DWORD WINAPI TransmitThread(LPVOID pParam)
{
	OVERLAPPED osWr = { 0 };

	osWr.hEvent = CreateEvent(NULL,TRUE,FALSE,NULL);

	while (bWriting)
	{
		WaitForSingleObject(hEventTxd,INFINITE);
		if (bWriting)
		{
			DWORD dwWritten;
			if (!WriteFile(hComm,(LPCVOID) &tbr,1,&dwWritten,&osWr))
				if (GetLastError() == ERROR_IO_PENDING)
					GetOverlappedResult(hComm,&osWr,&dwWritten,TRUE);
		}
	}

	CloseHandle(osWr.hEvent);				// close write event handle
	return 0;
	UNREFERENCED_PARAMETER(pParam);
}

static DWORD WINAPI EventThread(LPVOID pParam)
{
	DWORD dwEvent;

	bReading = TRUE;						// flag for SerialThread started
	while (bReading)
	{
		_ASSERT(hComm != NULL);
		WaitCommEvent(hComm,&dwEvent,NULL);	// wait for serial event
		if (dwEvent & EV_RXCHAR)			// signal char received
		{
			CommReceive();					// get data
			// interrupt request and emulation thread down
			if (Chipset.SoftInt && Chipset.Shutdn)
			{
				Chipset.bShutdnWake = TRUE;	// wake up from SHUTDN mode
				SetEvent(hEventShutdn);		// wake up emulation thread
			}
		}
		if (dwEvent & EV_TXEMPTY)			// signal transmit buffer empty
		{
			IOBit(TCS,TBZ,FALSE);			// clear transmitter busy bit
			CommTransmit();					// check for new char to transmit
		}
		if (dwEvent & EV_ERR)				// signal error received
		{
			DWORD dwError;

			ClearCommError(hComm,&dwError,NULL);
			if (dwError & (CE_FRAME | CE_OVERRUN | CE_BREAK))
				IOBit(RCS,RER,TRUE);		// receiver error
		}
	}
	return 0;
	UNREFERENCED_PARAMETER(pParam);
}

BOOL CommIsOpen(VOID)
{
	return hComm != NULL;					// have COM port handle
}

BOOL CommOpen(LPTSTR strWirePort,LPTSTR strIrPort)
{
	COMMTIMEOUTS CommTimeouts = { MAXDWORD, 0L, 0L, 0L, 0L };

	LPCTSTR strPort = (Chipset.IORam[IR_CTRL] & EIRU) ? strIrPort : strWirePort;

	CommClose();							// close port if already open

	dwBytesRead = 0L;						// no bytes received

	if (lstrcmp(strPort, _T(NO_SERIAL)))	// port defined
	{
		TCHAR szDevice[256] = _T("\\\\.\\");

		// check if device buffer is big enough
		_ASSERT(lstrlen(szDevice) + lstrlen(strPort) < (INT) ARRAYSIZEOF(szDevice));
		if (lstrlen(szDevice) + lstrlen(strPort) >= (INT) ARRAYSIZEOF(szDevice))
			return hComm != NULL;

		_tcscat(szDevice,strPort);			// device name
		hComm = CreateFile(szDevice,
						   GENERIC_READ | GENERIC_WRITE,
						   0,
						   NULL,
						   OPEN_EXISTING,
						   FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED,
						   NULL);

		if (hComm != INVALID_HANDLE_VALUE)
		{
			DWORD dwThreadId;

			nRp = 0;						// reset receiver state

			SetCommTimeouts(hComm,&CommTimeouts);
			CommSetBaud();

			CommTxBRK();					// update BRK condition

			// event to transmit character
			hEventTxd = CreateEvent(NULL,FALSE,FALSE,NULL);

			// create char transmit handler
			bWriting = TRUE;
			hCThreadTxd = CreateThread(NULL,0,&TransmitThread,NULL,CREATE_SUSPENDED,&dwThreadId);
			_ASSERT(hCThreadTxd);
			SetThreadPriority(hCThreadTxd,THREAD_PRIORITY_ABOVE_NORMAL);
			ResumeThread(hCThreadTxd);		// start thread

			// create Comm event handler
			bReading = FALSE;
			SetCommMask(hComm,EV_RXCHAR | EV_TXEMPTY | EV_ERR); // event on RX, TX, error
			hCThreadEv = CreateThread(NULL,0,&EventThread,NULL,CREATE_SUSPENDED,&dwThreadId);
			_ASSERT(hCThreadEv);
			SetThreadPriority(hCThreadEv,THREAD_PRIORITY_ABOVE_NORMAL);
			ResumeThread(hCThreadEv);		// start thread
			while (!bReading) Sleep(0);		// wait for SerialThread started
		}
		else
		{
			hComm = NULL;
		}
	}

	#if defined DEBUG_SERIAL
	{
		TCHAR buffer[256];
		wsprintf(buffer,_T("COM port %s.\n"),hComm ? _T("opened"): _T("open error"));
		OutputDebugString(buffer);
	}
	#endif
	return hComm != NULL;
}

VOID CommClose(VOID)
{
	if (hComm != NULL)						// port open
	{
		// workaround to fix problems with some Kermit server programs
		// reason: on one hand we have the character transmitting time base on the
		// selected baudrate, on the other hand the time between sending the last
		// character and closing the port. The last time is much longer on the real
		// calculator than on the emulator running at full speed, therefore the
		// slow down time on the emulator
		Sleep(25);							// slow down time

		bReading = FALSE;					// kill event thread
		SetCommMask(hComm,0L);				// clear all events and force WaitCommEvent to return
		WaitForSingleObject(hCThreadEv,INFINITE);
		CloseHandle(hCThreadEv);

		bWriting = FALSE;					// kill write thread
		SetEvent(hEventTxd);				// continue write thread
		WaitForSingleObject(hCThreadTxd,INFINITE);
		CloseHandle(hCThreadTxd);

		CloseHandle(hEventTxd);				// close Txd event
		CloseHandle(hComm);					// close port
		hComm = NULL;
		#if defined DEBUG_SERIAL
			OutputDebugString(_T("COM port closed.\n"));
		#endif
	}
	return;
}

VOID CommSetBaud(VOID)
{
	if (hComm != NULL)
	{
		const DWORD dwBaudrates[] = {  1200,  1920,  2400,  3840,   4800,   7680,   9600,  15360,
									  14400, 19200, 38400, 57600, 115200, 115200, 115200, 115200 };

		DCB dcb;
		UINT uBaudIndex;

		uBaudIndex = isModelApple(cCurrentRomType)
				   ? Chipset.IORam[BAUD]
				   : Chipset.IORam[BAUD] & 0x7;

		ZeroMemory(&dcb,sizeof(dcb));
		dcb.DCBlength = sizeof(dcb);
		dcb.BaudRate = dwBaudrates[uBaudIndex];
		dcb.fBinary = TRUE;
		dcb.fParity = TRUE;
		dcb.fOutxCtsFlow = FALSE;
		dcb.fOutxDsrFlow = FALSE;
		dcb.fDtrControl = DTR_CONTROL_DISABLE;
		dcb.fDsrSensitivity = FALSE;
		dcb.fOutX = FALSE;
		dcb.fErrorChar = FALSE;
		dcb.fNull = FALSE;
		dcb.fRtsControl = RTS_CONTROL_DISABLE;
		dcb.fAbortOnError = FALSE;			// may changed in further implementations
		dcb.ByteSize = 8;
		dcb.Parity = NOPARITY;				// no parity check, emulated by software
		dcb.StopBits = TWOSTOPBITS;

		#if defined DEBUG_SERIAL
		{
			TCHAR buffer[256];
			wsprintf(buffer,_T("CommsetBaud: %ld\n"),dcb.BaudRate);
			OutputDebugString(buffer);
		}
		#endif

		SetCommState(hComm,&dcb);
	}
	return;
}

BOOL UpdateUSRQ(VOID)						// USRQ handling
{
	BOOL bUSRQ = NINT2USRQ;
	IOBit(SRQ1,USRQ,bUSRQ);					// update USRQ bit
	return bUSRQ;
}

VOID CommTxBRK(VOID)
{
	if (Chipset.IORam[TCS] & BRK)			// BRK condition
	{
		if (hComm != NULL)					// com port open
		{
			// abort data transfer
			PurgeComm(hComm,PURGE_TXABORT | PURGE_TXCLEAR);
			SetCommBreak(hComm);			// set into BRK state
		}

		// TBF and TBZ bits of TCS are undefined

		if (Chipset.IORam[TCS] & LPB)		// is loopback bit set
		{
			dwBytesRead = nRp = 0;			// clear receive buffer
			cBuffer[dwBytesRead++] = 0;		// save character in receive buffer

			CommReceive();					// receive available byte
			IOBit(RCS,RER,TRUE);			// receiver error (no stop bit)
		}
	}
	else
	{
		if (hComm != NULL)					// com port open
		{
			ClearCommBreak(hComm);			// clear BRK state
		}
	}
	return;
}

VOID CommTransmit(VOID)
{
	BOOL bTxChar = FALSE;

	EnterCriticalSection(&csTxdLock);
	if (   (Chipset.IORam[TCS] & TBZ) == 0	// transmitter not busy
		&& (Chipset.IORam[TCS] & TBF) != 0)	// transmit buffer full
	{
		tbr = (Chipset.IORam[TBR_MSB] << 4) | Chipset.IORam[TBR_LSB];

		IOBit(TCS,TBF,FALSE);				// clear transmit buffer full bit
		IOBit(TCS,TBZ,TRUE);				// set transmitter busy bit

		bTxChar = TRUE;
	}
	LeaveCriticalSection(&csTxdLock);

	if (bTxChar)							// character to transmit
	{
		#if defined DEBUG_SERIAL
		{
			TCHAR buffer[256];
			if (isprint(tbr))
				wsprintf(buffer,_T("-> '%c'\n"),tbr);
			else
				wsprintf(buffer,_T("-> %02X\n"),tbr);
			OutputDebugString(buffer);
		}
		#endif

		if (Chipset.IORam[TCS] & LPB)		// is loopback bit set
		{
			if (dwBytesRead == 0) nRp = 0;	// no character received, reset read pointer
			cBuffer[nRp+dwBytesRead] = tbr;	// save character in receive buffer
			++dwBytesRead;

			CommReceive();					// receive available byte
		}

		if (hComm != NULL)					// com port open
		{
			SetEvent(hEventTxd);			// write TBR byte
		}
		else
		{
			IOBit(TCS,TBZ,FALSE);			// clear transmitter busy bit
		}
	}
	if (UpdateUSRQ())						// update USRQ bit
		INTERRUPT;
	return;
}

VOID CommReceive(VOID)
{
	OVERLAPPED os = { 0 };

	if (!(Chipset.IORam[IOC] & SON))		// UART off
	{
		dwBytesRead = 0L;					// no bytes received
		return;
	}

	EnterCriticalSection(&csRecvLock);
	do
	{
		if (Chipset.IORam[RCS] & RBF)		// receive buffer full
			break;

		// reject reading if com port is closed and not whole operation
		if (hComm)							// com port open
		{
			UINT uCnt = 0;					// retry counter

			while (dwBytesRead == 0L)		// buffer empty
			{
				if (ReadFile(hComm,cBuffer,sizeof(cBuffer),&dwBytesRead,&os) == FALSE)
					dwBytesRead = 0L;
				else						// bytes received
					nRp = 0;				// reset read pointer

				// something received or passed 1st retry
				if (dwBytesRead != 0L || ++uCnt > 1)
					break;					// quit

				Sleep(1);					// workaround, retry 1ms later
			}
		}

		if (dwBytesRead == 0L)				// receive buffer empty
			break;

		#if defined DEBUG_SERIAL
		{
			TCHAR buffer[256];
			if (isprint(cBuffer[nRp]))
				wsprintf(buffer,_T("<- '%c'\n"),cBuffer[nRp]);
			else
				wsprintf(buffer,_T("<- %02X\n"),cBuffer[nRp]);
			OutputDebugString(buffer);
		}
		#endif

		Chipset.IORam[RBR_MSB] = (cBuffer[nRp] >> 4);
		Chipset.IORam[RBR_LSB] = (cBuffer[nRp] & 0x0f);
		++nRp;
		--dwBytesRead;

		Chipset.IORam[RCS] |= RBF;			// receive buffer full
		if (UpdateUSRQ())					// update USRQ bit
			INTERRUPT;
	}
	while (FALSE);
	LeaveCriticalSection(&csRecvLock);
	return;
}
