// Copyright 1997-2000 Omni Development, Inc.  All rights reserved.
//
// This software may only be used and reproduced according to the
// terms in the file OmniSourceLicense.html, which should be
// distributed with this project and can also be found at
// http://www.omnigroup.com/DeveloperResources/OmniSourceLicense.html.

#import "OHInlineImageCell.h"

#import <Foundation/Foundation.h>
#import <AppKit/AppKit.h>
#import <OmniBase/OmniBase.h>
#import <OmniFoundation/OmniFoundation.h>
#import <OmniAppKit/OmniAppKit.h>
#import <OWF/OWF.h>
#import <OIF/OIF.h>

#import "OWAddress-OHDragging.h"
#import "OHColorPalette.h"
#import "OHDownloader.h"
#import "OHHTMLAnchor.h"
#import "OHHTMLDocument.h"
#import "OHHTMLPageView.h"
#import "OHHTMLView.h"
#import "OHImageMapHolder.h"
#import "OWScriptEventHandlerHolder.h"
#import "OHImageMapShape.h"

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OmniHTML/Cells.subproj/OHInlineImageCell.m,v 1.26 2000/02/03 01:48:19 kc Exp $")

@interface OHInlineImageCell (Private)
- (void)takeSizeFromOmniImage;
- (void)takeCellImageAndSizeFromOmniImage;
- (OWWebPipeline *)currentPipeline;
- (void)setOmniImage:(OIImage *)newOmniImage;
@end

@implementation OHInlineImageCell

static NSMutableDictionary *internalIconsDictionary;
static NSSize smallImageSize;
static NSSize smallLinkSize;
static NSImage *smallImageImage;
static NSImage *smallLinkImage;
static NSImage *smallBrokenImage;

#define SMALL_IMAGE_PADDING 2

+ (void)initialize;
{
    static BOOL initialized = NO;

    [super initialize];
    if (initialized)
	return;
    initialized = YES;

    internalIconsDictionary = [[NSMutableDictionary alloc] initWithCapacity:32];

    smallImageImage = [NSImage imageNamed:@"smallImage"];
    smallImageSize = [smallImageImage size];
    smallLinkImage = [NSImage imageNamed:@"smallLink"];
    smallLinkSize = [smallLinkImage size];
    smallBrokenImage = [NSImage imageNamed:@"smallBroken"];
}

// Bundle registration

+ (void)registerItemName:(NSString *)itemName bundle:(NSBundle *)bundle description:(NSDictionary *)description;
{
    if ([itemName isEqualToString:@"internalIcons"])
        [internalIconsDictionary addEntriesFromDictionary:description];
}

+ (OWAddress *)addressForPossiblyInternalIconString:(NSString *)iconKeyString;
{
    NSString *imageNameString;

    imageNameString = [internalIconsDictionary objectForKey:iconKeyString];
    if (!imageNameString)
	return nil;
    return [OWAddress addressWithURL:[OWURL urlWithScheme:@"omniweb" netLocation:nil path:[imageNameString stringByAppendingString:@".tiff"] params:nil query:nil fragment:nil]];
}


// Init and dealloc

/* TODO: Should this change to having a referringContent arg instead of referringAddress ? */

- initWithAddress:(OWAddress *)anAddress referringAddress:(OWAddress *)aReferringAddress htmlDocument:(OHHTMLDocument *)htmlDocument;
{
    OBPRECONDITION(htmlDocument);
    
    if (![super init])
	return nil;

    inlineFlags.isMap = NO;
    alternateText = nil;
    textFont = nil;
    inlineImageState = OHInlineImageCell_rasa;
    omniImage = nil;
    name = nil;
    nonretainedHTMLDocument = htmlDocument;

    eventHandlers = [[OWScriptEventHandlerHolder alloc] init];

    imageStateLock = [[NSRecursiveLock allocWithZone:[self zone]] init];

    // If the user doesn't specify a source address on the image tag, we don't have an address.
    if (anAddress)
        [self resetAddress:anAddress referrer:aReferringAddress];

    return self;
}

