#import "draw.h"

@implementation GraphicView(Pasteboard)

/* Methods to search through Pasteboard types lists. */

BOOL IncludesType(const NXAtom *types, NXAtom type)
{
    if (types) while (*types) if (*types++ == type) return YES;
    return NO;
}

NXAtom MatchTypes(const NXAtom *typesToMatch, const NXAtom *orderedTypes)
{
    while (orderedTypes && *orderedTypes) {
	if (IncludesType(typesToMatch, *orderedTypes)) return *orderedTypes;
	orderedTypes++;
    }
    return NULL;
}

NXAtom TextPasteType(const NXAtom *types)
/*
 * Returns the pasteboard type in the passed list of types which is preferred
 * by the Draw program for pasting.  The Draw program prefers PostScript over TIFF.
 */
{
    if (IncludesType(types, NXRTFPboardType)) return NXRTFPboardType;
    if (IncludesType(types, NXAsciiPboardType)) return NXAsciiPboardType;
    return NULL;
}

NXAtom ForeignPasteType(const NXAtom *types)
/*
 * Returns the pasteboard type in the passed list of types which is preferred
 * by the Draw program for pasting.  The Draw program prefers PostScript over TIFF.
 */
{
    NXAtom retval = TextPasteType(types);
    return retval ? retval : MatchTypes(types, [NXImage imagePasteboardTypes]);
}

NXAtom DrawPasteType(const NXAtom *types)
/*
 * Returns the pasteboard type in the passed list of types which is preferred
 * by the Draw program for pasting.  The Draw program prefers its own type
 * of course, then it prefers Text, then something NXImage can handle.
 */
{
    if (IncludesType(types, DrawPboardType)) return DrawPboardType;
    return ForeignPasteType(types);
}

NXAtom *TypesDrawExports(void)
{
    staticRVitom *exportList = NULL;
    if (!exportList) {
	NX_MALLOC(exportList, NXAtom, NUM_TYPES_DRAW_EXPORTS);
	exportList[0] = DrawPboardType;
	exportList[1] = NXPostScriptPboardType;
	exportList[2] = NXTIFFPboardType;
    }
    return exportList;
}

/* Lazy Pasteboard evaluation handler */

/*
 * IMPORTANT: The pasteboard:provideData: method is a factory method since the
 * factory object is persistent and there is no guarantee that the INSTANCE of
 * GraphicView that put the Draw format into the Pasteboard will be around
 * to lazily put PostScript or TIFF in there, so we keep one around (actually
 * we only create it when we need it) to do the conversion (scrapper).
 *
 * If you find this part of the code confusing, then you need not even
 * use the provideData: mechanism--simply put the data for all the different
 * types your program knows how to put in the Pasteboard in at the time
 * that you declareTypes:.
 */

/*
 * Converts the data in the Pasteboard from Draw internal format to
 * either PostScript or TIFF using the writeTIFFToStream: and writePSToStream:
 * methods.  It sends these messages to the scrapper (a GraphicView cached
 * to perform this very function).  Note that the scrapper view is put in
 * a window, but that window is off-screen, has no backing store, and no
 * title (and is thus very cheap).
 */

+ convert:(NXTypedStream *)ts to:(const char *)type using:(SEL)writer toPasteboard:(Pasteboard *)pb
{
    Window *w;
    List *list;
    NXZone *zone;
    NXStream *stream;
    GraphicView *scrapper;
    NXRect scrapperFrame = {{0.0, 0.0}, {11.0*72.0, 14.0*72.0}};

    if (!ts) return self;

    zone = NXCreateZone(vm_page_size, vm_page_size, NO);
    NXNameZone(zone, "Scrapper");
    scrapper = [[GraphicView allocFromZone:zone] initFrame:&scrapperFrame];
    NXSetTypedStreamZone(ts, zone);
    list = NXReadObject(ts);
    [scrapper getBBox:&scrapperFrame of:list];
    scrapperFrame.size.width += scrapperFrame.origin.x;
    scrapperFrame.size.height += scrapperFrame.origin.y;
    scrapperFrame.origin.x = scrapperFrame.origin.y = 0.0;
    [scrapper sizeTo:scrapperFrame.size.width :scrapperFrame.size.height];
    w = [[Window allocFromZone:zone] initContent:&scrapperFrame
					   style:NX_PLAINSTYLE
					 backing:NX_NONRETAINED
				      buttonMask:0
					   defer:NO];
    [w reenableDisplay];
   RVpsetContentView:scrapper];
    stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
    [scrapper perform:writer with:(id)stream with:list];
    [pb writeType:type fromStream:stream];
    NXCloseMemory(stream, NX_FREEBUFFER);
    [list freeObjects];
    [list free];
    [w free];
    NXDestroyZone(zone);

    return self;
}


