// 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 "OWPipeline.h"

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

#import "OWAddress.h" // Only for heuristic for compositeTypeString
#import "OWContentCache.h"
#import "OWContentContainer.h"
#import "OWContentInfo.h"
#import "OWContentProtocol.h"
#import "OWContentType.h"
#import "OWContentTypeLink.h"
#import "OWHeaderDictionary.h"
#import "OWURL.h" // Only for heuristic for compositeTypeString
#import "OWPipelineCoordinator.h"
#import "OWProcessor.h"

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OWF/Pipelines.subproj/OWPipeline.m,v 1.40 1999/10/18 00:32:16 luke Exp $")

@interface OWPipeline (Private)
// Status monitors
+ (void)updateStatusMonitors:(NSTimer *)timer;
// Methods managing the targetPipelinesMapTable
+ (void)addPipeline:(OWPipeline *)aPipeline forTarget:(id <OWTarget>)aTarget;
+ (void)startPipeline:(OWPipeline *)aPipeline forTarget:(id <OWTarget>)aTarget;
+ (void)removePipeline:(OWPipeline *)aPipeline forTarget:(id <OWTarget>)aTarget;
+ (void)target:(id <OWTarget>)aTarget acceptedContentFromPipeline:(OWPipeline *)acceptedPipeline;

//
- (NSSet *)cachedContentTypesMinusProcessed;
- (id <OWContent>)availableContentOfType:(OWContentType *)searchType;
- (void)readyContentType:(OWContentType *)aContentType;
- (void)processedContentType:(OWContentType *)aContentType;
- (void)startUnstartedProcessors;
- (void)deactivateIfPipelineHasNoProcessors;
- (void)releasePipelineCoordinator;
- (void)cleanupPipelineIfDead;
- (void)pipelineBuilt;
- (BOOL)hasNoProcessors;
- (void)createNextProcessor;
// Target stuff
- (void)notifyTargetOfTreeActivation;
- (void)notifyTargetOfTreeDeactivation;
- (void)notifyTargetOfTreeDeactivation:(id <OWTarget>)aTarget;
- (void)updateStatusOnTarget:(id <OWTarget>)target;
- (void)rebuildCompositeTypeString;
- (void)validateContentInfo;
@end

@implementation OWPipeline

NSString *OWPipelineHasErrorNotificationName = @"OWPipelineHasError";
NSString *OWPipelineWillDeallocateNotificationName = @"OWPipelineWillDeallocate";
NSString *OWPipelineHasErrorNotificationPipelineKey = @"pipeline";
NSString *OWPipelineHasErrorNotificationProcessorKey = @"processor";
NSString *OWPipelineHasErrorNotificationErrorNameKey = @"errorName";
NSString *OWPipelineHasErrorNotificationErrorReasonKey = @"errorReason";
NSString *OWPipelineTreeActivationNotificationName = @"OWPipelineTreeActivation";
NSString *OWPipelineTreeDeactivationNotificationName = @"OWPipelineTreeDeactivation";


static BOOL OWPipelineDebug = NO;
static NSNotificationCenter *fetchedContentNotificationCenter;
static NSNotificationCenter *pipelineErrorNotificationCenter;
static NSString *fetchedContentContentKey = @"content";
static NSString *permanentRedirectionContentKey = @"redirected-to";
static OFSimpleLockType targetPipelinesMapTableLock;
static NSMapTable *targetPipelinesMapTable;
static BOOL activeTreeHasUndisplayedChanges;
static id <OWPipelineActiveTreeMonitor> activeTreeMonitor;
static NSTimer *activeStatusUpdateTimer;

/* TODO: The fetchedContentContentKey and permanentRedirectionContentKey keys should be visible outside of this class (instead of using literal strings elsewhere) */

#define DEFAULT_SIMULTANEOUS_TARGET_CAPACITY (128)

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

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

    fetchedContentNotificationCenter = [[NSNotificationCenter alloc] init];
    pipelineErrorNotificationCenter = [[NSNotificationCenter alloc] init];
    OFSimpleLockInit(&targetPipelinesMapTableLock);
    targetPipelinesMapTable = NSCreateMapTable(NSNonRetainedObjectMapKeyCallBacks, NSObjectMapValueCallBacks, DEFAULT_SIMULTANEOUS_TARGET_CAPACITY);

    // Status monitor
    activeTreeHasUndisplayedChanges = NO;
    activeTreeMonitor = nil;
    activeStatusUpdateTimer = nil;
}

+ (void)setDebug:(BOOL)debug;
{
    OWPipelineDebug = debug;
}

// For notification of pipeline errors

+ (NSNotificationCenter *)pipelineErrorNotificationCenter;
{
    return pipelineErrorNotificationCenter;
}

// For notification of pipeline fetches

+ (void)addObserver:(id)anObserver selector:(SEL)aSelector address:(id <OWAddress>)anAddress;
{
    [fetchedContentNotificationCenter addObserver:anObserver selector:aSelector name:[anAddress cacheKey] object:nil];
}

- (void)addObserver:(id)anObserver selector:(SEL)aSelector
{
    [fetchedContentNotificationCenter addObserver:anObserver selector:aSelector name:nil object:self];
}

+ (void)removeObserver:(id)anObserver address:(id <OWAddress>)anAddress;
{
    [fetchedContentNotificationCenter removeObserver:anObserver name:[anAddress cacheKey] object:nil];
}

+ (void)removeObserver:(id)anObserver;
{
    [fetchedContentNotificationCenter removeObserver:anObserver];
}

// Target management

+ (void)cancelTarget:(id <OWTarget>)aTarget;
{
    NSArray *pipelines;

    OMNI_POOL_START {
        while ((pipelines = [self pipelinesForTarget:aTarget])) {
            unsigned int pipelineIndex, pipelineCount;

            pipelineCount = [pipelines count];
            for (pipelineIndex = 0; pipelineIndex < pipelineCount; pipelineIndex++) {
                [[pipelines objectAtIndex:pipelineIndex] cancelTarget];
            }
        }
    } OMNI_POOL_END;
}

