/************************************************************************\
 * File Version Information
 * $Header: /Altair32v2/Tcp_io.c 36    11/08/06 9:21p Racini $
 ************************************************************************/
/************************************************************************\
  MITS Altair Emulator
  TCP/IP Console support

  Copyright (c) 2000 Theo Pozzy
  Copyright (c) 2000-2006 Richard A. Cini

  The TCP I/O interface is intended to allow the emulator to act as a
  telnet server to facilitate using a remote terminal to connect to
  the emulator and act as the user console. This is accomplished through
  this set of routines along with Altair port emulation routines elsewhere.
  These routines do buffered polled I/O to a simple TCP stream and
  is implemented as a state machine.

  
Change Log:
  2000/10/25  TMP -- Initial coding
  2001/07/16  RAC -- Minor changes to merge with sio.c.
  2001/08/18  TMP -- Minor fix to strip parity bit from stream
  2001/08/19  RAC -- RELEASE MARKER -- v2.0
  2001/12/14  RAC -- RELEASE MARKER -- v2.1
  2002/01/31  RAC -- RELEASE MARKER -- v2.2
  2002/07/07  RAC -- RELEASE MARKER -- v2.3
  2002/08/23  RAC -- RELEASE MARKER -- v2.30.10
  2002/09/22  RAC -- Changed "return *pszInBuffPtr" in TcpRead to "return 0"
  2002/09/23  RAC -- Changed back to returning last character.
  2002/10/16  RAC -- Merged header file into this file; all exports in altair32.h
  2002/11/15  RAC -- RELEASE MARKER -- v2.40.2100
  2002/11/19  RAC -- Added support in TcpInit for configurable telnet port
  2003/03/03  RAC -- Merged changes from Joe Forgione
  2003/04/26  RAC -- RELEASE MARKER -- v2.50.2045
  2003/04/30  RS  -- Changes to support moving channel polling to scheduler.
  2004/03/09  RAC -- Diff'ed more changes from FJS (Fred J. Scipione)
  2004/07/30  RAC -- RELEASE MARKER -- v3.00.0135
  2006/05/12  RAC -- RELEASE MARKER -- v3.10.0200
  2006/11/15  RAC -- RELEASE MARKER -- v3.20.0400
\************************************************************************/
#include <winsock2.h>
#include <windows.h>
#include <stdlib.h>
#include <stdio.h>		// C-lib - I/O 
#include <commctrl.h>
#include "altair32.h"	// picks-up tcp_shutdown
#include "comcthlp.h"	// common control macro file


/**  Typedefs & Defines  **************************************/
#define	IN_BUF_SIZE		256		// should be plenty for user input via telnet
#define OUT_BUF_SIZE	1024	// output buffer size
#define TMXR_GUARD      8


/**  Module Globals  ******************************************/
static char		szInBuff[IN_BUF_SIZE + TMXR_GUARD];
static char		*pszInBuffPtr;	// Pointer to next character in buffer
static char		szOutBuff[OUT_BUF_SIZE + TMXR_GUARD];
static int		iInBuffCnt = 0;	// Number of bytes remaining in buffer
static int		iOutBuffCnt = 0; // Number of bytes in buffer
static int	 	TcpState = TCP_INIT; 
static SOCKET	sockListen;	// The socket used to listen for the inbound connection
static SOCKET	sockUser;		// The socket for the incoming client


/**  Forward Prototypes  **************************************/
static BOOL TcpAccept(void);
BOOL TcpShutdown(void);


/**************************************************************\
****  Exported Functions  *************************************|
\**************************************************************/

