// Copyright 1997-1999 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 "OWContentCache.h"

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

#import "OWContentType.h"
#import "OWCookie.h"
#import "OWZoneContent.h"

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OWF/Pipelines.subproj/OWContentCache.m,v 1.21 1999/09/27 23:34:14 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 NSLock *contentCacheLock;
static NSMutableDictionary *contentCacheDictionary;
static OFScheduler *expireScheduler;
static BOOL OWContentCacheDebug = NO;
static NSZone *zone;

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

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

    zone = NSCreateZone(NSPageSize(), NSPageSize(), YES);
    contentCacheLock = [[NSLock allocWithZone:zone] init];
    contentCacheDictionary = [[NSMutableDictionary allocWithZone:zone] 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 {
            contentCacheEnumerator = [contentCacheDictionary objectEnumerator];
            while ((contentCache = [contentCacheEnumerator nextObject])) {
                if (aCookie == nil || [aCookie appliesToAddress:[contentCache address]]) {
                    [contentCache flushCachedContent];
                }
            }
            if (aCookie == nil) {
                [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 ([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
{
    OWZoneContent *content;
    
    [cacheLock lock];
    content = [self contentOfType:[OWContentType contentTypeForString:@"Omni/AllocationZone"]];
    if (!content) {
        content = [[OWZoneContent allocWithZone:[self zone]] init];
        [[content contentZone] setName:[address addressString]];
        [self registerContent:content];
    } else {
        [content retain];
    }
    [cacheLock unlock];
    [content autorelease];
    return [content contentZone];
}

- (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;
{
    NSEnumerator *expireContentTypeEnumerator;
    OWContentType *contentType;
    NSEnumerator *expireContentEventEnumerator;
    NSException *savedException;

    [cacheLock lock];
    NS_DURING {
	expireContentTypeEnumerator = [expireContentEventDictionary keyEnumerator];
	expireContentEventEnumerator = [expireContentEventDictionary objectEnumerator];

	while ((contentType = [expireContentTypeEnumerator nextObject])) {
	    OFScheduledEvent *oldExpireEvent;

	    oldExpireEvent = [expireContentEventEnumerator nextObject];
	    [expireScheduler abortEvent:oldExpireEvent];
	    if (OWContentCacheDebug)
                NSLog(@"%@ (%@): flushing %@", OBShortObjectDescription(self), [address addressString], [contentType contentTypeString]);

            [contentTypes removeObject:contentType];
            [contentDictionary removeObjectForKey:contentType];
            [recyclableContentDictionary removeObjectForKey:contentType];
	}
	[expireContentEventDictionary removeAllObjects];
        savedException = nil;
    } NS_HANDLER {
	savedException = localException;
    } NS_ENDHANDLER;
    flags.contentIsError = NO;
    [cacheLock unlock];
    
    if (savedException)
	[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];
        [expireContentEventDictionary removeObjectForKey:contentType];
        savedException = nil;
    } NS_HANDLER {
	savedException = localException;
    } NS_ENDHANDLER;
    [cacheLock unlock];
    
    if (savedException)
	[savedException raise];
}

- (void)expireAtDate:(NSDate *)expireDate;
{
    [expireScheduler scheduleSelector:@selector(flushCachedContent) onObject:self withObject:nil atDate:expireDate];
}

- (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];
    // No need to retain&autorelease, because these the cache is persistent
    contentCache = [contentCacheDictionary objectForKey:cacheKey];
    if (!contentCache && shouldCreate) {
        NSString *cacheKeyZoneCopy;
        
        contentCache = [[self allocWithZone:zone] initWithAddress:(id)anAddress];
        cacheKeyZoneCopy = [cacheKey copyWithZone:zone];
        [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:zone];
    cacheLock = [[NSRecursiveLock allocWithZone:zone] init];
    contentTypes = [[NSMutableSet allocWithZone:zone] init];
    contentDictionary = [[NSMutableDictionary allocWithZone:zone] init];
    recyclableContentDictionary = [[NSMutableDictionary allocWithZone:zone] init];
    expireContentEventDictionary = [[NSMutableDictionary allocWithZone:zone] 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];
    }
    [cacheLock unlock];
}

@end

DEFINE_NSSTRING(OWContentCacheFlushedCacheNotification);