+ (void)abortTreeActivityForTarget:(id <OWTarget>)aTarget;
{
    NSArray *pipelines;

    pipelines = [self pipelinesForTarget:aTarget];
    if (pipelines) {
        unsigned int pipelineIndex, pipelineCount;

        pipelineCount = [pipelines count];
        for (pipelineIndex = 0; pipelineIndex < pipelineCount; pipelineIndex++) {
            [[pipelines objectAtIndex:pipelineIndex] abortTreeActivity];
        }
    }
}

+ (void)abortPipelinesForTarget:(id <OWTarget>)aTarget;
{
    NSArray *pipelines;

    pipelines = [self pipelinesForTarget:aTarget];
    if (pipelines) {
        unsigned int pipelineIndex, pipelineCount;

        pipelineCount = [pipelines count];
        for (pipelineIndex = 0; pipelineIndex < pipelineCount; pipelineIndex++) {
            [[pipelines objectAtIndex:pipelineIndex] abortTask];
        }
    }
}

+ (OWPipeline *)currentPipelineForTarget:(id <OWTarget>)aTarget;
{
    NSArray *pipelines;

    pipelines = [self pipelinesForTarget:aTarget];
    if (!pipelines || [pipelines count] == 0)
        return nil;
    return [pipelines objectAtIndex:0];
}

+ (NSArray *)pipelinesForTarget:(id <OWTarget>)aTarget;
{
    NSArray *pipelines, *pipelinesSnapshot;

    OBPRECONDITION(aTarget);
    OFSimpleLock(&targetPipelinesMapTableLock);
    pipelines = NSMapGet(targetPipelinesMapTable, aTarget);
    pipelinesSnapshot = pipelines ? [NSArray arrayWithArray:pipelines] : nil;
    OFSimpleUnlock(&targetPipelinesMapTableLock);
    return pipelinesSnapshot;
}

+ (OWPipeline *)firstActivePipelineForTarget:(id <OWTarget>)aTarget;
{
    NSArray *pipelines;
    unsigned int pipelineIndex, pipelineCount;

    pipelines = [self pipelinesForTarget:aTarget];
    if (!pipelines)
        return nil;
    
    pipelineCount = [pipelines count];
    for (pipelineIndex = 0; pipelineIndex < pipelineCount; pipelineIndex++) {
        OWPipeline *aPipeline;

        aPipeline = [pipelines objectAtIndex:pipelineIndex];
        if ([aPipeline treeHasActiveChildren])
            return aPipeline;
    }
    return nil;
}

+ (OWPipeline *)lastActivePipelineForTarget:(id <OWTarget>)aTarget;
{
    NSArray *pipelines;
    unsigned int pipelineIndex;

    pipelines = [self pipelinesForTarget:aTarget];
    if (!pipelines)
        return nil;

    pipelineIndex = [pipelines count];
    while (pipelineIndex--) {
        OWPipeline *aPipeline;

        aPipeline = [pipelines objectAtIndex:pipelineIndex];
        if ([aPipeline treeHasActiveChildren])
            return aPipeline;
    }
    return nil;
}


// Status Monitoring

+ (void)setActiveTreeMonitor:(id <OWPipelineActiveTreeMonitor>)monitor;
{
    activeTreeMonitor = monitor;
}

+ (void)activeTreeWasDisplayed;
{
    activeTreeHasUndisplayedChanges = NO;
}

+ (void)activeTreeHasChanged;
{
    if (activeTreeHasUndisplayedChanges)
        return;
    activeTreeHasUndisplayedChanges = YES;
    [self queueSelectorOnce:@selector(updateStatusMonitors:) withObject:nil];
}

+ (void)startActiveStatusUpdateTimer;
{
    OBPRECONDITION(activeStatusUpdateTimer == nil);

    activeStatusUpdateTimer = [[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(updateStatusMonitors:) userInfo:nil repeats:YES] retain];
}

+ (void)stopActiveStatusUpdateTimer;
{
    OBPRECONDITION(activeStatusUpdateTimer != nil);
    [activeStatusUpdateTimer invalidate];
    [activeStatusUpdateTimer release];
    activeStatusUpdateTimer = nil;
}


// Init and dealloc

+ (void)startPipelineWithContent:(id <OWContent>)aContent target:(id <OWTarget>)aTarget;
{
    OWPipeline *pipeline;

    pipeline = [[self alloc] initWithContent:aContent target:aTarget useCachedErrorContent:NO];
    [pipeline startProcessingContent];
    [pipeline release];
}

- initWithContent:(id <OWContent>)aContent target:(id <OWTarget>)aTarget useCachedErrorContent:(BOOL)useError;
{
    OBPRECONDITION(aContent);

    if (![super init])
	return nil;

    state = PipelineInit;
    flags.contentError = NO;
    flags.everHadContentError = NO;
    flags.disableContentCache = NO;
    flags.useCachedErrorContent = useError;
    flags.processingError = NO;
    processorArray = [[NSMutableArray alloc] initWithCapacity:2];
    unstartedProcessors = [[NSMutableArray alloc] init];
    processorArrayLock = [[NSLock alloc] init];
    lastContentType = [lastContent contentType];
    context = [[NSMutableDictionary alloc] init];
    contextLock = [[NSLock alloc] init];
    [self addContent:aContent];
    if (([lastContent conformsToProtocol:@protocol(OWAddress)]))
	[self setLastAddress:(id <OWAddress>)lastContent];

    // This has the side effect of setting our parentContentInfo
    [self setTarget:aTarget];

    if (OWPipelineDebug)
	NSLog(@"%@: init", [self shortDescription]);

    return self;
}

- initWithContent:(id <OWContent>)aContent target:(id <OWTarget>)aTarget;
{
    return [self initWithContent:aContent target:aTarget useCachedErrorContent:NO];
}

