#import "MultDoc.h"
#import "MultApp.h"

#import "Utility.h"

#import <appkit/MenuCell.h>
#import <appkit/PrintInfo.h>
#import <appkit/SavePanel.h>
#import <appkit/ScrollView.h>
#import <appkit/Text.h>
#import <mach/mach_init.h>
#import <objc/List.h>

@implementation MultDoc: Responder

// Canon Information Systems is not responsible for anything anyone does with
// this code, nor are they responsible for the correctness of this code.
// Basically, this has very little to do with the company I work for, and you
// can't blame them.

// This file is best read in a window as wide as this comment, and with tab
// settings of 4 spaces and indent setting of 4 spaces (at least, that's what
// I use).

// You are welcome to do as you would with this file under the following
// conditions.
// First, I accept no blame for anything that goes wrong no matter how you
// use it, no matter how catastrophic, not even if it stems from a bug in my
// code.  Second, please keep my notices on it when/if you distribute it.					 // Third, if you discover any bugs or have any comments, PLEASE TELL ME!
// Code won't get better without people picking it apart and giving the writer
// feedback.  Fourth, if you modify it, please keep a notice that your version
// is based on mine in the source files (and keep the notice that mine is based
// on four other pieces of code :<).  Thanks, and have fun.
// - Subrata Sircar, ssircar@canon.com

/* This class should handle all the document-level responsibilities   */
/* that a nice document in a multi-doc environment should handle:     */
/* creation, deletion, keeping track of dirty bits, not closing w/out */
/* allowing the user to save, etc.  Most of this code is lifted from  */
/* DrawDocument, or TextLab; heavy use is made of the firstResponder  */
/* mechanism.  Thanks again, guys.					 				  */

// The default implementation loads windows with scrollviews from a nib file.
// To modify, the revert/save/newFromFile methods should be changed to use
// the view (subclass) that acts as the content view of the Window.
//					- Subrata Sircar (ssircar@canon.com)

/* Version 0.9b			Apr-19-92		First Public Release	*/
/* Version 0.95b		Apr-29-92		Multiple Save Types		*/
/* Version 1.00b		Jan-19-93		Updated for 3.0			*/

/* Factory (Class) variables */

static char 		*default_format		= NULL;	// Default title format
static const char 	*extension			= NULL;	// Document extension
static const int 	myVersion			= 100;	// version number * 100
static id 			zoneList 			= nil;
static id			appDelegate			= NULL;	// app delegate instance
static NXCoord 		ORIGX 				= 100.0;// doc starting position
static NXCoord 		ORIGY 				= 100.0;
static int			posInc 				= 20.0;	// increment to position

#define			APP_DELEGATE		[[self class] appDelegate]
 
+ initialize
/* Class variable initialization.  DO NOT call from subclasses. */
{
    if (self == [MultDoc class]) {
		[self setVersion:myVersion];
		[self setExtension:LocalString("txt")];
		[self setDefault:LocalString("Untitled%d")];
	}
	return self;
}

/* Extension to use in Workspace file names */

+ setExtension:(const char *)newExtension
{
    extension = newExtension;	// Must be a constant string!
    return self;
}

+ (const char *)extension
{
	return extension;
}

/* Default Title string for new documents */

+ setDefault:(const char *)newDefault
{
	char temp[MAXPATHLEN+1];
	
    if (default_format) NX_FREE(default_format);
	sprintf(temp,"%s.%s",newDefault,[[self class] extension]);
    default_format = NXCopyStringBufferFromZone(temp,MyZone);
    return self;
}

+ (const char *)default
{
	return default_format;
}

/* The application delegate object is a class variable so all instances
 * can access it without keeping a pointer for each instance.
 */
+ setAppDelegate:anObject
{
	appDelegate = anObject;
	return self;
}

+ appDelegate
{
	return appDelegate;
}

/* Factory zone methods.  I adopt the virtual zone strategy used by Draw. */

+ (NXZone *)newZone
{
    if (!zoneList || ![zoneList count]) {
		return NXCreateZone(vm_page_size, vm_page_size, YES);
    } else {
		return (NXZone *)[zoneList removeLastObject];
    }
}

+ (void)reuseZone:(NXZone *)aZone
{
    if (!zoneList) zoneList = [List new];
    [zoneList addObject:(id)aZone];
    NXNameZone(aZone, "Unused");
}

+ allocFromZone:(NXZone *)aZone
{
    return [self notImplemented:@selector(allocFromZone:)];
}

+ alloc
{
    return [self notImplemented:@selector(alloc)];
}


/* New creation methods */

