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

// ABOUT MOKit
// by Mike Ferris (mike@lorax.com)
//
// 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 pertaining to the kit are reserved by the 
// author including the right to sell these objects as 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.  Permission is also granted to 
// redistribute the source code of MOKit for FREE as long as this copyright 
// notice is left intact and unchanged.  NO WARRANTY is expressed or implied.  
// The author will under no circumstances be held responsible for ANY 
// consequences 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/MODocumentWell.h"
#import "MOKit/MOString.h"
#import <errno.h>
#import <objc/objc-runtime.h>

#define CLASS_VERSION		1
#define CLASS_NAME			"MODocumentWell"

#define BUNDLE_TYPE							"bundle"

#define MOSTRING_CLASS_NAME					"MOString"
#define MOPATHSTRING_CLASS_NAME				"MOPathString"

#define PATH_SEPARATOR						"\t"

@interface MODocumentWell(Private)

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

@end

@implementation MODocumentWell

static id MOStringClass;
static id MOPathStringClass;

+ (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;
}

+ initialize
// Set the version.
{
	if (self == objc_lookUpClass(CLASS_NAME))  {
		[self setVersion:CLASS_VERSION];

		// Load the classes we use
		MOStringClass = [self MO_loadClassBundle:MOSTRING_CLASS_NAME];
		MOPathStringClass = [self MO_loadClassBundle:MOPATHSTRING_CLASS_NAME];
	}
	return self;
}

- initFrame:(const NXRect *)frm
{
	const char *myTypes[] = {NXFilenamePboardType};
	NXSize ghostSize = {48.0, 48.0};
	
	[super initFrame:frm];
	
	// set up initiual attributes
	pathList = [[List allocFromZone:[self zone]] init];
	icon = nil;
	delegate = nil;
	enabled = YES;
	isDragDestination = YES;
	isDragSource = YES;
	opensFiles = YES;
	bordered = YES;
	[self setOpaque:YES];
	acceptsFans = YES;
	
	// Create the image we'll use for compositing to create a ghost icon
	ghostHighlight = [[NXImage allocFromZone:[self zone]] initSize:&ghostSize];
	[ghostHighlight lockFocus];
		PSsetalpha(0.333);
		PSsetgray(1.0);
		PSrectfill(0.0, 0.0, ghostSize.width, ghostSize.height);
	[ghostHighlight unlockFocus];
	
	// Initialize the temporary ivars
	isReceiving = NO;
	dragPath = NULL;
	dragImage = nil;
	ghostImage = nil;
	
	isDragging = NO;
	
	tempString = [[MOStringClass allocFromZone:[self zone]] init];
	
	// Register ourselves for dragging
	[self registerForDraggedTypes:myTypes count:1];
	
	return self;
}

- free
{
	[self unregisterDraggedTypes];
	
	[[pathList freeObjects] free];
	[icon free];
	[ghostHighlight free];
	if (dragPath) NX_FREE(dragPath);
	[dragImage free];
	[ghostImage free];
	[tempString free];
	
	return [super free];
}

- MO_copyReset
{
	if (pathList)  {
		id oldPathList = pathList;
		int i, c = [oldPathList count];
		pathList = [[List allocFromZone:[self zone]] init];
		for (i=0; i<c; i++)  {
			[pathList addObject:[[oldPathList objectAt:i] 
						copyFromZone:[self zone]]];
		}
	}
	if (icon)  {
		icon = [icon copyFromZone:[self zone]];
	}
	if (ghostHighlight)  {
		ghostHighlight = [ghostHighlight copyFromZone:[self zone]];
	}
	if (tempString)  {
		tempString = [tempString copyFromZone:[self zone]];
	}
	isReceiving = NO;
	dragPath = NULL;
	dragImage = nil;
	ghostImage = nil;
	isDragging = NO;
	
	return self;
}

