//
//  MiscXmodem.m -- XMODEM/YMODEM file transfer over serial line
//          Written by W. Eric Norum Copyright (c) 1997 by W. Eric Norum.
//          $Revision: 2.1 $   $Date: 1997/04/02 15:32:59 $
//          All rights reserved.
//          This notice may not be removed from this source code.
//
//      This object is included in the MiscKit by permission from the author
//      and its use is governed by the MiscKit license, found in the file
//      "LICENSE.rtf" in the MiscKit distribution.  Please refer to that file
//      for a list of all applicable permissions and restrictions.
//      

/*
 * Xmodem file transfer
 *
 * Based on ``XMODEM/YMODEM PROTOCOL REFERENCE -- A compendium of documents
 * describing the XMODEM and YMODEM File Transfer Protocols'', Chuck Forsberg,
 * Omen Technology Incorporated, Portland, Oregon, 1988.
 */

#import <Foundation/NSDictionary.h>
#import "MiscXmodem.h"

/*
 * All timeouts are in seconds
 */
#define RECEIVE_INITIAL_TIMEOUT			2
#define RECEIVE_PACKET_TIMEOUT			4
#define RECEIVE_CRC_ADAPT_TIMEOUT		3
#define RECEIVE_PACKET_RETRY_LIMIT		20
#define RECEIVE_CRC_ADAPT_RETRY_LIMIT	8
#define TRANSMIT_TIMEOUT				60
#define TRANSMIT_STARTUP_BAD_NAK_LIMIT	100

#define SOH		'\001'
#define STX		'\002'
#define EOT		'\004'
#define ACK		'\006'
#define NAK		'\025'
#define CTRLZ	'\032'
#define CRC_NAK	'C'

/*
 * Define notification names and dictionary keys
 */
NSString *MiscXmodemPacketNotification = @"MiscXmodemPacketNotification";
NSString *MiscXmodemRetryNotification = @"MiscXmodemRetryNotification";
NSString *MiscXmodemCompletionNotification = @"MiscXmodemCompletionNotification";
NSString *MiscXmodemCompletionNotificationDataItem = @"MiscXmodemCompletionNotificationDataItem";
NSString *MiscXmodemCompletionNotificationFailureReason = @"MiscXmodemCompletionNotificationFailureReason";

@implementation MiscXmodem

/*
 * Report completion (successful or unsuccessful) of a transfer.
 */
- (void)finishTransferWithErrorString:(NSString *)message
{
	NSMutableDictionary *completionDictionary;

	completionDictionary = [NSMutableDictionary dictionaryWithCapacity:2];
	if (message) {
		[completionDictionary setObject:message
						forKey:MiscXmodemCompletionNotificationFailureReason];
	}
	else {
		if (isReceiving)
			[completionDictionary setObject:data
							forKey:MiscXmodemCompletionNotificationDataItem];
	}
	[[NSNotificationCenter defaultCenter]
						postNotificationName:MiscXmodemCompletionNotification
						object:self
						userInfo:completionDictionary];
	[data release];
	data = nil;
	[timer invalidate];
	timer = nil;
	transferInProgress = NO;
}

/*
 * Send a character
 */
- (void)sendChar:(unsigned char)c
{
	if (![self _writeRawBuffer:&c length:1])
		[self finishTransferWithErrorString:[self errorString]];
}
	
/*
 * Compute packet checksum
 */
- (void)computeChecksum:(unsigned char *)result
{
	unsigned char *cp = &pkBuffer[3];
	int length = (pkBuffer[0] == SOH) ? 128 : 1024;
	int checksum = 0;

	while (length--)
		checksum += *cp++;
	*result = checksum;
}

/*
 * Compute packet CRC
 */
