/* MiscSpaceMouseDriver.m				 
 *
 * This is a 3DMouse adaptor that will handle the SpaceMouse (produced by
 * SpaceControl & DLR Germany).
 *
 * For more interface-info see the header file. More in depth information
 * can be found in the source-code.
 *
 * Written by: 		Thomas Engel
 * Created:    		23.03.1994 (Copyleft)
 * Last modified: 	18.04.1994
 */

#import "MiscSpaceMouseDriver.h"
#import "MiscSpaceMouseFrontend.h"
#import <misckit/MiscSerialPort.h>

// Here we have some supporting funktions that will help us with lowlevel
// Error-correction.
// This code is almost the same as in the SGI driver.

static char smNibbleTable[] =
	{'0','A','B','3','D','5','6','G','H','9',':','K','<','M','N','?' };

static int smLowNibble( char ch )
{
	// if( ch != smNibbleTable[ ch & 0x0F ] )
  	//	[self setErrorFlag:1];

	return( ch & 0x0F );
}

// Here is our real OO-based driver part.
// Basically I have tried to keep it as OO as possible. But some parts
// are more runtime optimized.

@implementation MiscSpaceMouseDriver

void *SpaceMouseActivityEntry( DPSTimedEntry tag, double now, id myDriver )
{
	[myDriver checkActivity];
	return myDriver;
}

- setTarget:anObject
{
	// When becoming inactive we will reset all the activity checks.
	// The timeentry settings are save because it will only work when we are
	// connected to a device! So tentry does reflect the right conditions.
	
	if( [self isConnectedToDevice] )
	{
		// We will disable events here to ensure that they won't interfere
		// while we're trying to 'shut them down'.
		
		sendEvents = NO;
		
		if( anObject == nil )
		{
			if( tentry  ) DPSRemoveTimedEntry( tentry );
			if( isActive ) [target transformationDidEnd:self];
			isActive = NO;
			newActions = NO;
			tentry = 0;
		}
		else
		{
			if( !tentry )
			{
				tentry = DPSAddTimedEntry( 1, 
								   (DPSTimedEntryProc)SpaceMouseActivityEntry, 
								   self, NX_MODALRESPTHRESHOLD );
			}
			isActive = NO;
			newActions = NO;
		}
	}
	return [super setTarget:anObject];
}

- setFrontend:anObject
{
	frontend = anObject;
	return self;
}

- checkActivity
{
	// Here we will handle the activitv control.
	// If we are still isActive we might check the conditions.
	
	if( !isActive ) return self;
	
	// If there has been a new activity in the last time-period we will
	// assume that the transformation did not end yet. But we will reset the
	// flag.
	// Otherwise we will infor our target that no new action did take place.
	
	if( newActions )
		newActions = NO;
	else
	{
		[target transformationDidEnd:self];
		isActive = NO;
	}
	return self;
}

- (BOOL)connectToDevice:(const char *)device
{
	BOOL	suc;
	
	// Init a new port and don't forget to reset our buffers!
	
	if( [self isConnectedToDevice] ) return NO;

	smEventBuffer[0] = 0;

	port = [MiscSerialPort new];
	[port setBaud:9600];
	[port setDelegate:self];
	[port setDeviceName:device];
	
	suc = [port connect];
	sleep( 1 );

	if( suc )
	{
		// The first direct write is just to set the mouse the 9600 Baud.
		// Then we will set every value to a default. This way all our values
		// get initialized.
		
		[port transmitChars:"z\r\r" length:3];
		sleep( 1 );
		
		[self zeroMouseData];
		[self setMouseInDominantMode:YES 
			  withTranslationEnabled:YES
			  andRotationEnabled:YES];
	 	[self setRotScale:100];
	 	[self setTransScale:500];
	 	[self setQualityForTranslation:2 andRotation:4];
		[self setDataRateMin:10 max:15];
	 	[self setNullRadius:15];
		
		// Now lets init the new timed entry. We will check the start rotation
		// stuff.
		
		isActive = NO;
		newActions = NO;
		tentry = DPSAddTimedEntry( 1, 
								   (DPSTimedEntryProc)SpaceMouseActivityEntry, 
								   self, NX_MODALRESPTHRESHOLD );
	}
	else port = nil;
	
	return suc;
}

