#import "ShelfView.h"
#import "IconView.h"
#import "IconDragView.h"
#import "compositeBackground.h"

#import <appkit/appkit.h>
#import <ansi/stdio.h>
#import <ansi/string.h>


#define	MONSTERSHELF_FILE	".MonsterShelf"
#define	GRID_ENABLE		"GridEnabled"
#define	GRID_VALUE		"GridValue"
#define BACKGROUNDTILE		"BackgroundTile"

#define	MAX_GRID_VALUE		1024
#define MIN_GRID_VALUE		32
#define DEFAULT_GRID_STRING	"84"
#define DEFAULT_GRID_VALUE	84

#define round(x,y)		(((x)+(y-1))/(y)*(y))


@implementation ShelfView

+ initialize
{
    static NXDefaultsVector defaults = {
        {GRID_ENABLE, "NO"},
	{GRID_VALUE, DEFAULT_GRID_STRING},
	{BACKGROUNDTILE, "YES"},
        {NULL}
    };

    NXRegisterDefaults([NXApp appName], defaults);

    return self;
}


- initFrame:(const NXRect *) aFrame
{
    const char		*colorString;
    const char *const	types[1] = {NXFilenamePboardType};
    int			screenCount;
    char		*backgroundString;
    NXScreen		*screens;
    unsigned int	i;
    
    for (i=0; i<NUM_MOUNT_SLOTS; i++)
    	mountSlots[i] = nil;

    [super initFrame:aFrame];
    [[self window] setDelegate:self];
    [self registerForDraggedTypes:types count:1];

    /*
     *  Determine the background color.
     */
    useBGColor = NO;
    [NXApp getScreens:&screens count:&screenCount];
    
    if (screens[0].depth == NX_TwoBitGrayDepth)
	backgroundString = "BWBackgroundColor";
    else 
	backgroundString = "BackgroundColor";

    colorString = NXGetDefaultValue("NeXT1", backgroundString);
    if (colorString) {
	float	r, g, b;

	sscanf(colorString, "%f %f %f", &r, &g, &b );
	bgColor = NXConvertRGBAToColor(r, g, b, NX_NOALPHA);
	useBGColor = YES;
    }

    /*
     *  Now that we've set up our view's appearance, do the rest of the
     *  initialization we need to do.
     */
    [self readShelf];

    return self;
}


- free
{
    return [super free];
}


- (BOOL) acceptsFirstMouse
{
    return YES;
}


- (NXColor) backgroundColor
{
    if (useBGColor)
	return bgColor;
    else
    	return NX_COLORLTGRAY;
}


- (BOOL) isAnyViewAt:(NXPoint) aPoint besides:aView
{
    unsigned int	i;
    unsigned int	max = [[self subviews] count];
    
    for (i = 0;  i < max;  i++) {
	int	x = aPoint.x;
	int	y = aPoint.y;
    	NXRect	rect;
	
	if ([[self subviews] objectAt:i] == aView)
	    continue;

	[[[self subviews] objectAt:i] getFrame:&rect];
	if ((int) rect.origin.x == x && (int) rect.origin.y == y)
	    return YES;
    }
    
    return NO;
}


/*
 *  Align all of our IconViews on the grid.  Take care so that none overlap.
 */
- (void) alignSubviews
{
    unsigned int	i;
    unsigned int	max = [[self subviews] count];
    unsigned int	grid = [self gridValue];

    if (![self gridEnabled])
    	return;

    for (i = 0;  i < max;  i++) {
	id	view = [[self subviews] objectAt:i];
	NXRect	rect;
	NXPoint candidatePt;
	int	count;

	if (![view isKindOf:[IconView class]])
	    continue;

	/*
	 *  Make the icon the right size, and then compute the new origin.
	 */	
	[view getFrame:&rect];
	candidatePt.x = round((int) rect.origin.x, grid);
	candidatePt.y = round((int) rect.origin.y, grid);

	count = bounds.size.height / grid * bounds.size.width / grid;
	while (count-- > 0 && [self isAnyViewAt:candidatePt besides:view]) {
	    candidatePt.x += grid;
	    if (candidatePt.x + rect.size.width > bounds.size.width) {
	    	candidatePt.x = 0;
		candidatePt.y += grid;
		if (candidatePt.y + rect.size.height > bounds.size.height)
		    candidatePt.y = grid;
	    }
	}

	[view sizeTo:grid :grid];
	[view moveTo:candidatePt.x :candidatePt.y];
    }
    
    [self display];
}