- (void)dealloc;
{
    [textFont release];
    [OWPipeline cancelTarget:self];
    [imageMapHolder setInlineImage:nil];
    [imageMapHolder release];
    [alternateText release];
    [omniImage removeObserver:self];
    [omniImage release];
    [eventHandlers release];
    [imageStateLock release];
    [super dealloc];
}


// OHCell protocol

- (NSSize)naturalSize;
{
    NSSize contentSize;

    // If we have an image, return its size
    if (naturalSize.width > 0 && naturalSize.height > 0)
        return naturalSize;

    contentSize = smallImageSize;
    contentSize.width += SMALL_IMAGE_PADDING;
    contentSize.height += SMALL_IMAGE_PADDING;


    if (alternateText) {
        NSSize textSize;
        NSDictionary *attributes;
    
        attributes = [[NSDictionary alloc] initWithObjectsAndKeys:textFont, NSFontAttributeName, nil];
        textSize = [alternateText sizeWithAttributes:attributes];
        [attributes release];

        contentSize.width += textSize.width + SMALL_IMAGE_PADDING;
        contentSize.height = MAX(contentSize.height, textSize.height);
    } else if (link)
        contentSize.width += smallLinkSize.width + SMALL_IMAGE_PADDING;

    return contentSize;
}

- (BOOL)shouldScaleContentSizeBasedOnNaturalSize;
{
    return naturalSize.width > 0 && naturalSize.height > 0;
}

- (void)nullifyHTMLOwner;
{
    [super nullifyHTMLOwner];
    nonretainedHTMLDocument = nil;
}

- (BOOL)wantsLinkCursor;
{
    return (link != NULL) || inlineFlags.isMap;
}

- (OHHTMLLink *)linkAtPoint:(NSPoint)point;
{
    if (imageMapHolder) {
        NSPoint mapPoint;
        NSRect contentFrame;
        OHImageMapShape *mapLink;

        contentFrame = [self contentFrame];
        mapPoint = point;
        mapPoint.x -= contentFrame.origin.x;
        mapPoint.y -= contentFrame.origin.y;
        mapPoint.x = MIN(MAX(0, mapPoint.x), NSWidth(contentFrame));
        mapPoint.y = MIN(MAX(0, mapPoint.y), NSHeight(contentFrame));
        mapLink = [imageMapHolder shapeAtPoint:mapPoint];

        return mapLink;
    } else if (link) {
        if (inlineFlags.isMap) {
            OHHTMLAnchor *anchor;

            // Display the url that would have been triggered by a normal click
            anchor = [[OHHTMLAnchor alloc] initWithColorPalette:[OHColorPalette defaultPalette]];
            [anchor setAddress:[self addressAtPoint:point]];
            return [anchor autorelease];
        } else {
            return link;
        }
    } else {
        return nil;
    }
}

- (void)userClicked:(NSEvent *)event inHTMLView:(OHHTMLView *)htmlView;
{
    // Option-click (or right-click) on an image link, just as on text link, means to select the link but not follow it
    if (([event modifierFlags] & (NSAlternateKeyMask | NSControlKeyMask)) != 0 || [event type] == NSRightMouseDown) {
        if (imageMapHolder) {
            [imageMapHolder selectAnchorAtPoint:[self mapPointForEvent:event inHTMLView:htmlView]];
        } else if (link) {
            if (inlineFlags.isMap) {
                [[htmlView htmlPageView] selectAnchor:(OHHTMLAnchor *)[self linkAtPoint:[self mapPointForEvent:event inHTMLView:htmlView]]];
            } else {
                [[htmlView htmlPageView] selectAnchor:link];
            }
	}
        return;
    }

    // fetch image if user clicks on tiny icon on unfetched image
    if ([self needsFetch]) {
        NSRect contentFrame;
	NSPoint point;
    
	point = [htmlView convertPoint:[event locationInWindow] fromView:nil];
        contentFrame = [self contentFrame];
	point.x -= NSMinX(contentFrame);
	point.y = NSMaxY(contentFrame) - point.y;

        // If the user clicked on the image, not the link
	if (point.x < smallImageSize.width + SMALL_IMAGE_PADDING
	    && point.y < smallImageSize.height + SMALL_IMAGE_PADDING) {
	    [self queueSelectorOnce:@selector(fetch)];
	    return;
	}
    }

    // First check for handling image maps, then give super a chance to follow anchor
    if (![self clickActionForEvent:event inHTMLView:htmlView])
        [super userClicked:event inHTMLView:htmlView];
}


