/* SerialPort.c */
/*****************************************************************************/
/*                                                                           */
/*    System Dependency Library for Building Portable Software               */
/*    Macintosh Version                                                      */
/*    Written by Thomas R. Lawrence, 1993 - 1994.                            */
/*                                                                           */
/*    This file is Public Domain; it may be used for any purpose whatsoever  */
/*    without restriction.                                                   */
/*                                                                           */
/*    This package 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.                   */
/*                                                                           */
/*    Thomas R. Lawrence can be reached at tomlaw@world.std.com.             */
/*                                                                           */
/*****************************************************************************/

#include "MiscInfo.h"
#include "Audit.h"
#include "Debug.h"
#include "Definitions.h"

#ifdef THINK_C
	#pragma options(pack_enums)
#endif
#include <Serial.h>
#include <Devices.h>
#include <Events.h>
#ifdef THINK_C
	#pragma options(!pack_enums)
#endif

#include "SerialPort.h"
#include "Memory.h"
#include "EventLoop.h"
#include "Array.h"


#define DC1 (17) /* XOn character */
#define DC3 (19) /* XOff character */

/* this value is the length of time we'll wait for a write cell to drain.  if */
/* it doesn't drain in this much time, we return a timeout error. */
/* time units are in 1/60ths of a second */
#define IOTIMEOUT (6*60)  /* 6 seconds to write a byte to the output buffer */

/* this defines the size of the input buffer */
#define INPUTBUFFERSIZE (16384)

/* this defines how many bytes a single asynchronous write operation can handle. */
#define WRITECELLSIZE (384)

/* this defines how many write cells are allocated */
#define NUMWRITECELLS (16)

/* this structure is one write cell buffer, and can be used for one asynchronous */
/* write operation */
typedef struct
	{
		ParamBlockRec			MyPB;
		char							Buffer[WRITECELLSIZE];
		MyBoolean					InUseFlag;
		long							NumBytes;
	} WriteCell;


struct SerialPortRec
	{
		short						InputPortRefNum;
		short						OutputPortRefNum;
		MyBoolean				GracePeriodInEffect;
		char						InputBuffer[INPUTBUFFERSIZE];
		WriteCell				WriteCellArray[NUMWRITECELLS];
	};


struct SerialRefRec
	{
		long						PortIndex;
	};


EXECUTE(static MyBoolean				Initialized = False;)

EXECUTE(static long							RefCount = 0;)

EXECUTE(static ArrayRec*				ListOfRefs;)


/* prototype for callback routine */
#if __powerc
static pascal void	Callback(WriteCell* param);
#else
static pascal void	Callback(void);
#endif


/* initialize serial port subsystem.  the user calls this.  this is not called */
/* from the normal Level 0 initialization since this module is optional. */
MyBoolean						InitializeSerialPorts(void)
	{
		ERROR(Initialized,PRERR(ForceAbort,"InitializeSerialPorts:  already initialized"));
		EXECUTE(RefCount = 0;)
#if DEBUG
		ListOfRefs = NewArray();
		if (ListOfRefs == NIL)
			{
				return False;
			}
#endif
		EXECUTE(Initialized = True;)
		return True;
	}


/* shut down serial ports */
void								ShutdownSerialPorts(void)
	{
		ERROR(!Initialized,PRERR(ForceAbort,"ShutdownSerialPorts:  not initialized"));
		ERROR(RefCount != 0,PRERR(AllowResume,
			"ShutdownSerialPorts:  some connections still open"));
#if DEBUG
		ERROR(ArrayGetLength(ListOfRefs) != 0,PRERR(AllowResume,
			"ShutdownSerialPorts:  some references have not been disposed"));
		DisposeArray(ListOfRefs);
#endif
		EXECUTE(Initialized = False;)
	}


