#import "IconDragView.h"
#import "ShelfView.h"
#import "Controller.h"

#import <appkit/appkit.h>
#import <mach/mach.h>
#import <bsd/sys/file.h>


int mouseMoved(NXPoint *o, int n, int mask);

typedef enum { Unknown, SameDeviceOperation, WriteNotAllowed, CrossDeviceOperation } DragStatus;


@implementation IconDragView : IconView

- initFrame:(const NXRect *) newFrame
 image:anImage
 data:(const void *) someData
 andLength:(unsigned int) newLength
 useSize:(BOOL) sizeValid
 useTile:(BOOL) showTile
{
    const char *const	types[1] = {NXFilenamePboardType};

    altImage = nil;
    selected = NO;

    [self registerForDraggedTypes:types count:1];
    [super initFrame:newFrame image:anImage data:someData andLength:newLength
	   useSize:sizeValid useTile:showTile];
    
    return self;
}


- free
{
    [altImage free];
    return [super free];
}


- drawSelf:(const NXRect *) rects :(int) rectCount
{
    id	tImage = image;

    image = altImage ? altImage : image;
    [super drawSelf:rects :rectCount];
    image = tImage;

    return self;
}


- image
{
    return image;
}


- getData:(void **) aPtr andLength:(unsigned int *) aLength
{
    *aPtr = data;
    *aLength = length;
    return self;
}

- mouseDown:(NXEvent *) e
{
    BOOL		shift, control;
    NXPoint		mousePoint, offset;
    unsigned int	mask = NX_LMOUSEDRAGGEDMASK|NX_LMOUSEUPMASK;
    NXEvent		saveEvent;
    BOOL		newState;
    
    shift = (e->flags & NX_SHIFTMASK) ? YES : NO;
    control = (e->flags & NX_CONTROLMASK) ? YES : NO;
    mousePoint = e->location;
    saveEvent = *e;

    [window addToEventMask:mask];
    
    /*
     *  Handle selection
     */		
    if (shift && selected)
	newState = NO;
    else
	newState = YES;
    
    if (!shift && newState)
	[superview deselectAll:self];

    if (newState != selected) {
	selected = newState;
	[self display];
    }

    /*
     *  Detect drag
     */
    if (mouseMoved(&mousePoint, 2, mask)) {
    	NXPoint	imagePoint;

	[self getImagePoint:&imagePoint andTilePoint:NULL 
	      andHilitePoint:NULL];

	offset.x = mousePoint.x - e->location.x;
	offset.y = mousePoint.y - e->location.y;

	[self setState:NO];
	[superview setDragView:self onEvent:&saveEvent withOffset:&offset
			atLocation:&imagePoint];
	return self;
    }
    
    /*
     *  If no drag, try to get workspace to do something
     */
    if (selected) {
	BOOL	delivered = NO;
	id	workspace = [Application workspace];
	NXPoint	imageLoc, upperRight;
	NXRect	superRect;
	NXEvent	newEvent;

	[self getImagePoint:&imageLoc andTilePoint:NULL andHilitePoint:NULL];
	[self convertPoint:&imageLoc toView:nil];
	[window convertBaseToScreen:&imageLoc];
	imageLoc.y++;		/* grody!  necessary to make images line up */

	[superview getFrame:&superRect];
	upperRight.x = superRect.origin.x + superRect.size.width - 32;
	upperRight.y = superRect.origin.y + superRect.size.height - 32;

	/*
	 *  Before animating the icon, check for double click.  If we
	 *  see a second click come in, then open the file.  Otherwise,
	 *  just select it in Workspace.
	 */
	if (![NXApp peekNextEvent:NX_LMOUSEDOWNMASK into:&newEvent
	    waitFor:0.2 threshold:NX_MODALRESPTHRESHOLD]) {
	    [workspace slideImage:image from:&imageLoc to:&upperRight];
	    delivered = [workspace selectFile:data inFileViewerRootedAt:""];
	}
	else {
	    [workspace slideImage:image from:&imageLoc to:&upperRight];
	    (void) [NXApp getNextEvent:NX_LMOUSEDOWNMASK];
	    delivered = [workspace openFile:data];
	}

	if (!delivered)
	    NXBeep();

	[self setState:NO];
    }

    return self;
}


- (BOOL) acceptsFirstMouse
{
	return YES;
}


- (BOOL) acceptsFirstResponder
{
	return NO;
}


#define OPEN_DIR_ICON_FILE      ".opendir.tiff"
#define	DEFAULT_OPEN_DIR_FILE	"/usr/lib/NextStep/Workspace.app/WM.app/openFolder.tiff"
#define OPEN_HIGHLIGHT		"OpenHighlight.tiff"