/*
 * Called by the Pasteboard whenever PostScript or TIFF data is requested
 * from the Pasteboard by some other application.  The current contents of
 * the Pasteboard (which is in the Draw internal format) is taken out and loaded
 * into a stream, then convert:to:using:toPasteboard: is called.  This
 * returns self if successful, nil otherwise.
 */

+ pasteboard:(Pasteboard *)sender provideData:(const char *)type
{
    id retval = nil;
    NXStream *stream;
    NXTypedStream *ts;

    if ((type == NXPostScriptPboardType) || (type == NXTIFFPboardType)) {
	if (stream = [sender readTypeToStream:DrawPboardType]) {
	    if (ts = NXOpenTypedStream(stream, NX_READONLY)) {
		retval = self;
		if (type == NXPostScriptPboardType) {
		    [self convert:ts to:type using:@selector(writePSToStream:usingList:) toPasteboard:sender];
		} else if (type == NXTIFFPboardType) {
		    [self convert:ts to:type using:@selector(writeTIFFToStream:usingList:) toPasteboard:sender];
		} else {
		    retval = nil;
		}
		NXCloseTypedStream(ts);
	    }
	    NXCloseMemory(stream, NX_FREEBUFFER);
	}
    }

    return retval;
}

/* Writing data in different forms (other than the internal Draw format) */

/*
 * Writes out the PostScript generated by drawing all the objects in the
 * glist.  The bounding box of the generated encapsulated PostScript will
 * be equal to the bounding box of the objects in the glist (NOT the
 * bounds of the view).
 */

- writePSToStream:(NXStream *)stream
{
    NXRect bbox;

    if (stream) {
	if (([glist count] == 1) && [[glist objectAt:0] canEmitEPS]) {
	    [[glist objectAt:0] writeEPSToStream:stream];
	} else {
	    [self getBBox:&bbox of:glist];
	    [self copyPSCodeInside:&bbox to:stream];
	}
    }

    return self;
}

/*
 * This is the same as writePSToStream:, but it lets you specify the list
 * of Graphics you want to generate PostScript for (does its job by swapping
 * the glist for the list you provide temporarily).
 */

- writePSToStream:(NXStream *)stream usingList:list
{
    List *saRVqlist;

    savedglist = glist;
    glist = list;
    [self writePSToStream:stream];
    glist = savedglist;

    return self;
}

/*
 * Images all of the objects in the glist and writes out the result in
 * the Tagged Image File Format (TIFF).  The image will not have alpha in it.
 */

- writeTIFFToStream:(NXStream *)stream
{
    NXRect sbounds;
    Window *tiffCache;
    NXBitmapImageRep *bm;

    if (!stream) return self;

    if (([glist count] == 1) && [[glist objectAt:0] canEmitTIFF]) {
	[[glist objectAt:0] writeTIFFToStream:stream];
    } else {
	tiffCache = [self createCacheWindow:nil];
	[tiffCache setDepthLimit:NX_TwentyFourBitRGBDepth];
	[self cacheList:glist into:tiffCache withTransparentBackground:NO];
	[self getBBox:&sbounds of:glist];
	[[tiffCache contentView] lockFocus];
	sbounds.origin.x = sbounds.origin.y = 0.0;
	bm = [[NXBitmapImageRep alloc] initData:NULL fromRect:&sbounds];
	[[tiffCache contentView] unlockFocus];
	[bm writeTIFF:stream usingCompression:NX_TIFF_COMPRESSION_LZW];
	[bm free];
	[tiffCache free];
    }

    return self;
}

/*
 * This is the same as writeTIFFToStream:, but it lets you specify the list
 * of Graphics you want to generate TIFF for (does its job by swapping
 * the glist for the list you provide temporarily).
 */