// NSTextAttachment pretend subclass

- (NSFileWrapper *)fileWrapper;
{
    NSString *filename;
    NSFileWrapper *fileWrapper;

    // TODO - Look in cache for "Omni/Source" to get GIF or JPEG
    
    fileWrapper = [super fileWrapper];
    filename = [OHDownloader suggestedFilenameForAddress:(OWAddress *)[[omniImage contentInfo] address]];
    if (filename) {
        filename = [[filename stringByDeletingPathExtension] stringByAppendingString:@".tiff"];
        [fileWrapper setPreferredFilename:filename];
    }
    return fileWrapper;
}


// OHImageCell overrides

- (NSImage *)image;
{
    NSImage *retval;
    [imageStateLock lock];
    switch (inlineImageState) {
        case OHInlineImageCell_rasa:
        case OHInlineImageCell_aborting:
            retval = smallImageImage;
            break;
        case OHInlineImageCell_fetching:
        case OHInlineImageCell_fetched:
        case OHInlineImageCell_aborted:
            [cellImage setSize:[self contentFrameWithoutBorder].size];
            retval = cellImage;
            break;
        case OHInlineImageCell_failedToLoad:
            retval = smallBrokenImage;
            break;
        default:
            retval = nil;
            break;
    }
    [imageStateLock unlock];
    return retval;
}
	    
- (void)drawInteriorWithFrame:(NSRect)contentFrame inView:(OHHTMLView *)aView;
{
    NSImage *drawImage;
    NSSize realImageSize;
    NSRect frameRect, drawRect;
    NSColor *color;

    if (omniImage) {
        [super drawInteriorWithFrame:contentFrame inView:aView];
        return;
    }

    color = [self borderColor];
    [color set];
    frameRect = contentFrame;
    frameRect.size.height--;
    frameRect.size.width--;
    NSFrameRectWithWidth(frameRect, 0.0);

    if (inlineImageState == OHInlineImageCell_failedToLoad)
	drawImage = smallBrokenImage;
    else
	drawImage = smallImageImage;

    realImageSize = [drawImage size];
    drawRect = NSInsetRect(contentFrame, 1.0, 1.0);
    
    [drawImage compositeToPoint:NSMakePoint(NSMinX(drawRect), NSMaxY(drawRect)) fromRect:NSMakeRect(0.0, 0.0, MIN(realImageSize.width, NSWidth(drawRect)), MIN(realImageSize.height, NSHeight(drawRect))) operation:NSCompositeSourceOver];
    
    drawRect.origin.x += realImageSize.width + SMALL_IMAGE_PADDING;
    drawRect.size.width -= realImageSize.width + SMALL_IMAGE_PADDING;
    
    if (alternateText) {
	[alternateText drawWithFont:textFont color:color alignment:NSLeftTextAlignment
	 rectangle:drawRect];
    } else if (link) {
        NSSize smallLinkImageSize;

        smallLinkImageSize = [smallLinkImage size];
        [smallLinkImage compositeToPoint:NSMakePoint(NSMinX(drawRect), NSMaxY(drawRect)) fromRect:NSMakeRect(0.0, 0.0, MIN(smallLinkImageSize.width, NSWidth(drawRect)), MIN(smallLinkImageSize.height, NSHeight(drawRect))) operation:NSCompositeSourceOver];
    }
}