- (BOOL) gridEnabled
{
    const char *enabled = NXGetDefaultValue([NXApp appName], GRID_ENABLE);
    return (enabled && !strcmp(enabled, "YES"));
}


- (void) setGridEnabled:(BOOL) flag
{
    (void) NXWriteDefault([NXApp appName], GRID_ENABLE, flag ? "YES" : "NO");

    [window disableDisplay];

    [[self subviews] freeObjects];
    [IconView resetCachedImages];
    [self readShelf];

    [[window reenableDisplay] display];
}


- (unsigned int) gridValue
{
    const char *gridValue = NXGetDefaultValue([NXApp appName], GRID_VALUE);
    if (gridValue)
    	return atoi(gridValue);
    else
    	return DEFAULT_GRID_VALUE;
}

 
- setGridValue:(unsigned int) gridValue
{
    char gridString[20];

    if (gridValue == [self gridValue])
    	return self;

    if (gridValue < MIN_GRID_VALUE)
    	gridValue = MIN_GRID_VALUE;
    else if (gridValue > MAX_GRID_VALUE)
    	gridValue = MAX_GRID_VALUE;

    sprintf(gridString, "%d", (int)gridValue);
    (void) NXWriteDefault([NXApp appName], GRID_VALUE, gridString);

    return self;
}

- (BOOL) useBackgroundTile
{
  const char *useIt = NXGetDefaultValue([NXApp appName], BACKGROUNDTILE);
  return (useIt && !strcmp(useIt, "YES"));
}

- (void) setBackgroundEnabled:(BOOL) flag
{
  (void) NXWriteDefault([NXApp appName], BACKGROUNDTILE, flag ? "YES" : "NO");

  [window disableDisplay];

  [[self subviews] freeObjects];
  [IconView resetCachedImages];
  [self readShelf];

  [[window reenableDisplay] display];
}

- drawSelf:(const NXRect *) rects :(int) rectCount
{
    if (useBGColor) {
	NXSetColor(bgColor);
	NXRectFill(rects);
    }
    else
	compositeFromWorkspaceWindow(rects->origin.x, rects->origin.y,
				rects->size.width, rects->size.height);

    return self;
}


- deselectAll:sender
{
    [subviews makeObjectsPerform:@selector(setState:) with:(id) 0];
    return NO;
}


- removeView:aView
{
    NXRect	viewFrame;

    [aView getFrame:&viewFrame];
    [aView removeFromSuperview];
    [self display:&viewFrame :1 :NO];
    return self;
}


- addView:aView
{
    NXRect	viewFrame;

    [aView getFrame:&viewFrame];
    [self addSubview:aView];
    [self display:&viewFrame :1 :NO];
    return self;
}


- deleteView:aView
{
    [self removeView:aView];
    [aView free];
    [self writeShelf];
    return self;
}


/*
 *  Return true if the point is in the area we use to get rid of views
 */
- (BOOL) isDeadZone:(NXPoint *) aPoint
{
    NXRect	goodZone = bounds;
    NXInsetRect(&goodZone, 2, 2);
    return !NXMouseInRect(aPoint, &goodZone, NO);
}