- disconnectFromDevice
{
	if( [self isConnectedToDevice] )
	{
		[port disconnect];
		port = nil;
		isActive = NO;
		newActions = NO;
		if( tentry ) DPSRemoveTimedEntry(tentry);
		tentry = 0;
	}
	return self;
}

- (BOOL)isConnectedToDevice
{
	if( port ) 
			return YES;
	else	return NO;
}

- receiveChars:(char *)buffer length:(int)length
{
	// Here we'll get notified of arriving characters.
	// We are looking for the final \r CR character.
	// The first step is to add the chars from the buffer to our
	// parsing Line. This will continue until we have a full event.
	//
	// Our CommandBuffer is 200 chars long. This should be long enough
	// because the longest command is the Version command an this will deliver
	// about 50 chars. So missing one \r will cause no problems.
	
	int		i;
  	int		eventLength;
	int		maxLength;
	int		newLength;
	char	aChar;
			
	for( eventLength=0; eventLength<198; eventLength++)
		if( smEventBuffer[eventLength] == 0 ) break;

	maxLength = 198 - eventLength;
	if( maxLength > length ) maxLength = length;
	
	aChar = 0;
	for( i=0; i<maxLength; i++ )
	{
		aChar = buffer[i];
		if( aChar == '\r' ) break;
		smEventBuffer[eventLength] = aChar;
	    eventLength++;
	}
	
	// We will terminate the new eventString and we will calculate the length
	// of the remaining buffer.
	// This is only interesting when we ended with a \r !! So we can 
	// savely do (i+1) here!
	
	smEventBuffer[eventLength] = 0;
	newLength = length - (i+1);
	
	// If we did not end with a CR we'll have to wait for some more input.
	// If we did. We will copy the rest of the buffer to the eventBuffer, 
	// after we did parse the current event.

	// printf( "%d %d %d %s\r\n" , length, newLength, eventLength, 
	//			smEventBuffer);
	
	if( aChar != '\r' ) return self;
	 
	[self parseCommand:smEventBuffer];
	
	// If there is some code left inside the buffer we call ourself
	// recursivly again after erasing the current eventBuffer.
	
	smEventBuffer[0] = 0;
	
	if( newLength > 0 )
		[self receiveChars:&buffer[length-newLength] length:newLength];

	return self;
}

- setMouseInDominantMode:(BOOL)domFlag 
		  withTranslationEnabled:(BOOL)transFlag
		  andRotationEnabled:(BOOL)rotFlag
{
	rotationOn = rotFlag;
	translationOn = transFlag;
	dominantModeOn = domFlag;
	
	smSendBuffer[0] = 'm';
	smSendBuffer[1] = smNibbleTable[ rotFlag * 1 + 
									 transFlag * 2 + 
									 domFlag * 4 ];
	smSendBuffer[2] = '\r';
	
	// We don't need to ask for the result here. The mouse echos the
	// setting on its own.
	
	[port transmitChars:smSendBuffer length:3];
	return self;
}

- (BOOL)hasTranslationEnabled
{
	return translationOn;
}

- (BOOL)hasRotationEnabled
{
	return rotationOn;
}

- (BOOL)isInDominantMode
{
	return dominantModeOn;
}

- setQualityForTranslation:(int)transInt andRotation:(int)rotInt
{
	if( rotInt < 0 ) rotInt = 0;
	if( rotInt > 15 ) rotInt = 15;
	if( transInt < 0 ) transInt = 0;
	if( transInt > 15 ) transInt = 15;

	rotationQuality = rotInt;
	translationQuality = transInt;
	
	smSendBuffer[0] = 'q';
	smSendBuffer[1] = smNibbleTable[transInt];
	smSendBuffer[2] = smNibbleTable[rotInt];
	smSendBuffer[3] = '\r';
	
	[port transmitChars:smSendBuffer length:4];
//	[port transmitChars:"qQ\r" length:3];
	return self;
}