/* request a port.  if the port can not be allocated, it will return NIL. */
SerialPortRec*			RequestSerialPort(long BitsRate, SerialRefRec* PortIdentifier,
											ParityTypes Parity, HandShakeTypes HandShake,
											DataBitTypes NumDataBits, StopBitTypes NumStopBits)
	{
		SerialPortRec*		SerialPort;
		long							Scan;
		OSErr							Error;
		long							Config;
		short							BaudTemp;
		SerShk						Shaker;

		ERROR(!Initialized,PRERR(ForceAbort,"RequestSerialPort:  not initialized"));
		/* create the memory block.  it must never relocate after this. */
		ERROR(ArrayFindElement(ListOfRefs,PortIdentifier) == -1,PRERR(ForceAbort,
			"RequestSerialPort:  invalid reference"));
		CheckPtrExistence(PortIdentifier);
		SerialPort = (SerialPortRec*)AllocPtrCanFail(sizeof(SerialPortRec),"SerialPortRec");
		if (SerialPort == NIL)
			{
			 FailurePoint1:
				return NIL;
			}
		/* open the serial port */
		switch (PortIdentifier->PortIndex)
			{
				default: /* bad serial port ID number */
					EXECUTE(PRERR(ForceAbort,"RequestSerialPort:  bad serial port ID number"));
					break;
				case 0: /* modem port */
					Error = OpenDriver("\p.AOut",&(SerialPort->OutputPortRefNum));
					if (Error != noErr)
						{
							ReleasePtr((char*)SerialPort);
							goto FailurePoint1;
						}
					Error = OpenDriver("\p.AIn",&(SerialPort->InputPortRefNum));
					if (Error != noErr)
						{
							CloseDriver(SerialPort->OutputPortRefNum);
							ReleasePtr((char*)SerialPort);
							goto FailurePoint1;
						}
					break;
				case 1: /* printer port */
					Error = OpenDriver("\p.BOut",&(SerialPort->OutputPortRefNum));
					if (Error != noErr)
						{
							ReleasePtr((char*)SerialPort);
							goto FailurePoint1;
						}
					Error = OpenDriver("\p.BIn",&(SerialPort->InputPortRefNum));
					if (Error != noErr)
						{
							CloseDriver(SerialPort->OutputPortRefNum);
							ReleasePtr((char*)SerialPort);
							goto FailurePoint1;
						}
					break;
			}
		/* clear the in-use fields of the write cells */
		for (Scan = 0; Scan < NUMWRITECELLS; Scan += 1)
			{
				SerialPort->WriteCellArray[Scan].InUseFlag = False;
			}
		/* initialize the various important parameters */
		Config = 0;
		switch (Parity)
			{
				case eParityNone:
					Config |= noParity;
					break;
				case eParityEven:
					Config |= evenParity;
					break;
				case eParityOdd:
					Config |= oddParity;
					break;
				default:
					EXECUTE(PRERR(ForceAbort,"Unsupported parity"));
					break;
			}
		switch (NumDataBits)
			{
				case e8DataBits:
					Config |= data8;
					break;
				case e7DataBits:
					Config |= data7;
					break;
				case e6DataBits:
					Config |= data6;
					break;
				case e5DataBits:
					Config |= data5;
					break;
				default:
					EXECUTE(PRERR(ForceAbort,"Unsupported number of data bits"));
					break;
			}
		switch (NumStopBits)
			{
				case eOneStopBit:
					Config |= stop10;
					break;
				case eOneAndAHalfStopBits:
					Config |= stop15;
					break;
				case eTwoStopBits:
					Config |= stop20;
					break;
				default:
					EXECUTE(PRERR(ForceAbort,"Unsupported number of stop bits"));
					break;
			}
		SerReset(SerialPort->InputPortRefNum,Config);
		SerReset(SerialPort->OutputPortRefNum,Config);
		/* set the baud rate */
		BaudTemp = GetClosestAvailableBaudRate(BitsRate,PortIdentifier);
		Control(SerialPort->OutputPortRefNum,13,&BaudTemp);
		BaudTemp = GetClosestAvailableBaudRate(BitsRate,PortIdentifier);
		Control(SerialPort->InputPortRefNum,13,&BaudTemp);
		/* setting the buffer */
		SerSetBuf(SerialPort->InputPortRefNum,&(SerialPort->InputBuffer[0]),INPUTBUFFERSIZE);
		/* setting handshake parameters */
		Shaker.xOn = DC1;
		Shaker.xOff = DC3;
		Shaker.fXOn = (HandShake == eHandShakeXonXoff);
		Shaker.fCTS = (HandShake == eHandShakeDtrCts) || (HandShake == eHandShakeCtsOnly);
		Shaker.errs = 0;
		Shaker.evts = 0;
		Shaker.fInX = (HandShake == eHandShakeXonXoff);
		Shaker.fDTR = (HandShake == eHandShakeDtrCts) || (HandShake == eHandShakeDtrOnly);
		SerHShake(SerialPort->InputPortRefNum,&Shaker);
		SerHShake(SerialPort->OutputPortRefNum,&Shaker);
		/* connection now established */
		SerialPort->GracePeriodInEffect = False;
		EXECUTE(RefCount += 1;)
		return SerialPort;
	}


