#import "Deck.h"
#import "Document.h"
#import "AppWithDoc.h"
#import <appkit/Window.h>
#import <appkit/ScrollView.h>
#import <appkit/TextField.h>
#import <appkit/Text.h>
#import <appkit/SavePanel.h>
#import <appkit/Listener.h>
#import <appkit/Speaker.h>
#import <appkit/nextstd.h>
#import <objc/hashtable.h>	/* for NXCopyStringBuffer() */
#import <objc/List.h>	/* for NXCopyStringBuffer() */
#import <text/pathutil.h>


@implementation Document
/*
 * This class is used to keep track of a document. 
 *
 * Its contents and window instance variables keep track of the contents of the
 * document as well as its window and sub Views. The name and directory
 * specify where the document is to be saved. haveSavedDocument keeps track
 * of whether a disk file is associated with the document yet (i.e. if it has
 * ever been saved). 
 *
 * The Document class's responsibilities: 
 *
 * 1. Manage the window which displays the document's contents. This includes
 * ensuring that if the window contains an unsaved document and the user
 * tries to close it, the user gets an opportunity to save his changes. 
 * 2. Saving the document to a disk file. 
 */

/* Private Functions */

static const char DEFAULT_TITLE[] = "Untitled%d.%s";
#define SCROLLVIEW_BORDER NX_NOBORDER
#define WINDOW_MASK (NX_MOUSEUPMASK|NX_MOUSEDRAGGEDMASK|NX_FLAGSCHANGEDMASK)

static void getContentSizeForView(id view, NXSize * contentSize)
/*
 * Calculates the size of the window's contentView by accounting for the
 * existence of the ScrollView around the GraphicView.  No scrollers are
 * assumed since we are interested in the minimum size need to enclose the
 * entire view and, if the entire view is visible, we don't need scroll bars! 
 */
{
    NXRect      viewFrame;
    [view getFrame:&viewFrame];
    [ScrollView getFrameSize:contentSize
     forContentSize:&viewFrame.size
     horizScroller:NO vertScroller:NO
     borderType:SCROLLVIEW_BORDER];
}

static id   createWindowFor(id view, NXRect * windowContentRect)
/*
 * Creates a window for the specified view. If windowContentRect is not NULL,
 * then it is used as the contentView of the newly created window. 
 */
{
    NXSize      screenSize;
    id          scrollView, window;
    NXRect      defaultWindowContentRect;
    if (!windowContentRect) {
	windowContentRect = &defaultWindowContentRect;
	getContentSizeForView(view, &windowContentRect->size);
	[NXApp getScreenSize:&screenSize];
	if (windowContentRect->size.width > screenSize.width / 2.0) {
	    windowContentRect->size.width = floor(screenSize.width / 2.0);
	}
	if (windowContentRect->size.height > screenSize.height - 60.0) {
	    windowContentRect->size.height = screenSize.height - 60.0;
	}
	windowContentRect->origin.x = screenSize.width - 85.0 -
	    windowContentRect->size.width;
	windowContentRect->origin.y = floor((screenSize.height -
				     windowContentRect->size.height) / 2.0);
    }
    window = [Window newContent:windowContentRect
	      style:NX_TITLEDSTYLE
	      backing:NX_BUFFERED
	      buttonMask:NX_ALLBUTTONS
	      defer:YES];

    scrollView = [ScrollView newFrame:windowContentRect];
    [scrollView setVertScrollerRequired:NO];
    [scrollView setHorizScrollerRequired:NO];
    [scrollView setBorderType:SCROLLVIEW_BORDER];
    [scrollView setDocView:view];
    [window setContentView:scrollView];
    [window addToEventMask:WINDOW_MASK];
    [window makeFirstResponder:view];

    return window;
}


/* Factory methods */

+ new
/*
 * Creates a new, empty, document
 */
{
	self = [super new];
	haveSavedDocument = NO;
	return self;
}
+ newFromContents:content
/*
 * Creates a new, empty, document, with contents and a window
 */
{
    self = [self new];
    contents = content;
    if ([contents isKindOf:[View class]])
		[self getWindowForView:contents];
    [window setDocEdited:YES];
    return self;
}

+ newFromFile:(const char *)file
/*
 * Opens an existing document from the specified file. 
 */
{
    self = [super new];
    if (contents = [NXApp readFromFile:file]) {
	if ([contents isKindOf:[View class]])
	    [self getWindowForView:contents];
	haveSavedDocument = YES;
	return self;
    } else {
	NotifyArg("Open", "File %s not found.", file);
	[self free];
	return nil;
    }
}

/* Create and Destroy Methods */
/*
 * Generate windows; set delegates
 */
-getWindowFromNib:(const char *)nibFile
{
    return [self getWindow:[NXApp loadNibSection:nibFile owner:self]];
}