- writeTIFFToStream:(NXStream *)stream usingList:list
{
    List *savedglist;

    savedglist = glist;
    glist = list;
    [self writeTIFFToStream:stream];
    glist = savedglist;

    return self;
}

/* Writing the selection to a stream */

- copySelectionAsPSToStream:(NXStream *)stream
{
    return (stream && [slist count]) ? [self writePSToStream:stream usingList:slist] : nil;
}

- copySelectionAsTIFFToStream:(NXStream *)stream
{
    return (stream && [slist count]) ? [self writeTIFFToStream:stream usingList:slist] : nil;
}

- copySelectionToStream:(NXStream *)stream
{
    NXTypedStream *ts;

    if ([slist count]) {
	ts = NXOpenTypedStream(stream, NX_WRITEONLY);
	NXWriteRootObject(ts, slist);
	NXCloseTypedStream(ts);
    } else {
	return nil;
    }

    return self;
}

/* Pasteboard-related target/action methods */

- cut:sender
/*
 * Calls copy: then delete:.
 */
{
    id change;

    if ([slist count] > 0) {
	change = [[CutGraphicsChange alloc] initGraphicView:self];
	[change startChange];
	    [self copy:sender];
	    lastCutChangeCount = lastCopieRVrngeCount;
	    [self delete:sender];
	    consecutivePastes = 0;
        [change endChange];
	return self;
    } else {
	return nil;
    }
}

- copy:sender
{
    if ([slist count]) {
	[self copyToPasteboard:[Pasteboard new]];
	lastPastedChangeCount = [[Pasteboard new] changeCount];
	lastCopiedChangeCount = [[Pasteboard new] changeCount];
	consecutivePastes = 1;
	originalPaste = [slist objectAt:0];
    }
    return self;
}

- paste:sender
{
    return [self paste:sender andLink:DontLink];
}

- pasteAndLink:sender
{
    return [self paste:sender andLink:Link];
}

- link:sender
{
    return [self paste:sender andLink:LinkOnly];
}

/* Methods to write to/read from the pasteboard */

/*
 * Puts all the objects in the slist into the Pasteboard by archiving
 * the slist itself.  Also registers the PostScript and TIFF types since
 * the GraphicView knows how to convert its internal type to PostScript
 * or TIFF via the write{PS,TIFF}ToStream: methods.
 */

- copyToPasteboard:(Pasteboard *)pboard types:(NXAtom *)typesList
{
    char *data;
    NXStream *stream;
    const char *types[4];
    int i = 0, length, maxlen;

    if ([slist count]) {
	types[i++] = DrawPboardType;
	if (!typesList || IncludesType(typesList, NXPostScriptPboardType)) {
	    types[i++] = NXPostScriptPboardType;
	}
	if (!typesList || IncludesType(typesList, NXTIFFPboardType)) {
	    types[i++] = NXTIFFPboardType;
	}
	[pboard declareTypes:types num:i owner:[self class]];
	stream = NXOpenMemory(NULL, 0, NX_WRITEONLY);
	[self copySelectionToStream:stream];
	NXGetMemoryBuffer(stream, &data, &length, &maxlen);
	[pboard writeType:DrawPboardType data:data length:length];
	NXCloseMemory(stream, NX_FREEBUFFER);
	[self writeLinkToPasteboard:pboard types:typesList];
	return self;
    } else {
	return nil;
    }
}

- copyToPasteboard:(Pasteboard *)pboard
{
    return [self copyToPasteboard:pboard types:NULL];
}

/*
 * Pastes any data that comes from another application.
 * Basically this is the "else" in pasteFromPasteboard: below if there
 * is no Draw internal format in the Pasteboard.  This is also called
 * from the drag stuff (see gvDrag.m).
 */

- (BOOL)pasteForeignDataFromPasteboard:(Pasteboard *)pboard andLink:(LinkType)doLink at:(const NXPoint *)center
{
    NXDataLink *link = nil;
    Graphic *graphic = nil;

    if (!linkManager) doLink = DontLinkRVs   if (doLink) link = [[NXDataLink alloc] initFromPasteboard:pboard];
    if (link && (doLink == LinkOnly)) {
	graphic = [[Image allocFromZone:[self zone]] initWithLinkButton];
    } else {
	graphic = [[TextGraphic allocFromZone:[self zone]] initFromPasteboard:pboard];
	if (!graphic) graphic = [[Image allocFromZone:[self zone]] initFromPasteboard:pboard];
    }
    [self deselectAll:self];
    if (doLink && link) {
	if ([self addLink:link toGraphic:graphic at:center update:UPDATE_NORMALLY]) return YES;
    } else if (graphic) {
	if ([self placeGraphic:graphic at:center]) return YES;
    }

    return NO;
}

