#import "draw.h"

@implementation GraphicView(Links)

/* See the Links.rtf file for overview about Object Links in Draw. */

#define BUFFER_SIZE 1100
#define INT_WIDTH 11

/*
* Returns an NXSelection describe the current Graphic's selected in the view.
* If the user did Select All, then the gvFlags.selectAll bit is set and we
* return the allSelection NXSelection. If the user dragged out a rectangle,
* then the dragRect rectangle is set and we return a ByRect NXSelection.
* Otherwise, we return a ByGraphic NXSelection.
*
* We use the writeIdentifierTo: mechanism so that Group Graphic's can ask
* their components to write out all their identifiers so that we have a
* maximal chance of getting all the objects.
*/

- (NXSelection *)currentSelection
{
NXRect sbounds;
int i, graphicCount;
NXSelection *retval = nil;
char *s, *selbuf, buffer[BUFFER_SIZE];

if (![slist count]) return [NXSelection emptySelection];

if (gvFlags.selectAll) {
if ([slist count] == [glist count]) {
return [NXSelection allSelection];
} else {
gvFlags.selectAll = NO;
}
}

if (dragRect) {
sbounds = *dragRect;
sprintf(buffer, "%d %d %d %d %d", ByRect,
(int)sbounds.origin.x, (int)sbounds.origin.y,
(int)(sbounds.size.width+0.5), (int)(sbounds.size.height+0.5));
selbuf = buffer;
} else {
graphicCount = 0;
i = [slist count];
while (i--) graphicCount += [[slist objectAt:i] graphicCount];
if (graphicCount > (BUFFER_SIZE / INT_WIDTH)) {
NX_MALLOC(selbuf, char, graphicCount * INT_WIDTH);
} else {
selbuf = buffer;
}
sprintf(buffer, "%d %d", ByList, graphicCount);
s = selbuf + strlen(selbuf);
i = [slist count];
while (i--) {
*s++ = ' ';
[[slist objectAt:i] writeIdentifierTo:s];
s += strlen(s);
}
}

retval = [[NXSelection allocFromZone:[self zone]] initWithDescription:selbuf length:strlen(selbuf)+1];

if (selbuf != buffer) free(selbuf);

return retval;
}

/*
* Used for destination selections only.
* Just extracts the unique identifier for the destination Image
* or TextGraphic and then searches through the glist to find that
* Graphic and returns it.
*
* Again, we use the graphicIdentifiedBy: mechanism so that we
* descend into Group's of Graphics to find a destination.
*/

- (Graphic *)findGraphicInSelection:(NXSelection *)selection
{
int i;
Graphic *graphic;
const char *selectionInfo;
int selectionInfoLength, identifier, selectionType;

selectionInfo = [selection descriptionOfLength:&selectionInfoLength];
if (selectionInfo) {
sscanf(selectionInfo, "%d %d", &selectionType, &identifier);
if (selectionType == ByGraphic) {
for (i = [glist count]-1; i >= 0; i--) {
if (graphic = [[glist objectAt:i] graphicIdentifiedBy:identifier]) return graphic;
}
}
}

return nil;
}

/*
* Returns YES and theRect is valid only if the selection is one which
* the user created by dragging out a rectangle.
*/

- (BOOL)getRect:(NXRect *)theRect forSelection:(NXSelection *)selection
{
NXRect stackRect;
const char *selectionInfo;
int selectionInfoLength;
DrawSelectionType selectionType;

if (selectionInfo = [selection descriptionOfLength:&selectionInfoLength]) {
if (!theRect) theRect = &stackRect;
sscanf(selectionInfo, "%d %f %f %f %f", (int *)&selectionType,
&(theRect->origin.x), &(theRect->origin.y),
&(theRect->size.width), &(theRect->size.height));
if (selectionType == ByRect) return YES;
}

return NO;
}

/*
* For source selections only.
* Returns the list of Graphics in the current document which were
* in the selection passed to this method. Note that any Group
* which includes a Graphic in the passed selection will be included
* in its entirety.
*/