/**************************************************************\
*
*  FUNCTION:    TELNET_TcpInStatus
*
*  INPUTS:      None
*
*  RETURNS:     0 if no data is ready, 1 if data is available
*
*  COMMENTS:
*
\**************************************************************/
int TELNET_TcpInStatus(void)
{
	int iReturn;
	BOOL bCont = TRUE;			// True if state machine should continue (on this call)

	while(bCont) {
		switch(TcpState) {
		case TCP_INIT:
			bCont = TELNET_TcpInit();
			break;
		case TCP_LISTENING:
			bCont = TcpAccept();
			break;
		case TCP_CONNECTED:
			//2003/04/30 RS *BEGIN_CHANGE
			//bCont = TELNET_TcpGetData();
			//break;
			//2003/04/30 RS *END_CHANGE
		case TCP_DATA_READY:
			if (iInBuffCnt == 0) {
				// No data left, change state
				TcpState = TCP_CONNECTED;
			}
			bCont = FALSE;
			break;
		case TCP_SHUTDOWN:
			TELNET_TcpShutdown();
			break;
		default:
			bCont = FALSE;
			break;
		}
	}
	if (TcpState == TCP_DATA_READY)
		iReturn = 1;
	else
		iReturn = 0;			// No data available

	return iReturn;
}

/**************************************************************\
*
*  FUNCTION:    TELNET_TcpRead
*
*  INPUTS:      None
*
*  RETURNS:     Character (>= 0) or 0 if no data has been read
*
*  COMMENTS:	In order to simulate the way most serial ports
*				work on the Altair, if no data is ready, this
*				routine will return the last character in the
*				buffer, or zero if no characters have ever been
*				put in the buffer.
*
\**************************************************************/
int TELNET_TcpRead(void)
{
	int nData;
	BOOL bCont = TRUE;			// True if state machine should continue (on this call)

	while(bCont) {
		switch(TcpState) {
		case TCP_INIT:
			bCont = TELNET_TcpInit();
		break;
		case TCP_LISTENING:
			bCont = TcpAccept();
		break;
		case TCP_CONNECTED:					// here in loop if no data available
			bCont = TELNET_TcpGetData();
		break;
		case TCP_DATA_READY:
			// get next character from buffer and return it. If no data left, return
			// last character we've read (which is preset to zero the first time through).
			if (iInBuffCnt) {
				// --iInBuffCnt;			// FJS
				nData = *pszInBuffPtr;
				if (--iInBuffCnt)			// If there's more data // FJS
					++pszInBuffPtr;			// Point to the next char
				return nData;
			}

			TcpState = TCP_CONNECTED;		// We don't have data ready anymore
		return *pszInBuffPtr;				// Return whatever we did on the last read
		case TCP_SHUTDOWN:
			TELNET_TcpShutdown();
		break;
		default:
			bCont = FALSE;
		// break;
		}
	}
	return 0 /* EOS */;						// No data available
}

/**************************************************************\
*
*  FUNCTION:    TELNET_TcpOutStatus
*
*  INPUTS:      None
*
*  RETURNS:     1 if data can be written (i.e., buffer isn't full), else 0
*
*  COMMENTS:
*
\**************************************************************/
int TELNET_TcpOutStatus(void)
{
	// int iReturn;
	BOOL bCont = TRUE;		// True if state machine should continue (on this call)

	while(bCont) {
		switch(TcpState) {
		case TCP_INIT:
			bCont = TELNET_TcpInit();
		break;
		case TCP_LISTENING:
			bCont = TcpAccept();
		break;
		case TCP_SHUTDOWN:
			TELNET_TcpShutdown();			// changes TcpState?
		break;
		case TCP_CONNECTED:
			// bCont = FALSE;				// Break out of loop - we're connected
		// break;
		default:							// TCP_Closed?
			bCont = FALSE;
		// break;
		}
	}
	if (	(TcpState == TCP_CONNECTED || TcpState == TCP_DATA_READY)
			&& (iOutBuffCnt < OUT_BUF_SIZE))
		return /* iReturn = */ 1;	// Connected and there's more room in the buffer
	// else
		// iReturn = 0;				// Not connected or no more room in buffer

	return /* iReturn */ 0;
}