- copyFromZone:(NXZone *)zone;
{
	id obj = [super copyFromZone:zone];
	
	[obj MO_copyReset];
	
	return obj;
}

- drawSelf:(const NXRect *)rects :(int)rectCount
{
	NXPoint toPoint;
	
	if (bordered)  {
		NXDrawGrayBezel(&bounds, NULL);
	}
	
	if (!isReceiving)  {
		// draw the icon
		if (icon)  {
			[self getOrigin:&toPoint forImage:icon];
			[icon composite:NX_SOVER toPoint:&toPoint];
		}
	}  else  {
		// we are in the middle of dragging so draw the ghostImage.
		if (ghostImage)  {
			[self getOrigin:&toPoint forImage:ghostImage];
			[ghostImage composite:NX_SOVER toPoint:&toPoint];
		}
	}
	
	return self;
}

#define STICKINESS		5.0
- mouseDown:(NXEvent *)theEvent
{
	NXEvent saveEvent, *e;
	int oldMask;
	NXPoint origPoint, p;
	NXCoord distance;
	NXRect fromRect;

	if ((enabled) && ([pathList count]>0))  {
		// double-click opens our document
		if (theEvent->data.mouse.click > 1)  {
			[self openDocument:self];
			return self;
		}
		
		// single click might be a drag, so watch for it.
		saveEvent = *theEvent;
		origPoint = theEvent->location;
		[self convertPoint:&origPoint fromView:nil];
		oldMask = [window addToEventMask:NX_MOUSEDRAGGEDMASK];

		for (;;)  {
			e = [NXApp getNextEvent:(NX_MOUSEUPMASK | NX_MOUSEDRAGGEDMASK)];
						
			p = e->location;
			[self convertPoint:&p fromView:nil];
			
			if (e->type == NX_MOUSEDRAGGED)  {
				// if we've gone at least STICKINESS units from the original
				// mousedown, start dragging our file.
				distance = sqrt(((p.x - origPoint.x) * (p.x - origPoint.x)) + 
							((p.y - origPoint.y) * (p.y - origPoint.y)));
				if ((isDragSource) && (distance >= STICKINESS))  {
					[self getOrigin:&(fromRect.origin) forImage:icon];
					fromRect.size.width = fromRect.size.height = 48.0;
					isDragging = YES;
					[self dragFile:[self path] fromRect:&fromRect 
								slideBack:NO event:&saveEvent];
					isDragging = NO;
					break;
				}
			}  else if (e->type == NX_MOUSEUP)  {
				break;
			}
		}
		
		[window setEventMask:oldMask];
		
	}
	return self;
}

- openDocument:sender
{
	NXPoint p;
	int i, c = [pathList count];
	
	if ((opensFiles) && (c > 0))  {
		// ask the delegate if its ok.
		if ((delegate) && ([delegate 
					respondsTo:@selector(docWellWillOpenDocument:)]))  {
			if (![delegate docWellWillOpenDocument:self])  {
				return self;
			}
		}
		
		// open the first one with animation.
		[self getOrigin:&p forImage:icon];
		[[Application workspace] openFile:[[pathList objectAt:0] stringValue] 
					fromImage:icon 
					at:&p inView:self];
		
		// now open the rest without animation.
		for (i=1; i<c; i++)  {
			[[Application workspace] 
						openFile:[[pathList objectAt:i] stringValue]];
		}
		
		// inform the delegate that it opened.
		if ((delegate) && ([delegate 
					respondsTo:@selector(docWellDidOpenDocument:)]))  {
			// Send it delayed so we're out of mouseDown: before it happens
			[delegate perform:@selector(docWellDidOpenDocument:) 
						with:self afterDelay:1 cancelPrevious:YES];
		}
	}
    return self;
}

