// MODocController.m
//
// by Mike Ferris
// Part of MOKit
// Copyright 1993, all rights reserved.

// ABOUT MOKit
// 
// MOKit is a collection of useful and general objects.  Permission is 
// granted by the author to use MOKit in your own programs in any way 
// you see fit.  All other rights to the kit are reserved by the author 
// including the right to sell these objects as part of a LIBRARY or as 
// SOURCE CODE.  In plain English, I wish to retain rights to these 
// objects as objects, but allow the use of the objects as pieces in a 
// fully functional program.  NO WARRANTY is expressed or implied.  The author 
// will under no circumstances be held responsible for ANY consequences to 
// you from the use of these objects.  Since you don't have to pay for 
// them, and full source is provided, I think this is perfectly fair.

#import "MOKit/MODocController.h"
#import "MOKit/MODocType.h"
#import "MOKit/MODocManager.h"
#import "MOKit/MOString.h"
#import "MOKit/MOPathString.h"
#import "MOKit/MOClassVariable.h"
#import <objc/objc-runtime.h>

#define CLASS_VERSION						0
#define CLASS_NAME							"MODocController"

#define BUNDLE_TYPE							"bundle"

#define MOSTRING_CLASS_NAME					"MOString"
#define MOPATHSTRING_CLASS_NAME				"MOPathString"
#define MODOCMANAGER_CLASS_NAME				"MODocManager"
#define MOCLASSVARIABLE_CLASS_NAME			"MOClassVariable"
#define MODOCTYPE_CLASS_NAME				"MODocType"

#define UNTITLED_STRING						"Untitled"

#define MOKIT_BUNDLE						"MOKitBundle"
#define SAVEACCESSORY_NIBNAME				"MODocSaveAccessory.nib"

@interface MODocController(Private)

+ (Class)MO_loadClassBundle:(const char *)className;
+ (BOOL)MO_loadMOKitBundle;

@end

@implementation MODocController

// True class variables via hashtables
static MOClassVariable *MO_classControllerName;
static MOClassVariable *MO_classDocTypes;

static Class MOStringClass;
static Class MOPathStringClass;
static Class MODocManagerClass;
static Class MOClassVariableClass;
static Class MODocTypeClass;

static NXBundle *MOKitBundle;

+ (Class)MO_loadClassBundle:(const char *)className
// Finds the bundle of the same name as the class, grabs it and loads the
// class from it and returns the named class.
{
	char pathBuff[MAXPATHLEN+1];
	id classBundle = nil;
	Class class = nil;
	
	// Load the bundle
	if ((class = objc_lookUpClass(className)) == nil)  {
		// class is not already loaded... load it.
		
		// Look for the bundle in the main bundle first, 
		// else try in this class's bundle.
		if (![[NXBundle mainBundle] getPath:pathBuff forResource:className 
					ofType:BUNDLE_TYPE])  {
			if (![[NXBundle bundleForClass:[self class]] getPath:pathBuff 
						forResource:className ofType:BUNDLE_TYPE])  {
				NXLogError("[%s loadClassBundle] failed to "
						"find %s class bundle.", [self name], className);
				return nil;
			}
		}
		classBundle = [[NXBundle allocFromZone:[self zone]] 
					initForDirectory:pathBuff];
		if (!classBundle)  {
			NXLogError("[%s loadClassBundle] failed to "
						"create bundle for class %s.", [self name], className);
			return nil;
		}
		if ((class = [classBundle classNamed:className]) == nil)  {
			NXLogError("[%s loadClassBundle] failed to "
						"load %s class from bundle.", [self name], className);
			return nil;
		}
	}
	
	return class;
}

