/*
 *  Copyright (C) 2002-2007  The DOSBox Team
 *
 *  This program 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 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program 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 this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

#include "dosbox.h"

#if C_MODEM

#include "setup.h"		// CommandLine
#include "serialport.h"
#include "nullmodem.h"

CNullModem::CNullModem(Bitu id, CommandLine* cmd):CSerial (id, cmd) {
	Bitu temptcpport=23;
	memset(&telClient, 0, sizeof(telClient));
	InstallationSuccessful = false;
	serversocket = 0;
	clientsocket = 0;
	serverport = 0;
	clientport = 0;

	rx_retry = 0;
	rx_retry_max = 100;

	tx_gather = 12;
	
	dtrrespect=false;
	tx_block=false;
	receiveblock=false;
	transparent=false;
	telnet=false;
	
	Bitu bool_temp=0;

	// usedtr: The nullmodem will
	// 1) when it is client connect to the server not immediately but
	//    as soon as a modem-aware application is started (DTR is switched on).
	// 2) only transfer data when DTR is on.
	if(getBituSubstring("usedtr:", &bool_temp, cmd)) {
		if(bool_temp==1) {
			dtrrespect=true;
			transparent=true;
		}
	}
	// transparent: don't add additional handshake control.
	if(getBituSubstring("transparent:", &bool_temp, cmd)) {
		if(bool_temp==1) transparent=true;
		else transparent=false;
	}
	// telnet: interpret telnet commands.
	if(getBituSubstring("telnet:", &bool_temp, cmd)) {
		if(bool_temp==1) {
			transparent=true;
			telnet=true;
		}
	}
	// rxdelay: How many milliseconds to wait before causing an
	// overflow when the application is unresponsive.
	if(getBituSubstring("rxdelay:", &rx_retry_max, cmd)) {
		if(!(rx_retry_max<=10000)) {
			rx_retry_max=50;
		}
	}
	// txdelay: How many milliseconds to wait before sending data.
	// This reduces network overhead quite a lot.
	if(getBituSubstring("txdelay:", &tx_gather, cmd)) {
		if(!(tx_gather<=500)) {
			tx_gather=12;
		}
	}
	// port is for both server and client
	if(getBituSubstring("port:", &temptcpport, cmd)) {
		if(!(temptcpport>0&&temptcpport<65536)) {
			temptcpport=23;
		}
	}
	// socket inheritance
	if(getBituSubstring("inhsocket:", &bool_temp, cmd)) {
#ifdef NATIVESOCKETS
		if(Netwrapper_GetCapabilities()&NETWRAPPER_TCP_NATIVESOCKET) {
			if(bool_temp==1) {
				int sock;
				if (control->cmdline->FindInt("-socket",sock,true)) {
					dtrrespect=false;
					transparent=true;
					// custom connect
					Bit8u peernamebuf[16];
					LOG_MSG("inheritance port: %d",sock);
					clientsocket = new TCPClientSocket(sock);
					if(!clientsocket->isopen) {
						LOG_MSG("Serial%d: Connection failed.",COMNUMBER);
						delete clientsocket;
						clientsocket=0;
						return;
					}
					clientsocket->SetSendBufferSize(256);
					clientsocket->GetRemoteAddressString(peernamebuf);
					// transmit the line status
					if(!transparent) setRTSDTR(getRTS(), getDTR());

					LOG_MSG("Serial%d: Connected to %s",COMNUMBER,peernamebuf);
					setEvent(SERIAL_POLLING_EVENT, 1);

					CSerial::Init_Registers ();
					InstallationSuccessful = true;

					setCTS(true);
					setDSR(true);
					setRI (false);
					setCD (true);
					return;
				} else {
					LOG_MSG("Serial%d: -socket start parameter missing.",COMNUMBER);
					return;
				}
			}
		} else {
#endif
			LOG_MSG("Serial%d: socket inheritance not supported on this platform.",
				COMNUMBER);
			return;
		}
	}
	std::string tmpstring;
	if(cmd->FindStringBegin("server:",tmpstring,false)) {
		// we are a client
		const char* hostnamechar=tmpstring.c_str();
		Bitu hostlen=strlen(hostnamechar)+1;
		if(hostlen>sizeof(hostnamebuffer)) {
			hostlen=sizeof(hostnamebuffer);
			hostnamebuffer[sizeof(hostnamebuffer)-1]=0;
		}
		memcpy(hostnamebuffer,hostnamechar,hostlen);
		clientport=temptcpport;
		if(dtrrespect) {
			// we connect as soon as DTR is switched on
			setEvent(SERIAL_NULLMODEM_DTR_EVENT, 50);
			LOG_MSG("Serial%d: Waiting for DTR...",COMNUMBER);
		} else ClientConnect();
	} else {
		// we are a server
		serverport = (Bit16u)temptcpport;
		serversocket = new TCPServerSocket(serverport);
		if(!serversocket->isopen) return;
		LOG_MSG("Serial%d: Nullmodem server waiting for connection on port %d...",
			COMNUMBER,serverport);
		setEvent(SERIAL_SERVER_POLLING_EVENT, 50);
	}

	// ....

	CSerial::Init_Registers ();
	InstallationSuccessful = true;

	setCTS(dtrrespect||transparent);
	setDSR(dtrrespect||transparent);
	setRI (false);
	setCD (dtrrespect);
}

CNullModem::~CNullModem () {
	if(serversocket) delete serversocket;
	if(clientsocket) delete clientsocket;
	// remove events
	for(Bitu i = SERIAL_BASE_EVENT_COUNT+1;
				i <= SERIAL_NULLMODEM_EVENT_COUNT; i++)
		removeEvent(i);
}

void CNullModem::WriteChar(Bit8u data) {
	
	if(clientsocket)clientsocket->SendByteBuffered(data);
	if(!tx_block) {
		//LOG_MSG("setevreduct");
		setEvent(SERIAL_TX_REDUCTION, (float)tx_gather);
		tx_block=true;
	}
}

Bits CNullModem::readChar() {
	
	Bits rxchar = clientsocket->GetcharNonBlock();
	if(telnet && rxchar>=0) return TelnetEmulation(rxchar);
	else if(rxchar==0xff && !transparent) {// escape char
		// get the next char
		Bits rxchar = clientsocket->GetcharNonBlock();
		if(rxchar==0xff) return rxchar; // 0xff 0xff -> 0xff was meant
		rxchar&0x1? setCTS(true) : setCTS(false);
		rxchar&0x2? setDSR(true) : setDSR(false);
		if(rxchar&0x4) receiveError(0x10);
		return -1;	// no "payload" received
	} else return rxchar;
}

void CNullModem::ClientConnect(){
	Bit8u peernamebuf[16];
	clientsocket = new TCPClientSocket((char*)hostnamebuffer,
										(Bit16u)clientport); 
	if(!clientsocket->isopen) {
		LOG_MSG("Serial%d: Connection failed.",idnumber+1);
		delete clientsocket;
		clientsocket=0;
		return;
	}
	clientsocket->SetSendBufferSize(256);
	clientsocket->GetRemoteAddressString(peernamebuf);
	// transmit the line status
	if(!transparent) setRTSDTR(getRTS(), getDTR());

	LOG_MSG("Serial%d: Connected to %s",idnumber+1,peernamebuf);
	setEvent(SERIAL_POLLING_EVENT, 1);
}

void CNullModem::Disconnect() {
	// it was disconnected; free the socket and restart the server socket
	LOG_MSG("Serial%d: Disconnected.",idnumber+1);
	delete clientsocket;
	clientsocket=0;
	setDTR(false);
	setCTS(false);
	if(serverport) {
		serversocket = new TCPServerSocket(serverport);
		if(serversocket->isopen) 
			setEvent(SERIAL_SERVER_POLLING_EVENT, 50);
		else delete serversocket;
	}
}

void CNullModem::handleUpperEvent(Bit16u type) {
	
	switch(type) {
		case SERIAL_POLLING_EVENT: {
			// periodically check if new data arrived, disconnect
			// if required. Add it back.
			if(!receiveblock && clientsocket) {
				if(((!(LSR&LSR_RX_DATA_READY_MASK)) || rx_retry>=rx_retry_max )
					&&(!dtrrespect | (dtrrespect&& getDTR()) )) {
					rx_retry=0;
					Bits rxchar = readChar();
					if(rxchar>=0) {
						receiveblock=true;
						setEvent(SERIAL_RX_EVENT, bytetime-0.01f);
						receiveByte((Bit8u)rxchar);
					}
					else if(rxchar==-2) Disconnect();
					else setEvent(SERIAL_POLLING_EVENT, 1);
				} else {
					rx_retry++;
					setEvent(SERIAL_POLLING_EVENT, 1);
				}
			} 
			break;
		}
		case SERIAL_RX_EVENT: {
			// receive time is up, try to receive another byte.
			receiveblock=false;
			
			if((!(LSR&LSR_RX_DATA_READY_MASK) || rx_retry>=rx_retry_max)
				&&(!dtrrespect | (dtrrespect&& getDTR()) )
				) {
				rx_retry=0;
				Bits rxchar = readChar();
				if(rxchar>=0) {
					receiveblock=true;
					setEvent(SERIAL_RX_EVENT, bytetime-0.01f);
					receiveByte((Bit8u)rxchar);
				}
				else if(rxchar==-2) Disconnect();
				else setEvent(SERIAL_POLLING_EVENT, 1);
			} else {
				setEvent(SERIAL_POLLING_EVENT, 1);
				rx_retry++;
			}
			break;
		}
		case SERIAL_TX_EVENT: {
			ByteTransmitted();
			break;
		}
		case SERIAL_THR_EVENT: {
			ByteTransmitting();
			// actually send it
			setEvent(SERIAL_TX_EVENT,bytetime+0.01f);
			break;				   
		}
		case SERIAL_SERVER_POLLING_EVENT: {
			// As long as nothing is connected to out server poll the
			// connection.
			if((clientsocket=serversocket->Accept())) {
				Bit8u peeripbuf[16];
				clientsocket->GetRemoteAddressString(peeripbuf);
				LOG_MSG("Serial%d: A client (%s) has connected.",idnumber+1,peeripbuf);
				// new socket found...
				clientsocket->SetSendBufferSize(256);
				setEvent(SERIAL_POLLING_EVENT, 1);
				
				// we don't accept further connections
				delete serversocket;
				serversocket=0;

				// transmit the line status
				setRTSDTR(getRTS(), getDTR());
			} else {
				// continue looking
				setEvent(SERIAL_SERVER_POLLING_EVENT, 50);
			}
			break;
		}
		case SERIAL_TX_REDUCTION: {
			// Flush the data in the transmitting buffer.
			if(clientsocket) clientsocket->FlushBuffer();
			tx_block=false;
			break;						  
		}
		case SERIAL_NULLMODEM_DTR_EVENT: {
			if(getDTR()) ClientConnect();
			else setEvent(SERIAL_NULLMODEM_DTR_EVENT,50);
			break;
		}
	}
}

/*****************************************************************************/
/* updatePortConfig is called when emulated app changes the serial port     **/
/* parameters baudrate, stopbits, number of databits, parity.               **/
/*****************************************************************************/
void CNullModem::updatePortConfig (Bit16u divider, Bit8u lcr) {
	
}