- setUpNib
/*
 * Load the document's nib file and perform any nib-specific initialization
 */
{
	if (LoadLocalNib(LocalString("MultDoc.nib"),self,NO,MyZone)) {
    	[[view docView] setDelegate:self];
    	[window makeFirstResponder:[view docView]];
		return self;
	} else {
		char temp[MAXPATHLEN+1];
		sprintf(temp,"%s %s\n",LocalString("Could not load requested resource"),LocalString("MultDoc.nib")); 
		Notify(LocalString("Resource Error"),temp);
		return nil;
	}
}

- instanceAwake
/*
 * Set up any instance variables or other initialization that needs doing
 */
{
	NXRect frameRect;
    calcFrame(printInfo, &frameRect);
	if (frameRect.size.width > 1000) frameRect.size.width = 1000;
	if (frameRect.size.height > 700) frameRect.size.height = 700;
	[window sizeWindow:frameRect.size.width :frameRect.size.height];
	[window moveTo:ORIGX :ORIGY];
	ORIGX += posInc; ORIGY -= posInc;
	if (ORIGX > 700) ORIGX = 110;
	if (ORIGY < 0) ORIGY = 70;
    [window setDelegate:self];
    [self dirty:NO];
    [self setSavedDocument:NO];
	[self setEmpty:YES];
    [self setName:NULL andDirectory:NULL];
	return self;
}

+ newFromFile:(const char *)file
{
    NXStream *stream;
    char *strPtr = rindex(file, '.');
	
	if (strcmp([[self class] extension],++strPtr)) return [[self class] newFromPasteboard:[Pasteboard newByFilteringFile:file]];

    stream = NXMapFile(file,NX_READONLY);
    if (stream) {
    	self = [self new];
		[[view docView] readText:stream];
		[[view docView] sizeToFit];
		[view display];
		[self setName:file];
		[self setSavedDocument:YES];
		[self setEmpty:NO];
    	[window makeKeyAndOrderFront:self];
		NXCloseMemory(stream,NX_FREEBUFFER);
		return self;
    } else {
		Notify(LocalString("MultDoc: can't open file"),file);
		return nil;
    }
}

+ new
{
    self = [[self class] newFromZone:[[self class] newZone]];
	return self;
}

+ newFromZone:(NXZone *)zone;
/*
 * All creation methods end up filtered through newFromZone, so
 * it is as general as possible, leaving the subclass specific code
 * to the above functions.
 */
{
	self = [super allocFromZone:zone];
    printInfo = [PrintInfo new];
    [printInfo setMarginLeft:36.0 right:36.0 top:36.0 bottom:36.0];
	if ([self setUpNib]) {
		[self instanceAwake];
		return self;
	} else {
		[self free];
		return nil;
	}
}

+ newFromPasteboard:pasteboard
{
	char *stringPos, *tempPtr, *pathList;
	char tempBuf[MAXPATHLEN+1];
	char **filePath = NULL;
	int files = 0, length = 0, count = 0;
	BOOL				flag = NO;
	id					pBoard = [Pasteboard newByFilteringTypesInPasteboard:pasteboard];
    
	if ([pBoard findAvailableTypeFrom:&NXFilenamePboardType num:1]) {
		if ([pBoard readType:NXFilenamePboardType data:&pathList length:&length]) flag = YES;
	} else if ([pBoard findAvailableTypeFrom:&NXAsciiPboardType num:1]) {
		if ([pBoard readType:NXAsciiPboardType data:&pathList length:&length]) flag = YES;
	}

	if (!flag) {
		Notify(LocalString("MultDoc"),LocalString("Could not parse or filter ASCII data from the requested source."));
		return nil;
	}
	stringPos = tempPtr = pathList;
	    
	/* This code just parses the pathList for the filenames and explictly */
	/* null-terminates them after copying into a buffer.  It appends that */
	/* buffer to the dynamically-allocated filelist which keeps track of it */

	/* the number of tabs + 1 equals the number of files dragged in */

	while (stringPos = index(stringPos, '\t')) {
		count = (int)(stringPos-tempPtr);
		strncpy(tempBuf,tempPtr,count);
		*(tempBuf+count) = '\0';
		filePath = addFile(tempBuf,filePath,files,MyZone);
		files++;
		stringPos++;
		tempPtr=stringPos;
    }
	count = strlen(tempPtr);
	strncpy(tempBuf,tempPtr,count);
	*(tempBuf+count) = '\0';
	filePath = addFile(tempBuf,filePath,files,MyZone);
	files++;
	
	while (files--) {
		if (![MultDoc newFromFile:filePath[files]]) Notify(LocalString("MultDoc: can't open file"),filePath[files]);
	}
	freeList(filePath);
	[pBoard deallocatePasteboardData:pathList length:length];
	return self;
}