+ (BOOL)MO_loadMOKitBundle
// Finds the MOKit bundle (hopefully) in which a nib we need is stored.
{
	char pathBuff[MAXPATHLEN+1];
	
	// Look for the bundle in the main bundle first, 
	// else try in this class's bundle.
	if (![[NXBundle mainBundle] getPath:pathBuff forResource:MOKIT_BUNDLE 
				ofType:BUNDLE_TYPE])  {
		if (![[NXBundle bundleForClass:[self class]] getPath:pathBuff 
					forResource:MOKIT_BUNDLE ofType:BUNDLE_TYPE])  {
			NXLogError("[%s MO_loadMOKitBundle] failed to "
					"find MOKit bundle.", [self name]);
			return NO;
		}
	}
	MOKitBundle = [[NXBundle allocFromZone:[self zone]] 
				initForDirectory:pathBuff];
	if (!MOKitBundle)  {
		NXLogError("[%s MO_loadMOKitBundle] failed to "
					"create bundle for MOKit bundle.", [self name]);
		return NO;
	}
	
	return YES;

}

+ initialize
// Set the version.  Load classes.  Load the MOKit bundle.  
// Init class variables.
{
	if (self == objc_lookUpClass(CLASS_NAME))  {
		[self setVersion:CLASS_VERSION];

		// Load all the classes we need
		MOStringClass = [self MO_loadClassBundle:MOSTRING_CLASS_NAME];
		MOPathStringClass = [self MO_loadClassBundle:MOPATHSTRING_CLASS_NAME];
		MODocManagerClass = [self MO_loadClassBundle:MODOCMANAGER_CLASS_NAME];
		MOClassVariableClass = [self MO_loadClassBundle:
					MOCLASSVARIABLE_CLASS_NAME];
		MODocTypeClass = [self MO_loadClassBundle:MODOCTYPE_CLASS_NAME];
		
		// Find the MOKit bundle which contains MOKit's nibs and 
		// other non-code resources.
		[self MO_loadMOKitBundle];
		
		// Set up class variables
		MO_classControllerName = [[MOClassVariableClass 
					allocFromZone:[self zone]] 
					initDoesFreeValues:YES];
		MO_classDocTypes = [[MOClassVariableClass 
					allocFromZone:[self zone]] 
					initDoesFreeValues:YES];
	}
	return self;
}

+ setControllerName:(const char *)typeName
// Sets the name of this document class type.  This is used in New 
// and Open submenus that MODocManager creates if there is more 
// than one document type being managed.
{
	id newVal;
	
	if (typeName)  {
		newVal = [[MOStringClass allocFromZone:[self zone]] 
				initStringValue:typeName];
	}  else  {
		newVal = nil;
	}
	[MO_classControllerName setObject:newVal forClass:self];
	return self;
}

+ (MOString *)controllerName
// Returns the controller name.
{
	return [MO_classControllerName getObjectForClass:self];
}

+ addDocType:(int)tag name:(const char *)name extension:(const char *)ext 
			openSelector:(SEL)openSel saveSelector:(SEL)saveSel
// Adds a new document type that controllers of this class support.  
// The name is the full name of the doc type.  Extension is the file 
// extension for docs of that type.  Tag is for your own use.  open 
// and save selectors take a single MODocType as their argument.  
// Either can be NULL if this is a read-only or write-only file type.
{
	List *typeList = [self docTypes];
	
	if (!name || !*name)  {
		NXLogError("[%s addDocType]:  Attempted to add a document type "
					"with no name.  Document types must have names.", 
					[self name]);
		return nil;
	}
	if (!typeList)  {
		typeList = [[List allocFromZone:[self zone]] init];
		[MO_classDocTypes setObject:typeList forClass:self];
	}

	[typeList addObject:[[MODocTypeClass allocFromZone:[self zone]] 
				initForControllerClass:self tag:tag name:name extension:ext 
				openSelector:openSel saveSelector:saveSel]];
	return self;
}

+ (List *)docTypes
// Returns the list of document types supported by this class.
{
	return (List *)[MO_classDocTypes getObjectForClass:self];
}

+ (MODocType *)docTypeForTag:(int)tag
// Returns the doc type whose tag is tag.
{
	List *docTypes = [self docTypes];
	int i, c = [docTypes count];
	MODocType *type = nil;
	
	for (i=0; i<c; i++)  {
		type = [docTypes objectAt:i];
		if ([type typeTag] == tag)  {
			return type;
		}
	}
	return nil;
}