- getWindowForView:newView
{
    view = newView;
    [self getWindow:createWindowFor(view, NULL)];
    [self resetScrollers];
    return self;
}

- getWindow:aWindow
{
    window=[NXApp locateWindow:aWindow];
    [window setDelegate:self];
    [window makeKeyAndOrderFront:self];
    [NXApp addDocument:[self filename]];
    return self;
}

- free
/*
 * Free all subsisting objects 
 */
{
    [view free];
    [contents free];
    [window free];
    NX_FREE(name);
    NX_FREE(directory);
    return [super free];
}

/* State Variables */

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

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

- view
/*
 * Returns the document view 
 */
{
    return view;
}


/*
 * Action Methods for manipulating the Contents    
 */

- find:sender
/*
 * Find a string.  Use a panel to set parameters 
 */
{
    Panel      *FindPanel;
    List       *viewList = [[sender superview] subviews];
    int         count = [viewList count];
    static id   panelView;
    id found;
    
    if (!panelView)
		while (count--) {
	    	panelView = [viewList objectAt:count];
		    if ([panelView isKindOfGivenName:"TextField"] &&
	    		[panelView isEditable])
				break;
		}
		
    if ([contents respondsTo:@selector(findSTR:)]) {
    	if (found = [contents findSTR:[panelView stringValue]]) {
			return found;
		} else {
		 	NotifyArg("Find", "Can't find more %s.",[panelView stringValue]);
			return nil;
		}
	} else {
		return panelView;
	}
}


- doAction:(SEL)action for:(STR)caller
    /*
     * Pass action methods on to Contents 
     */
{
    [contents perform:action with:self];
    return [view update];
}


/* Action methods for Referencing Apps */

- reference:(STR)refName
{
    port_t      refPort;
    int         res;
    int         len;
    NXSelPt     start, end;
    STR         refText;
    if (!(refPort = NXPortFromName(refName, NULL))) {
	Notify(refName, "Port not found.");
	return self;
    }
    [[self view] getSel:&start:&end];
    if (len = (end.cp - start.cp)) {
	refText = malloc(len + 1);
	[view getSubstring:refText start:start.cp length:len];
	refText[len] = '\0';
    } else {
	Notify(refName, "No Selection");
	return self;
    }
    if ([[[NXApp appSpeaker] setSendPort:refPort] openFile:refText ok:&res])
	Notify(refName, "Message failed.");
    return self;
}

- webster:sender
{
    return [self reference:"Webster"];
}

- librarian:sender
{
    return [self reference:"Librarian"];
}


- quotation:sender
{
    return [self reference:"Quotation"];
}


- 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;
    if (!haveSavedDocument) {
	savepanel = [NXApp saveAsPanel];
	if ([savepanel runModalForDirectory:directory file:name])
	    [self setName:[savepanel filename]];
	else
	    return self;
    }
    [self save];
    return self;
}

- saveAs:sender
/*
 * Save under a different name 
 */
{
    [window setDocEdited:YES];
    haveSavedDocument = NO;
    return [self save:sender];
}

- revertToSaved:sender
/*
 * Revert the document back to what is on the disk. 
 */
{
    id          newContents;
    if (!haveSavedDocument || ![window isDocEdited] || (NXRunAlertPanel
    	("Revert", "%s was edited.  Are you sure you want to undo changes?",
		"Revert", "Cancel", NULL, name) != NX_ALERTDEFAULT))
			return self;

    [window endEditingFor:self];

    if (newContents = [NXApp readFromFile:[self filename]]) {
	[contents free];
	contents = newContents;
	[view update];
	[window setDocEdited:NO];
    } else {
	NotifyArg("Revert", "I/O error. Can't find file %s.", [self filename]);
    }

    return self;
}

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

- (const STR)filename
/*
 * Gets the fully specified file name of the document. If directory is NULL,
 * then the Home is used. If name is NULL, then the default title is used. 
 */
{
    static char filename[MAXPATHLEN + 1];
    if (!directory && !name)
	[self setName:NULL andDirectory:NULL];

    strcpy(filename, fullPath(directory, name));
    return filename;
}

- (const STR)directory
{
    return directory;
}

- (const STR)name
{
    return name;
}

#define ISnewName (newName && *newName)
#define ISnewDir (newDirectory && (*newDirectory == '/'))