- (void)dealloc;
{
    if (OWPipelineDebug)
	NSLog(@"%@: dealloc", [self shortDescription]);

    OBPRECONDITION([processorArray count] == 0);

    [processorArray makeObjectsPerformSelector:@selector(nullifyPipeline)];
    
    [lastContent release];

    [lastAddress release];
    [context release];
    [contextLock release];
    [headerDictionary release];
    [headerURL release];
    [processorArray release];
    [unstartedProcessors release];
    [processorArrayLock release];
    [processedContentTypes release];
    [contentCacheForLastAddress release];
    [errorNameString release];
    [errorReasonString release];
    [super dealloc];
}


// OWTask subclass

- (id <OWAddress>)lastAddress;
{
    id <OWAddress> addressAutoreleased;

    OFSimpleLock(&displayablesSimpleLock);
    addressAutoreleased = [[lastAddress retain] autorelease];
    OFSimpleUnlock(&displayablesSimpleLock);
    return addressAutoreleased;
}

- (BOOL)treeHasActiveChildren;
{
    return (state != PipelineInit && state != PipelineDead) || [super treeHasActiveChildren];
}

- (void)activateInTree;
{
    if (OWPipelineDebug)
        NSLog(@"%@: tree activation", [self shortDescription]);
    
    [super activateInTree];
    [self notifyTargetOfTreeActivation];
}

- (void)deactivateInTree;
{
    if (OWPipelineDebug)
        NSLog(@"%@: tree deactivation", [self shortDescription]);
    
    if (flags.everHadContentError && !flags.delayedForError) {
        // We had an error, wait around to display it.

        // Pretend we were still active, so that we'll get deactivated again next time through -treeActiveStatusMayHaveChanged
        taskFlags.wasActiveOnLastCheck = YES;

        // Note that we've already been deactivated so we don't do this again next time.
        flags.delayedForError = YES;

        // Schedule another deactivate test for 8 seconds from now.
        [[OFScheduler mainScheduler] scheduleSelector:@selector(treeActiveStatusMayHaveChanged) onObject:self withObject:nil afterTime:8.0];
    } else {
        // No error, or we've already delayed to display it.
        [super deactivateInTree];
        
        [self notifyTargetOfTreeDeactivation];
        [self cleanupPipelineIfDead];
    }
}

- (void)abortTask;
{
    NSArray *processorsCopy;
    NSEnumerator *processorEnumerator;
    OWProcessor *processor;

    if (state == PipelineAborting || state == PipelineDead)
        return;

    [processorArrayLock lock];
    state = PipelineAborting;
    [processorArray removeObjectsInArray:unstartedProcessors];
    if ([processorArray count])
        firstProcessor = [processorArray objectAtIndex:0];
    else
        firstProcessor = nil;
    [unstartedProcessors removeAllObjects];
    [unstartedProcessors release];
    unstartedProcessors = nil;
    processorsCopy = [[NSArray alloc] initWithArray:processorArray];
    processorEnumerator = [processorsCopy objectEnumerator];
    [processorsCopy release];
    [processorArrayLock unlock];

    while ((processor = [processorEnumerator nextObject])) {
        if (OWPipelineDebug)
            NSLog(@"%@: aborting %@", [self shortDescription], [processor shortDescription]);
        [processor abortProcessing];
    }
    [pipelineCoordinator pipelineAbort:self];
    [self releasePipelineCoordinator];

    if (!errorNameString)
        errorNameString = [@"Stopped" retain];
    if (!errorReasonString)
        errorReasonString = [@"Pipeline stopped by user" retain];
    // Half-processed content isn't valid
    flags.everHadContentError = YES;
    // We don't need to wait to display this, since the user knows why we've stopped
    flags.delayedForError = YES;

    [self tellInspectorPipelineTreeDidChange];
    [self updateStatusOnTarget];

    [self deactivateIfPipelineHasNoProcessors];
}

- (void)nullifyContentInfo;
{
    [super nullifyContentInfo];
    [self rebuildCompositeTypeString];
}

- (NSTimeInterval)estimatedRemainingTimeInterval;
{
    NSDate *firstBytesDate;
    int workDone, workToBeDone;

    [processorArrayLock lock];
    firstBytesDate = [firstProcessor firstBytesDate];
    workDone = [firstProcessor bytesProcessed];
    workToBeDone = [firstProcessor totalBytes];
    [processorArrayLock unlock];
    
    if (!firstBytesDate || workDone == 0 || workToBeDone == 0 || workDone >= workToBeDone)
        return 0.0;

    return -[firstBytesDate timeIntervalSinceNow] * (workToBeDone - workDone) / workDone;
}

- (BOOL)hadError;
{
    return flags.everHadContentError || flags.processingError;
}

- (BOOL)isRunning;
{
    if ([self hadError])
        return NO;
    switch (state) {
        case PipelineInit:
        case PipelinePaused:
        case PipelineAborting:
        case PipelineDead:
            return NO;
        default:
            return YES;
    }
}

- (BOOL)hasThread;
{
    BOOL hasThread = NO;
    
    [processorArrayLock lock];
    if (firstProcessor)
        hasThread = [firstProcessor status] == OWProcessorRunning;
    [processorArrayLock unlock];

    return hasThread;
}

- (NSString *)errorNameString;
{
    return errorNameString;
}

- (NSString *)errorReasonString;
{
    return errorReasonString;
}

- (NSString *)compositeTypeString;
{
    NSString *string;

    OFSimpleLock(&displayablesSimpleLock);
    string = [[compositeTypeString retain] autorelease];
    OFSimpleUnlock(&displayablesSimpleLock);

    return string;
}

- (unsigned int)workDone;
{
    unsigned int result;

    [processorArrayLock lock];
    result = [firstProcessor bytesProcessed];
    [processorArrayLock unlock];
    if (result == 0)
        result = maximumWorkToBeDone;
    return result;
}

- (unsigned int)workToBeDone;
{
    unsigned int result;

    [processorArrayLock lock];
    result = [firstProcessor totalBytes];
    [processorArrayLock unlock];
    if (!result)
        result = maximumWorkToBeDone;
    else if (result > maximumWorkToBeDone)
        maximumWorkToBeDone = result;
    return result;
}