+ (MODocType *)docTypeForName:(const char *)name
// Returns the doc type whose name is name.
{
	List *docTypes = [self docTypes];
	int i, c = [docTypes count];
	MODocType *type = nil;
	
	for (i=0; i<c; i++)  {
		type = [docTypes objectAt:i];
		if (([type typeName]) && (name) && 
					(strcmp([type typeName], name) == 0))  {
			return type;
		}
	}
	return nil;
}

+ (MODocType *)docTypeForExtension:(const char *)extension
// Returns the doc type whose extension is extension.
// If we find a match, return it.  If we don't find a match, and we have
// an extension-less doc type, return that.
{
	List *docTypes = [self docTypes];
	int i, c = [docTypes count];
	MODocType *type = nil;
	const char *curExt = NULL;
	
	for (i=0; i<c; i++)  {
		type = [docTypes objectAt:i];
		curExt = [type typeExtension];
		if ((!extension || !*extension) && (!curExt || !*curExt))  {
			return type;
		}
		if ((curExt) && (extension) && 
					(strcmp(curExt, extension) == 0))  {
			return type;
		}
	}  
	if (extension && *extension)  {
		return [self docTypeForExtension:NULL];
	}
	return nil;
}

+ (List *)getOpenTypesList:(List *)list
// Fills list with the types which support reading from our docTypes list.
// list is returned.  If list is nil, a List object is allocated for it.
{
	List *docTypes = [self docTypes];
	int i, c = [docTypes count];
	MODocType *type = nil;
	
	if (!list)  {
		list = [[List allocFromZone:[self zone]] init];
	}
	
	for (i=0; i<c; i++)  {
		type = [docTypes objectAt:i];
		if ([type canOpenType])  {
			[list addObject:type];
		}
	}
	
	return list;
}

+ (List *)getSaveTypesList:(List *)list
// Fills list with the types which support saving from our docTypes list.
// list is returned.  If list is nil, a List object is allocated for it.
{
	List *docTypes = [self docTypes];
	int i, c = [docTypes count];
	MODocType *type = nil;
	
	if (!list)  {
		list = [[List allocFromZone:[self zone]] init];
	}
	
	for (i=0; i<c; i++)  {
		type = [docTypes objectAt:i];
		if ([type canSaveType])  {
			[list addObject:type];
		}
	}
	
	return list;
}

+ startUnloading
// We have to deallocate the class variables.
{
	[MO_classControllerName free];
	[MO_classDocTypes free];
	
	return self;
}

- initFromFile:(const char *)path manager:aManager
// This is the initializer that will be used normally.  It just calls the DI.
{
	return [self initWithFrameName:NULL fromFile:path manager:aManager];
}

- initWithFrameName:(const char *)theFrameName
// Just call the DI.
{
	return [self initWithFrameName:theFrameName fromFile:NULL manager:nil];
}

- initWithFrameName:(const char *)theFrameName 
			fromFile:(const char *)path manager:aManager
// Designated Initializer.  A null file means a new untitled doc.
{
	[super initWithFrameName:theFrameName];
	
	[self setManager:aManager];
	docName = [[MOPathStringClass allocFromZone:[self zone]] init];
	untitledString = [[MOPathStringClass allocFromZone:[self zone]] init];
	docType = nil;
	
	isDirty = NO;
	saveAccessoryPanel = nil;
	saveAccessoryBox = nil;
	saveAccessoryPopupButton = nil;
	
	[self setFile:path];
	
	return self;
}

- free
// free our ivars
{
	[docName free];
	[untitledString free];
	if (manager)  [manager removeDocument:self];
	
	return [super free];
}

- nibDidLoad
// Set the window title, move the window according to the frameCycle.
{
	[super nibDidLoad];
	
	if (window)  {
		[self resetWindowTitle];
		if (manager)  {
			NXPoint pt;
			[manager getNextWindowLocation:&pt];
			[window moveTopLeftTo:pt.x :pt.y];
		}
	}
	return self;
}

- (BOOL)doClear
// override this to totally clear this doc into a blank one.
{
	return YES;
}

- (BOOL)doPrint
// override this to print the document (including running print panel, etc...).
{
	return YES;
}