- (BOOL)setPath:(const char *)path andIcon:(NXImage *)theIcon
{
	// We will tokenize this string.  Technically the contents of this string
	// might not be a path (it might be several separated by tabs), but we
	// use a MOPathString since tokenize will make the tokens instances of
	// the same class as the string being tokenized.
	MOPathString *pathString = NULL;
	
	// handle setting to null first
	if ((!path) || (!*path))  {
		[pathList freeObjects];
		[icon free];
		icon = nil;
		return YES;
	}
	// Ask the delegate if the file is cool
	if ((delegate) && ([delegate 
				respondsTo:@selector(docWell:isValidDocument:)]))  {
		if (![delegate docWell:self isValidDocument:path])  {
			return NO;
		}
	}

	// now refill the pathList by toking the path given.
	pathString = [[MOPathStringClass allocFromZone:[self zone]] 
				initStringValue:path];
	[pathList freeObjects];
	[pathString tokenize:PATH_SEPARATOR into:pathList];
	[pathString free];
	
	[icon free];
	if (theIcon)  {
		icon = theIcon;
	}  else  {
		icon = [[Application workspace] getIconForFile:path];
	}
	
	return YES;
}

- (const char *)path
{
	int i, c = [pathList count];
	
	[tempString setNull];
	
	for (i=0; i<c; i++)  {
		if (i>0)  [tempString catStringValue:PATH_SEPARATOR];
		[tempString cat:[pathList objectAt:i]];
	}
	
	return [tempString stringValue];
}

- icon
{
	return icon;
}

- setDelegate:anObject
{
	delegate = anObject;
	return self;
}

- delegate
{
	return delegate;
}

- setEnabled:(BOOL)flag
{
	enabled = flag;
	return self;
}

- (BOOL)isEnabled
{
	return enabled;
}

- setIsDragDestination:(BOOL)flag
{
	isDragDestination = flag;
	return self;
}

- (BOOL)isDragDestination
{
	return isDragDestination;
}

- setIsDragSource:(BOOL)flag
{
	isDragSource = flag;
	return self;
}

- (BOOL)isDragSource
{
	return isDragSource;
}

- setDoesOpenFiles:(BOOL)flag
{
	opensFiles = flag;
	return self;
}

- (BOOL)doesOpenFiles
{
	return opensFiles;
}

- setBordered:(BOOL)flag
{
	bordered = flag;
	[self setOpaque:bordered];
	[self displayFromOpaqueAncestor:NULL :0 :NO];
	return self;
}

- (BOOL)isBordered
{
	return bordered;
}

- setAcceptsFans:(BOOL)flag
{
	acceptsFans = flag;
	return self;
}

- (BOOL)acceptsFans
{
	return acceptsFans;
}

// dragging destination informal protocol
- (NXDragOperation)draggingEntered:(id <NXDraggingInfo>)sender
{
	NXDragOperation retval = NX_DragOperationNone;
	NXPoint toPoint = {0.0, 0.0};
	
	// See if we don't accept drags or we are the source of this drag
	if ((!enabled) || (!isDragDestination) || (isDragging))  {
		return retval;
	}
	
	// get the filename.
	[self getFilenameFromPboard:[sender draggingPasteboard]];
	
	if (!dragPath)  {
		return retval;
	}
	
	if ((strstr(dragPath, PATH_SEPARATOR)) && !acceptsFans)  {
		return retval;
	}
	
	// ask the delegate if the potential document is cool.
	if ((delegate) && ([delegate 
				respondsTo:@selector(docWell:isValidDocument:)]))  {
		if (![delegate docWell:self isValidDocument:dragPath])  {
			retval = NX_DragOperationNone;
			// We are stopping the drag, we must clean up
			if (dragPath) NX_FREE(dragPath);
			dragPath = NULL;
			[self displayFromOpaqueAncestor:NULL:0:NO];
			return retval;
		}
	}
	retval = [sender draggingSourceOperationMask];
	if (retval & NX_DragOperationGeneric)  {
		retval = NX_DragOperationGeneric;
		isReceiving = YES;
		dragImage = [sender draggedImage];
		ghostImage = [dragImage copy];
		[ghostImage lockFocus];
			[ghostHighlight composite:NX_SATOP toPoint:&toPoint];
		[ghostImage unlockFocus];
	}  else  {
		retval = NX_DragOperationNone;
		// We are stopping the drag, we must clean up
		if (dragPath) NX_FREE(dragPath);
		dragPath = NULL;
	}
	[self displayFromOpaqueAncestor:NULL:0:NO];
	return retval;
}