/* close a port */
void								CloseSerialPort(SerialPortRec* SerialPort)
	{
		ERROR(!Initialized,PRERR(ForceAbort,"CloseSerialPort:  not initialized"));
		CheckPtrExistence(SerialPort);
		/* make sure no callbacks are waiting to be called */
		KillIO(SerialPort->OutputPortRefNum);
		KillIO(SerialPort->InputPortRefNum);
		/* dump the drivers */
		CloseDriver(SerialPort->InputPortRefNum);
		CloseDriver(SerialPort->OutputPortRefNum);
		/* release the memory */
		ReleasePtr((char*)SerialPort);
		EXECUTE(RefCount -= 1;)
	}


/* get how many serial ports there are on the system */
long								GetNumSerialPorts(void)
	{
		ERROR(!Initialized,PRERR(ForceAbort,"GetNumSerialPorts:  not initialized"));
		return 2;
	}


/* get the ID of a serial port from the list */
SerialRefRec*				GetIndexedSerialPort(long Index)
	{
		SerialRefRec*			Ref;

		ERROR(!Initialized,PRERR(ForceAbort,"GetIndexedSerialPort:  not initialized"));
		ERROR((Index < 0) || (Index >= GetNumSerialPorts()),PRERR(ForceAbort,
			"GetIndexedSerialPort:  port index is out of range"));
		Ref = (SerialRefRec*)AllocPtrCanFail(sizeof(SerialRefRec),"SerialRefRec");
		if (Ref == NIL)
			{
			 FailurePoint1:
				return NIL;
			}
		Ref->PortIndex = Index;
		EXECUTE(if (!ArrayAppendElement(ListOfRefs,Ref)) {ReleasePtr((char*)Ref); Ref = NIL;});
		return Ref;
	}


/* get the name associated with a serial port identifier */
char*								GetSerialPortName(SerialRefRec* TheIdentifier)
	{
		char*							Name;

		ERROR(!Initialized,PRERR(ForceAbort,"GetSerialPortName:  not initialized"));
		ERROR(ArrayFindElement(ListOfRefs,TheIdentifier) == -1,PRERR(ForceAbort,
			"GetSerialPortName:  invalid reference"));
		CheckPtrExistence(TheIdentifier);
		switch (TheIdentifier->PortIndex)
			{
				case 0:  /* modem port */
					Name = AllocPtrCanFail(10,"SerialPortName");
					if (Name != NIL)
						{
							CopyData("Modem Port",Name,10);
						}
					break;
				case 1:  /* printer port */
					Name = AllocPtrCanFail(12,"SerialPortName");
					if (Name != NIL)
						{
							CopyData("Printer Port",Name,12);
						}
					break;
				default:
					EXECUTE(PRERR(ForceAbort,"GetSerialPortName:  bad port number"));
					break;
			}
		return Name;
	}


/* dispose of a serial port reference */
void								DisposeSerialRef(SerialRefRec* TheIdentifier)
	{
		CheckPtrExistence(TheIdentifier);
		ERROR(ArrayFindElement(ListOfRefs,TheIdentifier) == -1,PRERR(ForceAbort,
			"DisposeSerialRef:  invalid reference"));
		EXECUTE(ArrayDeleteElement(ListOfRefs,ArrayFindElement(ListOfRefs,TheIdentifier)));
		ReleasePtr((char*)TheIdentifier);
	}