- (NSString *)statusString;
{
    NSString *string = nil;
    
    [processorArrayLock lock];
    if (errorReasonString)
        string = errorReasonString;
    else if (firstProcessor)
        string = [firstProcessor statusString];
    [processorArrayLock unlock];

    if (string)
        return string;
    
    switch (state) {
        case PipelineInit:
            return @"Pipeline created";
        case PipelineBuilding:
            return @"Pipeline building";
        case PipelineRunning:
            return @"Pipeline running";
        case PipelinePaused:
            return @"Pipeline paused";
        case PipelineAborting:
            return @"Pipeline stopping";
        case PipelineDead:
            return [super statusString];
    }
    return nil; // NOTREACHED
}

- (void)deepFlushContentCache;
{
    [super deepFlushContentCache];
    [self flushContentCache];
}

- (unsigned int)taskPriority;
{
    // Give higher priority to longer pipelines, since we really want to finish what we start before we start another pipeline.  This fixes OmniWeb so if you hit a page with, say, 50 inline images, you don't have to wait for all the images to load before any of them start to display.  With this hack, images that are loaded will immediately start imaging.
    return [parentContentInfo baseTaskPriority] - addedContentCount;
}



// Pipeline management

- (void)startProcessingContent;
{
    if (state == PipelineAborting || state == PipelineDead)
	return;
    
    [processorArrayLock lock];
    if (state == PipelineAborting || state == PipelineDead) {
	[processorArrayLock unlock];
	return;
    }

    // If another (later) pipeline has grabbed our target before we got a chance to even start, well, c'est la vie.  We suicide rather than fight for her.
    if (!nonretainedTarget) {
        [processorArrayLock unlock];
        [self abortTask];
        return;
    }

    if (state == PipelineInit)
        [isa startPipeline:self forTarget:nonretainedTarget];

    state = PipelineBuilding;
    [processorArrayLock unlock];

    if (OWPipelineDebug)
	NSLog(@"%@: startProcessingContent %@", [self shortDescription], [lastContentType shortDescription]);
    if ([lastContent conformsToProtocol:@protocol(OWAddress)])
	[self setLastAddress:(id <OWAddress>)lastContent];
    if (!pipelineCoordinator)
        pipelineCoordinator = [[OWPipelineCoordinator pipelineCoordinatorForAddress:lastAddress] retain];
    if (OWPipelineDebug)
        NSLog(@"%@: startProcessingContent: coordinator %@", [self shortDescription], [pipelineCoordinator shortDescription]);
    [self treeActiveStatusMayHaveChanged];
    [pipelineCoordinator buildPipeInPipeline:self];
}

- (void)fetch;
{
    if (state == PipelineInit)
        [self startProcessingContent];
    else
        [self refetchContentFromLastAddress];
}

- (BOOL)canRefetchContentFromLastAddress;
{
    return state == PipelineInit || state == PipelineDead;
}

- (void)refetchContentFromLastAddress;
{
    [self flushContentCache];

    if (![self canRefetchContentFromLastAddress]) {
        OWPipeline *clonePipeline;

        clonePipeline = [self copy];
        [clonePipeline startProcessingContent];
        [clonePipeline release];
        return;
    }

    state = PipelineInit;
    [self addContent:lastAddress];
    [self setLastAddress:lastAddress];
    flags.contentError = NO;
    flags.everHadContentError = NO;
    flags.disableContentCache = NO;
    flags.processingError = NO;
    [errorNameString release];
    errorNameString = nil;
    [errorReasonString release];
    errorReasonString = nil;

    [processorArrayLock lock];
    if (!unstartedProcessors)
        unstartedProcessors = [[NSMutableArray alloc] init];
    [processorArrayLock unlock];
    
    [self startProcessingContent];
}

// Target

- (id <OWTarget>)target;
{
    return [[nonretainedTarget retain] autorelease];
}

- (void)setTarget:(id <OWTarget>)newTarget;
{
    id <OWTarget> oldTarget;

    flags.contentError = NO;
    if (nonretainedTarget == newTarget)
        return;

    // Don't retain here since we could be in the middle of the -dealloc method
    // on nonretainedTarget.
    oldTarget = nonretainedTarget;

    if (nonretainedTarget)
        [isa removePipeline:self forTarget:nonretainedTarget];
    nonretainedTarget = newTarget;

    if (nonretainedTarget) {
        [isa addPipeline:self forTarget:nonretainedTarget];
        [self setParentContentInfo:[nonretainedTarget parentContentInfo]];
        OBASSERT(parentContentInfo);

        targetContentType = [nonretainedTarget targetContentType];
        [typeString release];
        typeString = [[nonretainedTarget targetTypeFormatString] retain];
        [self rebuildCompositeTypeString];

        if ([nonretainedTarget respondsToSelector:@selector(pipelineDidBegin:)])
            [(id <OWOptionalTarget>)nonretainedTarget pipelineDidBegin:self];

        [self notifyTargetOfTreeActivation];
    } else {
        [self setParentContentInfo:[OWContentInfo orphanParentContentInfo]];
        [self cleanupPipelineIfDead];
    }

    [self notifyTargetOfTreeDeactivation:oldTarget];
    OBPOSTCONDITION(nonretainedTarget == newTarget);
}

- (void)nullifyTarget;
{
    [self setTarget:nil];
    OBPOSTCONDITION(nonretainedTarget == nil);
}

- (void)cancelTarget;
{
    flags.contentError = NO;
    if (!nonretainedTarget)
        return;

    [isa removePipeline:self forTarget:nonretainedTarget];
    nonretainedTarget = nil;

    [self setParentContentInfo:[OWContentInfo orphanParentContentInfo]];
    [self cleanupPipelineIfDead];
    OBPOSTCONDITION(nonretainedTarget == nil);
}

- (void)parentContentInfoLostContent;
{
    if ([nonretainedTarget respondsToSelector:@selector(parentContentInfoLostContent)])
        [(id <OWOptionalTarget>)nonretainedTarget parentContentInfoLostContent];
}