- getOpenImageForDirectory:(const char *) dirname
{
  struct stat	st;
  char	buf[MAXPATHLEN];
  id		newImage = nil;

  /*
   *  See if there's a .opendir.tiff
   */
  strncpy(buf, dirname, sizeof(buf));
  strncat(buf, "/", sizeof(buf));
  strncat(buf, OPEN_DIR_ICON_FILE, sizeof(buf));
  if (access(buf, R_OK) == 0) {
    newImage = [[NXImage allocFromZone:[self zone]] initFromFile:buf];
    if (newImage)
      return newImage;
  }
    
  /*
   *  Before we use the default image, see if this thing is the
   *  root of a mounted file system, and use the appropriate icon
   *  if it is.
   */
  if (stat(dirname, &st) >= 0 && st.st_ino == 2) {
    NXSize size;
    [image getSize:&size];
    newImage = [[NXImage allocFromZone:[self zone]] initSize:&size];
    if ([newImage lockFocus]) {
      NXPoint	zeroPoint = {0,0};
      id		highlight = [NXImage allocFromZone:[self zone]];
      char        buf[MAXPATHLEN];
      NXBundle   *bundle;

      bundle = [NXBundle bundleForClass:[self class]];
      if ([bundle getPath:buf forResource:OPEN_HIGHLIGHT ofType:"tiff"]) {
	[highlight initFromFile:buf];
	[image composite:NX_COPY toPoint:&zeroPoint];
	[highlight composite:NX_SOVER toPoint:&zeroPoint];
	[newImage unlockFocus];
	return newImage;
      }
    }
  }

  if (access(DEFAULT_OPEN_DIR_FILE, R_OK) == 0)
    newImage = [[NXImage allocFromZone:[self zone]] 	
		initFromFile:DEFAULT_OPEN_DIR_FILE];

  if (newImage != nil)
    return newImage;
  else
    return image;
}


/*
 *  Figure out the various parameters of the drag.
 */
- (DragStatus) dragStatusForPath:(char *) dragPath
{
    char		*destinationPath;
    unsigned int	junk;
    struct stat		st;

    [self getData:(void **) &destinationPath andLength:&junk];

    /*
     *  See if we should be a dragging destination.  We need some way to
     *  make this extendible.  One way would be to associate a remote
     *  object with the node, and ask it what it means to drag the
     *  incoming object over the one on the screen...
     *
     *  For now, interpret the data as a string.  Stat it to see if it's
     *  a directory, and if it is, say yes subject to access constraints.
     */
    if (stat(destinationPath, &st) < 0 || (st.st_mode & S_IFMT) != S_IFDIR)
	return WriteNotAllowed;
    if (access(destinationPath, W_OK|X_OK) < 0)
	return WriteNotAllowed;

    /*
     *  Cool!  We're a directory.  Check the directory from which we're
     *  dragging, and see it's the same as this one, or if it's contained
     *  in this one.
     */
    if (stat(destinationPath, &st) < 0)
	return Unknown;
    else {
	dev_t	destDev = st.st_dev;
	ino_t	destIno = st.st_ino;

	if (lstat(dragPath, &st) >= 0 &&
	    st.st_dev == destDev && destIno == st.st_ino)
	    return WriteNotAllowed;
    }

    /*
     *  Handle the case where the dragging destination and the dragging
     *  source are on different file systems.  In this case, we want to
     *  copy by default, not move.
     */
    if (stat(destinationPath, &st) >= 0) {
	dev_t	destinationDevice = st.st_dev;
	if (lstat(dragPath, &st) >= 0)
	    if (st.st_dev != destinationDevice)
		return CrossDeviceOperation;
	    else
		return SameDeviceOperation;
    }
    
    return Unknown;
}


/*
 *  Figure out what the drag operation should be for dragging a view onto
 *  another view.
 */