/**************************************************************\
*
*  FUNCTION:    TELNET_TcpWrite
*
*  INPUTS:      Character to write
*
*  RETURNS:     Nothing
*
*  COMMENTS:	Errors are effectively ignored
*
\**************************************************************/
void TELNET_TcpWrite(UCHAR cData)
{
	BOOL bCont = TRUE;			// True if state machine should continue (on this call)

	while(bCont) {
		switch(TcpState) {
		case TCP_INIT:
			bCont = TELNET_TcpInit();
		break;
		case TCP_LISTENING:
			bCont = TcpAccept();
		break;
		case TCP_CONNECTED:
		case TCP_DATA_READY:	// It's ok if there's data available on the input side also
			cData &= 0x7f;				// strip parity bit
			//2003/04/30 RS *BEGIN_CHANGE
			szOutBuff[iOutBuffCnt] = cData;
			if (iOutBuffCnt < OUT_BUF_SIZE)
				++iOutBuffCnt;
			if (iOutBuffCnt > 15)		// don't bother MR. TCP until we've
				bCont = TELNET_TcpPutData(0);	// got at least 16 characters buffered
			else
				bCont = FALSE;
			//2003/04/30 RS *END_CHANGE
		break;
		case TCP_SHUTDOWN:
			TELNET_TcpShutdown();
		break;
		default:
			bCont = FALSE;
		// break;
		}
	}
}


/**************************************************************\
****  Internal Subroutines  ***********************************|
\**************************************************************/

/**************************************************************\
*
*  FUNCTION:    TELNET_TcpInit
*
*  INPUTS:      None
*
*  RETURNS:     Continue flag - TRUE if caller should continue in state machine
*
*  COMMENTS:	There is no error reporting to help troubleshoot problems
*
\**************************************************************/
BOOL TELNET_TcpInit(void)
{
	char buf[80];
	WSADATA wsd;
	struct sockaddr_in saIn;

	iInBuffCnt = 0;					// Reset buffers
	iOutBuffCnt = 0;
	pszInBuffPtr = szInBuff;
	*pszInBuffPtr = '\0';			// Make sure there's a NUL character in the buffer

	if (WSAStartup(MAKEWORD(2,2), &wsd) != 0) {
		return FALSE;				// failed to load Winsock
	}
	// Create the socket
	sockListen = socket(AF_INET, SOCK_STREAM, 0);
	if (sockListen == SOCKET_ERROR) {
		return FALSE;
	}
	// Bind the socket to the local interface and port
	saIn.sin_addr.s_addr = htonl(INADDR_ANY);
	saIn.sin_family = AF_INET;
	saIn.sin_port = htons(simstate.usTelnetPort);
	sprintf(buf, "Port: %u",simstate.usTelnetPort);
	Status_SetText (hwndStatusBar, STATBAR_CONS, 0, buf);
	//debug code
	//saIn.sin_port = htons(DEFAULT_PORT);

	if (bind(sockListen, (struct sockaddr *)&saIn, sizeof(saIn)) == SOCKET_ERROR) {
		return FALSE;
	}
	// Start the listen
	if (listen(sockListen, 1) != 0) {
		return FALSE;
	}
	TcpState = TCP_LISTENING;
	return TRUE;
}

/**************************************************************\
*
*  FUNCTION:    TcpAccept - see if there is a connection pending
*
*  INPUTS:      None
*
*  RETURNS:     Continue flag - TRUE if caller should continue in state machine
*
*  COMMENTS:	There is no error reporting to help troubleshoot problems
*
\**************************************************************/
static BOOL TcpAccept(void)
{
	int iAddrSize;
	struct sockaddr_in saUser;
	int iTcpRet;
	unsigned long ul = 1;

	// Set the socket into non-blocking mode
	iTcpRet = ioctlsocket(sockListen, FIONBIO, (unsigned long *)&ul);
	if (iTcpRet == SOCKET_ERROR) {
		return FALSE;
	}
	iAddrSize = sizeof(saUser);
	sockUser = accept(sockListen, (struct sockaddr *)&saUser, &iAddrSize);
	if (sockUser == INVALID_SOCKET) {
		// We could explicitly check the error type, but since the behavior
		// is the same, we don't.
		int WSALastErr = WSAGetLastError();
		if (WSALastErr == WSAEWOULDBLOCK)
			return FALSE;
		else {
			//wsprintf(szBuffer, "Tcp_io.c: TcpAccept: unexpected error: %d.\n", WSALastErr);
			//OutputDebugString(szBuffer);
			return FALSE;
		}
	}
	TcpState = TCP_CONNECTED;
	return TRUE;
}

