/*
 *	PDP-8/E Simulator
 *
 *	Copyright © 1994-2015 Bernhard Baehr
 *
 *	PluginManager.m - Manager for I/O Device Plugins
 *
 *	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 "PluginManager.h"
#import "PluginAPI.h"
#import "PDP8.h"
#import "SkipController.h"
#import "IOFlagController.h"
#import "Assembler.h"
#import "Disassembler.h"
#import "OctalFormatter.h"
#import "HelpMenuManager.h"
#import "Unicode.h"
#import "NSFileManager+Additions.h"


#define PLUGIN_EXTENSION		@"pdp8Plugin"
#define PLUGIN_APPSUPPORT_PATH		@"Application Support/PDP-8:E Simulator"
#define PLUGIN_NEXTTOAPP_PATH		@"../PDP-8:E Simulator Plugins"


@implementation PluginManager


- (BOOL) canInstallIOTs:(NSArray *)mnemonics withIOAddresses:(NSArray *)ioAddresses
	noLoadMessage:(NSString *)noLoadMessage
{
	NSString *ioAddress;

	if (! mnemonics || ! [mnemonics isKindOfClass:[NSArray class]]) {
		NSRunAlertPanel (NSLocalizedString(
			@"Invalid IOT information in the I/O description of the plug-in", @""),
			noLoadMessage, nil, nil, nil);
		return NO;
	}
	if (! ioAddresses || ! [ioAddresses isKindOfClass:[NSArray class]]) {
		NSRunAlertPanel (NSLocalizedString(
			@"Invalid I/O address information in the I/O description of the plug-in.", @""),
			noLoadMessage, nil, nil, nil);
		return NO;
	}
	if ([mnemonics count] != 8 * [ioAddresses count]) {
		NSRunAlertPanel (NSLocalizedString(
			@"The number of IOTs does not match number of I/O addresses in the I/O description "
			"of the plug-in.", @""), noLoadMessage, nil, nil, nil);
		return NO;		
	}
	NSEnumerator *enumerator = [ioAddresses objectEnumerator];
	while ((ioAddress = [enumerator nextObject])) {
		NSNumber *number;
		if (! [[OctalFormatter formatterWithBitMask:077 wildcardAllowed:NO] getObjectValue:&number
			forString:ioAddress errorDescription:nil]) {
			NSRunAlertPanel ([NSString stringWithFormat:NSLocalizedString(
				@"Invalid I/O address %C%@%C in the I/O description of the plug-in.",
				@""), UNICODE_LEFT_DOUBLEQUOTE, ioAddress, UNICODE_RIGHT_DOUBLEQUOTE],
				noLoadMessage, nil, nil, nil);
			return NO;
		}
		int addr = [number intValue];
		if (! [pdp8 isIOAddressAvailable:addr]) {
			NSRunAlertPanel ([NSString stringWithFormat:NSLocalizedString(
				@"The I/O address %2.2o requested by the plug-in is already in use.", @""),
				addr], noLoadMessage, nil, nil, nil);
			return NO;
		}
	}
	return YES;
}


- (void) installIOTs:(NSArray *)mnemonics withAddresses:(NSArray *)ioAddresses forPlugin:(PDP8Plugin *)plugin
{
	int i;
	NSString *ioAddress;

	int base = 0;
	NSEnumerator *ioAddrEnum = [ioAddresses objectEnumerator];
	while ((ioAddress = [ioAddrEnum nextObject])) {
		NSNumber *number;
		[[OctalFormatter formatterWithBitMask:077 wildcardAllowed:NO] getObjectValue:&number
			forString:ioAddress errorDescription:nil];
		int addr = [number intValue];
		[pdp8 setPluginPointer:plugin forIOAddress:addr];
		NSArray *iots = [plugin iotsForAddress:addr];
		if (iots) {
			NSArray *skiptests = [plugin skiptestsForAddress:addr];
			int opcode = 06000 | (addr << 3);
			for (i = 0; i < 8; i++) {
				NSValue *iot = [iots objectAtIndex:i];
				[pdp8 setIOT:iot forOpcode:opcode | i];
				NSValue *skiptest = skiptests ? [skiptests objectAtIndex:i] : nil;
				[skipController addSkiptest:skiptest forInstruction:iot];
				NSString *mnemonic = [mnemonics objectAtIndex:base + i];
				[[Assembler sharedAssembler] addMnemonic:mnemonic forIOT:opcode | i];
				[[Disassembler sharedDisassembler] addMnemonic:mnemonic forIOT:opcode | i];
			}
		}
		base += 8;
	}
}


- (BOOL) canInstallIOFlags:(NSArray *)ioFlags noLoadMessage:(NSString *)noLoadMessage
{
	if (! ioFlags || ! [ioFlags isKindOfClass:[NSArray class]]) {
		NSRunAlertPanel (NSLocalizedString(
			@"Invalid I/O flag information in the I/O description of the plug-in.", @""),
			noLoadMessage, nil, nil, nil);
		return NO;
	}
	if ([ioFlags count] > [ioFlagController numberOfAvailableFlags]) {
		NSRunAlertPanel (NSLocalizedString(@"There are not enough I/O flags available.", @""),
			noLoadMessage, nil, nil, nil);
		return NO;
	}
	return YES;
}


- (void) installIOFlags:(NSArray *)ioFlags forPlugin:(PDP8Plugin *)plugin
{
	NSString *ioFlagName;
	
	NSEnumerator *enumerator = [ioFlags objectEnumerator];
	while ((ioFlagName = [enumerator nextObject]))
		[plugin setIOFlag:[ioFlagController addIODevice:ioFlagName] forIOFlagName:ioFlagName];
}


- (void) resetAllDevices
{
	PDP8Plugin *plugin;
	
	NSEnumerator *enumerator = [pluginInstances objectEnumerator];
	while ((plugin = [enumerator nextObject]))
		[plugin resetDevice];
}


- (NSString *) noLoadMessage:(NSString *)pluginName
{
	return [NSString stringWithFormat:NSLocalizedString(@"The plug-in %C%@%C will not be loaded.", @""),
		UNICODE_LEFT_DOUBLEQUOTE, pluginName, UNICODE_RIGHT_DOUBLEQUOTE];
}


- (PDP8Plugin *) loadPlugin:(NSBundle *)bundle
{
	PDP8Plugin *plugin = nil;
	Class principalClass = [bundle principalClass];
	if (principalClass && [principalClass isSubclassOfClass:[PDP8Plugin class]]) {
		plugin = [[principalClass alloc] init];
		if (plugin) {
			[plugin apiVersion];	// plugin must overrides this method, otherwise we crash
			[plugin setBundle:bundle];
			[plugin setPDP8:pdp8];
			NSDictionary *ioInfo = [plugin ioInformation];
			NSArray *ioFlagNames = [ioInfo objectForKey:IO_INFO_IOFLAGS_KEY];
			NSArray *ioAddresses = [ioInfo objectForKey:IO_INFO_IOADDRESSES_KEY];
			NSArray *mnemonics = [ioInfo objectForKey:IO_INFO_IOTS_KEY];
			if ([self canInstallIOFlags:ioFlagNames
				noLoadMessage:[self noLoadMessage:[plugin pluginName]]] &&
				[self canInstallIOTs:mnemonics withIOAddresses:ioAddresses
					noLoadMessage:[self noLoadMessage:[plugin pluginName]]]) {
				[self installIOFlags:[ioInfo objectForKey:IO_INFO_IOFLAGS_KEY]
					forPlugin:plugin];
				[self installIOTs:mnemonics withAddresses:ioAddresses forPlugin:plugin];
				[plugin loadNibs];
				[plugin pluginDidLoad];
				[[HelpMenuManager sharedHelpMenuManager] addBundleHelp:bundle];
			} else {
				[plugin release];
				// unload is a 10.5 method (does nothing on 10.4), but we use the 10.4 SDK
				// so use performSelector: to avoid unknown message warning
				[bundle performSelector:@selector(unload)];
				return nil;
			}
		} else {
			NSRunAlertPanel (NSLocalizedString(@"Cannot instantiate the plug-in.", @""),
				[self noLoadMessage:[[bundle bundlePath] lastPathComponent]], nil, nil, nil);
		}
	} else {
		NSRunAlertPanel (principalClass ?
			NSLocalizedString(@"The plug-in has an invalid principal class.", @"") :
			NSLocalizedString(@"Cannot determine the principal class of the plug-in.", @""),
			[self noLoadMessage:[[bundle bundlePath] lastPathComponent]], nil, nil, nil);
		
	}
	return [plugin autorelease];
}


- (NSArray *) loadAllPlugins
{
	NSEnumerator	*searchPathEnum;
	NSString	*path;
	NSString	*bundleName;
	
	NSMutableArray	*bundleSearchPaths = [NSMutableArray array];
	NSMutableArray	*plugins = [NSMutableArray array];
 
	searchPathEnum = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory,
		NSAllDomainsMask - NSSystemDomainMask, YES) objectEnumerator];
	while ((path = [searchPathEnum nextObject]))
		[bundleSearchPaths addObject:[path stringByAppendingPathComponent:PLUGIN_APPSUPPORT_PATH]];
	[bundleSearchPaths addObject:[[[NSBundle mainBundle] bundlePath]
		stringByAppendingPathComponent:PLUGIN_NEXTTOAPP_PATH]];
	[bundleSearchPaths addObject:[[NSBundle mainBundle] builtInPlugInsPath]];
 	searchPathEnum = [bundleSearchPaths objectEnumerator];
	while ((path = [searchPathEnum nextObject])) {
		NSDirectoryEnumerator *bundlePathEnum =
			[[NSFileManager defaultManager] enumeratorAtPath:path];
		while (bundlePathEnum && (bundleName = [bundlePathEnum nextObject])) {
			[bundlePathEnum skipDescendents];
			if ([[bundleName pathExtension] isEqualToString:PLUGIN_EXTENSION]) {
				NSBundle *bundle = [NSBundle bundleWithPath:
					[[NSFileManager defaultManager] resolveAliasPath:
						[path stringByAppendingPathComponent:bundleName]]];
				if (bundle) {
					PDP8Plugin *plugin = [self loadPlugin:bundle];
					if (plugin)
						[plugins addObject:plugin];
				} else
					NSRunAlertPanel (
						NSLocalizedString(@"Loading of the plug-in bundle failed.",
						@""), [self noLoadMessage:bundleName], nil, nil, nil);
			}
		}
	}
	return plugins;
}


- (void) notifyApplicationWillFinishLaunching:(NSNotification *)notification
{
	pluginInstances = [[self loadAllPlugins] retain];
}


- (void) notifyApplicationDidFinishLaunching:(NSNotification *)notification
{
	[[NSNotificationCenter defaultCenter] postNotificationName:PLUGINS_LOADED_NOTIFICATION object:nil];
}


- (void) awakeFromNib
{
	[[NSNotificationCenter defaultCenter] addObserver:self
		selector:@selector(notifyApplicationWillFinishLaunching:)
		name:NSApplicationWillFinishLaunchingNotification object:nil];
	[[NSNotificationCenter defaultCenter] addObserver:self
		selector:@selector(notifyApplicationDidFinishLaunching:)
		name:NSApplicationDidFinishLaunchingNotification object:nil];
}


@end
