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

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

#import "OWContentTypeLink.h"
#import "OWConversionPathElement.h"
#import "OWUnknownDataStreamProcessor.h"

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OWF/Pipelines.subproj/OWContentType.m,v 1.16 1999/03/30 21:26:29 kc Exp $")

@interface OWContentType (Private)
+ (void)reloadExpirationTimeIntervals:(NSNotification *)notification;
+ (void)registerAliasesDictionary:(NSDictionary *)extensionsDictionary;
+ (void)registerExtensionsDictionary:(NSDictionary *)extensionsDictionary;
+ (void)registerIconsDictionary:(NSDictionary *)iconsDictionary;
+ (void)registerFlagsDictionary:(NSDictionary *)iconsDictionary;
- _initWithContentTypeString:(NSString *)aString;
- (void)_addReverseContentType:(OWContentType *)sourceContentType;
- (NSSet *)links;
@end

@implementation OWContentType

NSTimeInterval OWContentTypeNeverExpireTimeInterval = -1.0;
NSTimeInterval OWContentTypeExpireWhenFlushedTimeInterval = 1e+10;
NSString *OWContentTypeNeverExpireString = @"NeverExpire";
NSString *OWContentTypeExpireWhenFlushedString = @"ExpireWhenFlushed";
NSString *OWContentTypeReloadExpirationTimeIntervalsNotificationName = @"OWContentTypeReloadExpirationTimeIntervals";

static NSLock *contentTypeLock;
static NSMutableDictionary *contentTypeDictionary;
static NSMutableArray *contentEncodings;
static NSMutableDictionary *extensionToContentTypeDictionary;
static OWContentType *wildcardContentType;
static NSTimeInterval defaultExpirationTimeInterval = 0.0;
static NSZone *zone;

// This is a hack.
static NSString *privateSupertypes[] = {
    @"documenttitle", @"omniaddress", @"objectstream", @"omni", @"owftpdirectory", @"timestamp", @"url", @"gopher", nil
};

+ (void)didLoad;
{
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reloadExpirationTimeIntervals:) name:OFControllerDidInitNotification object:nil];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(reloadExpirationTimeIntervals:) name:OWContentTypeReloadExpirationTimeIntervalsNotificationName object:nil];
}

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

    [super initialize];

    if (initialized)
	return;

    initialized = YES;

    zone = NSCreateZone(NSPageSize(), NSPageSize(), YES);
    contentTypeLock = [[NSRecursiveLock allocWithZone:zone] init];
    contentTypeDictionary = [[NSMutableDictionary allocWithZone:zone] initWithCapacity:32];
    extensionToContentTypeDictionary = [[NSMutableDictionary allocWithZone:zone] initWithCapacity:32];
    contentEncodings = [[NSMutableArray alloc] initWithCapacity:5];

    wildcardContentType = [self contentTypeForString:@"*/*"];
}

// Bundle registration

+ (void)registerItemName:(NSString *)itemName bundle:(NSBundle *)bundle description:(NSDictionary *)description;
{
    if ([itemName isEqualToString:@"aliases"])
	[self registerAliasesDictionary:description];
    else if ([itemName isEqualToString:@"extensions"])
	[self registerExtensionsDictionary:description];
    else if ([itemName isEqualToString:@"icons"])
        [self registerIconsDictionary:description];
    else if ([itemName isEqualToString:@"guesses"])
	[OWUnknownDataStreamProcessor registerGuessesDictionary:description];
    else if ([itemName isEqualToString:@"flags"])
        [self registerFlagsDictionary:description];
}


// Defaults

+ (void)updateExpirationTimeIntervalsFromDefaults;
{
    NSDictionary *defaultsDictionary;
    NSEnumerator *contentTypeEnumerator;
    NSString *aContentTypeString;
    
    defaultsDictionary = [[OFUserDefaults sharedUserDefaults] dictionaryForKey:@"OWContentTypeExpirationTimeIntervals"];
    contentTypeEnumerator = [defaultsDictionary keyEnumerator];
    while ((aContentTypeString = [contentTypeEnumerator nextObject])) {
	OWContentType *contentType;
	NSString *expirationString;

	contentType = [self contentTypeForString:aContentTypeString];
	expirationString = [defaultsDictionary objectForKey:aContentTypeString];
	if ([expirationString isEqualToString:OWContentTypeNeverExpireString])
            [contentType setExpirationTimeInterval:OWContentTypeNeverExpireTimeInterval];
        else if ([expirationString isEqualToString:OWContentTypeExpireWhenFlushedString])
	    [contentType setExpirationTimeInterval:OWContentTypeExpireWhenFlushedTimeInterval];
	else
	    [contentType setExpirationTimeInterval:[expirationString floatValue]];
    }
}