- (void)updateStatusOnTarget;
{
    [self updateStatusOnTarget:nonretainedTarget];
}


// Content

- (id <OWContent>)lastContent;
{
    return lastContent;
}

- (OWContentType *)lastContentType;
{
    return lastContentType;
}

- (OWContentCache *)contentCacheForLastAddress;
{
    return contentCacheForLastAddress;
}

- (void)addContent:(id <OWContent>)newContent;
{
    if (newContent != lastContent) {
	[lastContent release];
	lastContent = [newContent retain];
        addedContentCount++;
    }
    lastContentType = [lastContent contentType];

    // TODO: Wim: This is necessary to set the contentInfo's address correctly, and early enough for scripts to see it. It may not be necessary to move us to the new content info's task list yet. If that causes problems, just set the address.
    // TODO2: Wim: This is probably no longer necessary after latest restructuring (11Mar1999), check it and see
    [self validateContentInfo];
}

- (void)contentError;
{
    flags.contentError = YES;
    flags.everHadContentError = YES;
    flags.delayedForError = NO;
    [contentCacheForLastAddress setContentIsError:YES];
    [contentCacheForLastAddress expireErrorContentAfterTimeInterval:5.0];
}

- (void)disableContentCache;
{
    flags.disableContentCache = YES;
}

- (void)cacheContent;
{
    if (flags.disableContentCache || !contentCacheForLastAddress)
	return;
    [contentCacheForLastAddress registerContent:lastContent];
}

- (void)permanentRedirection:(id <OWAddress>)redirectTo;
{
    NSDictionary *userInfo;
    /* TODO: This method may be unneeded --- its functionality might be best subsumed into -cacheContent --- see the comment in OWHTTPSession. */

    userInfo = [[NSDictionary alloc] initWithObjectsAndKeys:redirectTo, permanentRedirectionContentKey, nil];
    
    [fetchedContentNotificationCenter postNotificationName:[lastAddress cacheKey] object:self userInfo:userInfo];
}

- (void)flushContentCache;
{
    [contentCacheForLastAddress flushCachedContent];
}

- (id)contextObjectForKey:(NSString *)key;
{
    id object;

    [contextLock lock];
    object = [context objectForKey:key];
    [contextLock unlock];

    return object;
}

- (void)setContextObject:(id)anObject forKey:(NSString *)key;
{
    [contextLock lock];
    if (anObject)
	[context setObject:anObject forKey:key];
    else
	[context removeObjectForKey:key];
    [contextLock unlock];
}

- (id)setContextObjectNoReplace:(id)anObject forKey:(NSString *)key;
{
    id existingObject;
    
    [contextLock lock];
    existingObject = [context objectForKey:key];
    if (anObject && !existingObject) {
        [context setObject:anObject forKey:key];
        existingObject = anObject;
    }
    [contextLock unlock];
    return existingObject;
}

- (OWHeaderDictionary *)headerDictionary;
{
    if (!headerDictionary)
        headerDictionary = [[OWHeaderDictionary alloc] init];

    return headerDictionary;
}

- (void)setHeaderDictionary:(OWHeaderDictionary *)newHeaderDictionary fromURL:(OWURL *)newHeaderURL;
{
    if (headerDictionary != newHeaderDictionary) {
        [headerDictionary release];
        headerDictionary = [newHeaderDictionary retain];
    }
    if (headerURL != newHeaderURL) {
        [headerURL release];
        headerURL = [newHeaderURL retain];
    }
}

//

- (OWProcessor *)firstProcessor;
{
    OWProcessor *processor;

    [processorArrayLock lock];
    processor = [[firstProcessor retain] autorelease];
    [processorArrayLock unlock];
    return processor;
}

- (void)addProcessor:(OWProcessor *)aProcessor;
{
    if (state == PipelineAborting || state == PipelineDead)
	return;
    [processorArrayLock lock];
    if (state != PipelineAborting && state != PipelineDead) {
	[processorArray addObject:aProcessor];
	[unstartedProcessors addObject:aProcessor];
	firstProcessor = [processorArray objectAtIndex:0];
    }
    [processorArrayLock unlock];
}

- (void)removeProcessor:(OWProcessor *)aProcessor;
{
    [[self retain] autorelease];
    [processorArrayLock lock];
    [aProcessor nullifyPipeline];
    [processorArray removeObject:aProcessor];
    if ([processorArray count])
	firstProcessor = [processorArray objectAtIndex:0];
    else
	firstProcessor = nil;
    [processorArrayLock unlock];
    [self deactivateIfPipelineHasNoProcessors];
}

- (void)processorDidRetire:(OWProcessor *)aProcessor;
{
    [self removeProcessor:aProcessor];
}

- (void)processorStatusChanged:(OWProcessor *)aProcessor ;
{
    [self updateStatusOnTarget];
}

- (void)processor:(OWProcessor *)aProcessor hasErrorName:(NSString *)name;
{
    [self processor:aProcessor hasErrorName:name reason:nil];
}

- (void)processor:(OWProcessor *)aProcessor hasErrorName:(NSString *)name reason:(NSString *)reason;
{
    NSMutableDictionary *userInfo;

    userInfo = [[NSMutableDictionary alloc] initWithCapacity:3];
    [userInfo setObject:self forKey:OWPipelineHasErrorNotificationPipelineKey];
    if (aProcessor)
	[userInfo setObject:aProcessor forKey:OWPipelineHasErrorNotificationProcessorKey];
    [userInfo setObject:name forKey:OWPipelineHasErrorNotificationErrorNameKey];
    [userInfo setObject:reason forKey:OWPipelineHasErrorNotificationErrorReasonKey];
    [pipelineErrorNotificationCenter postNotificationName:OWPipelineHasErrorNotificationName object:self userInfo:userInfo];
    [userInfo release];

    flags.processingError = YES;
    if (name != errorNameString) {
        [errorNameString release];
        errorNameString = [name retain];
    }
    if (reason != errorReasonString) {
        [errorReasonString release];
        errorReasonString = [reason retain];
    }
    
    [self updateStatusOnTarget];
}