- draggingExited:(id <NXDraggingInfo>)sender
{
	isReceiving = NO;
	if (dragPath) NX_FREE(dragPath);
	dragPath = NULL;
	dragImage = nil;
	[ghostImage free];
	ghostImage = nil;
	[self displayFromOpaqueAncestor:NULL:0:NO];
	return self;
}

- (BOOL)prepareForDragOperation:(id <NXDraggingInfo>)sender
{
	NXPoint toPoint;
	
	// get the filename.
	[self getFilenameFromPboard:[sender draggingPasteboard]];
	
	// ask the delegate if the path that is about to be set is cool.
	if ((delegate) && ([delegate 
				respondsTo:@selector(docWell:willAcceptDocument:)]))  {
		if (![delegate docWell:self willAcceptDocument:dragPath])  {
			isReceiving = NO;
			// We are stopping the drag, we must clean up
			if (dragPath) NX_FREE(dragPath);
			dragPath = NULL;
			dragImage = nil;
			[ghostImage free];
			ghostImage = nil;
			[self displayFromOpaqueAncestor:NULL:0:NO];
			return NO;
		}
	}
	
	// The next line makes a copy of the drag image which 
	// is our respoinsibility... see the -performDragOperation: method
	// to see what happens to it.
	dragImage = [sender draggedImageCopy];
	[self getOrigin:&toPoint forImage:dragImage];
	[self convertPoint:&toPoint toView:nil];
	[window convertBaseToScreen:&toPoint];
	[sender slideDraggedImageTo:&toPoint];
	[self lockFocus];
	[dragImage composite:NX_SOVER toPoint:&toPoint];
	[self unlockFocus];
	
	return YES;
}

- (BOOL)performDragOperation:(id <NXDraggingInfo>)sender
{
	// set the path (the dragImage is given to -setPath:andIcon:
	// and thus our icon ivar is set to point to the icon.
	if (![self setPath:dragPath andIcon:dragImage])  {
		isReceiving = NO;
		if (dragPath) NX_FREE(dragPath);
		dragPath = NULL;
		dragImage = nil;
		[ghostImage free];
		ghostImage = nil;
		[self displayFromOpaqueAncestor:NULL:0:NO];
		return NO;
	}

	return YES;
}

- concludeDragOperation:(id <NXDraggingInfo>)sender
{
	isReceiving = NO;
	if (dragPath) NX_FREE(dragPath);
	dragPath = NULL;
	dragImage = nil;
	[ghostImage free];
	ghostImage = nil;
	[self displayFromOpaqueAncestor:NULL:0:NO];

	// tell the delegate the path has been set.
	if ((delegate) && ([delegate 
				respondsTo:@selector(docWell:didAcceptDocument:)]))  {
		[delegate perform:@selector(docWellDidAcceptDocument:) 
					with:self afterDelay:1 cancelPrevious:YES];
	}
	return self;
}

