/******************************************************************************
 *
 * Copyright (c) 1997-1999 Palm Computing, Inc. or its subsidiaries.
 * All rights reserved.
 *
 * File: MemoTransfer.c
 *
 * Description:
 *      Memo Book routines to transfer records.
 *
 * History:
 *      9/17/97  roger - Created
 *
 *****************************************************************************/

#include <PalmOS.h>

#include "MemoDB.h"
#include "MemoMain.h"
#include "MemoRsc.h"


#define identifierLengthMax		40
#define mimeVersionString			"MIME-Version: 1.0\015\012"
#define mimeMultipartString 		"Content-type: multipart/mixed;"
#define mimeBoundaryString 		"boundary="
#define memoFilenameExtension		".txt"
#define simpleBoundary				"simple boundary"
#define delimiter						"--" simpleBoundary
#define crlf							"\015\012"

#define importBufferMaxLength		 80

#define stringZLen -1 				// pass to WriteFunc to calculate strlen


// Stream interface to exgsockets to optimize performance
#define maxStreamBuf 512  // MUST BE LARGER than importBufferMaxLength
 
typedef struct StreamType {
	ExgSocketPtr socket;
	UInt16 pos;
	UInt16 len;
	UInt16 bufSize;
	Char   buf[maxStreamBuf];
} StreamType;



/***********************************************************************
 *
 * FUNCTION:    PrvStreamInit
 *
 * DESCRIPTION: Function to put Initialize a stream socket. 
 *
 * PARAMETERS:  streamP - the output stream
 *				exgSocketP - pointer to an intitialized exgSocket
 *
 * RETURNED:    nothing
 *
 * REVISION HISTORY:
 *         Name   Date      Description
 *         ----   ----      -----------
 *		   gavin   10/5/99   Initial revision
 *
 ***********************************************************************/
static void PrvStreamInit(StreamType *streamP, ExgSocketPtr exgSocketP)
{
	streamP->socket = exgSocketP;
	streamP->bufSize = maxStreamBuf;
	streamP->pos = 0;
	streamP->len = 0;
}

/***********************************************************************
 *
 * FUNCTION:    PrvStreamFlush
 *
 * DESCRIPTION: Function to put a string to the exg transport. 
 *
 * PARAMETERS:  streamP - the output stream
 *
 * RETURNED:    nothing
 *
 * REVISION HISTORY:
 *         Name   Date      Description
 *         ----   ----      -----------
 *		   gavin   10/5/99   Initial revision
 *
 ***********************************************************************/
static void PrvStreamFlush(StreamType *streamP)
{
	Err err = 0;
	while (streamP->len && !err)
		streamP->len -= ExgSend(streamP->socket,streamP->buf,streamP->len,&err);

}

/***********************************************************************
 *
 * FUNCTION:    PrvStreamWrite
 *
 * DESCRIPTION: Function to put a string to the exg transport. 
 *
 * PARAMETERS:  streamP - the output stream
 *				stringP - the string to put
 *
 * RETURNED:    nothing
 *				If the all the string isn't sent an error is thrown using ErrThrow.
 *
 * REVISION HISTORY:
 *         Name   Date      Description
 *         ----   ----      -----------
 *		   gavin   10/5/99   Initial revision
 *
 ***********************************************************************/
static UInt32 PrvStreamWrite(StreamType *streamP, const Char * stringP, Int32 length, Err *errP)
{
	UInt32 count = 0;
	*errP = 0;
	
	while (count < length && !*errP)
	{
		if (streamP->len < streamP->bufSize) 
			{
			streamP->buf[streamP->len++] = *stringP++;
			count++;
			}
		else
		    streamP->len -= ExgSend(streamP->socket, streamP->buf, streamP->len, errP);
	}
	return count;
}

/***********************************************************************
 *
 * FUNCTION:    PrvStreamRead
 *
 * DESCRIPTION: Function to get a character from the input stream. 
 *
 * PARAMETERS:  streamP - the output stream
 *
 * RETURNED:    a character of EOF if no more data
 *
 * REVISION HISTORY:
 *         Name   Date      Description
 *         ----   ----      -----------
 *		   gavin   10/5/99   Initial revision
 *
 ***********************************************************************/
static UInt32 PrvStreamRead(StreamType * streamP, Char *bufP, UInt32 length, Err *errP)
{
	UInt32 count = 0;
	
	*errP = 0;
	while (count < length)
	{
		if (streamP->pos < streamP->len)
			bufP[count++] = streamP->buf[streamP->pos++];
		else
		{	streamP->pos = 0;
			streamP->len = ExgReceive(streamP->socket, streamP->buf, streamP->bufSize, errP);
			if (!streamP->len || *errP)
				break;
		}
	}
	return count;
}

/***********************************************************************
 *
 * FUNCTION:    PrvStreamSocket
 *
 * DESCRIPTION: returns the socket from a stream. 
 *
 * PARAMETERS:  streamP - the output stream
 *
 * RETURNED:    The socket associated with the stream
 *
 * REVISION HISTORY:
 *         Name   Date      Description
 *         ----   ----      -----------
 *		   gavin   10/5/99   Initial revision
 *
 ***********************************************************************/