- (int)translationQuality
{
	return translationQuality;
}

- (int)rotationQuality
{
	return rotationQuality;
}

- setNullRadius:(int)anInt
{
	if( anInt < 0 ) anInt = 0;
	if( anInt > 15 ) anInt = 15;

	nullRadius = anInt;
	
	smSendBuffer[0] = 'n';
	smSendBuffer[1] = smNibbleTable[anInt];
	smSendBuffer[2] = '\r';
	
	[port transmitChars:smSendBuffer length:3];
//	[port transmitChars:"nQ\r" length:3];
	return self;
}

- (int)nullRadius
{
	return nullRadius;
}

- setDataRateMin:(int)minRate max:(int)maxRate
{
	if( minRate < 2 ) minRate = 2;
	if( minRate > 15 ) minRate = 15;
	if( maxRate < 2 ) maxRate = 2;
	if( maxRate > 15 ) maxRate = 15;
	if( minRate > maxRate ) minRate = maxRate;
	if( maxRate < minRate ) maxRate = minRate;
	
	minDataRate = minRate;
	maxDataRate = maxRate;
	
	smSendBuffer[0] = 'p';
	smSendBuffer[1] = smNibbleTable[maxRate];
	smSendBuffer[2] = smNibbleTable[minRate];
	smSendBuffer[3] = '\r';
	
	[port transmitChars:smSendBuffer length:4];
//	[port transmitChars:"pQ\r" length:3];
	return self;
}

- (int)minDataRate
{
	return minDataRate;
}

- (int)maxDataRate
{
	return maxDataRate;
}

- setRotScale:(float)aFloat
{
	rotScale = aFloat;
	[frontend takeScaleFrom:self];
	return self;
}

- (float)rotScale
{
	return rotScale;
}

- setTransScale:(float)aFloat
{
	transScale = aFloat;
	[frontend takeScaleRationFrom:self];
	return self;
}

- (float)transScale
{
	return transScale;
}

- queryDeviceVersion
{
	[port transmitChars:"vQ\r" length:3];
	return self;
}

- beepWithDuration:(int)anInt
{
	// Well we will always turn the beeper on. (=> + 15)
	// I don't know what turning the beeper off is good for.
	// If you can tell me.. I will add a new method for that.
	
	if( anInt < 0 ) anInt = 0;
	if( anInt > 7 ) anInt = 7;
	
	smSendBuffer[0] = 'b';
	smSendBuffer[1] = smNibbleTable[anInt + 15];
	smSendBuffer[2] = '\r';
	
	[port transmitChars:smSendBuffer length:3];
	return self;
}

- zeroMouseData
{
	[port transmitChars:"z\r" length:2];
	return self;
}

- setErrorFlag:(int)flag
{
	errorFlag = flag;
	return self;
}

- parseCommand:(const char *)buffer
{
	if( buffer[0] == 'd' ) [self parseTransformationEvent:buffer];
	if( buffer[0] == 'k' ) [self parseKeyEvent:buffer];
	if( buffer[0] == 'n' ) [self parseNullRadiusEvent:buffer];
	if( buffer[0] == 'q' ) [self parseQualityEvent:buffer];
	if( buffer[0] == 'p' ) [self parseDataRateEvent:buffer];
	if( buffer[0] == 'e' ) [self parseErrorEvent:buffer];
	if( buffer[0] == 'v' ) [self parseVersionEvent:buffer];
	if( buffer[0] == 'm' ) [self parseModeEvent:buffer];

	return self;
}