- (void)computeCRC:(unsigned char *)hi lo:(unsigned char *)lo
{
	unsigned char *cp = &pkBuffer[3];
	int length = (pkBuffer[0] == SOH) ? (128+2) : (1024+2);
	int crc = 0;
	/*
	 * The table is the CRC resulting from all possible eight bit
	 * values shifted out of the top of the register.
	 */
	static const unsigned short crctab[256] = {
      0x0000,  0x1021,  0x2042,  0x3063,  0x4084,  0x50A5,  0x60C6,  0x70E7,
      0x8108,  0x9129,  0xA14A,  0xB16B,  0xC18C,  0xD1AD,  0xE1CE,  0xF1EF,
      0x1231,  0x0210,  0x3273,  0x2252,  0x52B5,  0x4294,  0x72F7,  0x62D6,
      0x9339,  0x8318,  0xB37B,  0xA35A,  0xD3BD,  0xC39C,  0xF3FF,  0xE3DE,
      0x2462,  0x3443,  0x0420,  0x1401,  0x64E6,  0x74C7,  0x44A4,  0x5485,
      0xA56A,  0xB54B,  0x8528,  0x9509,  0xE5EE,  0xF5CF,  0xC5AC,  0xD58D,
      0x3653,  0x2672,  0x1611,  0x0630,  0x76D7,  0x66F6,  0x5695,  0x46B4,
      0xB75B,  0xA77A,  0x9719,  0x8738,  0xF7DF,  0xE7FE,  0xD79D,  0xC7BC,
      0x48C4,  0x58E5,  0x6886,  0x78A7,  0x0840,  0x1861,  0x2802,  0x3823,
      0xC9CC,  0xD9ED,  0xE98E,  0xF9AF,  0x8948,  0x9969,  0xA90A,  0xB92B,
      0x5AF5,  0x4AD4,  0x7AB7,  0x6A96,  0x1A71,  0x0A50,  0x3A33,  0x2A12,
      0xDBFD,  0xCBDC,  0xFBBF,  0xEB9E,  0x9B79,  0x8B58,  0xBB3B,  0xAB1A,
      0x6CA6,  0x7C87,  0x4CE4,  0x5CC5,  0x2C22,  0x3C03,  0x0C60,  0x1C41,
      0xEDAE,  0xFD8F,  0xCDEC,  0xDDCD,  0xAD2A,  0xBD0B,  0x8D68,  0x9D49,
      0x7E97,  0x6EB6,  0x5ED5,  0x4EF4,  0x3E13,  0x2E32,  0x1E51,  0x0E70,
      0xFF9F,  0xEFBE,  0xDFDD,  0xCFFC,  0xBF1B,  0xAF3A,  0x9F59,  0x8F78,
      0x9188,  0x81A9,  0xB1CA,  0xA1EB,  0xD10C,  0xC12D,  0xF14E,  0xE16F,
      0x1080,  0x00A1,  0x30C2,  0x20E3,  0x5004,  0x4025,  0x7046,  0x6067,
      0x83B9,  0x9398,  0xA3FB,  0xB3DA,  0xC33D,  0xD31C,  0xE37F,  0xF35E,
      0x02B1,  0x1290,  0x22F3,  0x32D2,  0x4235,  0x5214,  0x6277,  0x7256,
      0xB5EA,  0xA5CB,  0x95A8,  0x8589,  0xF56E,  0xE54F,  0xD52C,  0xC50D,
      0x34E2,  0x24C3,  0x14A0,  0x0481,  0x7466,  0x6447,  0x5424,  0x4405,
      0xA7DB,  0xB7FA,  0x8799,  0x97B8,  0xE75F,  0xF77E,  0xC71D,  0xD73C,
      0x26D3,  0x36F2,  0x0691,  0x16B0,  0x6657,  0x7676,  0x4615,  0x5634,
      0xD94C,  0xC96D,  0xF90E,  0xE92F,  0x99C8,  0x89E9,  0xB98A,  0xA9AB,
      0x5844,  0x4865,  0x7806,  0x6827,  0x18C0,  0x08E1,  0x3882,  0x28A3,
      0xCB7D,  0xDB5C,  0xEB3F,  0xFB1E,  0x8BF9,  0x9BD8,  0xABBB,  0xBB9A,
      0x4A75,  0x5A54,  0x6A37,  0x7A16,  0x0AF1,  0x1AD0,  0x2AB3,  0x3A92,
      0xFD2E,  0xED0F,  0xDD6C,  0xCD4D,  0xBDAA,  0xAD8B,  0x9DE8,  0x8DC9,
      0x7C26,  0x6C07,  0x5C64,  0x4C45,  0x3CA2,  0x2C83,  0x1CE0,  0x0CC1,
      0xEF1F,  0xFF3E,  0xCF5D,  0xDF7C,  0xAF9B,  0xBFBA,  0x8FD9,  0x9FF8,
      0x6E17,  0x7E36,  0x4E55,  0x5E74,  0x2E93,  0x3EB2,  0x0ED1,  0x1EF0
	};

	while (length--) {
		unsigned char c = *cp++;
		crc = crctab[(((crc)>>8) & 0xFF)] ^ (((crc)<<8) | (c));
	}
	*hi = crc >> 8;
	*lo = crc;
}

/*
 * Verify that received packet is correct
 */