static ExgSocketPtr PrvStreamSocket(StreamType *streamP)
{
	return streamP->socket;
}

/***********************************************************************
 *
 * FUNCTION:    GetChar
 *
 * DESCRIPTION: Function to get a character from the exg transport. 
 *
 * PARAMETERS:  exgSocketP - the exg connection
 *
 * RETURNED:    nothing
 *
 * REVISION HISTORY:
 *         Name   Date      Description
 *         ----   ----      -----------
 *         roger   8/15/97   Initial Revision
 *
 ***********************************************************************/
static UInt32 ReadFunction(const void * stream, Char * bufferP, UInt32 length)
{
	Err err;
	UInt32 bytesRead;
	bytesRead = PrvStreamRead((StreamType *)stream, bufferP, length, &err);
	if (err) ErrThrow(err);
	return bytesRead;
}


/***********************************************************************
 *
 * FUNCTION:    PutString
 *
 * DESCRIPTION: Function to put a string to the exg transport. 
 *
 * PARAMETERS:  exgSocketP - the exg connection
 *					 stringP - the string to put
 *
 * RETURNED:    nothing
 *					 If the all the string isn't sent an error is thrown using ErrThrow.
 *
 * REVISION HISTORY:
 *         Name   Date      Description
 *         ----   ----      -----------
 *         roger   8/15/97   Initial Revision
 *
 ***********************************************************************/
static UInt32 WriteFunction(void * stream, const Char * const bufferP, Int32 length)
{
	UInt32 len;
	Err err;
	
	// passing -1 length will assume a null terminated string
	if (length == -1)  length = StrLen(bufferP);
	
	len = PrvStreamWrite( stream, bufferP, length, &err);
	
	// If the bytes were not sent throw an error.
	if (len == 0 && length > 0)
		ErrThrow(err);
		
	return len;
}


/***********************************************************************
 *
 * FUNCTION:    SetDescriptionAndFilename
 *
 * DESCRIPTION: Derive and allocate a decription and filename from some text. 
 *
 * PARAMETERS:  textP - the text string to derive the names from
 *					 descriptionPP - pointer to set to the allocated description 
 *					 descriptionHP - MemHandle to set to the allocated description 
 *					 filenamePP - pointer to set to the allocated filename 
 *					 filenameHP - MemHandle to set to the allocated description 
 *
 * RETURNED:    a description and filename are allocated and the pointers are set
 *
 * REVISION HISTORY:
 *         Name   Date      Description
 *         ----   ----      -----------
 *         roger   11/4/97   Initial Revision
 *
 ***********************************************************************/
static void SetDescriptionAndFilename(Char * textP, Char **descriptionPP, 
	MemHandle *descriptionHP, Char **filenamePP, MemHandle *filenameHP)
{
	Char * descriptionP;
   Int16 descriptionSize;
	Coord descriptionWidth;
	Boolean descriptionFit;
	Char * spaceP;
	Char * filenameP;
	UInt8 filenameLength;
	Coord unused;
	
	
	descriptionSize = StrLen(textP);
	WinGetDisplayExtent(&descriptionWidth, &unused);
	FntCharsInWidth (textP, &descriptionWidth, &descriptionSize, &descriptionFit);
	
	if (descriptionSize > 0)
		{
		*descriptionHP = MemHandleNew(descriptionSize+sizeOf7BitChar('\0'));
		if (*descriptionHP)
			{
			descriptionP = MemHandleLock(*descriptionHP);
			MemMove(descriptionP, textP, descriptionSize);
			descriptionP[descriptionSize] = nullChr;
			}
		}
	else
		{
		*descriptionHP = DmGetResource(strRsc, BeamDescriptionStr);
		descriptionP = MemHandleLock(*descriptionHP);
		}
	
		
	if (descriptionSize > 0)
		{
		// Now form a file name.  Use only the first word or two.
		spaceP = StrChr(descriptionP, spaceChr);
		if (spaceP)
			// Check for a second space
			spaceP = StrChr(spaceP + sizeOf7BitChar(spaceChr), spaceChr);
		
		// If at least two spaces were found then use only that much of the description.
		// If less than two spaces were found then use all of the description.
		if (spaceP)
			filenameLength = spaceP - descriptionP;
		else
			filenameLength = StrLen(descriptionP);
		
		
		// Allocate space and form the filename
		*filenameHP = MemHandleNew(filenameLength + StrLen(memoFilenameExtension) + sizeOf7BitChar('\0'));
		filenameP = MemHandleLock(*filenameHP);
		if (filenameP)
			{
			MemMove(filenameP, descriptionP, filenameLength);
			MemMove(&filenameP[filenameLength], memoFilenameExtension, 
				StrLen(memoFilenameExtension) + sizeOf7BitChar('\0'));
			}
		}
	else
		{
		*filenameHP = DmGetResource(strRsc, BeamFilenameStr);
		filenameP = MemHandleLock(*filenameHP);
		}
	
	
	*descriptionPP = descriptionP;
	*filenamePP = filenameP;
}