/**************************************************************\
*
*  FUNCTION:    TELNET_TcpGetData - Check for data in buffer. If none, try a read.
*
*  INPUTS:      None
*
*  RETURNS:     Continue flag - TRUE if caller should continue in state machine
*
*  COMMENTS:	There is no error reporting to help troubleshoot
*				problems. If there is data ready, this routine
*				sets the next state to TCP_DATA_READY, but does
*				not take the data out of the buffer
*
\**************************************************************/
BOOL TELNET_TcpGetData(void)
{
	int iReadSts;

	if (iInBuffCnt > 0) {
		TcpState = TCP_DATA_READY;
		return TRUE;
	}

	// do a (non-blocking) read. recv returns 0 on closed, error code, or
	// actual number of characters read.
	iReadSts = recv(sockUser, szInBuff, IN_BUF_SIZE, 0);
	if (iReadSts != SOCKET_ERROR) {
		// We should check for WSAEWOULDBLOCK, and only remain in
		// this state if it's because there was no data ready (otherwise, we
		// should shut down the socket).
		// return FALSE; // FJS
		if (iReadSts == 0) {		// Socket closed - shut down
			TcpState = TCP_SHUTDOWN;
			return TRUE;
		}

		// We have data
		pszInBuffPtr = szInBuff;	// reset buffer pointer
		iInBuffCnt = iReadSts;
		TcpState = TCP_DATA_READY;
		return TRUE;
	}
	return FALSE /* was 0 */; // FJS
}


/**************************************************************\
*
*  FUNCTION:    TELNET_TcpPutData - Output a character
*
*  INPUTS:      Character to output
*
*  RETURNS:     Continue flag - TRUE if caller should continue in state machine
*
*  COMMENTS:	There is no error reporting to help troubleshoot problems
*
\**************************************************************/
BOOL TELNET_TcpPutData(UCHAR cData)
{
	int iWriteSts;

	//2003/04/30 RS *BEGIN_CHANGE
	//szOutBuff[iOutBuffCnt] = cData;
	//if (iOutBuffCnt < OUT_BUF_SIZE)
	//	++iOutBuffCnt;
	//2003/04/30 RS *END_CHANGE
	if (iOutBuffCnt < 1) return FALSE;
	
	// Fire off the write operation
	iWriteSts = send(sockUser, szOutBuff, iOutBuffCnt, 0);
	if (iWriteSts == SOCKET_ERROR) {
		//wsprintf(szBuffer, "Tcp_io.c: TcpPutData: unexpected error: %d.\n", WSAGetLastError());
		//OutputDebugString(szBuffer);
		return FALSE;
	}
	iOutBuffCnt = 0;				// reset buffer count
	return FALSE;
}


/**************************************************************\
*
*  FUNCTION:    TELNET_TcpShutdown - Shutdown connection
*
*  INPUTS:      None
*
*  RETURNS:     Continue flag - TRUE if caller should continue in state machine
*
*  COMMENTS:	There is no error reporting to help troubleshoot problems
*
\**************************************************************/

BOOL TELNET_TcpShutdown(void)
{
	int iStatus;

	iStatus = shutdown(sockListen, SD_BOTH); // Tear down EVERYTHING
	iStatus = closesocket(sockListen);		 // in case of port change
	TcpState = TCP_INIT;
	iStatus = shutdown(sockUser, SD_BOTH);
	iStatus = closesocket(sockUser);
	Status_SetText(hwndStatusBar, STATBAR_CONS, 0, "Port: NPA");
	return FALSE;
}
/* end of file: tcp_io.c */