- (List *)findGraphicsInSelection:(NXSelection *)selection
{
int i, count;
Graphic *graphic;
List *list = nil;
NXRect sBounds, gBounds;
int selectionInfoLength;
const char *s, *theGraphics;
DrawSelectionType selectionType;

if ([selection isEqual:[NXSelection allSelection]]) {
count = [glist count];
list = [[List allocFromZone:[self zone]] initCount:count];
for (i = 0; i < count; i++) [list addObject:[glist objectAt:i]];
} else if ([self getRect:&sBounds forSelection:selection]) {
count = [glist count];
list = [[List allocFromZone:[self zone]] init];
for (i = 0; i < count; i++) {
graphic = [glist objectAt:i];
[graphic getBounds:&gBounds];
NXInsetRect(&gBounds, -0.1, -0.1);
if (NXIntersectsRect(&gBounds, &sBounds)) [list addObject:graphic];
}
} else if (s = [selection descriptionOfLength:&selectionInfoLength]) {
sscanf(s, "%d %d", (int *)&selectionType, &count);
if (selectionType == ByList) {
if (s = strchr(s, ' ')) s = strchr(s+1, ' ');
if (s++) {
theGraphics = s;
list = [[List allocFromZone:[self zone]] init];
count = [glist count];
for (i = 0; i < count; i++) {
graphic = [glist objectAt:i];
s = theGraphics;
while (s && *s) {
if ([graphic graphicIdentifiedBy:atoi(s)]) {
[list addObject:graphic];
break;
}
if (s = strchr(s, ' ')) s++;
}
}
}
}
}

if (![list count]) {
[list free];
list = nil;
}

return list;
}

/*
* Importing/Exporting links.
*/

/*
* This method is called by copyToPasteboard:. It just puts a link to the currentSelection
* (presumably just written to the pasteboard by copyToPasteboard:) into the specified
* pboard. Note that it only does all this if we are writing all possible types to the
* pasteboard (typesList == NULL) or if we explicitly ask for the link to be written
* (typesList includes NXDataLinkPboardType).
*/

- writeLinkToPasteboard:(Pasteboard *)pboard types:(const NXAtom *)typesList
{
NXDataLink *link;

if (linkManager && (!typesList || IncludesType(typesList, NXDataLinkPboardType))) {
if (link = [[NXDataLink alloc] initLinkedToSourceSelection:[self currentSelection] managedBy:linkManager supportingTypes:TypesDrawExports() count:NUM_TYPES_DRAW_EXPORTS]) {
[pboard addTypes:&NXDataLinkPboardType num:1 owner:[self class]];
[link writeToPasteboard:pboard];
[link free];
}
[linkManager writeLinksToPasteboard:pboard]; // for embedded linked things
}

return self;
}

/*
* Sets up a link from the Draw document to another document.
* This is called by the drag stuff (gvDrag.m) and the normal copy/paste stuff (gvPasteboard.m).
* We allow for the case of graphic being nil as long as the link is capable of supplying
* data of a type we can handle (currently Text or Image).
*/

- (BOOL)addLink:(NXDataLink *)link toGraphic:(Graphic *)graphic at:(const NXPoint *)p update:(int)update
{
NXSelection *selection = nil;

if (!graphic && link && update != UPDATE_NEVER) {
if (TextPasteType([link types])) {
graphic = [[TextGraphic allocFromZone:[self zone]] initEmpty];
} else if (MatchTypes([link types], [NXImage imagePasteboardTypes])) {
graphic = [[Image allocFromZone:[self zone]] initEmpty];
}
update = UPDATE_IMMEDIATELY;
}

if (graphic && link) {
selection = [graphic selection];
if ([linkManager addLink:link at:selection]) {
if (!update) [link setUpdateMode:NX_UpdateNever];
[graphic setLink:link];
if (graphic = [self placeGraphic:graphic at:p]) {
if (update == UPDATE_IMMEDIATELY) {
[link updateDestination];
graphic = [self findGraphicInSelection:selection];
if (![graphic isValid]) {
NXRunLocalizedAlertPanel(NULL, "Import Link",
"Unable to import linked data.", NULL, NULL, NULL,
"Message given to user when import of linked data fails.");
[self removeGraphic:graphic];
} else {
return YES;
}
} else {
return YES;
}
}
}
}

[link free];
[selection free];
[graphic free];

return NO;
}

/*
* Keeping links up to date.
* These methods are called either to update a link that draw has to another
* document or to cause Draw to update another document that is linked to it.
*/

/*
* Sent whenever NeXTSTEP wants us to update some data in our document which
* we get by being linked to some other document.
*/

- pasteFromPasteboard:(Pasteboard *)pboard at:(NXSelection *)selection
{
id graphic;
NXRect gBounds;

if (graphic = [self findGraphicInSelection:selection]) {
gBounds = [graphic reinitFromPasteboard:pboard];
[self cache:&gBounds]; // updating a destination link
[window flushWindow];
[self dirty];
return self;
}

return nil;
}

/*
* Lazy pasteboard method for cheapCopyAllowed case ONLY.
* See copyToPasteboard:at:cheapCopyAllowed: below.
*/

- pasteboard:(Pasteboard *)sender provideData:(const char *)type
{
List *list;
NXStream *stream;
NXSelection *selection;

selection = [[NXSelection allocFromZone:[self zone]] initFromPasteboard:sender];
list = [self findGraphicsInSelection:selection];
if (list && (stream = NXOpenMemory(NULL, 0, NX_WRITEONLY))) {
if (type == NXPostScriptPboardType) {
[self writePSToStream:stream usingList:list];
} else if (type == NXTIFFPboardType) {
[self writeTIFFToStream:stream usingList:list];
}
[sender writeType:type fromStream:stream];
NXCloseMemory(stream, NX_FREEBUFFER);
}
[list free];
[selection free];

return self;
}