/***********************************************************************
 *
 * FUNCTION:    MemoSendRecordTryCatch
 *
 * DESCRIPTION: Send a record.  
 *
 * PARAMETERS:	 dbP - pointer to the database to add the record to
 * 				 recordNum - the record number to send
 * 				 recordP - pointer to the record to send
 * 				 exgSocketP - the exchange socket used to send
 *
 * RETURNED:    0 if there's no error
 *
 * REVISION HISTORY:
 *         Name   Date      Description
 *         ----   ----      -----------
 *         roger  12/11/97  Initial Revision
 *
 ***********************************************************************/
static Err MemoSendRecordTryCatch (DmOpenRef dbP, Int16 recordNum, 
	MemoDBRecordPtr recordP, ExgSocketPtr exgSocketP)
{
   volatile Err error = 0;
   StreamType stream;
	
		PrvStreamInit(&stream, exgSocketP);
		
		// An error can happen anywhere during the send process.  It's easier just to 
		// catch the error.  If an error happens, we must pass it into ExgDisconnect.
		// It will then cancel the send and display appropriate ui.
		ErrTry
			{
			MemoExportMime(dbP, recordNum, recordP, &stream, WriteFunction, true, false);
			}
		
		ErrCatch(inErr)
			{
			error = inErr;
			} ErrEndCatch

		PrvStreamFlush( &stream);
	return error;
}


/***********************************************************************
 *
 * FUNCTION:    MemoSendRecord
 *
 * DESCRIPTION: Send a record.  
 *
 * PARAMETERS:		dbP - pointer to the database to add the record to
 * 					recordNum - the record to send
 *
 * RETURNED:    true if the record is found and sent
 *
 * REVISION HISTORY:
 *         Name   Date      Description
 *         ----   ----      -----------
 *         roger   5/9/97   Initial Revision
 *
 ***********************************************************************/
extern void MemoSendRecord (DmOpenRef dbP, Int16 recordNum)
{
   MemoDBRecordPtr recordP;
   MemHandle recordH;
   MemHandle descriptionH;
   Err error;
	ExgSocketType exgSocket;
	MemHandle nameH;
	
	
	// important to init structure to zeros...
	MemSet(&exgSocket, sizeof(exgSocket), 0);
	
	// Form a description of what's being sent.  This will be displayed
	// by the system send dialog on the sending and receiving devices.
   recordH = DmQueryRecord (dbP, recordNum);
   recordP = (MemoDBRecordType *) MemHandleLock(recordH);
   
   // Set the description to be the beginning of the memo
	descriptionH = NULL;
	exgSocket.description = NULL;
	
	// Set the exg description to the record's description.
	SetDescriptionAndFilename(&recordP->note, &exgSocket.description, 
		&descriptionH, &exgSocket.name, &nameH);
	
	
	exgSocket.length = MemHandleSize(recordH);		// rough guess
	exgSocket.target = sysFileCMemo;
	error = ExgPut(&exgSocket);   // put data to destination
	if (!error)
		{
		error = MemoSendRecordTryCatch(dbP, recordNum, recordP, &exgSocket);

		ExgDisconnect(&exgSocket, error);
		}
	
	
	// Clean up
	if (descriptionH)
		{
		MemHandleUnlock (descriptionH);
		if (MemHandleDataStorage (descriptionH))
			DmReleaseResource(descriptionH);
		else
			MemHandleFree(descriptionH);
		}
	if (nameH)
		{
		MemHandleUnlock (nameH);
		if (MemHandleDataStorage (nameH))
			DmReleaseResource(nameH);
		else
			MemHandleFree(nameH);
		}
	MemHandleUnlock(recordH);
	
	
	return;
}


/***********************************************************************
 *
 * FUNCTION:    MemoSendCategoryTryCatch
 *
 * DESCRIPTION: Send all visible records in a category.  
 *
 * PARAMETERS:		dbP - pointer to the database to add the record to
 * 					categoryNum - the category of records to send
 * 					exgSocketP - the exchange socket used to send
 * 					index - the record number of the first record in the category to send
 *
 * RETURNED:    0 if there's no error
 *
 * REVISION HISTORY:
 *         Name   Date      Description
 *         ----   ----      -----------
 *         roger  12/11/97  Initial Revision
 *
 ***********************************************************************/