- (void) createViewForPath:(const char *) path at:(NXPoint *) point
{
    id			image = [[Application workspace] getIconForFile:path];
    id			newView = [IconDragView allocFromZone:[self zone]];
    NXCoord		grid = [self gridValue];
    struct stat		st;
    NXRect		aRect;
    unsigned int	i = 0;
    NXPoint		viewOrigin;

    if (stat(path, &st) < 0)
    	return;

    if (!point) {
	/*
	 *  If the caller didn't know where to put the view, stick it one of
	 *  our default slots.
	 */
	while (i < NUM_MOUNT_SLOTS && mountSlots[i])
	    ++i;
    
	if (i < NUM_MOUNT_SLOTS)
	    mountSlots[i] = newView;
    
	viewOrigin.x = i * [self gridValue];
	viewOrigin.y = [self gridValue];

    	aRect.origin = viewOrigin;
    }
    else 
	aRect.origin = *point;

    /*
     *  If the grid is on, make sure the size of the view we're about
     *  to create is pegged to the grid size.
     */
    if ([self gridEnabled]) {
        aRect.size.width = grid;
	aRect.size.height = grid;
    }
    
    [newView initFrame:&aRect image:image data:path andLength:strlen(path)+1
	     useSize:[self gridEnabled] useTile:[self useBackgroundTile]];

    [self addSubview:newView];
    [newView getFrame:&aRect];
    [self display:&aRect :1 :NO];
}


static BOOL
prefix(const char *prefix, const char *string)
{
    while (*prefix && *string && *prefix == *string) {
    	prefix++;
	string++;
    }
    return *prefix == '\0';
}


- (void) removeViewForPath:(const char *) fullPath
{
    int		i = [[self subviews] count];

    while (i > 0) {
    	char		*path;
	unsigned int	len;
	id		view = [[self subviews] objectAt:i];
	if ([view isKindOf:[IconView class]]) {
	    [view getData:(void *) &path andLength:&len];
	    if (prefix(fullPath, path)) {
	        unsigned int	j=0;

		while (j < NUM_MOUNT_SLOTS && mountSlots[j] != view)
		    ++j;
		if (j < NUM_MOUNT_SLOTS)
		    mountSlots[j] = nil;

	    	[self deleteView:view];
	    }
	}
	i--;
    }
}


/*
 *  Find the right position for the new image, based on the grid and the
 *  mouse's location.
 */
- (NXPoint) viewLocationForContext:(id <NXDraggingInfo>)dragContext
{
    NXPoint		newLoc;
    unsigned int	grid = [self gridValue];
    NXPoint		imagePt = [dragContext draggedImageLocation];
    NXPoint		mousePt = [dragContext draggingLocation];
    NXPoint		imageOffset;
    
    [draggedView getImagePoint:&imageOffset andTilePoint:NULL 
		 andHilitePoint:NULL];
 
    if ([self gridEnabled]) {
    	NXRect	rect;
	[draggedView getFrame:&rect];
	
    	newLoc.x = mousePt.x - ((int) mousePt.x % grid) +
		     (grid - rect.size.width) / 2;
	newLoc.y = mousePt.y - (int) mousePt.y % grid;
    }
    else {
	newLoc.x = imagePt.x - imageOffset.x;
	newLoc.y = imagePt.y - imageOffset.y;
    }
    return newLoc;
}


/*
 *  Make a ghost image to indicate that we're really a destination. 
 */
- (NXDragOperation)draggingEntered:(id <NXDraggingInfo>)sender
{
    NXPoint	newLoc;
    NXSize	aSize, *sizePtr = NULL;
    
    if ([self gridEnabled]) {
    	aSize.width = [self gridValue];
	aSize.height = aSize.width;
	sizePtr = &aSize;
    }

    draggedView = [[IconView allocFromZone:[self zone]]
		   initFromDragContext:sender andSize:sizePtr
		   isOn:[self useBackgroundTile]];

    if ([self gridEnabled]) {
	newLoc = [self viewLocationForContext:sender];
	[draggedView moveTo:newLoc.x :newLoc.y];
    
	[draggedView setGhost:YES];
	[self addView:draggedView];
    }

    return NX_DragOperationAll;
}