- (BOOL)verifyPacket
{
	if ((pkBuffer[1] - ~pkBuffer[2]) & 0xFF)
		return NO;
	if (rxCRC) {
		unsigned char lo, hi;
		[self computeCRC:&hi lo:&lo];
		if (hi || lo)
			return NO;
	}
	else {
		int length = (pkBuffer[0] == SOH) ? 128 : 1024;
		unsigned char checksum;
		[self computeChecksum:&checksum];
		if ((pkBuffer[3 + length] - checksum) & 0xFF)
			return NO;
	}
	return YES;
}

/*
 * Start the packet reception timer
 */
- (void)startPacketReceptionTimer:(NSTimeInterval)seconds
{
	timer = [NSTimer scheduledTimerWithTimeInterval:seconds
											target:self
											selector:@selector(receiveTimeout:)
											userInfo:nil
											repeats:NO];
}

/*
 * Start the transmit timer
 */
- (void)startTransmitTimer
{
	timer = [NSTimer scheduledTimerWithTimeInterval:TRANSMIT_TIMEOUT
											target:self
											selector:@selector(transmitTimeout:)
											userInfo:nil
											repeats:NO];
}

/*
 * Cancel the timer
 */
- (void)cancelTimer
{
	[timer invalidate];
	timer = nil;
}

/*
 * Create a transmit packet
 */
- (void)makePacket
{
	int pkLimit = tx1kPackets ? 1024+3 : 128+3;
	unsigned int dataCount = [data length];
	char c;
	const char *dp = [data bytes];
	unsigned int txIndex = txCount;

	/*
	 * All data sent?
	 */
	if ((txIndex == dataCount) && !txNewline) {
		pkBuffer[0] = EOT;
		pkIndex = 1;
		return;
	}
	
	/*
	 * Fill the data portion of the packet
	 */
	pkIndex = 3;
	for (;;) {
		if (txNewline) {
			pkBuffer[pkIndex++] = '\n';
			txNewline = NO;
			if (pkIndex == pkLimit)
				break;
		}
		if (txIndex < dataCount) {
			c = dp[txIndex++];
			if (isText) {
				if (c == '\n') {
					c = '\r';
					txNewline = YES;
				}
			}
		}
		else {
			if (tx1kPackets) {
				if (pkIndex < (1024 + 3 - 128)) {
					/*
					 * Switch to small packets
					 */
					tx1kPackets = NO;
					if (pkIndex > (128 + 3))
						/*
						 * Send remainder as more than one small packet
						 */
						[self makePacket];
						return;
				}
			}
			c = CTRLZ;
		}
		pkBuffer[pkIndex++] = c;
		if (pkIndex == pkLimit)
			break;
	}
	txCount = txIndex;

	/*
	 * Fill in the packet header
	 */
	if (tx1kPackets)
		pkBuffer[0] = STX;
	else
		pkBuffer[0] = SOH;
	pkBuffer[1] = ++lastPacketNumber;
	pkBuffer[2] = ~lastPacketNumber;

	/*
	 * Fill in the packet check
	 */
	if (txCRC) {
		pkBuffer[pkIndex] = 0;
		pkBuffer[pkIndex+1] = 0;
		[self computeCRC:pkBuffer+pkIndex lo:pkBuffer+pkIndex+1];
		pkIndex += 2;
	}
	else {
		[self computeChecksum:pkBuffer+pkIndex];
	}
}
	
/*
 * Handle incoming characters by overriding MiscSerialPort observer.
 */