- (void)startDragFromView:(OHHTMLView *)aView event:(NSEvent *)event;
{
    if (([event modifierFlags] & NSShiftKeyMask) || (!imageMapHolder && !link)) {
        [[self address] startDragOmniImage:omniImage imageAddress:[self address] fromView:aView event:event];
    } else if (imageMapHolder) {
        [imageMapHolder startDragForAnchorAtPoint:[self mapPointForEvent:event inHTMLView:aView] fromView:aView event:event];
    } else if (link) {
        if (inlineFlags.isMap) {
            [aView dragAddress:[self addressForEvent:event inHTMLView:aView] event:event];
        } else {
            [aView dragAnchor:link event:event];
        }
    }
}


// API

- (OHHTMLDocument *)htmlDocument;
{
    return nonretainedHTMLDocument;
}

- (void)setLink:(OHHTMLAnchor *)aLink;
{
    if (link == aLink)
	return;
    if (link) {
        [link removeContainedCell:self];
        [link release];
    }
    link = [aLink retain];
    [link addContainedCell:self];
    if (link && [link address]) {
	// If the inline image has a link, default to a 2-pixel border.  (If a border was specified in the HTML, it will override this default setting later.)
	[self setBorderWidth:2];
    }
}

// TODO WJS: Nobody cares about screen fonts anymore, convert to use new NSString methods for sizing.
- (void)setAlternateText:(NSString *)newAlternateText inFont:(NSFont *)aFont;
{
    OBPRECONDITION(aFont);
    
    if (alternateText == newAlternateText)
	return;
    [alternateText release];
    alternateText = [newAlternateText retain];
    if (!alternateText)
	return;
    
    if (textFont != aFont) {
        [textFont release];
        textFont = [aFont retain];
    }
}

- (void)setIsMap:(BOOL)newIsMap;
{
    inlineFlags.isMap = newIsMap;
}

- (void)setMapAddress:(OWAddress *)anAddress;
{
    OBASSERT(!imageMapHolder);
    if (imageMapHolder) // Shouldn't be called twice, but if it is...
        return;

    if (anAddress)
	inlineFlags.isMap = YES;

    imageMapHolder = [[OHImageMapHolder alloc] initWithImageMapAddress:anAddress inlineImage:self];
}

- (void)setName:(NSString *)newName
{
    if (name != newName) {
        [name release];
        name = [newName copy];
    }
}

- (void)resetAddress:(OWAddress *)newAddress referrer:(OWAddress *)aReferringAddress;
{
    BOOL wasFetched;
    OWWebPipeline *imagePipeline;

    if ([[self address] isEqual:newAddress])
        return;  // TODO: possibly we should reload?

    wasFetched = (inlineImageState != OHInlineImageCell_rasa);
    inlineImageState = OHInlineImageCell_rasa;

    imagePipeline = [[OWWebPipeline alloc] initWithContent:newAddress target:self useCachedErrorContent:YES];
    if (aReferringAddress)
        [imagePipeline setReferringAddress:aReferringAddress];
    [imagePipeline release];

    if (wasFetched || [[OFUserDefaults sharedUserDefaults] boolForKey:@"OHAutoFetchInlineImages"]) {
        [self fetch];
    }
}

- (OWAddress *)address;
{
    return [[self currentPipeline] lastAddress];
}

- (NSString *)name
{
    return name;
}

- (BOOL)needsFetch;
{
    return inlineImageState == OHInlineImageCell_rasa || inlineImageState == OHInlineImageCell_aborted || inlineImageState == OHInlineImageCell_failedToLoad;
}

- (BOOL)isFetching;
{
    return inlineImageState == OHInlineImageCell_fetching || inlineImageState == OHInlineImageCell_aborting;
}