static Err MemoSendCategoryTryCatch (DmOpenRef dbP, UInt16 categoryNum, 
	ExgSocketPtr exgSocketP, UInt16 index)
{
	volatile Err error = 0;
	volatile MemHandle outRecordH = 0;
   MemoDBRecordType *outRecordP;
	StreamType stream;
	
	// An error can happen anywhere during the send process.  It's easier just to 
	// catch the error.  If an error happens, we must pass it into ExgDisconnect.
	// It will then cancel the send and display appropriate ui.
	ErrTry
		{

		PrvStreamInit(&stream, exgSocketP);
		
		// Write out the beginning of a multipart mime message
		WriteFunction(&stream, 
			mimeVersionString
			"Content-type: multipart/mixed; boundary=\"" simpleBoundary "\"" crlf crlf, stringZLen);
      
		// Loop through all records in the category.
		while (DmSeekRecordInCategory(dbP, &index, 0, dmSeekForward, categoryNum) == 0)
			{
			// Emit the record.  If the record is private do not emit it.
			outRecordH = DmQueryRecord (dbP, index);
			
			if (outRecordH != 0)
				{
				outRecordP = (MemoDBRecordType *) MemHandleLock(outRecordH);
				
				// Emit a mime boundary
				WriteFunction(&stream, delimiter crlf, stringZLen);
				
				MemoExportMime(dbP, index, outRecordP, &stream, WriteFunction, true, true);
				
				MemHandleUnlock(outRecordH);
				}
			
			index++;
			}
		outRecordH = 0;
		dbP = 0;
		
		// All done.  Write out an epilogue.
		WriteFunction(&stream, delimiter "--" crlf crlf, stringZLen);
		}
	
	ErrCatch(inErr)
		{
		error = inErr;
		
		if (outRecordH)
			MemHandleUnlock(outRecordH);
		} ErrEndCatch
	
	PrvStreamFlush(&stream);
	
	return error;	
}

/***********************************************************************
 *
 * FUNCTION:    MemoSendCategory
 *
 * DESCRIPTION: Send all visible records in a category.  
 *
 * PARAMETERS:		 dbP - pointer to the database to add the record to
 * 					 categoryNum - the category of records to send
 *
 * RETURNED:    true if any records are found and sent
 *
 * REVISION HISTORY:
 *         Name   Date      Description
 *         ----   ----      -----------
 *         roger   5/9/97   Initial Revision
 *
 ***********************************************************************/
extern void MemoSendCategory (DmOpenRef dbP, UInt16 categoryNum)
{
	Err error;
   Char description[dmCategoryLength];
	UInt16 index;
	Boolean foundAtLeastOneRecord;
	ExgSocketType exgSocket;
	UInt16 mode;
	LocalID dbID;
	UInt16 cardNo;
	Boolean databaseReopened;
	
	
	// If the database was opened to show secret records, reopen it to not see 
	// secret records.  The idea is that secret records are not sent when a 
	// category is sent.  They must be explicitly sent one by one.
	DmOpenDatabaseInfo(dbP, &dbID, NULL, &mode, &cardNo, NULL);
	if (mode & dmModeShowSecret)
		{
		dbP = DmOpenDatabase(cardNo, dbID, dmModeReadOnly);
		databaseReopened = true;
		}
	else
		databaseReopened = false;
	
	
	// important to init structure to zeros...
	MemSet(&exgSocket, sizeof(exgSocket), 0);
	
	
	// Make sure there is at least one record in the category.
	index = 0;
	foundAtLeastOneRecord = false;
	while (true)
		{
		if (DmSeekRecordInCategory(dbP, &index, 0, dmSeekForward, categoryNum) != 0)
			break;
		
		foundAtLeastOneRecord = DmQueryRecord(dbP, index) != 0;
		if (foundAtLeastOneRecord)
			break;
		
		
		index++;
		}
	
	
	// We should send the category because there's at least one record to send.
	if (foundAtLeastOneRecord)
		{
		// Form a description of what's being sent.  This will be displayed
		// by the system send dialog on the sending and receiving devices.
		CategoryGetName (dbP, categoryNum, description);
		exgSocket.description = description;
		
		// Now form a file name
		exgSocket.name = MemPtrNew(StrLen(description) + StrLen(memoFilenameExtension) + sizeOf7BitChar('\0'));
		if (exgSocket.name)
			{
			StrCopy(exgSocket.name, description);
			StrCat(exgSocket.name, memoFilenameExtension);
			}
		
		exgSocket.length = 0;		// rough guess
		exgSocket.target = sysFileCMemo;
		error = ExgPut(&exgSocket);   // put data to destination
		
		if (!error)
			{
			error = MemoSendCategoryTryCatch (dbP, categoryNum, &exgSocket, index);
			
			ExgDisconnect(&exgSocket, error);
			}
		
		// Clean up
		if (exgSocket.name)
			MemPtrFree(exgSocket.name);
		}
	else
		FrmAlert(NoDataToBeamAlert);

	if (databaseReopened)
		DmCloseDatabase(dbP);
	
	return;
}


/***********************************************************************
 *
 * FUNCTION:		ReceiveData
 *
 * DESCRIPTION:		Receives data into the output field using the Exg API
 *
 * PARAMETERS:		dbP - database to put received memos in
 *						exgSocketP - socket from the app code sysAppLaunchCmdExgReceiveData
 *						numRecordsReceivedP - number of records received is returned here
 *
 * RETURNED:		error code or zero for no error.
 *
 *	REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			grant	6/25/99	Keep count of received records and return in numRecordsReceivedP
 *
 ***********************************************************************/
