/*
 *	PDP-8/E Simulator
 *
 *	Copyright © 1994-2015 Bernhard Baehr
 *
 *	PC8E.m - PC8-E Paper Tape Reader and Punch for the PDP-8/E Simulator
 *
 *	This file is part of PDP-8/E Simulator.
 *
 *	PDP-8/E Simulator 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 3 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, see <http://www.gnu.org/licenses/>.
 */


#import <Cocoa/Cocoa.h>
#import <mach/mach_time.h>

#import "PluginFramework/PluginAPI.h"
#import "PluginFramework/PDP8.h"
#import "PluginFramework/Utilities.h"
#import "PluginFramework/RegisterFormCell.h"
#import "PluginFramework/KeepInMenuWindow.h"
#import "FileDropControlTargetProtocol.h"
#import "PluginFramework/PaperTapeController.h"
#import "PluginFramework/NSThread+MainThread.h"

#define USE_PC8E_REGISTERS_DIRECTLY 1

#import "PC8E.h"
#import "PC8Eiot.h"


#define CODER_KEY_RBF		@"rbf"
#define CODER_KEY_PBF		@"pbf"
#define CODER_KEY_INFLAG	@"inflag"
#define CODER_KEY_INMASK	@"inmask"
#define CODER_KEY_OUTFLAG	@"outflag"
#define CODER_KEY_OUTMASK	@"outmask"

#define NO_OUTPUT		0
#define OUTPUT			1
#define NO_INPUT		0
#define INPUT			1

#define READER_CONTINUOUS_DELAY	3333333ull	// nanosec. * 300 = 1 sec. => delay for 300 char/sec.
#define READER_STARTSTOP_DELAY	40000000ull	// nanosec. * 25 = 1 sec. => delay for 25 char/sec.
#define PUNCH_DELAY		20000000ull	// nanosec. * 50 = 1 sec. => delay for 50 char/sec.
#define STOP_DELAY		6000000ull	// "stop delay" occurs 6 ms after RFC


@implementation PC8E


API_VERSION


#pragma mark Plugin Methods


- (NSArray *) iotsForAddress:(int)ioAddress
{
	return ioAddress == 01 ?
		[NSArray arrayWithObjects:
			[NSValue valueWithPointer:i6010],
			[NSValue valueWithPointer:i6011],
			[NSValue valueWithPointer:i6012],
			[NSValue valueWithPointer:nil],
			[NSValue valueWithPointer:i6014],
			[NSValue valueWithPointer:nil],
			[NSValue valueWithPointer:i6016],
			[NSValue valueWithPointer:nil],
			nil] :
		[NSArray arrayWithObjects:
			[NSValue valueWithPointer:i6020],
			[NSValue valueWithPointer:i6021],
			[NSValue valueWithPointer:i6022],
			[NSValue valueWithPointer:nil],
			[NSValue valueWithPointer:i6024],
			[NSValue valueWithPointer:nil],
			[NSValue valueWithPointer:i6026],
			[NSValue valueWithPointer:nil],
			nil];
}


- (NSArray *) skiptestsForAddress:(int)ioAddress
{
	return ioAddress == 01 ?
		[NSArray arrayWithObjects:
			[NSValue valueWithPointer:nil],
			[NSValue valueWithPointer:s6011],
			[NSValue valueWithPointer:nil],
			[NSValue valueWithPointer:nil],
			[NSValue valueWithPointer:nil],
			[NSValue valueWithPointer:nil],
			[NSValue valueWithPointer:nil],
			[NSValue valueWithPointer:nil],
			nil] :
		[NSArray arrayWithObjects:
			[NSValue valueWithPointer:nil],
			[NSValue valueWithPointer:s6021],
			[NSValue valueWithPointer:nil],
			[NSValue valueWithPointer:nil],
			[NSValue valueWithPointer:nil],
			[NSValue valueWithPointer:nil],
			[NSValue valueWithPointer:nil],
			[NSValue valueWithPointer:nil],
			nil];
}


- (void) setIOFlag:(unsigned long)flag forIOFlagName:(NSString *)name;
{
	if (inflag)
		outflag = flag;
	else
		inflag = flag;
}


- (void) CAF:(int)ioAddress
{
	if (ioAddress == 01) {		// CAF is called twice, ignore second call (01 == reader I/O address)
		RBF = 0;
		PBF = 0;		
		[pdp8 setInterruptMaskBits:inflag | outflag];
		[pdp8 clearIOFlagBits:inflag | outflag];
	}
}