/* find out the closest available baud rate to the one requested.  if the */
/* requested baud rate is supported, it is returned.  if not, then the closest */
/* available rate is returned. */
long								GetClosestAvailableBaudRate(long RequestedRate,
											SerialRefRec* PortIdentifier)
	{
		ERROR(!Initialized,PRERR(ForceAbort,
			"GetClosestAvailableBaudRate:  not initialized"));
		ERROR(ArrayFindElement(ListOfRefs,PortIdentifier) == -1,PRERR(ForceAbort,
			"GetClosestAvailableBaudRate:  invalid reference"));
		ERROR((PortIdentifier->PortIndex < 0) || (PortIdentifier->PortIndex >= 2),
			PRERR(ForceAbort,"GetClosestAvailableBaudRate:  bad port number"));
		CheckPtrExistence(PortIdentifier);
		/* for Macintosh, this doesn't actually depend on the port kind, but on */
		/* some systems it might */
		if (RequestedRate < 300)
			{
				return 300;
			}
		else if (RequestedRate > 57600)
			{
				return 57600;
			}
		else
			{
				return RequestedRate;
			}
	}


/* find out how much data is waiting to be read */
long								NumSerialPortBytesWaitingToRead(SerialPortRec* SerialPort)
	{
		long							NumBytes;

		ERROR(!Initialized,PRERR(ForceAbort,
			"NumSerialPortBytesWaitingToRead:  not initialized"));
		CheckPtrExistence(SerialPort);
		SerGetBuf(SerialPort->InputPortRefNum,&NumBytes);
		return NumBytes;
	}


/* find out how much data is waiting to leave the local buffers */
long								NumSerialPortBytesWaitingToWrite(SerialPortRec* SerialPort)
	{
		long							Scan;
		long							Count;

		ERROR(!Initialized,PRERR(ForceAbort,
			"NumSerialPortBytesWaitingToWrite:  not initialized"));
		CheckPtrExistence(SerialPort);
		Count = 0;
		for (Scan = 0; Scan < NUMWRITECELLS; Scan += 1)
			{
				if (SerialPort->WriteCellArray[Scan].InUseFlag)
					{
						/* if the flag gets cleared right before we read it, well, too bad... */
						Count += SerialPort->WriteCellArray[Scan].NumBytes;
					}
			}
		return Count;
	}


/* read some bytes from the port buffer into the specified buffer.  it is an */
/* error to read more bytes than there are waiting. */
void								ReadSerialPort(SerialPortRec* SerialPort, long NumBytesToRead,
											char* Buffer)
	{
		OSErr							Error;
		long							OldNumBytes;

		ERROR(!Initialized,PRERR(ForceAbort,"ReadSerialPort:  not initialized"));
		ERROR(NumSerialPortBytesWaitingToRead(SerialPort) < NumBytesToRead,
			PRERR(AllowResume,"ReadSerialPort:  reading too many bytes"));
		EXECUTE(OldNumBytes = NumBytesToRead;)
		Error = FSRead(SerialPort->InputPortRefNum,&NumBytesToRead,&(Buffer[0]));
		ERROR(OldNumBytes != NumBytesToRead,PRERR(ForceAbort,
			"ReadSerialPort: [FSRead] read error -- didn't read all the bytes even though they exist"));
	}