extern Err MemoReceiveData(DmOpenRef dbP, ExgSocketPtr exgSocketP, UInt16 *numRecordsReceivedP)
{
	volatile Err err;
	UInt16 numRecordsReceived;
	StreamType stream;
	
	// initialize new record count
	ErrNonFatalDisplayIf(numRecordsReceivedP == NULL, "NULL numRecordsReceivedP");
	*numRecordsReceivedP = 0;
	
	PrvStreamInit( &stream, exgSocketP);
	
	// accept will open a progress dialog and wait for your receive commands
	err = ExgAccept(exgSocketP);
	
	if (!err)
		{
		// Catch errors receiving records.  The import routine will clean up the
		// incomplete record.  This routine displays an error message.
		ErrTry
			{
			// Keep importing records until it can't
			while (MemoImportMime(dbP, &stream, ReadFunction, false, false, &numRecordsReceived))
				{
				*numRecordsReceivedP += numRecordsReceived;
				};
			
			// catch the records from the final MemoImportMime
			*numRecordsReceivedP += numRecordsReceived;
			}
		
		ErrCatch(inErr)
			{
			err = inErr;
			} ErrEndCatch
		
		ExgDisconnect(exgSocketP, err); // closes transfer dialog
		}
	
	return err;
}


/***********************************************************************
 *
 * FUNCTION:		MemoSetGoToParams
 *
 * DESCRIPTION:	Store the information necessary to navigate to the 
 *                record inserted into the launch code's parameter block.
 *
 * PARAMETERS:		 dbP        - pointer to the database to add the record to
 *						 exgSocketP - parameter block passed with the launch code
 *						 uniqueID   - unique id of the record inserted
 *
 * RETURNED:		nothing
 *
 * REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			art	10/17/97	Created
 *
 ***********************************************************************/
static void MemoSetGoToParams (DmOpenRef dbP, ExgSocketPtr exgSocketP, UInt32 uniqueID)
{
	UInt16	recordNum;
	UInt16	cardNo;
	LocalID 	dbID;

	
	if (! uniqueID) return;

	DmOpenDatabaseInfo (dbP, &dbID, NULL, NULL, &cardNo, NULL);

	// The this the the first record inserted, save the information
	// necessary to navigate to the record.
	if (! exgSocketP->goToParams.uniqueID)
		{
		DmFindRecordByID (dbP, uniqueID, &recordNum);

		exgSocketP->goToCreator = sysFileCMemo;
		exgSocketP->goToParams.uniqueID = uniqueID;
		exgSocketP->goToParams.dbID = dbID;
		exgSocketP->goToParams.dbCardNo = cardNo;
		exgSocketP->goToParams.recordNum = recordNum;
		}

	// If we already have a record then make sure the record index 
	// is still correct.  Don't update the index if the record is not
	// in your the app's database.
	else if (dbID == exgSocketP->goToParams.dbID &&
			   cardNo == exgSocketP->goToParams.dbCardNo)
		{
		DmFindRecordByID (dbP, exgSocketP->goToParams.uniqueID, &recordNum);

		exgSocketP->goToParams.recordNum = recordNum;
		}
}


/************************************************************
 *
 * FUNCTION: ReadThroughCRLF
 *
 * DESCRIPTION: Consume data up to and including the next CRLF.
 *
 * PARAMETERS: 
 *			inputStreamP	- pointer to where to import from
 *			bufferP - where the input stream is stored
 *			bufferLengthP - the length of bufferP used
 *
 * RETURNED: error code or zero for no error.
 *
 * ASSUMPTIONS:
 *			Buffer is full when called
 *  		Buffer is big enough to hold a full line (including LF)
 *			  ...so CR/LF will never split
 *	END CONDITION:
 *			Buffer is full when routine exits.
 *
 *
 *	REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			bob	1/26/98  initial revision
 *
 *************************************************************/
static Err ReadThroughCRLF(ReadFunctionF inputFunc, void * inputStreamP, Char * bufferP, UInt16 * bufferLengthP)
{
	char *c;
	UInt16 charsRead;
	Err err = 0;
	Boolean gotOne = false;
	
	while (*bufferLengthP > 0 && !gotOne)
	{
		c = StrChr(bufferP, crChr);
		if (c == NULL)
			c = &bufferP[*bufferLengthP];		// end of the buffer
		else if (c < bufferP + *bufferLengthP - 1)		// guard against buffer splitting cr/lf
		{
			c += sizeOf7BitChar(crChr) + sizeOf7BitChar(linefeedChr);
			gotOne = true;
		}
		
		// Consume everything up to the CR/NULL
		MemMove(bufferP, c, &bufferP[*bufferLengthP] - c);
		*bufferLengthP = &bufferP[*bufferLengthP] - c;

		// Read in more chars
		charsRead = inputFunc(inputStreamP, bufferP + *bufferLengthP, importBufferMaxLength - *bufferLengthP);
		*bufferLengthP += charsRead;
		bufferP[*bufferLengthP] = nullChr;
	}
	
	return err;
}


/************************************************************
 *
 * FUNCTION: MemoImportFinishRecord
 *
 * DESCRIPTION: Make sure record is null terminated, and close it.
 *
 * PARAMETERS: 
 *			dbP - pointer to the database to add the record to
 *			indexNew	- index of new record
 *			newRecordH - MemHandle to new record
 *			newRecordSize - bytes currently in new record
 *
 *	REVISION HISTORY:
 *			Name	Date			Description
 *			----	----			-----------
 *			bob	1998-02-05	Moved out of MemoImportMime
 *
 *************************************************************/
