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

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

#import "OWContentCache.h"
#import "OWContentType.h"
#import "OWDocumentTitle.h"
#import "OWPipeline.h"
#import "OWProxyServer.h"
#import "OWTimeStamp.h"
#import "OWURL.h"

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OWF/Content.subproj/Address.subproj/OWAddress.m,v 1.17 1999/07/16 02:49:42 kc Exp $")

@implementation OWAddress

static NSCharacterSet *nonShortcutCharacterSet;
static unsigned int uniqueKeyCount;
static NSLock *uniqueKeyCountLock;
static NSMutableDictionary *lowercaseEffectNameDictionary;

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

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

    // Note: If this changes, it should also be changed in OmniWeb's OWShortcutPreferences.m since it has no way of getting at it.  (Perhaps it should be a default.)  Ugly, but for now we're maintaining this character set in two places.
    nonShortcutCharacterSet = [[NSCharacterSet characterSetWithCharactersInString:@"./:"] retain];

    uniqueKeyCount = 0;
    uniqueKeyCountLock = [[NSLock alloc] init];

    lowercaseEffectNameDictionary = [[NSMutableDictionary alloc] initWithCapacity:8];

    // Must be lowercase
    [lowercaseEffectNameDictionary setObject:[NSNumber numberWithInt:OWAddressEffectFollowInWindow] forKey:@"followinwindow"];
    [lowercaseEffectNameDictionary setObject:[NSNumber numberWithInt:OWAddressEffectNewBrowserWindow] forKey:@"newbrowserwindow"];
    [lowercaseEffectNameDictionary setObject:[NSNumber numberWithInt:OWAddressEffectOpenBookmarksWindow] forKey:@"openbookmarkswindow"];
    [lowercaseEffectNameDictionary setObject:[NSNumber numberWithInt:OWAddressEffectOpenSearchWindow] forKey:@"opensearchwindow"];

    // Old effect names, for backward compatibility with OmniWeb 2
    [lowercaseEffectNameDictionary setObject:[NSNumber numberWithInt:OWAddressEffectFollowInWindow] forKey:@"follow"];
    [lowercaseEffectNameDictionary setObject:[NSNumber numberWithInt:OWAddressEffectNewBrowserWindow] forKey:@"x-popup"];
    [lowercaseEffectNameDictionary setObject:[NSNumber numberWithInt:OWAddressEffectOpenBookmarksWindow] forKey:@"x-as-list"];
    [lowercaseEffectNameDictionary setObject:[NSNumber numberWithInt:OWAddressEffectOpenSearchWindow] forKey:@"x-as-search"];
}
 
+ (NSString *)stringForEffect:(OWAddressEffect)anEffect;
{
    switch (anEffect) {
    case OWAddressEffectFollowInWindow:
	return @"FollowInWindow";
    case OWAddressEffectNewBrowserWindow:
	return @"NewBrowserWindow";
    case OWAddressEffectOpenBookmarksWindow:
	return @"OpenBookmarksWindow";
    case OWAddressEffectOpenSearchWindow:
	return @"OpenSearchWindow";
    }
    return nil;
}

+ (OWAddressEffect)effectForString:(NSString *)anEffectString;
{
    OWAddressEffect newEffect = OWAddressEffectFollowInWindow;

    if (anEffectString && ![(id)anEffectString isNull]) {
        NSNumber *effectNumber;

        effectNumber = [lowercaseEffectNameDictionary objectForKey:[anEffectString lowercaseString]];
        if (effectNumber)
            newEffect = [effectNumber intValue];
    }
    return newEffect;
}

static inline OWAddress *
addressForShortcut(NSString *originalString)
{
    NSString *string;
    NSDictionary *shortcutDictionary = nil;
    NSString *shortcutFormat;

    string = originalString;
    while ([string rangeOfCharacterFromSet:nonShortcutCharacterSet].location == NSNotFound) {
        if (!shortcutDictionary)
            shortcutDictionary = [[OFUserDefaults sharedUserDefaults] dictionaryForKey:@"OWAddressShortcuts"];
        if ((shortcutFormat = [shortcutDictionary objectForKey:string])) {
            // Use the matching shortcut
            string = [NSString stringWithFormat:shortcutFormat, string];
        } else {
            if ((shortcutFormat = [shortcutDictionary objectForKey:@"*"])) {
                // Use the default shortcut
                string = [NSString stringWithFormat:shortcutFormat, string];
            }
            break;
        }
    }
    if (string == originalString)
        return nil;
    return [OWAddress addressWithURL:[OWURL urlFromDirtyString:string]];
}