- 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        title[MAXPATHLEN + 5];
    char        oldName[MAXPATHLEN + 1];
    char        newNameBuf[MAXPATHLEN + 1];
    static int  uniqueCount = 1;
    if (directory && name)
	strcpy(oldName,[self filename]);
    else
	oldName[0] = '\0';

    if (ISnewName || !name) {
	if (!ISnewName) {
	    sprintf(newNameBuf, DEFAULT_TITLE,uniqueCount++,[NXApp extension]);
	    newName = newNameBuf;
	} else if (name) {
	    NX_FREE(name);
	}
	name = NXCopyStringBuffer(newName);
    }
    if (ISnewDir || !directory) {
	if (!ISnewDir)
	    newDirectory = NXHomeDirectory();
	else if (directory)
	    NX_FREE(directory);
	directory = NXCopyStringBuffer(newDirectory);
    }
    if (strcmp(oldName,[self filename])) {
	strcpy(title, name);
	strcat(title, " \320 ");/* \320 is a "long dash" */
	strcat(title, directory);
	[window setTitle:title];
	[NXApp renameDocument:oldName to:[self filename]];
    }
    return self;
}

- setName:(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. 
 */
{
    const char * lastComponent;
    const char * path;
    if (file) {
	path = parentname(file);
	lastComponent = basename(file);
	if (*path != '.')
	    return [self setName:lastComponent andDirectory:path];
	else
	    return [self setName:file andDirectory:NULL];
    }
    return self;
}


- save
/*
 * Writes out the document (i.e, the Contents). 
 *
 * See Contents's write: method for more details on how it is archived. 
 */
{
    if ([window isDocEdited])
	if ([NXApp write:contents toFile:[self filename]]) {
	    haveSavedDocument = YES;
	    [window setDocEdited:NO];
	} else {
	    NotifyArg("Save", "Can't create file %s.", [self filename]);
	}
    return self;
}

/* Text & Window delegate methods. */

- windowWillClose:sender
/*
 * If the Contents 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
 * ContentsDocument itself must be freed.  This is accomplished via Application's
 * delayedFree: mechanism.  Unfortunately, by the time delayedFree: frees the
 * Document, the window instance variable 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. 
 *
 * 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 ([window isDocEdited] && (haveSavedDocument || contents)) {
	save = NXRunAlertPanel("Close", "%s has changes. Save them?",
			       "Save", "Don't Save", "Cancel", name);
	if (save == NX_ALERTOTHER) {
	    return nil;
	} else {
	    [sender endEditingFor:self];	/* terminate any editing */
	    if (save == NX_ALERTDEFAULT)
		[self save:nil];
	}
    }
    window = nil;
    [NXApp delayedFree:self];
    [NXApp removeDocument:[self filename]];

    return self;
}

- windowWillResize:sender toSize:(NXSize *) size
/*
 * Constrains the size of the window to never be larger than the GraphicView
 * inside it (including the ScrollView around it). 
 */
{
    NXRect      frameRect, contentRect;
    getContentSizeForView(view, &contentRect.size);
    [[sender class] getFrameRect:&frameRect
     forContentRect:&contentRect
     style:[sender style]];

    if (size->width > frameRect.size.width)
	size->width = frameRect.size.width;
    if (size->height > frameRect.size.height)
	size->height = frameRect.size.height;

    return self;
}

- windowDidResize:sender
/*
 * Ensures that if the view is bigger than the window's content view, scroll
 * bars are added to the ScrollView. 
 */
{
    return [self resetScrollers];
}

- resetScrollers
/*
 * Checks to see if the new window size requires a vertical or horizontal
 * scroll bar.  Note that if a scroll bar is required, the window may have to
 * be resized to accomodate it.  Called whenever the user resizes the window 
 */
{
    id          scrollView;
    NXSize      scrollViewSize, contentSize;
    BOOL        resizeHoriz = NO, resizeVert = NO;
    NXRect      contentRect, windowFrame, viewFrame;
    if (window) {
	[window getFrame:&windowFrame];
	[[window class] getContentRect:&contentRect
	 forFrameRect:&windowFrame
	 style:[window style]];
	scrollView = [window contentView];
	[scrollView setHorizScrollerRequired:YES];
	[scrollView setVertScrollerRequired:YES];
	getContentSizeForView(view, &contentSize);
	if (contentRect.size.width >= contentSize.width ||
	    contentRect.size.height >= contentSize.height) {
	    if (contentRect.size.width >= contentSize.width) {
		[scrollView setHorizScrollerRequired:NO];
		resizeHoriz = YES;
	    }
	    if (contentRect.size.height >= contentSize.height) {
		[scrollView setVertScrollerRequired:NO];
		resizeVert = YES;
	    }
	    [view getFrame:&viewFrame];
	    [[scrollView class] getFrameSize:&scrollViewSize
	     forContentSize:&viewFrame.size
	     horizScroller:!resizeHoriz
	     vertScroller:!resizeVert
	     borderType:[scrollView borderType]];
	    if (!resizeHoriz)
		scrollViewSize.width = contentRect.size.width;
	    if (!resizeVert)
		scrollViewSize.height = contentRect.size.height;
	    [window sizeWindow:scrollViewSize.width:scrollViewSize.height];
	}
    }
    return self;
}

@end