- parseModeEvent:(const char *)buffer
{
	int	mode;
	
	mode = smLowNibble( buffer[1] );
	
	if( mode & 2 )
			translationOn = YES;
	else	translationOn = NO;

	if( mode & 1 )
			rotationOn = YES;
	else	rotationOn = NO;

	if( mode & 4 )
			dominantModeOn = YES;
	else	dominantModeOn = NO;

	[frontend takeTransModeFrom:self];
	[frontend takeRotModeFrom:self];
	[frontend takeDomModeFrom:self];
	return self;
}

- parseVersionEvent:(const char *)buffer
{
	return self;
}

- parseErrorEvent:(const char *)buffer
{
	return self;
}

- parseDataRateEvent:(const char *)buffer
{
	maxDataRate = smLowNibble( buffer[1] );
	minDataRate = smLowNibble( buffer[2] );
	[frontend takeMinDataRateFrom:self];
	[frontend takeMaxDataRateFrom:self];
	return self;
}

- parseQualityEvent:(const char *)buffer
{
	translationQuality = smLowNibble( buffer[1] );
	rotationQuality = smLowNibble( buffer[2] );
	[frontend takeTransQualityFrom:self];
	[frontend takeRotQualityFrom:self];
	return self;
}

- parseNullRadiusEvent:(const char *)buffer
{
	nullRadius = smLowNibble( buffer[1] );
	[frontend takeNullRadiusFrom:self];
	return self;
}

- parseKeyEvent:(const char *)buffer
{
	[self deliverKeyPress:"12"];
	return self;
}

- parseTransformationEvent:(const char *)buffer
{
	float	x, y, z, a, b, c;

	if( !sendEvents	) return self;
	
 	x = smLowNibble( buffer[ 1] ) * 4096 +
        smLowNibble( buffer[ 2] ) * 256  +
	    smLowNibble( buffer[ 3] ) * 16   +
	    smLowNibble( buffer[ 4] ) 		 - 32768;

 	y = smLowNibble( buffer[ 5] ) * 4096 +
        smLowNibble( buffer[ 6] ) * 256  +
	    smLowNibble( buffer[ 7] ) * 16   +
	    smLowNibble( buffer[ 8] )        - 32768;

 	z = smLowNibble( buffer[ 9] ) * 4096 +
        smLowNibble( buffer[10] ) * 256  +
	    smLowNibble( buffer[11] ) * 16   +
	    smLowNibble( buffer[12] )        - 32768;

 	a = smLowNibble( buffer[13] ) * 4096 +
        smLowNibble( buffer[14] ) * 256  +
	    smLowNibble( buffer[15] ) * 16   +
	    smLowNibble( buffer[16] )        - 32768;

 	b = smLowNibble( buffer[17] ) * 4096 +
        smLowNibble( buffer[18] ) * 256  +
	    smLowNibble( buffer[19] ) * 16   +
	    smLowNibble( buffer[20] )        - 32768;

 	c = smLowNibble( buffer[21] ) * 4096 +
        smLowNibble( buffer[22] ) * 256  +
	    smLowNibble( buffer[23] ) * 16   +
        smLowNibble( buffer[24] )        - 32768;

	x = x / transScale;
	y = y / transScale;
	z = z / transScale;
	a = a / rotScale;
	b = b / rotScale;
	c = c / rotScale;
	
	// If this is our first action after a long time of silence we will tell
	// our target about that. every new action will be registered here.
	
	newActions = YES;
	
	if( !isActive )
	{
		isActive = YES;
		[target transformationWillStart:self];
	}
	[self deliverTranslation:x :y :z andRotation:a :b :c];

	return self;
}

@end

/*
 * History: 18.04.94 Added the timed entry to find start and end of actions.
 *
 *			15.04.94 Included two seperate scales and frontend support.
 *
 *			11.04.94 added support for the general Misc3DMouseDriver.
 *
 *			04.04.94 Got the first data from the SpaceMouse. Now let the
 *					 party begin.
 *
 *			23.03.94 First code written.
 *
 *
 * Bugs: - Does not read the mouse version. It will need a MiscString someday.
 *
 *		 - The zeroing return msg is ignored. Well should not matter.
 */