void CNullModem::updateMSR () {
	
}

void CNullModem::transmitByte (Bit8u val, bool first) {

	// transmit it later in THR_Event
	if(first) {
		setEvent(SERIAL_THR_EVENT, bytetime/8);
	}
	else {
		//if(clientsocket) clientsocket->Putchar(val);
		setEvent(SERIAL_TX_EVENT, bytetime);
	}
	/*****************************/
	if(val==0xff) WriteChar(0xff);
	
	WriteChar(val);
}

Bits CNullModem::TelnetEmulation(Bit8u data) {
	Bit8u response[3];
	if(telClient.inIAC) {
		if(telClient.recCommand) {
			if((data != 0) && (data != 1) && (data != 3)) {
				LOG_MSG("Serial%d: Unrecognized telnet option %d",COMNUMBER, data);
				if(telClient.command>250) {
					/* Reject anything we don't recognize */
					response[0]=0xff;
					response[1]=252;
					response[2]=data; /* We won't do crap! */
					if(clientsocket) clientsocket->SendArray(response, 3);
				}
			}
			switch(telClient.command) {
				case 251: /* Will */
					if(data == 0) telClient.binary[TEL_SERVER] = true;
					if(data == 1) telClient.echo[TEL_SERVER] = true;
					if(data == 3) telClient.supressGA[TEL_SERVER] = true;
					break;
				case 252: /* Won't */
					if(data == 0) telClient.binary[TEL_SERVER] = false;
					if(data == 1) telClient.echo[TEL_SERVER] = false;
					if(data == 3) telClient.supressGA[TEL_SERVER] = false;
					break;
				case 253: /* Do */
					if(data == 0) {
						telClient.binary[TEL_CLIENT] = true;
							response[0]=0xff;
							response[1]=251;
							response[2]=0; /* Will do binary transfer */
							if(clientsocket) clientsocket->SendArray(response, 3);
					}
					if(data == 1) {
						telClient.echo[TEL_CLIENT] = false;
							response[0]=0xff;
							response[1]=252;
							response[2]=1; /* Won't echo (too lazy) */
							if(clientsocket) clientsocket->SendArray(response, 3);
					}
					if(data == 3) {
						telClient.supressGA[TEL_CLIENT] = true;
							response[0]=0xff;
							response[1]=251;
							response[2]=3; /* Will Suppress GA */
							if(clientsocket) clientsocket->SendArray(response, 3);
					}
					break;
				case 254: /* Don't */
					if(data == 0) {
						telClient.binary[TEL_CLIENT] = false;
						response[0]=0xff;
						response[1]=252;
						response[2]=0; /* Won't do binary transfer */
						if(clientsocket) clientsocket->SendArray(response, 3);
					}
					if(data == 1) {
						telClient.echo[TEL_CLIENT] = false;
						response[0]=0xff;
						response[1]=252;
						response[2]=1; /* Won't echo (fine by me) */
						if(clientsocket) clientsocket->SendArray(response, 3);
					}
					if(data == 3) {
						telClient.supressGA[TEL_CLIENT] = true;
						response[0]=0xff;
						response[1]=251;
						response[2]=3; /* Will Suppress GA (too lazy) */
						if(clientsocket) clientsocket->SendArray(response, 3);
					}
					break;
				default:
					LOG_MSG("MODEM: Telnet client sent IAC %d", telClient.command);
					break;
			}
			telClient.inIAC = false;
			telClient.recCommand = false;
			return -1; //continue;
		} else {
			if(data==249) {
				/* Go Ahead received */
				telClient.inIAC = false;
				return -1; //continue;
			}
			telClient.command = data;
			telClient.recCommand = true;
			
			if((telClient.binary[TEL_SERVER]) && (data == 0xff)) {
				/* Binary data with value of 255 */
				telClient.inIAC = false;
				telClient.recCommand = false;
					return 0xff;
			}
		}
	} else {
		if(data == 0xff) {
			telClient.inIAC = true;
			return -1;
		}
		return data;
	}
	return -1; // ???
}
	

/*****************************************************************************/
/* setBreak(val) switches break on or off                                   **/
/*****************************************************************************/

void CNullModem::setBreak (bool value) {
	CNullModem::setRTSDTR(getRTS(), getDTR());
}

/*****************************************************************************/
/* updateModemControlLines(mcr) sets DTR and RTS.                           **/
/*****************************************************************************/
void CNullModem::setRTSDTR(bool xrts, bool xdtr) {
	if(!transparent) {
		Bit8u control[2];
		control[0]=0xff;
		control[1]=0x0;
		if(xrts) control[1]|=1;
		if(xdtr) control[1]|=2;
		if(LCR&LCR_BREAK_MASK) control[1]|=4;
		if(clientsocket) clientsocket->SendArray(control, 2);
	}
}
void CNullModem::setRTS(bool val) {
	setRTSDTR(val, getDTR());
}
void CNullModem::setDTR(bool val) {
	setRTSDTR(getRTS(), val);
}
#endif
