// Copyright 1997-2002 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 <OWF/OWContentCache.h>

#import <Foundation/Foundation.h>
#import <OmniBase/OmniBase.h>
#import <OmniFoundation/OmniFoundation.h>

#import <OWF/OWContentType.h>
#import <OWF/OWCookie.h>

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OWF/Pipelines.subproj/OWContentCache.m,v 1.36 2002/03/09 01:53:51 kc Exp $")

@interface OWContentCache (Private)
+ (OWContentCache *)contentCacheForAddress:(id <OWAddress>)anAddress shouldCreate:(BOOL)shouldCreate;
- initWithAddress:(id <OWAddress>)anAddress;
- (void)expireContentOfType:(OWContentType *)aContentType;
- (void)expireContentOfType:(OWContentType *)contentType afterTimeInterval:(NSTimeInterval)expireTimeInterval;
@end

@implementation OWContentCache

static BOOL OWContentCacheDebug = NO;
static NSLock *contentCacheLock;
static NSMutableDictionary *contentCacheDictionary;
static OFScheduler *expireScheduler;

NSZone *OWContentZone = NULL;

+ (void)initialize;
{
    OFZone *contentZone;
    
    OBINITIALIZE;

    // Don't need to store a pointer to the OFZone for any reason...
    contentZone = [OFZone newZone];
    [contentZone setName: @"OWContentCache"];
    OWContentZone = contentZone->zone;
    
    contentCacheLock = [[NSLock allocWithZone:OWContentZone] init];
    contentCacheDictionary = [[NSMutableDictionary allocWithZone:OWContentZone] init];
    expireScheduler = [[[OFScheduler mainScheduler] subscheduler] retain];
}

+ (void)setDebug:(BOOL)newDebug;
{
    OWContentCacheDebug = newDebug;
}

+ (OWContentCache *)contentCacheForAddress:(id <OWAddress>)anAddress;
{
    return [self contentCacheForAddress:anAddress shouldCreate:YES];
}

+ (OWContentCache *)lookupContentCacheForAddress:(id <OWAddress>)anAddress;
{
    return [self contentCacheForAddress:anAddress shouldCreate:NO];
}

+ (void)flushCachedContent;
{
    [self flushCachedContentMatchingCookie:nil];
}

+ (void)flushCachedContentMatchingCookie:(OWCookie *)aCookie;
{
    NSEnumerator *contentCacheEnumerator;
    OWContentCache *contentCache;
    NSException *savedException;

    [contentCacheLock lock];
    OMNI_POOL_START {
        NS_DURING {
            if (OWContentCacheDebug)
                NSLog(@"OWContentCache: flush cached content matching cookie: %@", aCookie);

            contentCacheEnumerator = [contentCacheDictionary objectEnumerator];
            while ((contentCache = [contentCacheEnumerator nextObject])) {
                if (aCookie == nil || [aCookie appliesToAddress:[contentCache address]]) {
                    [contentCache flushCachedContent];
                }
            }
            if (aCookie == nil) {
                // This method only gets called with nil when the user has selected 'Flush Cache' from the menu.  This means get rid of everything.  (Don't put stuff in the content cache that you must have forever, use some other mechanism for that.)
                [contentCacheDictionary removeAllObjects];
                [expireScheduler abortSchedule];
                [[NSNotificationCenter defaultCenter] postNotificationName:OWContentCacheFlushedCacheNotification object:nil];
            }
            savedException = nil;
        } NS_HANDLER {
            savedException = localException;
        } NS_ENDHANDLER;
    } OMNI_POOL_END;
    [contentCacheLock unlock];

    if (savedException)
        [savedException raise];
}

- (void)dealloc;
{
    [address release];
    [cacheLock release];
    [contentTypes release];
    [contentDictionary release];
    [recyclableContentDictionary release];
    [expireContentEventDictionary release];
    [super dealloc];
}

- (void)addContent:(id <OWContent>)content;
{
    OWContentType *contentType;
    NSTimeInterval expireTimeInterval;

    contentType = [content contentType];
    if (!contentType)
	return;
    expireTimeInterval = [contentType expirationTimeInterval];
    if (expireTimeInterval == 0.0)
	return;

    [cacheLock lock];
    if (OWContentCacheDebug)
        NSLog(@"%@ (%@): adding %@", OBShortObjectDescription(self), [address addressString], [contentType contentTypeString]);

    [contentTypes addObject:contentType];
    [contentDictionary setObject:content forKey:contentType];
    [cacheLock unlock];
    
    [self expireContentOfType:contentType afterTimeInterval:expireTimeInterval];
}