- (void) clearAllFlags:(int)ioAddress
{
	if (ioAddress == 01)		// CAF is called twice, ignore second call (01 == reader I/O address)
		[self resetDevice];
}


- (void) resetDevice
{
	[self setRBF:0];
	[self setPBF:0];
	[pdp8 setInterruptMaskBits:inflag | outflag];
	[pdp8 clearIOFlagBits:inflag | outflag];
}


#pragma mark Thread Handling


- (void) realtimeDelay:(uint64_t)matStartTime lastStart:(uint64_t)matLastStartTime
{
	int speed = [pdp8 getGoSpeed];
	if (speed != GO_AS_FAST_AS_POSSIBLE) {
		uint64_t delay;
		if (matLastStartTime == 0)		// punch with 50 cps
			delay = PUNCH_DELAY;
		else {					// reader
			uint64_t delta = absolute2nanoseconds(matStartTime - matLastStartTime);
			if (delta > READER_STARTSTOP_DELAY)	// next char already buffered
				delay = 0;		
			else 
			if (delta > STOP_DELAY)			// start stop mode with 25 cps
				delay = READER_STARTSTOP_DELAY;
			else					// continuous read with 300 cps
				delay = READER_CONTINUOUS_DELAY;
		}
		if (speed == GO_WITH_PDP8_SPEED)
			mach_wait_until (matStartTime + nanoseconds2absolute(delay));
		else {
			while (mach_absolute_time() < matStartTime + nanoseconds2absolute(delay))
				;
		}
	}
}


- (void) canContinueInput
{
	if ([inputLock tryLockWhenCondition:NO_INPUT])
		[inputLock unlockWithCondition:INPUT];
#if ! defined(NS_BLOCK_ASSERTIONS)
	else
		NSLog (@"PDP-8 software bug: RFC or RCC executed before preceding tape read finished");
#endif
}


- (void) canContinueOutput
{
	if ([outputLock tryLockWhenCondition:NO_OUTPUT])
		[outputLock unlockWithCondition:OUTPUT];
#if ! defined(NS_BLOCK_ASSERTIONS)
	else
		NSLog (@"PDP-8 software bug: PPC or PLS executed before preceding tape punch finished");
#endif
}


- (void) pc8eReaderThread:(id)object
{
	[[NSAutoreleasePool alloc] init];
	uint64_t matStartTime = mach_absolute_time();
	uint64_t matLastStartTime;
	for (;;) {
		[inputLock lockWhenCondition:INPUT];
		matLastStartTime = matStartTime;
		matStartTime = mach_absolute_time();
		input = [reader getChar];
		if (input != EOF)
			[self setRBF:input & 0377];	// strip off Unicode characters etc.
		[inputLock unlockWithCondition:NO_INPUT];
		[self realtimeDelay:matStartTime lastStart:matLastStartTime];
		if (input != EOF)
			[pdp8 setIOFlagBits:inflag];
	}
}


- (void) pc8ePunchThread:(id)object
{
	[[NSAutoreleasePool alloc] init];
	for (;;) {
		[outputLock lockWhenCondition:OUTPUT];
		uint64_t matStart = mach_absolute_time();
		[self setPBF:output];
		BOOL done = [punch putChar:output handleBackspace:NO];
		[outputLock unlockWithCondition:NO_OUTPUT];
		[self realtimeDelay:matStart lastStart:0];
		if (done)
			[pdp8 setIOFlagBits:outflag];
	}
}


#pragma mark Register Access


- (unsigned short) getRBF
{
	return RBF;
}


- (void) notifyRBF
{
	NSAssertRunningOnMainThread ();
	[[NSNotificationCenter defaultCenter] postNotificationName:RBF_CHANGED_NOTIFICATION object:self];
	
}


- (void) setRBF:(unsigned short)rbf
{
	NSAssert1 ((rbf & ~0377) == 0, @"Bad RBF: 0%o", rbf);
	RBF = rbf;
	if ([NSThread isMainThread])
		[[NSNotificationCenter defaultCenter]
			postNotificationName:RBF_CHANGED_NOTIFICATION object:self];
	else
		[self performSelectorOnMainThread:@selector(notifyRBF) withObject:self waitUntilDone:NO];
}


- (unsigned short) getPBF
{
	return PBF;
}


- (void) notifyPBF
{
	NSAssertRunningOnMainThread ();
	[[NSNotificationCenter defaultCenter] postNotificationName:PBF_CHANGED_NOTIFICATION object:self];
	
}