/*
* Called by NeXTSTEP when some other document needs to be updated because
* they are linked to something in our document.
*/

- copyToPasteboard:(Pasteboard *)pboard at:(NXSelection *)selection cheapCopyAllowed:(BOOL)cheapCopyAllowed
{
List *list;
NXStream *stream;
NXTypedStream *ts;
id retval = self;
const char *types[3];

types[1] = NXPostScriptPboardType;
types[2] = NXTIFFPboardType;

if (cheapCopyAllowed) {
if (list = [self findGraphicsInSelection:selection]) {
types[0] = NXSelectionPboardType;
[pboard declareTypes:types num:3 owner:self];
[selection writeToPasteboard:pboard];
} else {
retval = nil;
}
[list free];
} else {
types[0] = DrawPboardType;
[pboard declareTypes:types num:3 owner:[self class]];
list = [self findGraphicsInSelection:selection];
if (list && (stream = NXOpenMemory(NULL, 0, NX_WRITEONLY))) {
if (ts = NXOpenTypedStream(stream, NX_WRITEONLY)) {
NXWriteRootObject(ts, list);
NXCloseTypedStream(ts);
}
[pboard writeType:DrawPboardType fromStream:stream];
NXCloseMemory(stream, NX_FREEBUFFER);
} else {
retval = nil;
}
[list free];
}

return retval;
}

/*
* Supports linking to an entire file (not just a selection therein).
* This occurs when you drag a file into Draw and link (see gvDrag).
* This is very analogous to the pasteFromPasteboard:at: above.
*/

- importFile:(const char *)filename at:(NXSelection *)selection
{
id graphic;
NXRect gBounds;

if (graphic = [self findGraphicInSelection:selection]) {
gBounds = [graphic reinitFromFile:filename];
[self cache:&gBounds]; // updating a link to an imported file
[window flushWindow];
[self dirty];
return self;
}

return nil;
}

/* Other Links methods */

/*
* Just makes the Link Inspector panel reflect whether any of the
* Graphic's currently selected are linked to some other document.
*/

- updateLinksPanel
{
int i, linkCount = 0;
Graphic *foundGraphic = nil, *graphic = nil;

if (linkManager) {
for (i = [slist count]-1; i >= 0; i--) {
if (graphic = [[slist objectAt:i] graphicLinkedBy:NULL]) {
if ([graphic isKindOf:[Group class]]) {
linkCount += 2;
break;
} else {
linkCount += 1;
foundGraphic = graphic;
}
}
}
if (linkCount == 1) {
[NXDataLinkPanel setLink:[foundGraphic link] andManager:linkManager isMultiple:NO];
} else if (linkCount) {
[NXDataLinkPanel setLink:[foundGraphic link] andManager:linkManager isMultiple:YES];
} else {
[NXDataLinkPanel setLink:nil andManager:linkManager isMultiple:NO];
}
}

return self;
}

- (NXDataLinkManager *)linkManager
{
return linkManager;
}

/*
* When we get a linkManager via this method, we must go and revive all the links.
* This is due to the fact that we don't archive ANY link information when we
* save a Draw document. However, the unique identifiers ARE archived, and thus,
* when we unarchive, we can recreate NXSelections with those unique identifiers
* and then ask the NXDataLinkManager for the link objects associated with those
* NXSelections.
*
* After we have revived all the links, we call breakLinkAndRedrawOutlines:
* with nil (meaning redraw the link outlines for all links).
*/

- setLinkManager:(NXDataLinkManager *)aLinkManager
{
if (!linkManager) {
linkManager = aLinkManager;
[glist makeObjectsPerform:@selector(reviveLink:) with:linkManager];
[self breakLinkAndRedrawOutlines:nil];
}
return self;
}

/*
* This is called when the user chooses Open Source.
* It uses the trick of drawing directly into the GraphicView
* which, of course, is only ephemeral since the REAL contents
* of the GraphicView are stored in the backing store.
* This is convenient because Open Source is only a temporary
* the the user calls to see where the data for his link is
* coming from.
*/