+ (OWContentType *)contentTypeForString:(NSString *)aString;
{
    OWContentType *contentType;
    
    if (!aString)
	return nil;

    aString = [aString lowercaseString];

    [contentTypeLock lock];
    contentType = [contentTypeDictionary objectForKey:aString];
    if (!contentType) {
        contentType = [[self allocWithZone:zone] _initWithContentTypeString:aString];
	[contentTypeDictionary setObject:contentType forKey:aString];
        if ([contentType isEncoding])
            [contentEncodings addObject:contentType];
    }
    [contentTypeLock unlock];

    return contentType;
}

+ (OWContentType *)wildcardContentType;
{
    return wildcardContentType;
}

/* TODO: This does not contain aliases! */
/* NB: Eventually, when we support type wildcarding in some reasonable way, this method will be unneeded because encodings will be returned by -indirectSourceContentTypes. Other code will have to change to check for encodings amongthose types, however. */
+ (NSArray *)contentEncodings
{
    return contentEncodings;
}

+ (NSArray *)contentTypes
{
    return [contentTypeDictionary allValues];
}

+ (void)setDefaultExpirationTimeInterval:(NSTimeInterval)newTimeInterval;
{
    defaultExpirationTimeInterval = newTimeInterval;
}

+ (OWContentTypeLink *)linkForTargetContentType:(OWContentType *)targetContentType fromContentType:(OWContentType *)sourceContentType orContentTypes:(NSSet *)sourceTypes;
{
    NSEnumerator *sourceTypeEnumerator;
    OWContentType *aSourceType;
    NSMutableArray *pathElements;
    unsigned int uncheckedElementIndex;
    OWConversionPathElement *firstWildcardElement;
    NSMutableSet *checkedContentTypes;

    pathElements = [NSMutableArray arrayWithCapacity:16];
    checkedContentTypes = [[[NSMutableSet alloc] initWithSet:sourceTypes] autorelease];

    if (sourceContentType) {
        [checkedContentTypes addObject:sourceContentType];
        // Add sourceContentType first, so it'll be checked first.
        [pathElements addObject:[OWConversionPathElement elementWithParent:nil usingLink:nil targetContentType:sourceContentType]];
    }
    
    sourceTypeEnumerator = [sourceTypes objectEnumerator];
    while ((aSourceType = [sourceTypeEnumerator nextObject]))
        [pathElements addObject:[OWConversionPathElement elementWithParent:nil usingLink:nil targetContentType:aSourceType]];
    
    firstWildcardElement = nil;
    for (uncheckedElementIndex = 0; uncheckedElementIndex < [pathElements count]; uncheckedElementIndex++) {
	OWConversionPathElement *currentElement;
	OWContentType *candidateContentType;
	NSEnumerator *linkEnumerator;
	OWContentTypeLink *aLink;

	currentElement = [pathElements objectAtIndex:uncheckedElementIndex];
	candidateContentType = [currentElement targetContentType];
	if (candidateContentType == targetContentType)
	    return [currentElement rootLink];
	if (!firstWildcardElement && candidateContentType == wildcardContentType)
	    firstWildcardElement = currentElement;
	linkEnumerator = [[candidateContentType links] objectEnumerator];
	while ((aLink = [linkEnumerator nextObject])) {
	    OWContentType *linkContentType;

	    linkContentType = [aLink targetContentType];
	    if ([checkedContentTypes containsObject:linkContentType])
		continue; // No looping from html->sgmlObjects->html->...
	    [checkedContentTypes addObject:linkContentType];
	    [pathElements addObject:[OWConversionPathElement elementWithParent:currentElement usingLink:aLink targetContentType:linkContentType]];
	}
    }

    return [firstWildcardElement rootLink];
}

+ (OWContentType *)contentTypeForExtension:(NSString *)extension;
{
    OWContentType            *contentType;

    if (!extension)
	return nil;

    [contentTypeLock lock];
    contentType = [extensionToContentTypeDictionary objectForKey:
		   [extension lowercaseString]];
    [contentTypeLock unlock];
    return contentType;
}

+ (OWContentType *)contentTypeForFilename:(NSString *)filename;
{
    OWContentType		*contentType;
    
    contentType = [self contentTypeForExtension:[filename pathExtension]];
    return contentType ? contentType : [OWUnknownDataStreamProcessor unknownContentType];
}