static inline OWAddress *
addressForObviousHostname(NSString *string)
{
    NSString *scheme;

    scheme = nil;
    if ([string hasPrefix:@"http."] || [string hasPrefix:@"www."]  || [string hasPrefix:@"home."])
        scheme = @"http://";
    else if ([string hasPrefix:@"gopher."])
        scheme = @"gopher://";
    else if ([string hasPrefix:@"ftp."])
        scheme = @"ftp://";

    if (scheme)
        return [OWAddress addressWithURL:[OWURL urlFromDirtyString:[scheme stringByAppendingString:string]]];
    else
        return nil;
}

static inline OWAddress *
addressForNotSoObviousHostname(NSString *string)
{
    NSRange rangeOfSlash;
    OWAddress *address;
    
    if ([string rangeOfString:@":"].location != NSNotFound)
        return nil;

    address = addressForObviousHostname(string);
    if (address)
        return address;

    // "omnigroup/products" --> "http://www.omnigroup.com/products"
    rangeOfSlash = [string rangeOfString:@"/"];
    if (rangeOfSlash.location != NSNotFound) {
        NSString *host;

        host = [string substringToIndex:rangeOfSlash.location];
        if ([host rangeOfString:@"."].location == NSNotFound) {
            address = addressForShortcut(host);
            if (address) {
                NSString *addressString;
                NSString *additionalPath;

                addressString = [address addressString];
                if (![addressString hasSuffix:@"/"])
                    addressString = [addressString stringByAppendingString:@"/"];
                additionalPath = [string substringFromIndex:NSMaxRange(rangeOfSlash)];
                return [OWAddress addressWithURL:[OWURL urlFromDirtyString:[addressString stringByAppendingString:additionalPath]]];
            }
        }
    }
    return [OWAddress addressWithURL:[OWURL urlFromDirtyString:[@"http://" stringByAppendingString:string]]];
}

+ (OWAddress *)addressWithURL:(OWURL *)aURL target:(NSString *)aTarget methodString:(NSString *)aMethodString methodDictionary:(NSDictionary *)aMethodDictionary effect:(OWAddressEffect)anEffect forceAlwaysUnique:(BOOL)shouldForceAlwaysUnique;
{
    if (!aURL)
	return nil;
    return [[[self alloc] initWithURL:aURL target:aTarget methodString:aMethodString methodDictionary:aMethodDictionary effect:anEffect forceAlwaysUnique:shouldForceAlwaysUnique] autorelease];
}

+ (OWAddress *)addressWithURL:(OWURL *)aURL target:(NSString *)aTarget effect:(OWAddressEffect)anEffect;
{
    if (!aURL)
	return nil;
    return [[[self alloc] initWithURL:aURL target:aTarget effect:anEffect] autorelease];
}

+ (OWAddress *)addressWithURL:(OWURL *)aURL;
{
    if (!aURL)
	return nil;
    return [[[self alloc] initWithURL:aURL target:nil effect:OWAddressEffectFollowInWindow] autorelease];
}

+ (OWAddress *)addressForString:(NSString *)anAddressString;
{
    if (!anAddressString)
	return nil;
    return [self addressWithURL:[OWURL urlFromString:anAddressString]];
}

+ (OWAddress *)addressForDirtyString:(NSString *)anAddressString;
{
    OWAddress *address;

    if (!anAddressString || [anAddressString length] == 0)
	return nil;
	
    // Did user enter a shortcut?  If so, use it.
    if ((address = addressForShortcut(anAddressString)))
         return address;

    // Did user type something without any ":"?  If so, prefix with "http://%@"
    if ((address = addressForNotSoObviousHostname(anAddressString)))
         return address;
         
    if ((address = [self addressWithURL:[OWURL urlFromDirtyString:anAddressString]]))
        return address;

    return [OWAddress addressWithURL:[OWURL urlFromDirtyString:[@"http://" stringByAppendingString:anAddressString]]];
}