// NSCopying protocol

- copyWithZone:(NSZone *)zone;
{
    OWPipeline *newPipeline;

    newPipeline = [[isa allocWithZone:zone] initWithContent:lastAddress target:nonretainedTarget useCachedErrorContent:NO];

    return newPipeline;
}

@end


@implementation OWPipeline (SubclassesOnly)

- (void)deactivate;
{
    if (OWPipelineDebug)
        NSLog(@"%@: done", [self shortDescription]);
    
    if (lastAddress && state != PipelineAborting && ![self hadError]) {
        // On a successful fetch, post a notification that we've fetched this address.  Bookmarks watch for these notifications.
        NSDictionary *userInfo;
        userInfo = lastContent ? [[NSDictionary alloc] initWithObjectsAndKeys:lastContent, fetchedContentContentKey, nil] : nil;
        [fetchedContentNotificationCenter postNotificationName:[lastAddress cacheKey] object:self userInfo:userInfo];
        [userInfo release];
    }

    if ([nonretainedTarget respondsToSelector:@selector(pipelineDidEnd:)])
        [(id <OWOptionalTarget>)nonretainedTarget pipelineDidEnd:self];

    [pipelineCoordinator pipebuildingComplete:self];
    [self releasePipelineCoordinator];
    state = PipelineDead;
    [lastContent release];
    lastContent = nil;
    [self treeActiveStatusMayHaveChanged];
}

- (void)setLastAddress:(id <OWAddress>)newLastAddress;
{
    OFSimpleLock(&displayablesSimpleLock);
    if (lastAddress != newLastAddress) {
        if ([newLastAddress isKindOfClass:[OWAddress class]])
            newLastAddress = [(OWAddress *)newLastAddress addressWithTarget:nil];
        if (lastAddress != newLastAddress) {
            [lastAddress release];
            lastAddress = [newLastAddress retain];
        }
    }
    [contentCacheForLastAddress release];
    contentCacheForLastAddress = [[OWContentCache contentCacheForAddress:lastAddress] retain];
    // Clean slate!
    [processedContentTypes release];
    processedContentTypes = nil;
    flags.disableContentCache = NO;

    if ([contentCacheForLastAddress contentIsError]) {
        if (flags.useCachedErrorContent)
            [self contentError];
        else
            [contentCacheForLastAddress flushCachedErrorContent];
    }

    OFSimpleUnlock(&displayablesSimpleLock);
}

// This is called by the OWPipelineCoordinator

- (void)_buildPipe;
{
    id <OWContent> targetContent;

    // First, make sure we have a valid contentInfo
    [self validateContentInfo];
    
    targetContent = [self availableContentOfType:targetContentType];
    if (targetContent || lastContent == nil || flags.contentError) {
	if (targetContent && lastContent != targetContent) {
	    [lastContent release];
	    lastContent = [targetContent retain];
	    lastContentType = [lastContent contentType];

            // Fix up our contentInfo for the new content....
            [self validateContentInfo];
	}
	[self pipelineBuilt];
	return;
    }
    [self createNextProcessor];
}


@end


@implementation OWPipeline (Private)

// Status monitors

+ (void)updateStatusMonitors:(NSTimer *)timer;
{
    NSArray *activeChildrenCopy;
    int pipelineIndex, pipelineCount;

    activeChildrenCopy = [OWContentInfo allActiveTasks];

    pipelineCount = [activeChildrenCopy count];
    for (pipelineIndex = 0; pipelineIndex < pipelineCount; pipelineIndex++) {
        OWPipeline *pipeline;

        pipeline = [activeChildrenCopy objectAtIndex:pipelineIndex];
        if ([pipeline isKindOfClass:[OWPipeline class]])
            [pipeline updateStatusOnTarget];
    }
    [activeTreeMonitor activePipelinesTreeHasChanged];
}


// Methods managing the targetPipelinesMapTable

+ (void)addPipeline:(OWPipeline *)aPipeline forTarget:(id <OWTarget>)aTarget;
{
    NSMutableArray *pipelines;

    OBPRECONDITION(aTarget);
    OBPRECONDITION(aPipeline);
    OFSimpleLock(&targetPipelinesMapTableLock);
    pipelines = NSMapGet(targetPipelinesMapTable, aTarget);
    if (!pipelines) {
        pipelines = [[NSMutableArray alloc] init];
        NSMapInsertKnownAbsent(targetPipelinesMapTable, aTarget, pipelines);
    }
    OBPRECONDITION([pipelines indexOfObjectIdenticalTo:aPipeline] == NSNotFound);
    [pipelines addObject:aPipeline];
    OFSimpleUnlock(&targetPipelinesMapTableLock);
}

+ (void)startPipeline:(OWPipeline *)aPipeline forTarget:(id <OWTarget>)aTarget;
{
    NSMutableArray *pipelines;
    unsigned int pipelineIndex;

    OBPRECONDITION(aTarget);
    OBPRECONDITION(aPipeline);
    OFSimpleLock(&targetPipelinesMapTableLock);
    pipelines = NSMapGet(targetPipelinesMapTable, aTarget);
    if (!pipelines) {
        pipelines = [[NSMutableArray alloc] init];
        NSMapInsertKnownAbsent(targetPipelinesMapTable, aTarget, pipelines);
    }
    pipelineIndex = [pipelines indexOfObjectIdenticalTo:aPipeline];
    OBPRECONDITION(pipelineIndex != NSNotFound);

    // Move pipeline to the end of the target's pipelines array
    [pipelines removeObjectAtIndex:pipelineIndex];
    [pipelines addObject:aPipeline];
    OFSimpleUnlock(&targetPipelinesMapTableLock);
}