// Instance methods

// Make sure no shenanigans go on

- (id)copyWithZone:(NSZone *)zone
{
    return self;
}

- (id)retain;
{
    return self;
}

- (id)autorelease;
{
    return self;
}

- (void)release;
{
}

- (void)dealloc;
{
}

//

- (void)setExtensions:(NSArray *)someExtensions;
{
    if (extensions == someExtensions)
	return;
    [extensions release];
    extensions = [someExtensions copyWithZone:zone];
}

- (NSArray *)extensions;
{
    return extensions;
}

- (NSString *)primaryExtension;
{
    if ([extensions count])
	return [extensions objectAtIndex:0];
    return nil;
}

- (void)setImageName:(NSString *)newImageName;
{
    // This normally only is called once per instance, and only from +registerIconsDictionary:
    if (imageName != newImageName) {
        NSString *oldImageName;

        oldImageName = imageName;
        imageName = [newImageName copyWithZone:zone];
        [oldImageName release];
    }
}

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

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

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

- (BOOL)isEncoding;
{
    return isEncoding;
}

- (BOOL)isPublic;
{
    return isPublic;
}

- (BOOL)isInteresting;
{
    return isInteresting;
}

// NSObject subclass and protocol

- (unsigned int)hash;
{
    return hash;
}

//

- (void)linkToContentType:(OWContentType *)targetContentType usingProcessorClassName:(NSString *)aProcessorClassName cost:(float)aCost;
{
    OWContentTypeLink *link;
    NSEnumerator *linkEnumerator;

    linkEnumerator = [links objectEnumerator];
    while ((link = [linkEnumerator nextObject])) {
	if ([link targetContentType] == targetContentType) {
            if ([link cost] < aCost) {
                // Keep existing, cheaper link instead.
                return;
            }
            [links removeObject:link];
            break;
        }
    }
    link = [[OWContentTypeLink allocWithZone:zone] initWithProcessorClassName:aProcessorClassName sourceContentType:self targetContentType:targetContentType cost:aCost];
    [links addObject:link];
    [link release];
    [targetContentType _addReverseContentType:self];
}

- (NSSet *)directSourceContentTypes
{
    return reverseLinks;
}

- (NSSet *)indirectSourceContentTypes
{
    NSMutableSet *indirectSources;
    NSMutableArray *targets;
    OWContentType *target, *source;
    NSEnumerator *sourceEnumerator;
    
    indirectSources = [[NSMutableSet alloc] init];
    targets = [[NSMutableArray alloc] initWithCapacity:5];
    
    [targets addObject:self];
    
    while ([targets count]) {
        target = [targets objectAtIndex:0];
	sourceEnumerator = [[target directSourceContentTypes] objectEnumerator];
	[targets removeObjectAtIndex:0];
	
	while ((source = [sourceEnumerator nextObject])) {
	    if (![indirectSources containsObject:source]) {
		[indirectSources addObject:source];
		[targets addObject:source];
	    }
	}
    }
    
    [targets release];
    
    return [indirectSources autorelease];
}

- (NSTimeInterval)expirationTimeInterval;
{
    return expirationTimeInterval;
}

- (void)setExpirationTimeInterval:(NSTimeInterval)newTimeInterval;
{
    expirationTimeInterval = newTimeInterval;
}


//