- (void)_serialDataArrived:(NSNotification *)notification
{
	NSData *newData;
	int newCount;

	/*
	 * Just pass the notification along if it does not apply to us
	 */
	if (!transferInProgress) {
		[super _serialDataArrived:notification];
		return;
	}

	/*
	 * Continue reading
	 */
	[fileHandle readInBackgroundAndNotify];

	/*
	 * Get the data
	 */
	newData = [[notification userInfo] objectForKey:NSFileHandleNotificationDataItem];
	newCount = [newData length];
	if (newCount == 0)
		return;

	/*
	 * Handle the incoming data
	 */
	if (isReceiving) {
		/*
		 * Ignore data till we are expecting a packet
		 */
		if (!rxExpectPacket)
			return;
		
		/*
		 * Don't overrun the packet buffer
		 */
		if (newCount > (sizeof pkBuffer - pkIndex))
			newCount = sizeof pkBuffer - pkIndex;
		[newData getBytes:pkBuffer + pkIndex length:newCount];

		/*
		 * Are we at the beginning of a packet?
		 */
		if (pkIndex == 0) {
			switch (pkBuffer[0]) {
			case SOH:
				pkCount = 3 + 128 + (rxCRC ? 2 : 1);
				break;

			case STX:
				pkCount = 3 + 1024 + (rxCRC ? 2 : 1);
				break;

			case EOT:
				[self sendChar:ACK];
				[self finishTransferWithErrorString:nil];
				return;
			}
		}
		pkIndex += newCount;
		if (pkIndex >= pkCount) {
			/*
			 * Reset packet index
			 * This throws away any extra characters received after the
			 * end of the packet.  Given the interlocked handshaking of
			 * these protocols there shouldn't be any characters after
			 * the packet, so tossing any that are there is acceptable.
			 */
			[self cancelTimer];
			pkIndex = 0;
			if ([self verifyPacket]) {
				/*
				 * Received the same packet as last time?
				 * Our ACK must have been lost.  Reacknowledge and ignore
				 * the duplicate packet.
				 */
				int packetNumber, expectedPacketNumber;
				
				rxSendDynamicCheck = NO;
				packetNumber = pkBuffer[1];
				if (packetNumber == lastPacketNumber) {
					[self sendChar:ACK];
					[self startPacketReceptionTimer:RECEIVE_PACKET_TIMEOUT];
					return;
				}

				/*
				 * Received the correct packet?
				 * Acknowledge packet and store data
				 */
				expectedPacketNumber = (lastPacketNumber + 1) & 0xFF;
				if (packetNumber == expectedPacketNumber) {
					int pkSize = ((pkBuffer[0] == SOH) ? 128 : 1024);
					nakCount = 0;
					[self sendChar:ACK];
					[self startPacketReceptionTimer:RECEIVE_PACKET_TIMEOUT];
					lastPacketNumber = packetNumber;
					transferByteCount += pkSize;
					transferPacketCount++;
					if (isText) {
						char cbuf[1024], *cp = cbuf;
						unsigned char *rp = pkBuffer+3;

						while (pkSize--) {
							if (*rp == CTRLZ)
								break;
							if (*rp != '\r')
								*cp++ = *rp;
							rp++;
						}
						if (cp != cbuf)
							[data appendBytes:cbuf length:cp-cbuf];
					}
					else {
						[data appendBytes:pkBuffer+3 length:pkSize];
					}
					[[NSNotificationCenter defaultCenter]
							postNotificationName:MiscXmodemPacketNotification
							object:self];
					return;
				}
			}

			/*
			 * Something is wrong with the packet.
			 * Wait for the timeout to send a NAK.
			 * This gives time to flush any other bad incoming characters.
			 */
			transferRetryCount++;
			if (nakCount++ == RECEIVE_PACKET_RETRY_LIMIT) {
				[self finishTransferWithErrorString:@"Too many bad packets"];
			}
			else {
				rxExpectPacket = NO;
				[self startPacketReceptionTimer:RECEIVE_PACKET_TIMEOUT];
			}
		}
	}
	else {
		char replyChar = *(const char *)[newData bytes];
		[self cancelTimer];
		if (lastPacketNumber == 0) {
			const char *cp = [newData bytes];
			for (;;) {
				replyChar = *cp++;
				switch (replyChar) {
				case '\r':
				case '\n':
					break;

				case NAK:
					txCRC = NO;
					replyChar = ACK;
					break;

				case CRC_NAK:
					txCRC = YES;
					replyChar = ACK;
					break;

				default:
					if (++nakCount == TRANSMIT_STARTUP_BAD_NAK_LIMIT) {
						[self finishTransferWithErrorString:
										@"Receiver did not send inital NAK"];
						return;
					}
					replyChar = NAK;
					break;
				}
				if (replyChar == ACK) {
					pkBuffer[0] = 0;
					break;
				}
				if (--newCount == 0) {
					[self startTransmitTimer];
					return;
				}
			}
		}
		switch (replyChar) {
		default:
			[self startTransmitTimer];
			return;
			
		case ACK:
			if (pkBuffer[0] == EOT) {
				[self finishTransferWithErrorString:nil];
				if (newCount > 1) {
					/*
					 * Pass leftover data to the intended recipient
					 */
					NSRange leftOver;

					leftOver.location = 1;
					leftOver.length = newCount - 1;
					[[NSNotificationCenter defaultCenter]
						postNotificationName:
										MiscSerialPortReadCompletionNotification
						object:self
						userInfo:[NSDictionary dictionaryWithObject:
									[newData subdataWithRange:leftOver]
									forKey:MiscSerialPortNotificationDataItem]];
				}
				return;
			}
			nakCount = 0;
			if (lastPacketNumber != 0) {
				transferPacketCount++;
				transferByteCount += (tx1kPackets ? 1024 : 128);
				[[NSNotificationCenter defaultCenter]
							postNotificationName:MiscXmodemPacketNotification
							object:self];
			}
			[self makePacket];
			break;

		case NAK:
			nakCount++;
			[[NSNotificationCenter defaultCenter]
							postNotificationName:MiscXmodemRetryNotification
							object:self];
			break;
		}
		if (![self _writeRawBuffer:pkBuffer length:pkIndex]) {
			[self finishTransferWithErrorString:[self errorString]];
			return;
		}
		[self startTransmitTimer];
	}
}