+ (void)removePipeline:(OWPipeline *)aPipeline forTarget:(id <OWTarget>)aTarget;
{
    NSMutableArray *pipelines;

    OBPRECONDITION(aTarget);
    OBPRECONDITION(aPipeline);
    OFSimpleLock(&targetPipelinesMapTableLock);
    pipelines = NSMapGet(targetPipelinesMapTable, aTarget);
    OBPRECONDITION(pipelines && [pipelines indexOfObjectIdenticalTo:aPipeline] != NSNotFound);
    [pipelines removeObjectIdenticalTo:aPipeline];
    if ([pipelines count] == 0) {
        NSMapRemove(targetPipelinesMapTable, aTarget);
        [pipelines release];
    }
    OFSimpleUnlock(&targetPipelinesMapTableLock);
}

+ (void)target:(id <OWTarget>)aTarget acceptedContentFromPipeline:(OWPipeline *)acceptedPipeline;
{
    NSArray *pipelines;
    unsigned int pipelineIndex, pipelineCount;

    pipelines = [self pipelinesForTarget:aTarget];
    pipelineCount = [pipelines count];
    for (pipelineIndex = 0; pipelineIndex < pipelineCount; pipelineIndex++) {
        OWPipeline *aPipeline;

        aPipeline = [pipelines objectAtIndex:pipelineIndex];
        if (aPipeline == acceptedPipeline)
            return;
        [aPipeline nullifyTarget];
    }
}

//

- (NSSet *)cachedContentTypesMinusProcessed;
{
    NSSet *cachedContentTypes;

    cachedContentTypes = [contentCacheForLastAddress contentTypes];
    if (processedContentTypes) {
        NSMutableSet             *contentTypes;

        contentTypes = [NSMutableSet setWithSet:cachedContentTypes];
        [contentTypes minusSet:processedContentTypes];
        return contentTypes;
    }

    return cachedContentTypes;
}

- (id <OWContent>)availableContentOfType:(OWContentType *)searchType;
{
    if (lastContentType == searchType)
	return lastContent;
    return [contentCacheForLastAddress contentOfType:searchType];
}

- (void)readyContentType:(OWContentType *)aContentType;
{
    if (lastContentType == aContentType)
	return;
    [lastContent release];
    lastContent = [contentCacheForLastAddress contentOfType:aContentType];
    [lastContent retain];
    lastContentType = [lastContent contentType];
}

- (void)processedContentType:(OWContentType *)aContentType;
{
    if (!aContentType)
	return;
    if (!processedContentTypes)
	processedContentTypes = [[NSMutableSet alloc] initWithCapacity:1];

    if (OWPipelineDebug)
        NSLog(@"%@: processedContentType %@", [self shortDescription], [aContentType shortDescription]);
    
    [processedContentTypes addObject:aContentType];
}

- (void)startUnstartedProcessors;
{
    NSArray *unstartedProcessorsCopy;
    unsigned int unstartedProcessorsIndex, unstartedProcessorsCount;

    [processorArrayLock lock];
    unstartedProcessorsCopy = [[NSArray alloc] initWithArray:unstartedProcessors];
    [unstartedProcessors removeAllObjects];
    [processorArrayLock unlock];

    unstartedProcessorsCount = [unstartedProcessorsCopy count];
    for (unstartedProcessorsIndex = 0; unstartedProcessorsIndex < unstartedProcessorsCount; unstartedProcessorsIndex++) {
        OWProcessor *unstartedProcessor;

        unstartedProcessor = [unstartedProcessorsCopy objectAtIndex:unstartedProcessorsIndex];
        if (OWPipelineDebug)
            NSLog(@"%@: starting %@", [self shortDescription], [unstartedProcessor shortDescription]);
        [unstartedProcessor startProcessing];
    }

    [unstartedProcessorsCopy release];
}

- (void)deactivateIfPipelineHasNoProcessors;
{
    if (state != PipelineDead && [self hasNoProcessors]) {
	[self deactivate];
    }
}

- (void)releasePipelineCoordinator;
{
    OWPipelineCoordinator *oldPipelineCoordinator;

    // Release the pipeline coordinator in a thread-safe manner
    oldPipelineCoordinator = pipelineCoordinator;
    pipelineCoordinator = nil;
    [oldPipelineCoordinator release];
}

- (void)cleanupPipelineIfDead;
{
    if (!nonretainedTarget && ![self treeHasActiveChildren]) {
        OBPRECONDITION(lastContent == nil || state == PipelineInit);
        if (state == PipelineInit)
            state = PipelineDead;

        [[self retain] autorelease];
        [self setParentContentInfo:nil];
        [contentInfo removeTask:self];
        [contentInfo release];
        contentInfo = nil;

        [[NSNotificationCenter defaultCenter] postNotificationName:OWPipelineWillDeallocateNotificationName object:self];
    }
}

- (void)pipelineBuilt;
{
    OWTargetContentDisposition contentDisposition;
    id <OWTarget> originalTarget;

    [pipelineCoordinator pipebuildingComplete:self];
    [self releasePipelineCoordinator];
    state = PipelineRunning;

    contentDisposition = OWTargetContentDisposition_ContentRejectedCancelPipeline;
    originalTarget = [nonretainedTarget retain];
    NS_DURING {
        if (flags.contentError) {
            if (OWPipelineDebug)
                NSLog(@"%@: delivering error indication", [self shortDescription]);
            if ([originalTarget respondsToSelector:@selector(pipeline:hasErrorContent:)])
                contentDisposition = [(id <OWOptionalTarget>)originalTarget pipeline:self hasErrorContent:lastContent];
        } else if (lastContentType == targetContentType) {
            if (OWPipelineDebug)
                NSLog(@"%@: delivering content", [self shortDescription]);
            contentDisposition = [originalTarget pipeline:self hasContent:lastContent];
        } else {
            if (OWPipelineDebug)
                NSLog(@"%@: delivering alternate content", [self shortDescription]);
            if ([originalTarget respondsToSelector:@selector(pipeline:hasAlternateContent:)])
                contentDisposition = [(id <OWOptionalTarget>)originalTarget pipeline:self hasAlternateContent:lastContent];
        }
    } NS_HANDLER {
        if (OWPipelineDebug)
            NSLog(@"%@: exception from target: %@", [self shortDescription], localException);
        [self processor:nil hasErrorName:[localException displayName] reason:[localException reason]];
    } NS_ENDHANDLER;

    if (OWPipelineDebug)
        NSLog(@"%@: contentDisposition=%d", [self shortDescription], contentDisposition); 

    OBASSERT(originalTarget == nonretainedTarget || contentDisposition == OWTargetContentDisposition_ContentUpdatedOrTargetChanged);
    switch (contentDisposition) {
        case OWTargetContentDisposition_ContentAccepted:
            [isa target:originalTarget acceptedContentFromPipeline:self];
            break;
        case OWTargetContentDisposition_ContentRejectedSavePipeline:
            break;
        case OWTargetContentDisposition_ContentRejectedCancelPipeline:
            [self nullifyTarget];
            break;
        case OWTargetContentDisposition_ContentUpdatedOrTargetChanged:
            break;
    }
    [originalTarget release];

    [self deactivateIfPipelineHasNoProcessors];
}