- (NSString *)pathForEncoding:(OWContentType *)contentEncoding givenOriginalPath:(NSString *)aPath;
{
    NSString *fileTypeExtension, *fileEncodingExtension;
    NSString *desiredTypeExtension;
    NSString *desiredEncodingExtension;
    OWContentType *fileContentType, *fileContentEncoding;

    desiredTypeExtension = [self primaryExtension];
    desiredEncodingExtension = [contentEncoding primaryExtension];
    
    fileTypeExtension = [aPath pathExtension];
    fileEncodingExtension = nil;

    fileContentType = [OWContentType contentTypeForExtension:fileTypeExtension];
    fileContentEncoding = nil;
    if (fileContentType) {
	aPath = [aPath stringByDeletingPathExtension];
	
	if ([fileContentType isEncoding]) {
	    fileContentEncoding = fileContentType;
	    fileEncodingExtension = fileTypeExtension;
    
	    fileTypeExtension = [aPath pathExtension];
	    fileContentType = [OWContentType contentTypeForExtension:fileTypeExtension];
	    if (fileContentType)
		aPath = [aPath stringByDeletingPathExtension];
	    else
		fileTypeExtension = nil;
	}
    } else
	fileTypeExtension = nil;

#warning WJS: Put in a preference for using preferred extensions over original extensions
    // if file's type is same as datastream's type, use preferred extension
    if (fileContentType == self && desiredTypeExtension) {
        aPath = [aPath stringByAppendingPathExtension:desiredTypeExtension];

    // if datastream's type is different from file's extension type, and datastream's type specifies an extension, append datastream type extension but leave file type extension in front of it
    } else if (desiredTypeExtension && ![desiredTypeExtension isEqualToString:fileTypeExtension] && self != [OWUnknownDataStreamProcessor unknownContentType]) {
        if (fileTypeExtension)
	    aPath = [aPath stringByAppendingPathExtension:fileTypeExtension];
        aPath = [aPath stringByAppendingPathExtension:desiredTypeExtension];

    // else, put old type extension back
    } else if (fileTypeExtension)
	aPath = [aPath stringByAppendingPathExtension:fileTypeExtension];


    // Same for encoding
    if (fileContentEncoding == contentEncoding && desiredEncodingExtension) {
        aPath = [aPath stringByAppendingPathExtension:desiredEncodingExtension];
    } else if (desiredEncodingExtension && ![desiredEncodingExtension isEqualToString:fileEncodingExtension]) {
        if (fileEncodingExtension)
            aPath = [aPath stringByAppendingPathExtension:fileEncodingExtension];
        aPath = [aPath stringByAppendingPathExtension:desiredEncodingExtension];
    } else if (fileEncodingExtension)
        aPath = [aPath stringByAppendingPathExtension:fileEncodingExtension];

    return aPath;
}


// Debugging (OBObject subclass)

- (NSString *)shortDescription;
{
    return contentTypeString;
}

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

    debugDictionary = [super debugDictionary];

    [debugDictionary setObject:contentTypeString forKey:@"contentType"];
    [debugDictionary setObject:[links allObjects] forKey:@"links"];
    [debugDictionary setObject:[NSString stringWithFormat:@"%g", expirationTimeInterval] forKey:@"expirationTimeInterval"];
    if (!isPublic)
        [debugDictionary setBoolValue:NO forKey:@"isPublic"];
    [debugDictionary setBoolValue:isInteresting forKey:@"isInteresting"];

    return debugDictionary;
}

@end

@implementation OWContentType (Private)

+ (void)reloadExpirationTimeIntervals:(NSNotification *)notification;
{
    [self updateExpirationTimeIntervalsFromDefaults];
}

+ (void)registerAliasesDictionary:(NSDictionary *)aliasesDictionary;
{
    NSEnumerator               *contentTypeEnumerator;
    NSString                   *aContentTypeString;
    NSEnumerator               *aliasesEnumerator;

    contentTypeEnumerator = [aliasesDictionary keyEnumerator];
    aliasesEnumerator = [aliasesDictionary objectEnumerator];

    [contentTypeLock lock];
    while ((aContentTypeString = [contentTypeEnumerator nextObject])) {
	id		 aliasesObject;
	NSArray		*aliasesArray;
	OWContentType   *contentType;
	NSEnumerator    *aliasEnumerator;
	NSString	*alias;
	
	aliasesObject = [aliasesEnumerator nextObject];
	if ([aliasesObject isKindOfClass:[NSArray class]])
	    aliasesArray = aliasesObject;
	else if ([aliasesObject isKindOfClass:[NSString class]])
	    aliasesArray = [NSArray arrayWithObject:aliasesObject];
	else
	    break;
	
	contentType = [self contentTypeForString:aContentTypeString];
	aliasEnumerator = [aliasesArray objectEnumerator];
        while ((alias = [aliasEnumerator nextObject])) {
            NSString *key = [[alias lowercaseString] copyWithZone:zone];
	    [contentTypeDictionary setObject:contentType forKey:key];
            [key release];
        }
    }
    [contentTypeLock unlock];
}