+ (OWAddress *)addressWithFilename:(NSString *)filename;
{
    if (!filename)
	return nil;
    if ([filename hasPrefix:@"/"])
	filename = [filename substringFromIndex:1];
    return [self addressWithURL:[OWURL urlWithScheme:@"file" netLocation:nil path:filename params:nil query:nil fragment:nil]];
}

//

- initWithURL:(OWURL *)aURL target:(NSString *)aTarget methodString:(NSString *)aMethodString methodDictionary:(NSDictionary *)aMethodDictionary effect:(OWAddressEffect)anEffect forceAlwaysUnique:(BOOL)shouldForceAlwaysUnique;
{
    if (![super init])
	return nil;

    url = [aURL retain];
    target = [aTarget retain];
    methodString = [aMethodString ? aMethodString : @"GET" retain];
    methodDictionary = [aMethodDictionary retain];
    effect = anEffect;
    forceAlwaysUnique = shouldForceAlwaysUnique;

    return self;
}

- initWithURL:(OWURL *)aURL target:(NSString *)aTarget effect:(OWAddressEffect)anEffect;
{
    return [self initWithURL:aURL target:aTarget methodString:nil methodDictionary:nil effect:anEffect forceAlwaysUnique:NO];
}

- initWithURL:(OWURL *)aURL;
{
    return [self initWithURL:aURL target:nil methodString:nil methodDictionary:nil effect:OWAddressEffectFollowInWindow forceAlwaysUnique:NO];
}

- (void)dealloc;
{
    [url release];
    [target release];
    [methodString release];
    [methodDictionary release];
    [cacheKey release];
    [pasteboardFilename release];
    [super dealloc];
}

// Queries

- (OWURL *)url;
{
    return url;
}

- (OWURL *)proxyURL;
{
    return [OWProxyServer proxyURLForURL:url];
}

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

- (NSDictionary *)methodDictionary;
{
    return methodDictionary;
}

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

- (NSString *)localFilename;
{
    NSString *scheme;
    NSString *path;

    scheme = [url scheme];
    if ([scheme isEqualToString:@"file"]) {
	path = [url path];
	if (!path)
	    path = [url schemeSpecificPart];
	path = [OWURL decodeURLString:path];
	if ([path hasPrefix:@"~"])
	    return path;
#ifdef WIN32
        return path; // file:/C:/tmp -> C:/tmp
#else
        return [@"/" stringByAppendingString:path]; // file:/tmp -> /tmp
#endif
    }
    return nil;
}

- (NSString *)addressString;
{
    return [url compositeString];
}

- (NSString *)stringValue;
{
    return [url compositeString];
}

// Effects

- (OWAddressEffect)effect;
{
    return effect;
}

- (NSString *)effectString;
{
    return [OWAddress stringForEffect:effect];
}

// Displaying an address

- (NSString *)drawLabel;
{
    return [url compositeString];
}

- (BOOL)isVisited;
{
    return [OWContentCache lookupContentCacheForAddress:self] != nil;
}

- (BOOL)isSecure;
{
    return [url isSecure];
}

// Equality and hashing

- (unsigned)hash;
{
    return [url hash];
}

// Exactly the same URL
- (BOOL)isEqual:(id)anObject;
{
    OWAddress *otherAddress;

    if (self == anObject)
	return YES;
    if (anObject == nil)
        return NO;
    otherAddress = anObject;
    if (otherAddress->isa != isa)
	return NO;
    if (effect != otherAddress->effect)
	return NO;
#warning TODO: why not compare target also?
    if (![url isEqual:otherAddress->url])
	return NO;
    if (![methodString isEqualToString:otherAddress->methodString])
	return NO;
    if (methodDictionary != otherAddress->methodDictionary && ![methodDictionary isEqual:otherAddress->methodDictionary])
	return NO;
    return YES;
}

// Not the same URL, but will fetch the same data. For example, if two URLs could differ only by the fragment, which would mean they have the same document.
- (BOOL)isSameDocumentAsAddress:(OWAddress *)otherAddress;
{
    if (!otherAddress)
        return NO;
    if (self == otherAddress || (self->cacheKey && (self->cacheKey == otherAddress->cacheKey)))
	return YES;
    return [[self cacheKey] isEqualToString:[otherAddress cacheKey]];
}

- (BOOL)representsFile;
{
    return [url path] ? YES : NO;
}