/* submit bytes to be written.  it returns True if successful, or False if */
/* the operation timed out or another error occurred */
MyBoolean						WriteSerialPort(SerialPortRec* SerialPort, long Length, char* Data)
	{
		long						StartTime;
		short						ChosenWriteCell;
		short						Scan;

		ERROR(!Initialized,PRERR(ForceAbort,"WriteSerialPort:  not initialized"));
		CheckPtrExistence(SerialPort);

		if (Length == 0)
			{
				return True;
			}

		/* entry point for writing chunks of data */
	 ReEntryPoint:
		ChosenWriteCell = -1;
		StartTime = TickCount();
		/* here we loop until we can find a free write cell */
		while (ChosenWriteCell == -1)
			{
				/* check timeout */
				if (TickCount() - StartTime > IOTIMEOUT)
					{
						/* a timeout occurred because we looped for a while and were unable */
						/* to write any data. */
						return False;
					}
				/* look for available write cells */
				for (Scan = 0; (Scan < NUMWRITECELLS) && (ChosenWriteCell == -1); Scan += 1)
					{
						if (!SerialPort->WriteCellArray[Scan].InUseFlag)
							{
								ChosenWriteCell = Scan;
							}
					}
				/* relinquish CPU so that user can do something productive while waiting. */
				if (ChosenWriteCell == -1)
					{
						RelinquishCPUCheckCancel();
					}
			}

		/* we found a buffer, so we can set it up for the async. write */
		SerialPort->WriteCellArray[ChosenWriteCell].InUseFlag = True;
		SerialPort->WriteCellArray[ChosenWriteCell].MyPB.ioParam.ioCompletion
			= NewIOCompletionProc(&Callback);
		SerialPort->WriteCellArray[ChosenWriteCell].MyPB.ioParam.ioVRefNum = 0;
		SerialPort->WriteCellArray[ChosenWriteCell].MyPB.ioParam.ioRefNum
			= SerialPort->OutputPortRefNum;
		SerialPort->WriteCellArray[ChosenWriteCell].MyPB.ioParam.ioBuffer
			= SerialPort->WriteCellArray[ChosenWriteCell].Buffer;
		SerialPort->WriteCellArray[ChosenWriteCell].MyPB.ioParam.ioPosMode = fsAtMark;

		/* how much to write? */
		if (Length > WRITECELLSIZE)
			{
				/* if the pending amount of data is larger than the size of a */
				/* write cell, then we only write the first writecell's worth of data */
				SerialPort->WriteCellArray[ChosenWriteCell].MyPB.ioParam.ioReqCount
					= WRITECELLSIZE;
				SerialPort->WriteCellArray[ChosenWriteCell].NumBytes = WRITECELLSIZE;
				CopyData(Data,SerialPort->WriteCellArray[ChosenWriteCell].Buffer,WRITECELLSIZE);
				PBWrite((ParamBlockRec*)&(SerialPort->WriteCellArray[ChosenWriteCell]),
					True/*async*/);
				Length -= WRITECELLSIZE;
				Data += WRITECELLSIZE;
				goto ReEntryPoint;
			}
		 else
			{
				SerialPort->WriteCellArray[ChosenWriteCell].MyPB.ioParam.ioReqCount = Length;
				SerialPort->WriteCellArray[ChosenWriteCell].NumBytes = Length;
				CopyData(Data,SerialPort->WriteCellArray[ChosenWriteCell].Buffer,Length);
				PBWrite((ParamBlockRec*)&(SerialPort->WriteCellArray[ChosenWriteCell]),
					True/*async*/);
			}
		if (TickCount() - StartTime > 1)
			{
				APRINT(("WriteBlock took %l ticks",(long)(TickCount() - StartTime)));
			}

		/* all done */
		return True;
	}


/* the callback routine must NOT be profiled */
#ifdef THINK_C
	#if __option(profile)
		#define ProfilingEnabled (True)
	#else
		#define ProfilingEnabled (False)
	#endif

	#pragma options(!profile)
#endif

#if __powerc
static void		Callback(WriteCell* param)
{
		param->InUseFlag = 0;
		param->NumBytes = 0;
}
#else
static pascal void	Callback(void)
	{
#ifndef __cplusplus
		register WriteCell*		HiddenParameter;

		asm{move.l A0,HiddenParameter}
		HiddenParameter->InUseFlag = 0;
		HiddenParameter->NumBytes = 0;
#else
		/* 00000000: 2F0C               MOVE.L    A4,-(A7) */
		/* 00000002: 2848               MOVEA.L   A0,A4 */
		/* 00000004: 426C 01D0          CLR.W     $01D0(A4) */
		/* 00000008: 42AC 01D2          CLR.L     $01D2(A4) */
		/* 0000000C: 285F               MOVEA.L   (A7)+,A4 */
		asm(0x2f0c,0x2848,0x426c,0x01d0,0x42ac,0x01d2,0x285f);
#endif
	}
#endif

#ifdef THINK_C
	#if ProfilingEnabled
		#pragma options(profile)
	#endif
#endif