- free
{
    NX_FREE(name);
    NX_FREE(directory);
	if ([printInfo isKindOf:[PrintInfo class]])	[printInfo free];
    if ([window isKindOf:[Window class]])		[window free];
    [[self class] reuseZone:MyZone];
    return [super free];
}

- view
/*
 * Returns the view associated with this document.
 */
{
    return view;
}

- (BOOL)needsSaving
/*
 * Returns the dirty BOOL associated with this document.
 */
{
    return dirty;
}

- (BOOL)isEmpty
/*
 * Returns the empty BOOL associated with this document.
 */
{
    return empty;
}

- (BOOL)hasSavedDocument
{
	return haveSavedDocument;
}

- setEmpty:(BOOL)flag
/*
 * Sets the BOOL to flag.
 */
{
    empty = flag;
    return self;
}

- dirty:(BOOL)flag
/*
 * Sets the BOOL to flag.
 */
{
    dirty = flag;
	[window setDocEdited:flag];
    return self;
}

- setSavedDocument:(BOOL)flag
/*
 * Sets the BOOL to flag.
 */
{
    haveSavedDocument = flag;
    return self;
}

- printInfo
{
    return printInfo;
}


/* Target/Action methods */

- save:sender
/*
 * Saves the file.  If this document has never been saved to disk,
 * then a SavePanel is put up to ask the user what file name she
 * wishes to use to save the document.
 */
{
    id			savepanel	= [APP_DELEGATE saveAsPanel:self];
	const char	*dir		= NULL;	

    if (![self hasSavedDocument]) {
		if ([savepanel runModalForDirectory:directory file:name])
			[self setName:[savepanel filename]];
		else return nil;
    }

    [self save];
	dir = [self directory];
//	chdir(dir);
	[APP_DELEGATE setDefaultDir:dir];
    return self;
}

- saveAs:sender
/* Changes the document name.  IF you cancel, it does the right thing 	*/
/* and undoes the changes to the state bits.							*/
{
	BOOL oldSavedFile, oldDirty;
	
	oldDirty = [self needsSaving];
	oldSavedFile = [self hasSavedDocument];
    [self dirty:YES];
    [self setSavedDocument:NO];
	if ([self save:sender]) return self;
	else {
		[self setSavedDocument:oldSavedFile];
		[self dirty:oldDirty];
		return nil;
	}
}

- revertToSaved:sender
/*
 * Revert the document back to what is on the disk.
 */ 
{
	MultDoc *oldPtr;
	
    if (![self hasSavedDocument] ||
		![self needsSaving] ||
		(NXRunAlertPanel(LocalString("Revert"),
						 LocalString("%s has been edited.  Are you sure you want to undo changes?"),
						 LocalString("Revert"),
						 LocalString("Cancel"), NULL, name) != NX_ALERTDEFAULT)) {
	return self;
    }

	oldPtr = self;
	self = [[oldPtr class] newFromFile:[oldPtr fileName]];
	[oldPtr free];
	return self;
}

- save
{	
    NXStream *ts;
    int fd;
    
    fd = open([self fileName], O_CREAT | O_WRONLY | O_TRUNC, 0666);
    ts = NXOpenFile(fd, NX_WRITEONLY);
    if (ts) {
		[[view docView] writeText:ts];
		NXFlush(ts);
		NXClose(ts);
		[self dirty:NO];
		[self setSavedDocument:YES];
    } else Notify(LocalString("MultDoc:save - can't create file"),[self fileName]);
    close(fd);
    return self;
}

- changeLayout:sender
/*
 * Puts up a PageLayout panel and allows the user to pick a different
 * size paper to work on.  After she does so, the view is resized to the
 * new paper size.
 * Since the PrintInfo is effectively part of the document, we dirty
 * the view (by performing the dirty method).
 */
{
    NXRect frame;

    if ([[APP_DELEGATE pageLayout:self] runModal] == NX_OKTAG) {
		calcFrame(printInfo, &frame);
		[window sizeWindow:frame.size.width :frame.size.height];
		[self dirty:YES];
		[window display];
    }

    return self;
}


/* Methods related to naming/saving this document. */

- (const char *)fileName
/*
 * Gets the fully specified file name of the document.
 * If directory is NULL, then the currentDirectory is used.
 * If name is NULL, then the default title is used.
 */
{
    static char filenamebuf[MAXPATHLEN+1];

    if (!directory && !name) [self setName:NULL andDirectory:NULL];
    if (directory) {
		strcpy(filenamebuf, directory);
		strcat(filenamebuf, "/");
    } else filenamebuf[0] = '\0';
    if (name) strcat(filenamebuf, name);

    return filenamebuf;
}