- (NXDragOperation) dragOperationForContext:(id <NXDraggingInfo>) dragInfo
{
    NXDragOperation	op = NX_DragOperationNone;
    NXDragOperation	maskOp = [dragInfo draggingSourceOperationMask];
    id			pb = [Pasteboard newName:NXDragPboard];
    char		*dragPath, *nextPath;
    unsigned int	junk;
    BOOL		first = YES;

    [pb readType:NXFilenamePboardType data:&dragPath length:&junk];

    /*
     *  We might have been asked to drag multiple files.  Loop over all of
     *  them to see if they all have the same drag op.  Fail if they don't!
     */
    nextPath = dragPath;
    do {
    	char		path[MAXPATHLEN];
	NXDragOperation	thisOp = maskOp;

	strncpy(path, nextPath, MAXPATHLEN);
	if (index(path, '\t'))
	    *index(path, '\t') = '\0';
	 
	switch ([self dragStatusForPath:path]) {
	    case WriteNotAllowed:
	    case Unknown:
		thisOp = NX_DragOperationNone;
		break;
	    case CrossDeviceOperation:
		if (maskOp & NX_DragOperationCopy) {
		    thisOp = NX_DragOperationCopy;
		    break;
		}
		/* fall through */
	    case SameDeviceOperation:
		if (maskOp & NX_DragOperationGeneric) {
		    thisOp = NX_DragOperationGeneric;
		    break;
		}
		/* fall through */
	    default:
		break;
	}

	/*
	 *  Bail out of this operation isn't the same as the last one!
	 */
	if (first) {
	    op = thisOp;
	    first = NO;
	}
	else
	    if (op != thisOp)
		return NX_DragOperationNone;
	
	/*
	 *  Advance to next file
	 */
	nextPath = index(nextPath, '\t');
	if (nextPath == NULL)
	    break;
	else
	    nextPath++;

    } while (1);

    return op;
}


/*
 *  Be a dragging destination...
 */
- (NXDragOperation)draggingEntered:(id <NXDraggingInfo>)sender
{
    lastDragOp = [self dragOperationForContext:sender];

    /*
     *  If we're going to do the drag, use the alternate image.
     */
    if (lastDragOp != NX_DragOperationNone) {
    	NXSize	imageSize;
	altImage = [self getOpenImageForDirectory:data];

	[image getSize:&imageSize];
	[altImage setScalable:YES];
	[altImage setSize:&imageSize];

	[self display];
    }

    return lastDragOp;
}


- (NXDragOperation)draggingUpdated:(id <NXDraggingInfo>)sender
{
    lastDragOp = [self dragOperationForContext:sender];
    return lastDragOp;
}


- draggingExited:(id <NXDraggingInfo>)sender
{
    /*
     *  Reset image to original image.
     */
    [altImage free];
    altImage = nil;
    [self display];
    [window flushWindow];
    return self;
}


/*
 *  Eat the result...
 */
- (BOOL) prepareForDragOperation:sender
{
    return YES;
}

- (BOOL) performDragOperation:(id <NXDraggingInfo>)sender
{
    return YES;
}


- concludeDragOperation:(id <NXDraggingInfo>)sender
{
    id			workspace = [Application workspace];
    id			pb = [Pasteboard newName:NXDragPboard];
    const char		*wsmOp;
    char		*pbData;
    unsigned int	pbLength;

    switch (lastDragOp) {
	case NX_DragOperationGeneric:
	    wsmOp = WSM_MOVE_OPERATION;
	    break;
	case NX_DragOperationLink:
	    wsmOp = WSM_LINK_OPERATION;
	    break;
	case NX_DragOperationCopy:
	    wsmOp = WSM_COPY_OPERATION;
	    break;
	default:
	    return NO;
    }

    [pb readType:NXFilenamePboardType data:&pbData length:&pbLength];
    [workspace performFileOperation:wsmOp source:"/" destination:data
    			files:pbData options:NULL];

    return [self draggingExited:sender];
}


- (BOOL) isOnRemovableMedia
{
    struct stat	st;
    char	*mountedList = NULL;
    id		workspace = [Application workspace];

    if (stat(data, &st) < 0)
    	device = 0;
    else
	device = st.st_dev;

    removable = NO;
    if ([workspace respondsTo:@selector(getMountedRemovableMedia:)] &&
        [workspace getMountedRemovableMedia:&mountedList])
	[self perform:@selector(isOnRemovableDevice:) withPaths:mountedList];

    if (mountedList)
	free(mountedList);

    return removable;
}


- isOnRemovableDevice:(const char *)path
{
    struct stat	st;
    
    if (path && stat(path, &st) >= 0)
	removable |= (device == st.st_dev);

    return self;
}


@end


/*
 * Returns true if mouse is dragged more than n pixels from 'o',
 * false if the mouse button is lifted.  If the mouse button is
 * lifted, the mouseUp event is not consumed.
 */
int
mouseMoved(NXPoint *o, int n, int mask)
{
    NXEvent e;
    NXPoint p;
    float   dx, dy;

    do {
	[NXApp peekNextEvent:mask into:&e waitFor:0x7fffffff
		   threshold:NX_MODALRESPTHRESHOLD];
	p = e.location;
	if (e.type == NX_LMOUSEUP)
	    break;

	(void) [NXApp getNextEvent:mask];       /* throw it away */
	dx = abs(p.x - o->x);
	dy = abs(p.y - o->y);

    } while ((dy < (float) n) && (dx < (float) n));
    *o = p;

    return e.type != NX_LMOUSEUP;
}