- showSelection:(NXSelection *)selection
{
id retval = self;
List *graphics = nil;
NXRect *newInvalidRect;
NXRect sBounds, linkBounds;

[self lockFocus];
if (invalidRect) {
[self drawSelf:invalidRect :1];
newInvalidRect = invalidRect;
invalidRect = NULL;
} else{
NX_MALLOC(newInvalidRect, NXRect, 1);
}
if ([self getRect:&linkBounds forSelection:selection]) {
PSsetgray(NX_LTGRAY);
NXFrameRectWithWidth(&linkBounds, 2.0);
*newInvalidRect = linkBounds;
graphics = [self findGraphicsInSelection:selection];
if (graphics) {
[self getBBox:&sBounds of:graphics];
NXUnionRect(&sBounds, newInvalidRect);
} else {
invalidRect = newInvalidRect;
[self scrollRectToVisible:invalidRect];
[window flushWindow];
retval = nil;
}
} else {
graphics = [self findGraphicsInSelection:selection];
if (graphics) {
[self getBBox:&sBounds of:graphics];
*newInvalidRect = sBounds;
} else {
retval = nil;
}
}

if (retval) {
NXFrameLinkRect(&sBounds, NO);
invalidRect = newInvalidRect;
NXInsetRect(invalidRect, -NXLinkFrameThickness(), -NXLinkFrameThickness());
[self scrollRectToVisible:invalidRect];
[window flushWindow];
}

[self unlockFocus];
[graphics free];

return retval;
}

/*
* Called when the Show Links button in the Link Inspector panel is clicked
* (the link argument will be nil in this case), or when a link is broken
* (the link argument will be the link that was broken).
*/

- breakLinkAndRedrawOutlines:(NXDataLink *)link
{
int i;
Graphic *graphic;
BOOL gotOne = NO;
NXRect eBounds, recacheBounds;

for (i = [glist count]-1; i >= 0; i--) {
graphic = [glist objectAt:i];
if (graphic = [graphic graphicLinkedBy:link]) {
if (link && ([graphic link] == link) &&
([link updateMode] == NX_UpdateNever)) {
[self removeGraphic:graphic];
}
if (!link || [linkManager areLinkOutlinesVisible]) {
[graphic getExtendedBounds:&eBounds];
if (gotOne) {
NXUnionRect(&eBounds, &recacheBounds);
} else {
recacheBounds = eBounds;
gotOne = YES;
}
}
}
}
if (gotOne) {
[self cache:&recacheBounds andUpdateLinks:NO];
[window flushWindow];
}

return self;
}

/*
* Tracking Link Changes.
*
* This is how we get "Continuous" updating links.
*
* We simply assume that a thing someone is linked to in our document
* changes whenever we have to redraw any rectangle in the GraphicView
* which intersects the linked-to rectangle. See cache:andUpdateLinks:
* in GraphicView.m.
*/

typedef struct {
NXRect linkRect;
NXDataLink *link;
BOOL dragged, all;
} LinkRect;

- updateTrackedLinks:(const NXRect *)sRect
{
int i;
LinkRect *lr;
List *graphics;
NXSelection *selection;
NXRect *lRect, newRect;

for (i = [linkTrackingRects count]-1; i >= 0; i--) {
if (NXIntersectsRect(sRect, (NXRect *)[linkTrackingRects elementAt:i])) {
lr = ((LinkRect *)[linkTrackingRects elementAt:i]);
[lr->link sourceEdited];
lRect = (NXRect *)[linkTrackingRects elementAt:i];
if (!lr->dragged && !lr->all && !NXContainsRect(lRect, sRect)) {
selection = [lr->link sourceSelection];
if (graphics = [self findGraphicsInSelection:selection]) {
[self getBBox:&newRect of:graphics];
*lRect = newRect;
[graphics free];
}
}
}
}

return self;
}

/* Add to linkTrackingRects. */

- startTrackingLink:(NXDataLink *)link
{
LinkRect trackRect;
List *graphics = nil;
NXSelection *selection;
BOOL all = NO, dragged = NO, piecemeal = NO;

selection = [link sourceSelection];
if ([selection isEqual:[NXSelection allSelection]]) {
all = YES;
trackRect.linkRect = bounds;
} else if ([self getRect:&trackRect.linkRect forSelection:selection]) {
dragged = YES;
} else if (graphics = [self findGraphicsInSelection:selection]) {
[self getBBox:&trackRect.linkRect of:graphics];
piecemeal = YES;
[graphics free];
} else {
return nil;
}

if (all || dragged || piecemeal) {
if (!linkTrackingRects) {
linkTrackingRects = [[Storage allocFromZone:[self zone]] initCount:1 elementSize:sizeof(LinkRect) description:"{ffff@}"];
}
[self stopTrackingLink:link];
trackRect.link = link;
trackRect.dragged = dragged;
trackRect.all = all;
[linkTrackingRects addElement:&trackRect];
}

return nil;
}

/* Remove from linkTrackingRects. */

- stopTrackingLink:(NXDataLink *)link
{
int i;

for (i = [linkTrackingRects count]-1; i >= 0; i--) {
if (((LinkRect *)[linkTrackingRects elementAt:i])->link == link) {
[linkTrackingRects removeElementAt:i];
return self;
}
}

return nil;
}

@end