- (void)registerContent:(id <OWContent>)content;
{
    OWContentType *contentType;
    NSTimeInterval expireTimeInterval;

    // Our NSObject(OWContent) category implements OWOptionalContent
    if ([(id <OWOptionalContent>)content shareable]) {
        [self addContent:content];
        return;
    }

    contentType = [content contentType];
    if (!contentType)
	return;
    expireTimeInterval = [contentType expirationTimeInterval];
    if (expireTimeInterval == 0.0)
	return;

    [cacheLock lock];
    if (OWContentCacheDebug)
        NSLog(@"%@ (%@): registering recyclable content %@", OBShortObjectDescription(self), [address addressString], [contentType contentTypeString]);
    [recyclableContentDictionary setObject:content forKey:contentType];

    [cacheLock unlock];
    
    [self expireContentOfType:contentType afterTimeInterval:expireTimeInterval];
}

- (void)recycleContent:(id <OWContent>)content;
{
    OWContentType *contentType;
    BOOL canRecycleContent;

    contentType = [content contentType];
    if (!contentType)
	return;

    // Note: Every object is guaranteed to respond to every method in the OWOptionalContent protocol, since there's a category on NSObject which implements all of it.
    canRecycleContent = [(id <OWOptionalContent>)content prepareForReturnToCache];

    [cacheLock lock];
    if ((id <OWContent>)[recyclableContentDictionary objectForKey:contentType] == content) {
        if (canRecycleContent &&
            [(id <OWOptionalContent>)content contentIsValid]) {
            if (OWContentCacheDebug)
                NSLog(@"%@ (%@): recycling %@", OBShortObjectDescription(self), [address addressString], [contentType contentTypeString]);
            [contentTypes addObject:contentType];
            [contentDictionary setObject:content forKey:contentType];
        } else
            content = nil;
        [recyclableContentDictionary removeObjectForKey:contentType];
    } else
	content = nil;
    [cacheLock unlock];
    
    if (content) {
	// Restart the expiration countdown
	[self expireContentOfType:contentType afterTimeInterval:[contentType expirationTimeInterval]];
    }
}

- (OFZone *)contentZone
{
    // This is obsolete.  Use the global OWContentZone intead.  We don't want a zone for every content cache!
    OBASSERT(NO);
    return NULL;
}

- (BOOL)contentIsError;
{
    return flags.contentIsError;
}

- (void)setContentIsError:(BOOL)newContentIsError;
{
    flags.contentIsError = newContentIsError;
}

- (id <OWAddress>)address;
{
    return address;
}

- (NSSet *)contentTypes;
{
    NSSet *contentTypesCopy;
    
    [cacheLock lock];
    contentTypesCopy = [[[NSSet alloc] initWithSet:contentTypes] autorelease];
    [cacheLock unlock];
    return contentTypesCopy;
}

- (id <OWContent>)peekAtContentOfType:(OWContentType *)contentType;
{
    id <OWContent, OWOptionalContent> content;

    if (!contentType)
        return nil;

    [cacheLock lock];
    content = [contentDictionary objectForKey:contentType];
    if (!content)
        content = [recyclableContentDictionary objectForKey:contentType];
    if (content) {
        if (![content contentIsValid]) {
            // The cached content is no longer valid (probably a redirected OWDataStream):  flush it from the cache, and don't return it.
            [contentTypes removeObject:contentType];
            [contentDictionary removeObjectForKey:contentType];
            [recyclableContentDictionary removeObjectForKey:contentType];
            content = nil;
        } else {
            // Retain and autorelease the content in this thread so it won't get lost in another thread.
            [[content retain] autorelease];
        }
    }
    [cacheLock unlock];

    return content;
}