- saveAccessoryPopupAction:sender
// The action of the save accessory document type popup.  It sets 
// the Save panels required file extension appropriately.
{
	id savePanel = [SavePanel new];
	List *saveTypes = [[self class] getSaveTypesList:nil];
	
	[savePanel setRequiredFileType:[[saveTypes objectAt:
				[sender selectedRow]] typeExtension]];
	
	[saveTypes free];
	return self;
}

- getSaveAccessoryForSaveTypes:(List *)saveTypes 
			currentType:(MODocType *)currentType
// This will prepare and return the saveAccessory view for the given sender 
// document.  If necessary it will load the accessory nib first.  It returns 
// nil if there is only a single save type since no accessory is needed then.
// Subclasses may override this to use a different nib file.  If a subclass
// does override, it is important that the new nib file NOT use the existing
// saveAccessory... outlets for MODocController.  Declare all new outlets for
// your save accessory nib in your subclass and use those.  The saveAccessory
// outlets in this class are not used outside this method, but other subclasses
// may not override, and they should get the old nib.
{
	id popup = nil;
	int i, c;
	MOString *scratch = nil;
	MODocType *type = nil;
	
	c = [saveTypes count];
	if (c <= 1)  {
		return nil;
	}
	
	if (!saveAccessoryBox)  {
		if (MOKitBundle)  {
			char pathBuff[MAXPATHLEN+1];
			if (![MOKitBundle getPath:pathBuff 
						forResource:SAVEACCESSORY_NIBNAME ofType:NULL])  {
				NXLogError("%s: failed to find nib file %s in MOKit bundle.", 
						[[self class] name], SAVEACCESSORY_NIBNAME);
				return nil;
			}
			if (![NXApp loadNibFile:pathBuff owner:self])  {
				NXLogError("%s: failed to load nib file %s.", 
						[[self class] name], pathBuff);
				return nil;
			}
		}  else  {
			if (![NXApp loadNibSection:SAVEACCESSORY_NIBNAME owner:self])  {
				NXLogError("%s: failed to load nib file %s.", 
						[[self class] name], SAVEACCESSORY_NIBNAME);
				return nil;
			}
		}
		[saveAccessoryBox removeFromSuperview];
		[saveAccessoryPanel free];
		saveAccessoryPanel = nil;
	}
	
	popup = [saveAccessoryPopupButton target];
	
	// remove all but the first item in the list
	for (i=[popup count]-1; i>0; i--)  {
		[[popup removeItemAt:i] free];
	}
	
	// set the first item's title to ""
	[[[popup itemList] cellAt:0:0] setTitle:""];
	
	scratch = [[MOString allocFromZone:[self zone]] init];

	// add the new items
	for (i=0; i<c; i++)  {
		type = [saveTypes objectAt:i];
		[scratch setFromFormat:"%s (%s)", 
					[type typeName], 
					([type typeExtension]?[type typeExtension]:"<none>")];
		[popup addItem:[scratch stringValue]];
		// If this is the current type...
		if (type == currentType)  {
			[saveAccessoryPopupButton setTitle:[scratch stringValue]];
		}
	}
	
	// If there is no current type, select the first one.
	if (currentType == nil)  {
		type = [saveTypes objectAt:0];
		[scratch setFromFormat:"%s (%s)", 
					[type typeName], 
					([type typeExtension]?[type typeExtension]:"<none>")];
		[saveAccessoryPopupButton setTitle:[scratch stringValue]];
	}
	
	// remove the first item (which was still left from the old.
	[[popup removeItemAt:0] free];
	
	// set the target/action
	[popup setTarget:self];
	[popup setAction:@selector(saveAccessoryPopupAction:)];
	
	[popup sizeToFit];
			
	return saveAccessoryBox;
}

- save:sender
// Called from various places when we are supposed to save.
{
	BOOL ret = YES;

	if ([docName isEmpty])  {
		return [self saveAs:self];
	}
	ret = ([self perform:[docType saveSelector] with:docType] != nil);
	if (ret)  {
		[self setDirty:NO];
	}
	return ret?self:nil;
}