// dragging source informal protocol
- (NXDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal
{
	if (isDragSource)  {
		return (NX_DragOperationCopy | NX_DragOperationLink | 
					NX_DragOperationGeneric);
	}  else  {
		return NX_DragOperationNone;
	}
}

- getFilenameFromPboard:pb
{
	const char *fileType[] = {NXFilenamePboardType};
	const char *theType;
	char *path;
	int length;
	
	if (dragPath) NX_FREE(dragPath);
	dragPath = NULL;
		
	// get the filename.
	theType = [pb findAvailableTypeFrom:fileType num:1];
	
	if (theType)  {
		if ([pb readType:theType data:&path length:&length])  {
			dragPath = NXCopyStringBufferFromZone(path, [self zone]);
			[pb deallocatePasteboardData:path length:length];
		}  else  {
			NXLogError("[%s getFilenameFromPboard:]: "
						"Pasteboard error.\n", [[self class] name]);
		}
	}
	
	return self;
}

- getOrigin:(NXPoint *)toPoint forImage:(NXImage *)image
{
	NXSize imageSize;
	if (image)  {
		[image getSize:&imageSize];
		toPoint->x = bounds.origin.x + 
					((bounds.size.width - imageSize.width) / 2);
		toPoint->y = bounds.origin.y + 
					((bounds.size.height - imageSize.height) / 2);
	}
	return (image?self:nil);
}

// archiving

- awake
{
	const char *myTypes[] = {NXFilenamePboardType};
	NXSize ghostSize = {48.0, 48.0};

	[self setOpaque:bordered];

	// Create the image we'll use for compositing to create a ghost icon
	ghostHighlight = [[NXImage allocFromZone:[self zone]] initSize:&ghostSize];
	[ghostHighlight lockFocus];
		PSsetalpha(0.333);
		PSsetgray(1.0);
		PSrectfill(0.0, 0.0, ghostSize.width, ghostSize.height);
	[ghostHighlight unlockFocus];
	
	// Initialize the temporary ivars
	isReceiving = NO;
	dragPath = NULL;
	dragImage = nil;
	ghostImage = nil;
	
	isDragging = NO;
	
	tempString = [[MOStringClass allocFromZone:[self zone]] init];
	
	// Register ourselves for dragging
	[self registerForDraggedTypes:myTypes count:1];
	
	return self;
}

- read:(NXTypedStream *)stream
{
	int classVersion;
	MOPathString *pathString = nil;
	
	[super read:stream];
	
	classVersion = NXTypedStreamClassVersion(stream, CLASS_NAME);
	
	switch (classVersion)  {
		case 0:		// First version, prior to multiple file support.
			pathString = (MOPathString *)NXReadObject(stream);
			pathList = [[List allocFromZone:[self zone]] init];
			[pathList addObject:pathString];
			icon = NXReadObject(stream);
			delegate = NXReadObject(stream);
			NXReadTypes(stream, "CCCCC", &enabled, &isDragDestination, 
						&isDragSource, &opensFiles, &bordered);
			acceptsFans = YES;
			break;
		case 1:		// Multiple file support was added.
			pathList = (List *)NXReadObject(stream);
			icon = NXReadObject(stream);
			delegate = NXReadObject(stream);
			NXReadTypes(stream, "CCCCCC", &enabled, &isDragDestination, 
						&isDragSource, &opensFiles, &bordered, &acceptsFans);
			break;
		default:
			NXLogError("[%s read:] class version %d cannot read "
						"instances archived with version %d", 
						CLASS_NAME, CLASS_VERSION, classVersion);
			pathList = [[List allocFromZone:[self zone]] init];
			icon = nil;
			delegate = nil;
			enabled = YES;
			isDragDestination = YES;
			isDragSource = YES;
			opensFiles = YES;
			bordered = YES;
			acceptsFans = YES;
			break;
	}
	
	return self;
}

- write:(NXTypedStream *)stream
{
	[super write:stream];
	
	NXWriteObject(stream, pathList);
	NXWriteObject(stream, icon);
	NXWriteObjectReference(stream, delegate);
	NXWriteTypes(stream, "CCCCCC", &enabled, &isDragDestination, 
				&isDragSource, &opensFiles, &bordered, &acceptsFans);
	
	return self;
}

- (const char *)getInspectorClassName
// Return the class name of our inspector.
{
	return "MODocWellInspector";
}

@end