static void MemoImportFinishRecord(DmOpenRef dbP, UInt16 indexNew, MemHandle *newRecordHPtr, UInt16 *newRecordSizePtr, void * inputStream)
{
	Char * newRecordP;
	MemHandle newHandle;						// Used to follow resized records which move


	ErrNonFatalDisplayIf(*newRecordHPtr == NULL, "Null record MemHandle.");
	
	// Make sure the record is nullChr terminated
	newRecordP = MemHandleLock(*newRecordHPtr);
	if (newRecordP[*newRecordSizePtr - sizeof(char)] != nullChr)
	{
		MemHandleUnlock(*newRecordHPtr);
		newHandle = DmResizeRecord(dbP, indexNew, *newRecordSizePtr + sizeOf7BitChar(nullChr));
		if (newHandle)
			*newRecordHPtr = newHandle;
		else
			ErrThrow(true);
		
		newRecordP = MemHandleLock(*newRecordHPtr);
		DmWrite(newRecordP, *newRecordSizePtr, "", sizeOf7BitChar(nullChr));
	}

	// let the record go
	MemHandleUnlock(*newRecordHPtr);
	DmReleaseRecord(dbP, indexNew, true);
	
	
	*newRecordHPtr = NULL;
	*newRecordSizePtr = 0;
}


/************************************************************
 *
 * FUNCTION: MemoImportMime
 *
 * DESCRIPTION: Import a Mime record.
 *
 * PARAMETERS: 
 *			dbP - pointer to the database to add the record to
 *			inputStream	- pointer to where to import the record from
 *			inputFunc - function to get input from the stream
 *			obeyUniqueIDs - true to obey any unique ids if possible
 *			beginAlreadyRead - whether the begin statement has been read
 *			numRecordsRecievedP - number of records received
 *
 * RETURNS: true if the input was read
 *
 *	REVISION HISTORY:
 *			Name	Date			Description
 *			----	----			-----------
 *			rsf	4/24/97		Initial Revision
 *			bob	1998-01-26	re-wrote MIME parser part to get delimiters right
 *			grant	6/25/99		Return count of records received in numRecordsReceivedP.
 *			kwk	06/25/99		Moved return out of ErrTry block.
 *
 *************************************************************/