/*
 *  Move the dragged image, but only do it if we need to (that is, if the
 *  mouse moved to a new grid cell).
 */
- (NXDragOperation)draggingUpdated:(id <NXDraggingInfo>)sender
{
    NXPoint	mouseLoc;
    NXPoint	newLoc = [self viewLocationForContext:sender];
    NXRect	aFrame;

    if (![self gridEnabled])
	return NX_DragOperationAll;

    /*
     *  If the icon was dragged off the edge, hide it somewhere!
     */
    mouseLoc = [sender draggingLocation];
    if ([self isDeadZone:&mouseLoc]) {
    	newLoc.x = -100;
	newLoc.y = -100;
    }

    [draggedView getFrame:&aFrame];
    if (aFrame.origin.x != newLoc.x || aFrame.origin.y != newLoc.y) {
    	[draggedView moveTo:newLoc.x :newLoc.y];

        [self display:&aFrame :1 :NO];		/* erase old */

     	aFrame.origin = newLoc;
        [self display:&aFrame :1 :NO];		/* draw new */
    }

    return NX_DragOperationAll;
}


/*
 *  Get rid of the resources we used to drag the image around.
 */
- draggingExited:(id <NXDraggingInfo>)sender
{
    [self removeView:draggedView];
    [draggedView free];

    return self;
}


/*
 *  Eat the result...
 */
- (BOOL) prepareForDragOperation:sender
{
    NXPoint	mouseLoc;
    NXRect	aFrame;
    NXPoint	newLoc = [self viewLocationForContext:sender];

    /*
     *  If the dragged item landed in the dead zone, get rid of it.  If
     *  the dragged item originated with us, we "accept" the image to tell
     *  the source to free it.
     */
    mouseLoc = [sender draggingLocation];
    if ([self isDeadZone:&mouseLoc]) {
	[draggedView getFrame:&aFrame];
	[draggedView free];
	[self display:&aFrame :1 :NO];
    	return [sender isDraggingSourceLocal];
    }

    /*
     *  Turn the dragged IconView into an IconDragView that's actually
     *  capable of being a drag destination, too.
     */
    [draggedView moveTo:newLoc.x :newLoc.y];
    [self addView:[IconDragView copyIconView:draggedView]];
    [draggedView free];
    
    return YES;
}


- (BOOL) performDragOperation:sender
{
    return YES;
}


/*
 *  Actually write the stuff way down here.  It's completely at the end
 *  of the operation, so a slow write won't hose the UI.
 */	
- concludeDragOperation:(id <NXDraggingInfo>)sender
{
    [self writeShelf];
    return self;
}


/*
 *  Be a drag source, too
 */
- setDragView:aView onEvent:(NXEvent *) e withOffset:(NXPoint *) offset atLocation:(const NXPoint *) location
{
    id			pb = [Pasteboard newName:NXDragPboard];
    void		*data;
    unsigned int	length;
    NXPoint		myLoc;

    /*
     *  Initiate a drag operation.  Copy stuff into the pasteboard,
     *  then start dragging.  To simplify matters elsewhere, we try
     *  to make a local dragging operation look just like a non-local
     *  one.
     */
    dragSourceView = aView;
    keepSourceOnShelf = (e->flags & NX_ALTERNATEMASK) ? YES : NO;

    [aView getData:&data andLength:&length];
    [pb declareTypes:&NXFilenamePboardType num:1 owner:nil];
    [pb writeType:NXFilenamePboardType data:data length:length];

    myLoc = *location;
    [aView convertPoint:&myLoc toView:self];
    [self dragImage:[aView image] at:&myLoc
		    offset:offset event:e pasteboard:pb
		    source:self slideBack:YES];

    return self;
}