/*
 * Handle a timeout during data transmission
 */
- (void)transmitTimeout:(NSTimer *)t
{
	timer = nil;
	[self finishTransferWithErrorString:@"Timed out"];
}

/*
 * Handle a timeout during data reception
 */
- (void)receiveTimeout:(NSTimer *)t
{
	timer = nil;
	if (rxSendDynamicCheck) {
		if (++nakCount <= RECEIVE_CRC_ADAPT_RETRY_LIMIT) {
			[self startPacketReceptionTimer:RECEIVE_CRC_ADAPT_TIMEOUT];
			pkIndex = 0;
			rxExpectPacket = YES;
			[self sendChar:CRC_NAK];
			return;
		}
		rxSendDynamicCheck = NO;
		rxCRC = NO;
		nakCount = 0;
	}
	if (++nakCount <= RECEIVE_PACKET_RETRY_LIMIT) {
		if (rxExpectPacket) {
			transferRetryCount++;
			[[NSNotificationCenter defaultCenter]
							postNotificationName:MiscXmodemRetryNotification
							object:self];
		}
		[self startPacketReceptionTimer:RECEIVE_PACKET_TIMEOUT];
		pkIndex = 0;
		rxExpectPacket = YES;
		[self sendChar:NAK];
		return;
	}
	[self finishTransferWithErrorString:@"Too many retries"];
}

- (void)xmodemCancelTransfer
{
	if (transferInProgress)
		[self finishTransferWithErrorString:@"Transfer cancelled"];
}

- (void)dealloc
{
	[data release];
	[timer invalidate];
	[super dealloc];
}

- (BOOL)xmodemStartReceiving
{
	if (transferInProgress)
		return NO;
	pkIndex = 0;
	lastPacketNumber = 0;
	transferByteCount = 0;
	transferPacketCount = 0;
	transferRetryCount = 0;
	nakCount = 0;
	transferInProgress = YES;
	isReceiving = YES;
	data = [[NSMutableData alloc] initWithCapacity:5000];
	if (useDynamicCheck) {
		rxSendDynamicCheck = YES;
		rxCRC = YES;
	}
	else  {
		rxSendDynamicCheck = NO;
		rxCRC = useCRC;
	}
	rxExpectPacket = NO;
	[self startPacketReceptionTimer:RECEIVE_INITIAL_TIMEOUT];
	return YES;
}

- (BOOL)xmodemStartSending:(NSData *)dataSource
{
	if (transferInProgress)
		return NO;
	data = [dataSource copy];
	lastPacketNumber = 0;
	transferByteCount = 0;
	transferPacketCount = 0;
	transferRetryCount = 0;
	nakCount = 0;
	transferInProgress = YES;
	isReceiving = NO;
	txCount = 0;
	txNewline = NO;
	tx1kPackets = use1kPackets;
	[self startTransmitTimer];
	return YES;
}

- (void)xmodemSetFileTypeText:(BOOL)flag		{ isText = flag; }
- (void)xmodemSet1kPackets:(BOOL)flag			{ use1kPackets = flag; }
- (void)xmodemSetCheckTypeDynamic:(BOOL)flag	{ useDynamicCheck = flag; }
- (void)xmodemSetCheckTypeCRC:(BOOL)flag		{ useCRC = flag; }

- (BOOL)xmodemIsFileTypeText					{ return isText; }
- (BOOL)xmodemIs1kPackets						{ return use1kPackets; }
- (BOOL)xmodemIsCheckTypeDynamic				{ return useDynamicCheck; }
- (BOOL)xmodemIsCheckTypeCRC					{ return useCRC; }

- (unsigned int)xmodemTransferPacketCount		{ return transferPacketCount; }
- (unsigned int)xmodemTransferByteCount			{ return transferByteCount; }
- (unsigned int)xmodemTransferRetryCount		{ return transferRetryCount; }

@end