extern Boolean 
MemoImportMime(DmOpenRef dbR, void * inputStream, ReadFunctionF inputFunc, 
	Boolean /* obeyUniqueIDs */, Boolean /* beginAlreadyRead */, UInt16 *numRecordsReceivedP)
{
	char *c;
	char boundaryString[69+2] = "";
	MemHandle newRecordH = NULL;
	Char * newRecordP;
	MemHandle newHandle;						// Used to follow resized records which move
	volatile UInt16 indexNew = dmMaxRecordIndex;
	volatile DmOpenRef dbP = dbR;
	volatile Err err = 0;
	Char buffer[importBufferMaxLength + 1];
	UInt16 bufferLength = 0;
	UInt16 charsRead;
	UInt16 charsToWrite;
	UInt16 newRecordSize = 0;
	Char * nextCrChr;
	int addCr;
	Char * boundaryP;
	Char * boundaryEndP;
	UInt16 numRecordsReceived = 0;
	UInt32 firstRecordID = 0;
	
	// Keep the buffer always null terminated so we can use string functions on it.
	buffer[importBufferMaxLength] = nullChr;
	
	// Read chars into the buffer
	charsRead = inputFunc( inputStream, buffer, importBufferMaxLength - bufferLength);
	bufferLength += charsRead;
	buffer[bufferLength] = nullChr;
	
	if (charsRead == 0)
		return false;
		
	// An error happens usually due to no memory.  It's easier just to 
	// catch the error.  If an error happens, we remove the last record.  
	// Then we throw a second time so the caller receives it and displays a message.
	ErrTry
	{
		// MIME start, find MIME ID and version
		if (StrNCompare(buffer, mimeVersionString, StrLen(mimeVersionString)) == 0)
		{
			// Remove the MIME header
			MemMove(buffer, &buffer[StrLen(mimeVersionString)], bufferLength - StrLen(mimeVersionString));
			bufferLength -= StrLen(mimeVersionString);
			
			// Read chars into the buffer
			charsRead = inputFunc( inputStream, &buffer[bufferLength], importBufferMaxLength - bufferLength);
			bufferLength += charsRead;
			buffer[bufferLength] = nullChr;
			
			// scan header for a multi-part identifier
			// skip anything else until we get an entirely blank line
			do {
				if (StrNCompare(buffer, mimeMultipartString, StrLen(mimeMultipartString)) == 0)
				{
				// found a multi-part header, parse out the boundary string
					boundaryP = StrStr(buffer, mimeBoundaryString);
					boundaryP += StrLen(mimeBoundaryString);
					
					// Remove the boundary stuff so we can read in more into the buffer
					MemMove(buffer, boundaryP, &buffer[bufferLength] - boundaryP);
					bufferLength = (&buffer[bufferLength] - boundaryP);
					
					// Read chars into the buffer
					charsRead = inputFunc( inputStream, &buffer[bufferLength], importBufferMaxLength - bufferLength);
					bufferLength += charsRead;
					buffer[bufferLength] = nullChr;
					
					boundaryP = buffer;
					if (*boundaryP == '"')
						{
						boundaryP++;
						boundaryEndP = StrChr(boundaryP, '"');
						}
					else
						{
						boundaryEndP = StrChr(boundaryP, crChr);
						}
					if (boundaryEndP == NULL)
					{
						err = exgErrBadData;
						ErrThrow(false);
					}
					boundaryString[0] = '-';
					boundaryString[1] = '-';
					MemMove(&boundaryString[2], boundaryP, boundaryEndP - boundaryP);
					boundaryString[boundaryEndP - boundaryP + 2] = nullChr;
					
					c = StrChr(boundaryEndP, crChr);
					if (c == NULL)
					{
						err = exgErrBadData;
						ErrThrow(false);
					}
					c += sizeOf7BitChar(crChr) + sizeOf7BitChar(linefeedChr);
					
					// Remove the boundary stuff so we can read in more into the buffer
					MemMove(buffer, c, &buffer[bufferLength] - c);
					bufferLength = (&buffer[bufferLength] - c);
				}
				else
				{
					// just an ordinary header line, skip it
					err = ReadThroughCRLF(inputFunc, inputStream, buffer, &bufferLength);
					if (err) ErrThrow(false);
				}
				
				// Read chars into the buffer
				charsRead = inputFunc( inputStream, &buffer[bufferLength], importBufferMaxLength - bufferLength);
				bufferLength += charsRead;
				buffer[bufferLength] = nullChr;
				
			// stop at blank line by itself or EOF	
			} while (buffer[0] != crChr && buffer[0] != nullChr);	
			
			// We've now parsed the MIME header.  Preamble, segments, and postamble below.
		} // end of MIME parser
			
		do {
			// find the boundary and remove it, along with any header info in the body part
			if (*boundaryString != nullChr)
			{
				// Keep reading until we find a boundary
				while (buffer[0] != nullChr && StrNCompare(buffer, boundaryString, StrLen(boundaryString)) != 0)
				{
					err = ReadThroughCRLF(inputFunc, inputStream, buffer, &bufferLength);
					if (err) ErrThrow(false);
				}
				
				// Remove the boundary by removing all text until the end of the line.
				err = ReadThroughCRLF(inputFunc, inputStream, buffer, &bufferLength);
				if (err) ErrThrow(false);
				
				while (buffer[0] != nullChr && buffer[0] != crChr)
				{
					err = ReadThroughCRLF(inputFunc, inputStream, buffer, &bufferLength);
					if (err) ErrThrow(false);
				}
				err = ReadThroughCRLF(inputFunc, inputStream, buffer, &bufferLength);
				if (err) ErrThrow(false);
			}
			
			// could be that everything was header, and we're out of data.
			// weird error, but MemHandle it.
			if (bufferLength == 0)
			{
				err = exgErrBadData;
				ErrThrow(false);
			}
			
			
			addCr = 0;
			while (bufferLength > 0 &&
				(*boundaryString == nullChr || StrNCompare(buffer, boundaryString, StrLen(boundaryString)) != 0))
			{
				// find CR or end of buffer
				nextCrChr = StrChr(buffer, crChr);
				if (nextCrChr != NULL)
					charsToWrite = nextCrChr - buffer;
				else
					charsToWrite = bufferLength;
				
				// if we're going to overflow record, close it out (leave room for terminating null)
				if (newRecordSize + charsToWrite + addCr > memoMaxLength)
				{
					// since we try to stop parsing at each CR, and most records from other sources (MIME)
					// should have a CR at least every 76 characters, we probably don't have to worry about
					// word wrap.  Still, beaming a lot of just plain text could break records on random
					// boundaries...
					MemoImportFinishRecord(dbP, indexNew, &newRecordH, &newRecordSize, inputStream);
					addCr = 0;
					numRecordsReceived++;
				}
				
				// Make a record if we need one
				if (newRecordH == NULL)
				{
					indexNew = dmMaxRecordIndex;
					newRecordH = DmNewRecord(dbP, (UInt16 *)&indexNew, bufferLength);
					if (newRecordH == 0)
						ErrThrow(false);
					newRecordSize = 0;
				}
				
				// Write the buffer out to the record
				newHandle = DmResizeRecord(dbP, indexNew, newRecordSize + charsToWrite + addCr);
				if (newHandle)
					newRecordH = newHandle;
				else
					ErrThrow(true);
				
				newRecordP = MemHandleLock(newRecordH);
				if (addCr != 0)
					DmWrite(newRecordP, newRecordSize++, "\n", 1);
				DmWrite(newRecordP, newRecordSize, buffer, charsToWrite);
				newRecordSize += charsToWrite;
				MemHandleUnlock(newRecordH);
				
				// Remove the chars written so we can read more into the buffer
				if (nextCrChr != NULL)
				{
					if (charsToWrite < importBufferMaxLength-1)
					{
						MemMove(buffer, nextCrChr+2, bufferLength-(charsToWrite+2));	// delete LF
						bufferLength -= charsToWrite+2;
					}
					else
					// CR/LF was split by end of buffer, so DON'T delete the CR, catch it next time 'round
					{
						MemMove(buffer, nextCrChr, bufferLength-(charsToWrite));		// don't delete CR or LF
						bufferLength -= charsToWrite;
						nextCrChr = NULL;
					}
				}
				else
					buffer[bufferLength = 0] = nullChr;
				
				// Now read more
				charsRead = inputFunc( inputStream, &buffer[bufferLength], importBufferMaxLength - bufferLength);
				bufferLength += charsRead;
				buffer[bufferLength] = nullChr;
				
				if (nextCrChr != NULL)
					addCr = 1;
				else
					addCr = 0;
			}	// end of segment parser

			// Set the category for the record
			if (PrvStreamSocket(inputStream)->appData) {
				UInt16	attr;
				Err		err;
				UInt16	categoryID = PrvStreamSocket(inputStream)->appData;
				
				// Get the attributes
				err = DmRecordInfo(dbP, indexNew, &attr, NULL, NULL);

				// Set them to include the category, and mark the record dirty
				if ((attr & dmRecAttrCategoryMask) != categoryID) {
					attr &= ~dmRecAttrCategoryMask;
					attr |= categoryID | dmRecAttrDirty;
					err = DmSetRecordInfo(dbP, indexNew, &attr, NULL);
				}
			}

			MemoImportFinishRecord(dbP, indexNew, &newRecordH, &newRecordSize, inputStream);
			numRecordsReceived++;

			// save the uniqueID of the first record we loaded
			// we will goto this record when we are done (after sorting)
			if (!firstRecordID)
				{
					// Store the information necessary to navigate to the record inserted.
					DmRecordInfo(dbP, indexNew, NULL, &firstRecordID, NULL);
				}

			// Now that the record is imported check if we need to import any more
			
			// Stop if there isn't any more input
			if (bufferLength == 0)
				break;
			
			// Stop if the boundary is followed by "--"
			if ((*boundaryString != nullChr)
				&& bufferLength >= StrLen(boundaryString) + 2
				&& StrNCompare(&buffer[StrLen(boundaryString)], "--", 2) == 0)
				break;
				
		} while (true);	// end of segment parser
	}	// end of Try
	
	ErrCatch(inErr)
		{
		// Remove the incomplete record
		if (inErr)
			DmRemoveRecord(dbP, indexNew);
		
		ErrThrow(err ? err : exgMemError);
		} ErrEndCatch

	// if we got at least one record, sort and set goto parameters...
	if (firstRecordID)
		{
		MemoSort(dbP);
		MemoSetGoToParams (dbP, PrvStreamSocket(inputStream), firstRecordID);
		}
	
	// return number of records received
	*numRecordsReceivedP = numRecordsReceived;

	return false;
}