- draggedImage:(NXImage *)image beganAt:(NXPoint *)screenPoint
{
    NXRect	theFrame;

    [dragSourceView getFrame:&theFrame];
    [self display:&theFrame :1 :NO];
    if (!keepSourceOnShelf)
	[self removeView:dragSourceView];

    return self;
}


/*
 *  A drag operation, with us as the source, finished.  If it was an
 *  unsuccessful drag then, put the source back!  If it was a successful
 *  drag, and we weren't the destination, then remove the thing from the
 *  shelf.
 */
- draggedImage:(NXImage *)image endedAt:(NXPoint *)screenPoint
     deposited:(BOOL)didDeposit
{
    char		*path;
    unsigned int	len;
    struct stat		st;

    /*
     *  Check to see if we should keep the source dir on the shelf.  We
     *  do this if the keepSourceOnShelf flag is set, and if the file
     *  under the icon still exists.
     */
    [dragSourceView getData:(void **) &path andLength:&len];
    if (keepSourceOnShelf && path && stat(path, &st) == 0) {
	keepSourceOnShelf = NO;
	return self;
    }
    
    /*
     *  The source isn't on the screen, so either get rid of the source, or
     *  put it back.
     */
    if (didDeposit)
    	[self deleteView:dragSourceView];
    else
    	[self addView:dragSourceView];

    return self;
}


- (NXDragOperation) draggingSourceOperationMaskForLocal:(BOOL)flag
{
    return NX_DragOperationAll;
}


/*
 *  Open the shelf file.
 */
- (FILE *) openShelfFor:(char *) how
{
    char	path[MAXPATHLEN];

    sprintf(path, "%s/%s", NXHomeDirectory(), MONSTERSHELF_FILE);
    return fopen(path, how);
}


/*
 *  Close it.
 */
- closeShelf:(FILE *) file
{
    fclose(file);
    return self;
}


/*
 *  Read the contents of the shelf in from a file.  The file's format consists
 *  of lines of the form:
 *
 *	x y path
 *
 *  where the two numbers x,y specify the origin of the particular view on the
 *  shelf, and path specifies the path to the workspace.  Somewhat bogusly,
 *  we assume the path starts at character 14.
 */
- readShelf
{
    FILE	*file;
    char	line[MAXPATHLEN + 30];
    char	*path;
    NXPoint	point;

    file = [self openShelfFor:"r"];
    if (file == NULL)
	return self;

    while (fgets(line, sizeof(line), file)) {

	/*
	 *  Parse the line in the shelf.  It's too bad that we can't use
	 *  sscanf to parse the whole line!
	 */
	sscanf(line, "%f %f", &point.x, &point.y);

	/* file string starts after second number, char 14 */
	if (strlen(line) > 14) {
	    path = line + 14;
	    if (rindex(path, '\n') != NULL)
	    	*rindex(path, '\n') = '\0';
	}
	else
	    continue;

	/*
	 *  Make a spot for this guy...
	 */
	[self createViewForPath:path at:&point];
    }
    
    [self closeShelf:file];
    if ([self gridEnabled])
	[self alignSubviews];

    return self;
}


/*
 *  Write the contents of the shelf out to the shelf file.
 */
- writeShelf
{
    FILE	*file;
    NXRect	rect;
    int		i;

    file = [self openShelfFor:"w"];
    if (file == NULL)
	return self;

    for (i = 0;  i < [[self subviews] count];  i++) {
	id		view = [[self subviews] objectAt:i];
	unsigned int	length;
	char		*path;

	if (![view isKindOf:[IconView class]] || [view isOnRemovableMedia])
	    continue;
	
	[view getData:(void **) &path andLength:&length];
	[view getFrame:&rect];

	fprintf(file, "%6.0f %6.0f %s\n", rect.origin.x, rect.origin.y, path);
    }
    
    [self closeShelf:file];
    return self;
}

@end