- (const char *)directory
{
    return directory;
}

- (const char *)name
{
    return name;
}

- setName:(const char *)newName andDirectory:(const char *)newDirectory
/*
 * Updates the name and directory of the document.
 * newName or newDirectory can be NULL, in which case the name or directory
 * will not be changed (unless one is currently not set, in which case
 * a default name will be used).
 */
{
    char newNameBuf[MAXPATHLEN+1];
    static int uniqueCount = 1;

    if ((newName && *newName) || !name) {
		if (!newName || !*newName) {
			sprintf(newNameBuf, [[self class] default], uniqueCount++);
			newName = newNameBuf;
		} else if (name) NX_FREE(name);
		name = NXCopyStringBufferFromZone(newName, MyZone);
    }

    if ((newDirectory && (*newDirectory == '/')) || !directory) {
		if (!newDirectory || (*newDirectory != '/')) newDirectory = [APP_DELEGATE currentDirectory];
		else if (directory) NX_FREE(directory);
		directory = NXCopyStringBufferFromZone(newDirectory, MyZone);
    }

    [window setTitleAsFilename:[self fileName]];
    NXNameZone(MyZone, [self fileName]);

    return self;
}

- setName:(const char *)file
/*
 * If file is a full path name, then both the name and directory of the
 * document is updated appropriately, otherwise, only the name is changed.
 */
{
    char *lastComponent;
    char path[MAXPATHLEN+1];

    if (file) {
		strcpy(path, file);
		lastComponent = rindex(path, '/');
		if (lastComponent) {
			*lastComponent++ = '\0';
			return [self setName:lastComponent andDirectory:path];
		} else return [self setName:file andDirectory:NULL];
	}

    return self;
}

/* Window delegate methods. */

- windowWillClose:sender
/*
 * If the document has been edited, then this asks the user if she
 * wants to save the changes before closing the window.  When the window
 * is closed, the document itself must be freed.  This is accomplished
 * via Application's delayedFree: mechanism.  Unfortunately, by the time
 * delayedFree: frees the document, the window and view instance variables
 * will already have automatically been freed by virtue of the window's being
 * closed.  Thus, those instance variables must be set to nil to avoid their
 * being freed twice.  (This behavior is set in the window flags in IB.)
 *
 * Returning nil from this method informs the caller that the window should
 * NOT be closed.  Anything else implies it should be closed.
 */
{
    int save;

    if ([self needsSaving] && ![self isEmpty]) {
		[window makeKeyAndOrderFront:self];
		save = NXRunAlertPanel(LocalString("Close"),
							   LocalString("%s has changes. Save them?"),
							   LocalString("Save"),
							   LocalString("Don't Save"),
							   LocalString("Cancel"),
							   name);
		if (save != NX_ALERTDEFAULT && save != NX_ALERTALTERNATE) return nil;
		else {
			[window endEditingFor:self];	/* terminate any editing */
			if (save == NX_ALERTDEFAULT) {
				if (![self save:nil]) return nil;
			}
		}
    }

    window = nil;
    view = nil;
    if (printInfo) [NXApp setPrintInfo:nil];
    [NXApp delayedFree:self];
//	chdir([APP_DELEGATE currentDirectory]);
    return self;
}

- windowDidBecomeMain:sender
/*
 * Switch the Application's PrintInfo to the document's when the document
 * window becomes the main window.  Also set the cursor appropriately
 * depending on which tool is currently selected.
 */
{
    [NXApp setPrintInfo:printInfo];
    return self;
}

- windowWillMiniaturize:sender toMiniwindow:counterpart
{
    char *dot;
    char title[MAXPATHLEN+1];

    strcpy(title, [self name]);
    dot = rindex(title, '.');
    if (dot && !strcmp(++dot, extension)) *(--dot) = '\0';
    [counterpart setTitle:title];
    return self;
}


/* Validates whether a menu command makes sense now */

- (BOOL)validateCommand:menuCell
{
    SEL action = [menuCell action];

    if (action == @selector(revertToSaved:)) return ([self needsSaving] && [self hasSavedDocument]);
    else if (action == @selector(saveAs:)) return (![self isEmpty] && [self hasSavedDocument]);
    else if (action == @selector(close:)) return YES;
     
    return YES;
}

/* Text Delegate methods */

- textDidGetKeys:sender isEmpty:(BOOL)flag
{

	if (![self needsSaving]) [self dirty:YES];
	[self setEmpty:flag];
	return self;
}

@end