/************************************************************
 *
 * FUNCTION: MemoExportMime
 *
 * DESCRIPTION: Export a record as a Imc Mime record
 *
 * PARAMETERS: 
 *			dbP - pointer to the database to export the records from
 *			index - the record number to export
 *			recordP - whether the begin statement has been read
 *			outputStream - pointer to where to export the record to
 *			outputFunc - function to send output to the stream
 *			writeUniqueIDs - true to write the record's unique id
 *
 * RETURNS: nothing
 *
 *	REVISION HISTORY:
 *			Name	Date		Description
 *			----	----		-----------
 *			rsf	8/6/97	Initial Revision
 *
 *************************************************************/

void MemoExportMime(DmOpenRef /* dbP */, Int16 /* index */, MemoDBRecordType * recordP, 
	void * outputStream, WriteFunctionF outputFunc, Boolean /* writeUniqueIDs */, 
	Boolean outputMimeInfo)
{
	Char * c;
	Char * eolP;
	UInt32 len;


	// Write out all of the memo.  All linefeeds must be replaced with CRLF combos.
	c = &recordP->note;
	
	if (outputMimeInfo)
		{
		if (!ImcStringIsAscii(c))
			outputFunc(outputStream, "Content-Type: Text/plain; charset=ISO-8859-1" crlf, stringZLen);
		
		outputFunc(outputStream, crlf, stringZLen);
		}
	
	while (*c != '\0')
		{
		eolP = StrChr(c, linefeedChr);
		if (eolP)
			{
			len = outputFunc( outputStream, c, eolP - c);
				
			outputFunc(outputStream, crlf, stringZLen);
			c = eolP + sizeOf7BitChar(linefeedChr);
			}
		else if (*c != '\0')
			{
			eolP = StrChr(c, '\0');
			len = outputFunc( outputStream, c, eolP - c);
			
			c = eolP;
			}
		}
	outputFunc(outputStream, crlf, stringZLen);	// always end with an extra crlf
}