// OWContent protocol

- (OWContentType *)contentType;
{
    return [[self proxyURL] contentType];
}

- (OWContentInfo *)contentInfo;
{
    return nil;
}

- (unsigned long int)cacheSize;
{
    return 100; // Perhaps we should do something less arbitrary?
}

- (BOOL)shareable;
{
    return YES;
}


// OWAddress protocol

- (NSString *)cacheKey;
{
    if (cacheKey)
	return cacheKey;
	
    if (![self isAlwaysUnique]) {
	cacheKey = [[url cacheKey] retain];
	return cacheKey;
    }
    [uniqueKeyCountLock lock];
    cacheKey = [[NSString alloc] initWithFormat:@"%d", uniqueKeyCount++];
    [uniqueKeyCountLock unlock];
    return cacheKey;
}

- (NSString *)shortDisplayString;
{
    return [url shortDisplayString];
}

- (NSString *)bestKnownTitle;
{
    NSString *bestKnownTitle;

    bestKnownTitle = [OWDocumentTitle titleForAddress:self];
    if (bestKnownTitle)
	return bestKnownTitle;
    return [self shortDisplayString];
}

- (BOOL)isAlwaysUnique;
{
    if (forceAlwaysUnique)
	return YES;
    if (methodDictionary)
	return YES;
    return NO;
}

// Getting related addresses

// If you don't use the method with pipeline, below, then #fragments won't properly be based off of the current document in the cache.  Not always a problem, but be aware of it. 
- (OWAddress *)addressForRelativeString:(NSString *)relativeAddressString;
{
    return [self addressForRelativeString:relativeAddressString inPipeline:nil target:nil effect:OWAddressEffectFollowInWindow];
}

- (OWAddress *)addressForRelativeString:(NSString *)relativeAddressString target:(NSString *)aTarget effect:(OWAddressEffect)anEffect;
{
    return [self addressForRelativeString:relativeAddressString inPipeline:nil target:aTarget effect:anEffect];
}

- (OWAddress *)addressForRelativeString:(NSString *)relativeAddressString inPipeline:(OWPipeline *)pipeline target:(NSString *)aTarget effect:(OWAddressEffect)anEffect;
{
    if (![relativeAddressString hasPrefix:@"#"]) {
        // If it's not a fragment, life is easy.
        return [OWAddress addressWithURL:[url urlFromRelativeString:relativeAddressString] target:aTarget methodString:nil methodDictionary:nil effect:anEffect forceAlwaysUnique:NO];
    } else {
        OWAddress *relativeAddress;

        // If we're given a fragment, it should be based off of the pipeline's address, not ourself, because we may be the document's <base href=""> address, and fragments are a special case.
        if (pipeline) {
            OWAddress *pipelineAddress;

            pipelineAddress = [pipeline lastAddress];
            if (!pipelineAddress)
                pipelineAddress = [pipeline contextObjectForKey:@"HistoryAddress"];
            if (pipelineAddress)
                return [pipelineAddress addressForRelativeString:relativeAddressString inPipeline:nil target:aTarget effect:anEffect];
        }

        relativeAddress = [OWAddress addressWithURL:[url urlFromRelativeString:relativeAddressString] target:aTarget methodString:nil methodDictionary:nil effect:anEffect forceAlwaysUnique:NO];
        // Force the new address to use the exact same document in the cache as we use, EVEN if we are unique (eg, the result of a FORM or some such).  The advantage to this is that image maps in form results that use "#mapname" will not force a second fetch, which will cause a form to post twice.  Also, relative links in form documents don't cause a refetch.
        relativeAddress->cacheKey = [[self cacheKey] retain];

        return relativeAddress;
    }
}

//

- (OWAddress *)addressForDirtyRelativeString:(NSString *)relativeAddressString;
{
    OWAddress *address;

    if (!relativeAddressString || [relativeAddressString length] == 0)
	return self;

    address = addressForObviousHostname(relativeAddressString);
    if (address)
	return address;
	
    address = [OWAddress addressWithURL:[url urlFromRelativeString:relativeAddressString]];
    if (address)
        return address;

    return [OWAddress addressWithURL:[OWURL urlFromDirtyString:[@"http://" stringByAppendingString:relativeAddressString]]];
}

//