- saveAs:sender
// Called by DocManager in response to Save As command.
{
	id savePanel = [SavePanel new];
	MOPathString *dir, *file, *extension;
	
	List *saveTypes = [[self class] getSaveTypesList:nil];
	id saveAccessory = nil;
	
	if ([saveTypes count] == 0)  {
		[saveTypes free];
		return nil;
	}
	
	saveAccessory = [self getSaveAccessoryForSaveTypes:saveTypes 
			currentType:docType];
	
	[savePanel setAccessoryView:saveAccessory];
	[savePanel setDelegate:self];
	[savePanel setTreatsFilePackagesAsDirectories:NO];
	if (!docType)  {
		[savePanel setRequiredFileType:[[saveTypes objectAt:0] typeExtension]];
	}  else  {
		[savePanel setRequiredFileType:[docType typeExtension]];
	}
	if ([docName isEmpty])  {
		dir = [[MOPathStringClass allocFromZone:[self zone]] 
					initPath:NXHomeDirectory()];
		file = [untitledString copy];
	}  else  {
		file = [docName file];
		dir = [docName directory];
	}
	if (![savePanel runModalForDirectory:[dir stringValue] 
				file:[file stringValue]])  {
		[file free];
		[dir free];
		[saveTypes free];
		return nil;
	}
	
	[docName setPath:[savePanel filename]];
	extension = [docName fileExtension];
	docType = [[self class] docTypeForExtension:[extension stringValue]];
	[extension free];
	[self resetWindowTitle];
	[self save:self];
	
	[file free];
	[dir free];
	[saveTypes free];
	return self;
}

- saveTo:sender
// Called by DocManager in response to Save To command.  The bit about 
// the flushWindow is so the title bar doesn't flash the save to path and 
// then reset to the old path visibly.
{
	id copyString = [docName copy];
	BOOL copyDirty = [self isDirty];
	
	if (window)  {
		[window disableFlushWindow];
	}
	[self saveAs:self];
	
	[self setDirty:copyDirty];
	[docName free];
	docName = copyString;
	[self resetWindowTitle];
	
	if (window)  {
		[window reenableFlushWindow];
		[window flushWindow];
	}
	return self;
}

- revert:sender
// Called when a file is opened or reverted.
{
	BOOL ret = YES;
	
	if ([docName isEmpty])  {
		ret = [self doClear];
	}  else  {
		ret = ([self perform:[docType openSelector] with:docType] != nil);
	}
	if (ret)  {
		[self resetWindowTitle];
		[self setDirty:NO];
	}
	return ret?self:nil;
}

- close:sender
// Closes the document's window.
{
	[window performClose:self];
	return self;
}

- print:sender
// Prints the doc.
{
	BOOL ret = [self doPrint];
	return ret?self:nil;
}

- saveIfNeeded
// Save ourself only if we are dirty.
{
	if ([self isDirty])  [self save:self];
	return self;
}

- setManager:aManager
// Set the docManager that manages us.
{
	if (manager)  [manager removeDocument:self];
	manager = aManager;
	if (manager)  [manager addDocument:self];
	return self;
}

- manager
// Return our manager.
{
	return manager;
}

- setFile:(const char *)fName
// Name our file.  If fName is NULL, set up an untitled name.  The file
// should be of a type we can read.  Also, if the filename is not of a 
// type we can save, make it untitled after loading.
{
	id extension = nil;
	
	[docName setStringValue:fName];
	if ([docName isEmpty])  {
		docType = nil;
		if (manager)  {
			[untitledString setFromFormat:"%s-%d", UNTITLED_STRING, 
						[manager nextUntitledNum]];
		}  else  {
			[untitledString setFromFormat:"%s", UNTITLED_STRING];
		}
	}
	
	[self window:YES];
	if (window)  {
		[window disableFlushWindow];
	}
	if (![docName isEmpty])  {
		extension = [docName fileExtension];
		docType = [[self class] docTypeForExtension:[extension stringValue]];
	}
	if (![self revert:self])  {
		NXRunAlertPanel("Error", "Failed to revert document %s.", "OK", 
					NULL, NULL, [docName stringValue]);
	}
	
	// If we can't save files of whatever type this is, set us to untitled.
	// This must be done after the revert.
	if (![docName isEmpty])  {
		if (!docType || (![docType canSaveType]))  {
			[docName setNull];
			docType = nil;
			if (manager)  {
				[untitledString setFromFormat:"%s-%d", UNTITLED_STRING, 
							[manager nextUntitledNum]];
			}  else  {
				[untitledString setFromFormat:"%s", UNTITLED_STRING];
			}
			[self resetWindowTitle];
		}
	}
	if (extension)  [extension free];
	if (window)  {
		[window reenableFlushWindow];
		[window flushWindow];
	}
	[self showWindow:self];

	return self;
}