- (BOOL)hasNoProcessors;
{
    BOOL noMoreProcessors;

    [processorArrayLock lock];
    noMoreProcessors = [processorArray count] == 0;
    [processorArrayLock unlock];
    return noMoreProcessors;
}

- (void)createNextProcessor;
{
    OWContentTypeLink *link;
    Class processorClass;
    BOOL shouldStartProcessors;

    link = [OWContentType linkForTargetContentType:targetContentType fromContentType:lastContentType orContentTypes:[self cachedContentTypesMinusProcessed]];
    if (!link) {
	[self pipelineBuilt];
	return;
    }
    [self readyContentType:[link sourceContentType]];

    if ([link targetContentType] == [OWContentType wildcardContentType])
        [self processedContentType:lastContentType];

    processorClass = [OFBundledClass classNamed:[link processorClassName]];

    [processorArrayLock lock];
    shouldStartProcessors = [unstartedProcessors count] == 0;
    [processorArrayLock unlock];

    [[[processorClass alloc] initWithPipeline:self] release];

    if (shouldStartProcessors)
	[self startUnstartedProcessors];
}


// Target stuff

- (void)notifyTargetOfTreeActivation;
{
    id <OWOptionalTarget> retainedTarget = [nonretainedTarget retain];
    NSNotification *activationNote;

    [self updateStatusOnTarget];

    activationNote = [NSNotification notificationWithName:OWPipelineTreeActivationNotificationName object:self];
    if ([retainedTarget respondsToSelector:@selector(pipelineTreeDidActivate:)])
        [retainedTarget pipelineTreeDidActivate:activationNote];
    [retainedTarget release];
    [[NSNotificationCenter defaultCenter] postNotification:activationNote];
}

- (void)notifyTargetOfTreeDeactivation;
{
    [self notifyTargetOfTreeDeactivation:nonretainedTarget];
}

- (void)notifyTargetOfTreeDeactivation:(id <OWTarget>)aTarget;
{
    id <OWTarget, OWOptionalTarget> retainedTarget = [aTarget retain];
    NSNotification *deactivationNote;

    [self updateStatusOnTarget:retainedTarget];

    deactivationNote = [NSNotification notificationWithName:OWPipelineTreeDeactivationNotificationName object:self];
    if ([retainedTarget respondsToSelector:@selector(pipelineTreeDidDeactivate:)])
        [retainedTarget pipelineTreeDidDeactivate:deactivationNote];
    [retainedTarget release];
    [[NSNotificationCenter defaultCenter] postNotification:deactivationNote];
}

- (void)updateStatusOnTarget:(id <OWTarget>)target;
{
    id <OWOptionalTarget, OWTarget> retainedTarget = [target retain];

    if ([retainedTarget respondsToSelector:@selector(updateStatusForPipeline:)]) {
        if (retainedTarget == (id)nonretainedTarget)
            [retainedTarget updateStatusForPipeline:self];
        else
            [retainedTarget updateStatusForPipeline:[isa lastActivePipelineForTarget:retainedTarget]];
    }
    [retainedTarget release];
}


- (void)rebuildCompositeTypeString;
{
    NSString *contentTypeString;

    OFSimpleLock(&displayablesSimpleLock);
    
    [compositeTypeString release];

    contentTypeString = [contentInfo typeString];
    if (!contentTypeString) {
        if ([nonretainedTarget respondsToSelector:@selector(expectedContentTypeString)])
            contentTypeString = [(id <OWOptionalTarget>)nonretainedTarget expectedContentTypeString];
        if (!contentTypeString && [lastAddress isKindOfClass:[OWAddress class]])
            contentTypeString = [[OWContentType contentTypeForFilename:[[(OWAddress *)lastAddress url] path]] readableString];
        if (!contentTypeString)
            contentTypeString = @"Unknown";
    }

    if (typeString)
        compositeTypeString = [[NSString alloc] initWithFormat:typeString, contentTypeString];
    else
        compositeTypeString = [contentTypeString retain];

    OFSimpleUnlock(&displayablesSimpleLock);
}

- (void)validateContentInfo;
{
    OWContentInfo *newContentInfo;

    newContentInfo = [lastContent contentInfo];
    if (contentInfo != newContentInfo) {
        OFSimpleLock(&displayablesSimpleLock);

        if (contentInfo) {
            [contentInfo removeTask:self];
            [contentInfo release];
        }
        contentInfo = [newContentInfo retain];
        [contentInfo setAddress:lastAddress];
        [contentInfo addTask:self];

        OFSimpleUnlock(&displayablesSimpleLock);

        [self rebuildCompositeTypeString];
    }
}

// Debugging

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

    debugDictionary = [super debugDictionary];

    if (nonretainedTarget)
        [debugDictionary setObject:OBShortObjectDescription(nonretainedTarget) forKey:@"target"];
    if (lastContent)
        [debugDictionary setObject:[(NSObject *)lastContent shortDescription] forKey:@"lastContent"];
    if (processorArray)
        [debugDictionary setObject:processorArray forKey:@"processorArray"];
    if (context)
        [debugDictionary setObject:context forKey:@"context"];

    return debugDictionary;
}

@end