- (void)fetch;
{
    if (![self needsFetch])
	return;
    if (imageMapHolder)
	[imageMapHolder fetch];
    inlineImageState = OHInlineImageCell_fetching;
    [[self currentPipeline] fetch];
}

- (void)startAnimation;
{
    [omniImage startAnimation];
}

- (void)stopAnimation;
{
    [omniImage stopAnimation];
}

- (OWScriptEventHandlerHolder *)eventHandlers
{
    return eventHandlers;
}

// OIImageObserver protocol

- (void)imageDidSize:(OIImage *)anOmniImage;
{
    OBASSERT(anOmniImage == omniImage);
    [self takeSizeFromOmniImage];
}

- (void)imageDidUpdate:(OIImage *)anOmniImage;
{
    OBASSERT(anOmniImage == omniImage);
    [self takeCellImageAndSizeFromOmniImage];
}

- (void)imageDidAbort:(OIImage *)anOmniImage;
{
    OBASSERT(anOmniImage == omniImage);
}


// OWTarget protocol

// TODO: This code reports a user abort to the script (if any) as an error, not as an abort. Ideally we should distinguish between the two cases.

- (OWContentType *)targetContentType;
{
    return [OIImage contentType];
}

- (OWTargetContentDisposition)pipeline:(OWPipeline *)aPipeline hasContent:(id <OWContent>)someContent;
{
    OIImage *newOmniImage;

    inlineImageState = OHInlineImageCell_fetching;
    newOmniImage = (OIImage *)someContent;

    if (omniImage != newOmniImage) {
        // I used to assert that omniImage != newOmniImage, but that assertion failed today (on 7/12/99) when I was dragging the mouse over the menu bar on our new web site. --kc
        [omniImage removeObserver:self];
        [omniImage release];
        [cellImage release];
        cellImage = nil;
        omniImage = [newOmniImage retain];
        [omniImage addObserver:self];
    }
    if ([omniImage image]) {
	[self takeCellImageAndSizeFromOmniImage];
    } else if ([omniImage hasSize]) {
        [self takeSizeFromOmniImage];
    }

    return OWTargetContentDisposition_ContentAccepted;
}

- (NSString *)targetTypeFormatString;
{
    if ([self isMarginalCell])
        return @"Margin %@";
    else
        return @"Inline %@";
}

- (OWContentInfo *)parentContentInfo;
{
    return [nonretainedHTMLDocument contentInfo];
}


// OWOptionalContent protocol

- (OWTargetContentDisposition)pipeline:(OWPipeline *)aPipeline hasAlternateContent:(id <OWContent>)someContent;
{
    if ([someContent isKindOfClass:[OWDataStream class]] && [(OWDataStream *)someContent resetContentTypeAndEncoding]) {
        // OK, we've reset the content type and encoding, let's try again
        [aPipeline addContent:someContent];
        [aPipeline startProcessingContent];
        return OWTargetContentDisposition_ContentUpdatedOrTargetChanged;
    }

    inlineImageState = OHInlineImageCell_failedToLoad;
    [OWScriptEvent sendBackgroundEvent:OWSE_Error instigator:self responder:[self eventHandlers]];
    [self redisplayCell];
    return OWTargetContentDisposition_ContentRejectedSavePipeline;
}

- (OWTargetContentDisposition)pipeline:(OWPipeline *)aPipeline hasErrorContent:(id <OWContent>)someContent;
{
    if ([someContent contentType] == [self targetContentType]) {
        // The image had some error, but display whatever we managed to get anyway
        return [self pipeline:aPipeline hasContent:someContent];
    }

    inlineImageState = OHInlineImageCell_failedToLoad;
    [OWScriptEvent sendBackgroundEvent:OWSE_Error instigator:self responder:[self eventHandlers]];
    [self redisplayCell];
    return OWTargetContentDisposition_ContentRejectedSavePipeline;
}