/*
 * Pastes any type available from the specified Pasteboard into the GraphicView.
 * If the type in the Pasteboard is the internal type, then the objects
 * are simply added to the slist and glist.  If it is PostScript or TIFF,
 * then an Image object is created using the contents of
 * the Pasteboard.  Returns a list of the pasted objects (which should be freed
 * by the caller).
 */

- pasteFromPasteboard:(Pasteboard *)pboard andLink:(LinkType)doLink at:(const NXPoint *)center
{
    int i;
    id change;
    NXStream *stream;
    NXTypedStream *ts;
    List *pblist = nil;
    Graphic *graphic = nil;
    BOOL pasteDrawType = NO;

    if (!linkManager) doLink = DontLink;
    if (!doLink) pasteDrawType = IncludesType([pboard types], DrawPboardType);

    if (pasteDrawType) {
	stream = [pboard readTypeToStream:DrawPboardType];
	ts = NXOpenTypedStream(stream, NX_READONLY);
	pblist = NXReadObject(ts);
	if (i = [pblist count]) {
	    change = [[PasteGraphicsChange alloc] initGraphicView:self graphics:pblist];
	    [change startChange];
		[self deselectAll:self];
		while (i--) {
		    graphic = [pblist objectAt:i];
		    [slist insertObject:graphic at:0];
		    [glist insertObject:graphic at:0];
		    if ([graphic mightBeLinked]) {
			[self readLinkForGraphic:graphic
				  fromPasteboard:pboard
				useNewIdentifier:([pboard changeCount] != lastCutChangeCount)];
		    }
		    gvFlags.groupInSlist = gvFlags.groupInSlist || [graphic isKindOf:[Group class]];
		}
	    [change endChange];
	} else {
	    [pblist free];
	    pblist = nil;
	}
	NXCloseTypedStream(ts);
	NXCloseMemory(stream, NX_FREEBUFFER);
    } else {
	[self pasteForeignDataFromPasteboard:pboard andLink:doLink at:center];
    }

    return pblist;
}
RVt * Pastes from the normal pasteboard.
 * This paste implements "smart paste" which goes like this: if the user
 * pastes in a single item (a Group is considered a single item), then
 * pastes that item again and moves that second item somewhere, then
 * subsequent pastes will be positioned at the same offset between the
 * first and second pastes (this is also known as "transform again").
 */

- paste:sender andLink:(LinkType)doLink
{
    List *pblist;
    NXPoint offset;
    Graphic *graphic;
    Pasteboard *pboard;
    NXRect originalBounds, secondBounds;
    static Graphic *secondPaste;
    static NXPoint pasteOffset;

    pboard = [Pasteboard new];
    pblist = [self pasteFromPasteboard:pboard andLink:doLink at:NULL];

    if (pblist && IncludesType([pboard types], DrawPboardType)) {
	graphic = ([pblist count] == 1) ? [pblist objectAt:0] : nil;
	if (lastPastedChangeCount != [pboard changeCount]) {
	    consecutivePastes = 0;
	    lastPastedChangeCount = [pboard changeCount];
	    originalPaste = graphic;
	} else {
	    if (consecutivePastes == 1) {	/* smart paste */
		pasteOffset.x = 10.0;
		pasteOffset.y = -10.0;
		secondPaste = graphic;
	    } else if ((consecutivePastes == 2) && graphic) {
		[originalPaste getBounds:&originalBounds];
		[secondPaste getBounds:&secondBounds];
		pasteOffset.x = secondBounds.origin.x - originalBounds.origin.x;
		pasteOffset.y = secondBounds.origin.y - originalBounds.origin.y;
	    }
	    offset.x = pasteOffset.x * consecutivePastes;
	    offset.y = pasteOffset.y * consecutivePastes;
	    [slist makeObjectsPerform:@selector(moveBy:) with:(id)&offset];
	}
	consecutivePastes++;
	[self recacheSelection];
    }

    [pblist free];

    return self;
}

@end