- (id <OWContent>)contentOfType:(OWContentType *)contentType;
{
    id <OWContent, OWOptionalContent> content;

    if (!contentType)
        return nil;

    [cacheLock lock];
    content = [contentDictionary objectForKey:contentType];
    if (content) {
        if (![content contentIsValid]) {
            // The cached content is no longer valid (probably a redirected OWDataStream):  flush it from the cache, and don't return it.
            [contentTypes removeObject:contentType];
            [contentDictionary removeObjectForKey:contentType];
            content = nil;
        } else {
            [[content retain] autorelease];
            if (![content shareable]) {
                if (OWContentCacheDebug)
                    NSLog(@"%@ (%@): granting %@", OBShortObjectDescription(self), [address addressString], [contentType contentTypeString]);
                [recyclableContentDictionary setObject:content forKey:contentType];
                [contentTypes removeObject:contentType];
                [contentDictionary removeObjectForKey:contentType];
            }
        }
    }
    [cacheLock unlock];

    if (content) {
        // Restart the expiration countdown
        [self expireContentOfType:contentType afterTimeInterval:[contentType expirationTimeInterval]];
    }

    return content;
}

// Removing content from the cache

- (void)flushCachedContent;
{
    [self flushCachedContentExceptTypes:nil];
}

- (void)flushCachedContentExceptTypes:(NSSet *)saveTypes;
{
    NSException *savedException = nil;

    OMNI_POOL_START {
        [cacheLock lock];
        NS_DURING {
            NSArray *flushTypes;
            unsigned int flushTypeIndex, flushTypeCount;

            if (saveTypes != nil) {
                NSMutableArray *typeAccumulator;
                NSEnumerator *expireContentTypeEnumerator;
                OWContentType *flushType;

                typeAccumulator = [[NSMutableArray alloc] init];
                expireContentTypeEnumerator = [expireContentEventDictionary keyEnumerator];
                while ((flushType = [expireContentTypeEnumerator nextObject]) != nil) {
                    if (![saveTypes containsObject:flushType])
                        [typeAccumulator addObject:flushType];
                }
                flushTypes = typeAccumulator;
            } else {
                flushTypes = [[expireContentEventDictionary allKeys] retain];
            }
    
            if (OWContentCacheDebug)
                NSLog(@"%@ (%@): flushing cached content, keeping %@", OBShortObjectDescription(self), [address addressString], saveTypes);
            flushTypeIndex = [flushTypes count];
            while (flushTypeIndex-- != 0) {
                OWContentType *flushType;
                OFScheduledEvent *oldExpireEvent;

                flushType = [flushTypes objectAtIndex:flushTypeIndex];
                oldExpireEvent = [expireContentEventDictionary objectForKey:flushType];
                [expireScheduler abortEvent:oldExpireEvent];
                if (OWContentCacheDebug)
                    NSLog(@"%@ (%@): flushing %@", OBShortObjectDescription(self), [address addressString], [flushType contentTypeString]);
    
                [contentTypes removeObject:flushType];
                [contentDictionary removeObjectForKey:flushType];
                [recyclableContentDictionary removeObjectForKey:flushType];
                [expireContentEventDictionary removeObjectForKey:flushType];
            }
            [flushTypes release];
        } NS_HANDLER {
            savedException = localException;
        } NS_ENDHANDLER
        flags.contentIsError = NO;
        [cacheLock unlock];
    } OMNI_POOL_END;
    
    if (savedException != nil)
	[savedException raise];
}

- (void)flushCachedErrorContent;
{
    [cacheLock lock];
    if (flags.contentIsError)
        [self flushCachedContent];
    [cacheLock unlock];
}

- (void)flushContentOfType:(OWContentType *)contentType;
{
    NSException *savedException;
    
    [cacheLock lock];
    NS_DURING {
	if (OWContentCacheDebug)
            NSLog(@"%@ (%@): flushing %@", OBShortObjectDescription(self), [address addressString], [contentType contentTypeString]);

	[contentTypes removeObject:contentType];
        [contentDictionary removeObjectForKey:contentType];
        [recyclableContentDictionary removeObjectForKey:contentType];
        [expireScheduler abortEvent:[expireContentEventDictionary objectForKey:contentType]];
        [expireContentEventDictionary removeObjectForKey:contentType];
        savedException = nil;
    } NS_HANDLER {
	savedException = localException;
    } NS_ENDHANDLER;
    [cacheLock unlock];
    
    if (savedException)
	[savedException raise];
}

- (void)expireAtDate:(NSDate *)expireDate;
{
    [self expireAfterTimeInterval:[expireDate timeIntervalSinceNow]];
}