+ (void)registerExtensionsDictionary:(NSDictionary *)extensionsDictionary;
{
    NSEnumerator *contentTypeEnumerator;
    NSString *aContentTypeString;
    NSEnumerator *extensionsEnumerator;

    contentTypeEnumerator = [extensionsDictionary keyEnumerator];
    extensionsEnumerator = [extensionsDictionary objectEnumerator];

    [contentTypeLock lock];
    while ((aContentTypeString = [contentTypeEnumerator nextObject])) {
	id extensionsObject;
	NSArray *extensionsArray;
	OWContentType *contentType;
	NSEnumerator *extensionEnumerator;
	NSString *extension;
	
	extensionsObject = [extensionsEnumerator nextObject];
	if ([extensionsObject isKindOfClass:[NSArray class]])
	    extensionsArray = extensionsObject;
	else if ([extensionsObject isKindOfClass:[NSString class]])
	    extensionsArray = [NSArray arrayWithObject:extensionsObject];
	else
	    break;
	
	contentType = [self contentTypeForString:aContentTypeString];
	[contentType setExtensions:extensionsArray];
	
	extensionEnumerator = [extensionsArray objectEnumerator];
        while ((extension = [extensionEnumerator nextObject])) {
            NSString *key = [[extension lowercaseString] copyWithZone:zone];
	    [extensionToContentTypeDictionary setObject:contentType
                forKey:key];
            [key release];
        }
    }
    [contentTypeLock unlock];
}

+ (void)registerIconsDictionary:(NSDictionary *)iconsDictionary;
{
    NSEnumerator *contentTypeEnumerator;
    NSString *aContentTypeString;
    NSEnumerator *imageNamesEnumerator;

    contentTypeEnumerator = [iconsDictionary keyEnumerator];
    imageNamesEnumerator = [iconsDictionary objectEnumerator];

    while ((aContentTypeString = [contentTypeEnumerator nextObject])) {
        NSString *imageNameString;
        OWContentType *contentType;

        imageNameString = [imageNamesEnumerator nextObject];
        if ([imageNameString zone] != zone)
            imageNameString = [[imageNameString copyWithZone:zone] autorelease];
        contentType = [self contentTypeForString:aContentTypeString];
        [contentType setImageName:imageNameString];
    }
}

+ (void)registerFlagsDictionary:(NSDictionary *)iconsDictionary;
{
    NSEnumerator *contentTypeEnumerator;
    NSString *aContentTypeString;

    contentTypeEnumerator = [iconsDictionary keyEnumerator];

    while ((aContentTypeString = [contentTypeEnumerator nextObject])) {
        id flags = [iconsDictionary objectForKey:aContentTypeString];
        NSString *flag;
        OWContentType *contentType;
        NSEnumerator *flagEnumerator;

        contentType = [self contentTypeForString:aContentTypeString];

        if ([flags isKindOfClass:[NSArray class]]) {
            flagEnumerator = [flags objectEnumerator];
            flag = [flagEnumerator nextObject];
        } else {
            flagEnumerator = nil;
            flag = flags;
        }

        for( ; flag; flag = [flagEnumerator nextObject]) {
            if ([flag isEqualToString:@"boring"])
                contentType->isInteresting = NO;
            else if([flag isEqualToString:@"private"])
                contentType->isPublic = NO;
        }
    }
}
        

- _initWithContentTypeString:(NSString *)aString;
{
    unsigned int privateTypeIndex;

    if (![super init])
	return nil;

    contentTypeString = [aString copyWithZone:zone];
    hash = [contentTypeString hash];
    links = [[NSMutableSet allocWithZone:zone] init];
    reverseLinks = nil;
    extensions = nil;
    expirationTimeInterval = defaultExpirationTimeInterval;

    isEncoding = [contentTypeString hasPrefix:@"encoding/"];
    isPublic = YES;
    isInteresting = YES;

    for (privateTypeIndex = 0; privateSupertypes[privateTypeIndex]; privateTypeIndex++) {
	if ([contentTypeString hasPrefix:privateSupertypes[privateTypeIndex]]) {
	    isPublic = NO;
	    break;
	}
    }

    if ([contentTypeString hasPrefix:@"image/"])
        readableString = @"Image";
    else if ([contentTypeString isEqualToString:@"text/html"])
        readableString = @"HTML";
    else {
        NSRange range;

        range = [contentTypeString rangeOfString:@"/"];
        if (range.location != NSNotFound)
            readableString = [[[contentTypeString substringFromIndex:NSMaxRange(range)] capitalizedString] copyWithZone:zone];
        else
            readableString = nil;
    }
    
    return self;
}

- (void)_addReverseContentType:(OWContentType *)sourceContentType;
{
    if (!reverseLinks)
        reverseLinks = [[NSMutableSet allocWithZone:zone] initWithCapacity:5];

    [reverseLinks addObject:sourceContentType];
}

- (NSSet *)links;
{
    return links;
}

@end