- (void) setPBF:(unsigned short)pbf
{
	NSAssert1 ((pbf & ~0377) == 0, @"Bad PBF: 0%o", pbf);
	PBF = pbf;
	if ([NSThread isMainThread])
		[[NSNotificationCenter defaultCenter]
			postNotificationName:PBF_CHANGED_NOTIFICATION object:self];
	else
		[self performSelectorOnMainThread:@selector(notifyPBF) withObject:self waitUntilDone:NO];
}


#pragma mark Notifications


- (void) notifyApplicationWillTerminate:(NSNotification *)notification
{
	// NSLog (@"PC8E notifyApplicationWillTerminate");
	[[NSNotificationCenter defaultCenter] removeObserver:self name:NSUserDefaultsDidChangeNotification
		object:nil];
	NSMutableData *data = [NSMutableData data];
	NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingWithMutableData:data];
	[self encodeWithCoder:archiver];
	[archiver finishEncoding];
	[archiver release];
	[[NSUserDefaults standardUserDefaults] setObject:data forKey:[self pluginName]];
}


- (void) notifyPluginsLoaded:(NSNotification *)notification
{
	[window orderBackFromDefaults:self];
	[window makeFirstResponder:window];
}


#pragma mark Initialization


- (id) initWithCoder:(NSCoder *)coder
{
	self = [super init];
	[self setRBF:[coder decodeIntForKey:CODER_KEY_RBF]];
	[self setPBF:[coder decodeIntForKey:CODER_KEY_PBF]];
	[coder decodeBoolForKey:CODER_KEY_INFLAG] ?
		[pdp8 setIOFlagBits:inflag] : [pdp8 clearIOFlagBits:inflag];
	[coder decodeBoolForKey:CODER_KEY_INMASK] ?
		[pdp8 setInterruptMaskBits:inflag] : [pdp8 clearInterruptMaskBits:inflag];
	[coder decodeBoolForKey:CODER_KEY_OUTFLAG] ?
		[pdp8 setIOFlagBits:outflag] : [pdp8 clearIOFlagBits:outflag];
	[coder decodeBoolForKey:CODER_KEY_OUTMASK] ?
		[pdp8 setInterruptMaskBits:outflag] : [pdp8 clearInterruptMaskBits:outflag];
	return self;
}


- (void) encodeWithCoder:(NSCoder *)coder
{
	[coder encodeInt:[self getRBF] forKey:CODER_KEY_RBF];
	[coder encodeInt:[self getPBF] forKey:CODER_KEY_PBF];
	[coder encodeBool:[pdp8 getIOFlagBits:inflag] ? YES : NO forKey:CODER_KEY_INFLAG];
	[coder encodeBool:[pdp8 getInterruptMaskBits:inflag] ? YES : NO forKey:CODER_KEY_INMASK];
	[coder encodeBool:[pdp8 getIOFlagBits:outflag] ? YES : NO forKey:CODER_KEY_OUTFLAG];
	[coder encodeBool:[pdp8 getInterruptMaskBits:outflag] ? YES : NO forKey:CODER_KEY_OUTMASK];
}


- (void) pluginDidLoad
{
	[rbfCell setupRegisterFor:self getRegisterValue:@selector(getRBF) setRegisterValue:@selector(setRBF:)
		changedNotificationName:RBF_CHANGED_NOTIFICATION mask:0377 base:8];
	[pbfCell setupRegisterFor:self getRegisterValue:@selector(getPBF) setRegisterValue:@selector(setPBF:)
		changedNotificationName:PBF_CHANGED_NOTIFICATION mask:0377 base:8];
	NSData *data = [[NSUserDefaults standardUserDefaults] dataForKey:[self pluginName]];
	if (data) {
		NSKeyedUnarchiver *unarchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
		self = [self initWithCoder:unarchiver];
		[unarchiver finishDecoding];
		[unarchiver release];
	}
	inputLock = [[NSConditionLock alloc] initWithCondition:NO_INPUT];
	outputLock = [[NSConditionLock alloc] initWithCondition:NO_OUTPUT];
	[NSThread detachNewThreadSelector:@selector(pc8eReaderThread:) toTarget:self withObject:nil];
	[NSThread detachNewThreadSelector:@selector(pc8ePunchThread:) toTarget:self withObject:nil];
	[[NSNotificationCenter defaultCenter] addObserver:self
		selector:@selector(notifyApplicationWillTerminate:)
		name:NSApplicationWillTerminateNotification object:nil]; 
	[[NSNotificationCenter defaultCenter] addObserver:self
		selector:@selector(notifyPluginsLoaded:)
		name:PLUGINS_LOADED_NOTIFICATION object:nil];
}


@end