- (BOOL)isUntitled
// Returns YES if this doc has no file.
{
	return [docName isEmpty];
}

- (const char *)file
// Return our filename (or the untitled name if we have no file).
{
	if (![docName isEmpty])  {
		return [docName stringValue];
	}  else  {
		return [untitledString stringValue];
	}
}

- docName
// Return the actual MOPathString used to store our doc name.  This method
// can be useful if you want to use the MOPathString features.  Otherwise, 
// use the -file method.
{
	return docName;
}

- resetWindowTitle
// Make sure that our window title is correct.
{
	if (window)  [window setTitleAsFilename:[self file]];
	return self;
}

- setDirty:(BOOL)flag
// Mark ourselves as edited.
{
	isDirty = flag;
	if (window)  [window setDocEdited:flag];
	return self;
}

- (BOOL)isDirty
// Returns whether or not the doc is dirty.
{
	return isDirty;
}

- windowDidBecomeMain:sender
// Inform the manager that we should be the current document.
{
	if (manager)  [manager setCurrentDocument:self];
	return self;
}

- windowDidResignMain:sender
// Inform the manager that we should no longer be the current document.
{
	if (manager)  [manager setCurrentDocument:nil];
	return self;
}

- windowWillClose:sender
// If we are dirty, ask if we should save before closing and give the chance
// to cancel.
{
	int ret;
	
	if ([self isDirty])  {
		ret = NXRunAlertPanel("Close", "Document %s has unsaved changes.  "
					"Do you wish to save, close anyway, or cancel?", 
					"Save", "Close Anyway", "Cancel", [self file]);
		if (ret==NX_ALERTDEFAULT)  {
			// save
			if (![self save:self])  {
				return nil;
			}
		}  else if (ret==NX_ALERTALTERNATE)  {
			// close anyway
			// do nothing, just let it close
		}  else  {
			// cancel
			return nil;
		}
	}
	// if we get to here, we are closing
	[NXApp delayedFree:self];
	return self;
}

- awake
// Initialize stuff we don't archive.
{
	saveAccessoryPanel = nil;
	saveAccessoryBox = nil;
	saveAccessoryPopupButton = nil;
	return self;
}

- read:(NXTypedStream *)strm
// This method is probably not useful.  But here it is anyway.
{
	int classVersion;

	[super read:strm];
	
	classVersion = NXTypedStreamClassVersion(strm, CLASS_NAME);
	
	switch (classVersion)  {
		case 0:		// First version.
			docName = NXReadObject(strm);
			untitledString = NXReadObject(strm);
			manager = NXReadObject(strm);
			NXReadType(strm, "C", &isDirty);
			docType = nil;
			[self setFile:[docName stringValue]];
			break;
		default:
			NXLogError("[%s read:] class version %d cannot read "
						"instances archived with version %d", 
						CLASS_NAME, CLASS_VERSION, classVersion);
			docName = [[MOPathStringClass allocFromZone:[self zone]] init];
			untitledString = [[MOPathStringClass allocFromZone:
						[self zone]] init];
			manager = nil;
			isDirty = NO;
			docType = nil;
			[self setFile:NULL];
			break;
	}
	return self;
}

- write:(NXTypedStream *)strm
// This method is probably not useful.  But here it is anyway.
{
	[super write:strm];
	NXWriteObject(strm, docName);
	NXWriteObject(strm, untitledString);
	NXWriteObjectReference(strm, manager);
	NXWriteType(strm, "C", &isDirty);
	return self;
}

@end