- (void)expireAfterTimeInterval:(NSTimeInterval)expireInterval;
{
    [expireScheduler scheduleSelector:@selector(flushCachedContent) onObject:self withObject:nil afterTime:expireInterval];
}

- (void)expireErrorContentAfterTimeInterval:(NSTimeInterval)expireInterval;
{
    [expireScheduler scheduleSelector:@selector(flushCachedErrorContent) onObject:self withObject:nil afterTime:expireInterval];
}

- (NSDate *)expireDateForContentOfType:(OWContentType *)contentType;
{
    NSDate *date;
    
    [cacheLock lock];
    date = [[expireContentEventDictionary objectForKey:contentType] date];
    [cacheLock unlock];

    return date;
}

// Debugging

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

    debugDictionary = [super debugDictionary];

    if (address)
        [debugDictionary setObject:[address addressString] forKey:@"address"];
    if (contentTypes)
        [debugDictionary setObject:[contentTypes allObjects] forKey:@"contentTypes"];
    if (contentDictionary)
        [debugDictionary setObject:contentDictionary forKey:@"contentDictionary"];
    if (recyclableContentDictionary)
        [debugDictionary setObject:recyclableContentDictionary forKey:@"recyclableContentDictionary"];
    if (expireContentEventDictionary)
        [debugDictionary setObject:expireContentEventDictionary forKey:@"expireContentEventDictionary"];

    return debugDictionary;
}

- (NSString *)shortDescription;
{
    return [address cacheKey];
}

@end

@implementation OWContentCache (Private)

+ (OWContentCache *)contentCacheForAddress:(id <OWAddress>)anAddress shouldCreate:(BOOL)shouldCreate;
{
    OWContentCache *contentCache;
    NSString *cacheKey;

    cacheKey = [anAddress cacheKey];
    if (!cacheKey)
        return nil;

    [contentCacheLock lock];
    contentCache = [contentCacheDictionary objectForKey:cacheKey];
    [[contentCache retain] autorelease];
    
    if (!contentCache && shouldCreate) {
        NSString *cacheKeyZoneCopy;
        
        contentCache = [[self allocWithZone:OWContentZone] initWithAddress:(id)anAddress];
        cacheKeyZoneCopy = [cacheKey copyWithZone:OWContentZone];
        [contentCacheDictionary setObject:contentCache forKey:cacheKeyZoneCopy];
        [cacheKeyZoneCopy release];
        [contentCache release];
    }
    [contentCacheLock unlock];

    return contentCache;
}

- initWithAddress:(id <OWAddress>)anAddress;
{
    if (![super init])
        return nil;

    address = [anAddress copyWithZone:OWContentZone];
    cacheLock = [[NSRecursiveLock allocWithZone:OWContentZone] init];
    contentTypes = [[NSMutableSet allocWithZone:OWContentZone] init];
    contentDictionary = [[NSMutableDictionary allocWithZone:OWContentZone] init];
    recyclableContentDictionary = [[NSMutableDictionary allocWithZone:OWContentZone] init];
    expireContentEventDictionary = [[NSMutableDictionary allocWithZone:OWContentZone] init];
    return self;
}

- (void)expireContentOfType:(OWContentType *)aContentType;
{
    [self flushContentOfType:aContentType];
}

- (void)expireContentOfType:(OWContentType *)contentType afterTimeInterval:(NSTimeInterval)expireTimeInterval;
{
    OFScheduledEvent *expireContentEvent, *oldExpireEvent;

    [cacheLock lock];
    oldExpireEvent = [expireContentEventDictionary objectForKey:contentType];
    if (oldExpireEvent)
        [expireScheduler abortEvent:oldExpireEvent];
    if (expireTimeInterval != OWContentTypeNeverExpireTimeInterval) {
        expireContentEvent = [expireScheduler scheduleSelector:@selector(expireContentOfType:) onObject:self withObject:contentType afterTime:expireTimeInterval];
        [expireContentEventDictionary setObject:expireContentEvent forKey:contentType];
    } else {
	[expireContentEventDictionary removeObjectForKey:contentType];
    }
    [cacheLock unlock];
}

@end

DEFINE_NSSTRING(OWContentCacheFlushedCacheNotification);