- (void)pipelineDidEnd:(OWPipeline *)aPipeline;
{
    if (!omniImage && inlineImageState != OHInlineImageCell_failedToLoad) {
	inlineImageState = OHInlineImageCell_failedToLoad;
        [OWScriptEvent sendBackgroundEvent:OWSE_Error instigator:self responder:[self eventHandlers]];
	[self redisplayCell];
    }
    if (inlineImageState == OHInlineImageCell_fetching) {
	inlineImageState = OHInlineImageCell_fetched;
        [OWScriptEvent sendBackgroundEvent:OWSE_Load instigator:self responder:[self eventHandlers]];
    }
}

- (NSString *)expectedContentTypeString;
{
    return @"Image";
}

@end


@implementation OHInlineImageCell (SubclassesOnly)

- (BOOL)clickActionForEvent:(NSEvent *)event inHTMLView:(OHHTMLView *)htmlView;
{
    if (!inlineFlags.isMap)
	return NO;

    if (imageMapHolder) {
        [imageMapHolder followAnchorAtPoint:[self mapPointForEvent:event inHTMLView:htmlView]];
    } else if (link) {
        [[htmlView htmlPageView] displayDocumentAtAddress:[self addressForEvent:event inHTMLView:htmlView]];
    }
    return YES;
}

- (OWAddress *)addressAtPoint:(NSPoint)point;
{
    return [[link address] addressWithGetQuery:[NSString stringWithFormat:@"%d,%d", (int)(point.x), (int)(point.y)]];
}

- (OWAddress *)addressForEvent:(NSEvent *)event inHTMLView:(OHHTMLView *)htmlView;
{
    return [self addressAtPoint:[self mapPointForEvent:event inHTMLView:htmlView]];
}

- (NSPoint)mapPointForEvent:(NSEvent *)event inHTMLView:(OHHTMLView *)htmlView;
{
    NSRect contentFrame;
    NSPoint mapPoint;

    contentFrame = [self contentFrame];

    mapPoint = [htmlView convertPoint:[event locationInWindow] fromView:nil];

    mapPoint.x -= contentFrame.origin.x;
    mapPoint.y -= contentFrame.origin.y;
    mapPoint.x = MIN(MAX(0, mapPoint.x), NSWidth(contentFrame));
    mapPoint.y = MIN(MAX(0, mapPoint.y), NSHeight(contentFrame));

    return mapPoint;
}

@end


@implementation OHInlineImageCell (Private)

    // call from any thread
- (void)takeSizeFromOmniImage;
{
    [self setImageSize:[omniImage size]];
}

    // call from any thread
- (void)takeCellImageAndSizeFromOmniImage;
{
    NSImage *newImage;

    newImage = [omniImage image];
    if (newImage != cellImage) {
	if (cellImage)
	    [cellImage release];
	cellImage = [newImage retain];
    }
    [cellImage setScalesWhenResized:YES];
    [self takeSizeFromOmniImage];

    [self redisplayCell];
}

- (OWWebPipeline *)currentPipeline;
{
    // We want a slightly different notion of "current" for image cells than for web pages...  We don't want -currentPipelineForTarget: (which returns the currently displayed pipeline), we want the last pipeline we created pointing at ourselves.
    return (OWWebPipeline *)[[OWPipeline pipelinesForTarget:self] lastObject];
    // Note that the above handles the cases where pipelinesForTarget: returns nil or a zero-length array (by returning nil).
}

- (void)setOmniImage:(OIImage *)newOmniImage;
{
}

// Debugging

- (NSMutableDictionary *)debugDictionary;
{
    NSMutableDictionary *debugDictionary;

    debugDictionary = [super debugDictionary];
    if (alternateText)
        [debugDictionary setObject:alternateText forKey:@"alternateText"];
    if (inlineFlags.isMap)
        [debugDictionary setObject:inlineFlags.isMap ? @"YES" : @"NO" forKey:@"inlineFlags.isMap"];

    return debugDictionary;
}

@end