- (OWAddress *)addressWithGetQuery:(NSString *)query;
{
    return [OWAddress addressWithURL:[url urlForQuery:query] target:target methodString:nil methodDictionary:nil effect:OWAddressEffectFollowInWindow forceAlwaysUnique:YES];
}

- (OWAddress *)addressWithPath:(NSString *)aPath;
{
    return [OWAddress addressWithURL:[url urlForPath:aPath] target:target methodString:methodString methodDictionary:methodDictionary effect:effect forceAlwaysUnique:forceAlwaysUnique];
}

- (OWAddress *)addressWithMethodString:(NSString *)newMethodString;
{
    if (methodString == newMethodString)
	return self;
    return [OWAddress addressWithURL:url target:target methodString:newMethodString methodDictionary:nil effect:effect forceAlwaysUnique:forceAlwaysUnique];
}

- (OWAddress *)addressWithMethodString:(NSString *)newMethodString
  methodDictionary:(NSDictionary *)newMethodDictionary
  forceAlwaysUnique:(BOOL)shouldForceAlwaysUnique;
{
    return [OWAddress addressWithURL:url target:target methodString:newMethodString methodDictionary:newMethodDictionary effect:effect forceAlwaysUnique:shouldForceAlwaysUnique];
}

- (OWAddress *)addressWithTarget:(NSString *)newTarget;
{
    if (target == newTarget)
	return self;
    return [OWAddress addressWithURL:url target:newTarget methodString:methodString methodDictionary:methodDictionary effect:effect forceAlwaysUnique:forceAlwaysUnique];
}

- (OWAddress *)addressWithEffect:(OWAddressEffect)newEffect;
{
    if (effect == newEffect)
	return self;
    return [OWAddress addressWithURL:url target:target methodString:methodString methodDictionary:methodDictionary effect:newEffect forceAlwaysUnique:forceAlwaysUnique];
}

- (OWAddress *)addressWithForceAlwaysUnique:(BOOL)shouldForceAlwaysUnique;
{
    if (forceAlwaysUnique == shouldForceAlwaysUnique)
	return self;
    return [OWAddress addressWithURL:url target:target methodString:methodString methodDictionary:methodDictionary effect:effect forceAlwaysUnique:shouldForceAlwaysUnique];
}

- (OWAddress *)addressWithoutFragment;
{
    OWURL *urlWithoutFragment;

    urlWithoutFragment = [url urlWithoutFragment];
    if (url == urlWithoutFragment)
	return self;
    return [OWAddress addressWithURL:urlWithoutFragment target:target methodString:methodString methodDictionary:methodDictionary effect:effect forceAlwaysUnique:forceAlwaysUnique];
}


// NSCopying protocol

- (id)copyWithZone:(NSZone *)zone
{
    OWURL *newURL;
    NSString *newTarget, *newMethodString;
    NSDictionary *newMethodDictionary;
    OWAddress *newAddress;

    if (NSShouldRetainWithZone(self, zone))
        return [self retain];

    newURL = [url copyWithZone:zone];
    newTarget = [target copyWithZone:zone];
    newMethodString = [methodString copyWithZone:zone];
    newMethodDictionary = [methodDictionary copyWithZone:zone];
        
    newAddress = [[isa allocWithZone:zone] initWithURL:newURL target:newTarget methodString:newMethodString methodDictionary:newMethodDictionary effect:effect forceAlwaysUnique:forceAlwaysUnique];

    [newURL release];
    [newTarget release];
    [newMethodString release];
    [newMethodDictionary release];
    
    return newAddress;
}

// Debugging

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

    debugDictionary = [super debugDictionary];
    if (url)
	[debugDictionary setObject:url forKey:@"url"];
    if (target)
	[debugDictionary setObject:target forKey:@"target"];
    if (methodString)
	[debugDictionary setObject:methodString forKey:@"methodString"];
    if (methodDictionary)
	[debugDictionary setObject:methodDictionary forKey:@"methodDictionary"];
    [debugDictionary setObject:[OWAddress stringForEffect:effect] forKey:@"effect"];
    [debugDictionary setObject:forceAlwaysUnique ? @"YES" : @"NO" forKey:@"forceAlwaysUnique"];

    return debugDictionary;
}

- (NSString *)shortDescription;
{
    return [[self url] shortDescription];
}

@end
