//
//  MacPADSocket.m
//  MacPAD Version Check
//
//  Created by Kevin Ballard on Sun Dec 07 2003.
//  Copyright (c) 2003-2004 TildeSoft. All rights reserved.
//

#import "MacPADSocket.h"

// Constant strings
NSString *MacPADErrorCode = @"MacPADErrorCode";
NSString *MacPADErrorMessage = @"MacPADErrorMessage";
NSString *MacPADNewVersionAvailable = @"MacPADNewVersionAvailable";

// NSNotifications
NSString *MacPADErrorOccurredNotification = @"MacPADErrorOccurredNotification";
NSString *MacPADCheckFinishedNotification = @"MacPADCheckFinishedNotification";

enum {
    kNumberType,
    kStringType,
    kPeriodType
};

@implementation MacPADSocket

// Code
- (id)init
{
    if (self = [super init]) {
        _fileHandle = nil;
        _fileURL = nil;
        _currentVersion = nil;
        _newVersion = nil;
        _releaseNotes = nil;
        _productPageURL = nil;
        _productDownloadURLs = nil;
        _buffer = nil;
    }
    return self;
}

- (void)initiateCheck:(id)sender;
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//	DBLog(@"In thread...reading url");
    NSDictionary *dict = [NSDictionary dictionaryWithContentsOfURL:_fileURL];
//	DBLog(@"URL read");
    [self processDictionary:dict];
    [pool release];
}

- (void)performCheck:(NSURL *)url withVersion:(NSString *)version
{
    // Make sure we were actually *given* stuff
    if (url == nil || version == nil) {
        // Bah
        [self returnError:kMacPADResultMissingValues message:@"URL or version was nil"];
        return;
    }
    
    // Save the current version and URL
    [_currentVersion release];
    _currentVersion = [version copy];
    [_fileURL release];
    _fileURL = [url copy];
 
//	DBLog(@"Detaching thread for updater");
    [NSThread detachNewThreadSelector:@selector(initiateCheck:) toTarget:self withObject:nil];
}

- (void)performCheckWithVersion:(NSString *)version
{
    // This method makes use of the MacPAD.url file inside the application bundle
    // If this file isn't there, or it's not in the correct format, this will return
    // error kMacPADResultMissingValues with an appropriate message
    // If it is there, it calls performCheck:withVersion: with the URL
    NSString *path = [[NSBundle mainBundle] pathForResource:@"MacPAD" ofType:@"url"];
    if (path == nil) {
        // File is missing
        [self returnError:kMacPADResultMissingValues message:@"MacPAD.url file was not found"];
        return;
    }
    NSString *contents = [NSString stringWithContentsOfFile:path];
    if (contents == nil) {
        // The file can't be opened
        [self returnError:kMacPADResultMissingValues message:@"The MacPAD.url file can't be opened"];
        return;
    }
    
    NSString *urlString;
    NSRange range = [contents rangeOfString:@"URL="];
    if (range.location != NSNotFound) {
        // We have a URL= prefix
        range.location += range.length;
        range.length = [contents length] - range.location;
        urlString = [contents substringWithRange:range];
    } else {
        // The file is the URL
        urlString = contents;
    }
    // Strip whitespace
    urlString = [urlString stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
    // Perform the check
    [self performCheck:[NSURL URLWithString:urlString] withVersion:version];
}

- (void)performCheckWithURL:(NSURL *)url
{
    // Gets the version from the Info.plist file and calls performCheck:withVersion:
    NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
    [self performCheck:url withVersion:version];
}

- (void)performCheck
{
    // Gets the version from the Info.plist file and calls performCheckWithVersion:
    NSString *version = [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleVersion"];
    [self performCheckWithVersion:version];
}

- (void)setDelegate:(id)delegate
{
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    if (_delegate != nil) {
        // Unregister with the notification center
        [nc removeObserver:_delegate name:MacPADErrorOccurredNotification object:self];
        [nc removeObserver:_delegate name:MacPADCheckFinishedNotification object:self];
        [_delegate autorelease];
    }
    _delegate = [delegate retain];
    // Register the new MacPADSocketNotification methods for the delegate
    // Only register if the delegate implements it, though
    if ([_delegate respondsToSelector:@selector(macPADErrorOccurred:)]) {
        [nc addObserver:_delegate selector:@selector(macPADErrorOccurred:)
                          name:MacPADErrorOccurredNotification object:self];
    }
    if ([_delegate respondsToSelector:@selector(macPADCheckFinished:)]) {
        [nc addObserver:_delegate selector:@selector(macPADCheckFinished:)
                          name:MacPADCheckFinishedNotification object:self];
    }
}

- (NSString *)releaseNotes
{
    if (_releaseNotes == nil) {
        return @"";
    } else {
        return [[_releaseNotes copy] autorelease];
    }
}

- (NSString *)newVersion
{
    if (_newVersion == nil) {
        return @"";
    } else {
        return [[_newVersion copy] autorelease];
    }
}

- (NSString *)productPageURL
{
    if (_productPageURL == nil) {
        return @"";
    } else {
        return [[_productPageURL copy] autorelease];
    }
}

- (NSString *)productDownloadURL
{
    if (_productDownloadURLs != nil && [_productDownloadURLs count] >= 1) {
        return [_productDownloadURLs objectAtIndex:0];
    } else {
        return @"";
    }
}

- (NSArray *)productDownloadURLs
{
    if (_productDownloadURLs == nil) {
        return [NSArray array];
    } else {
        return [[_productDownloadURLs copy] autorelease];
    }
}

- (void)returnError:(MacPADResultCode)code message:(NSString *)msg
{
    NSNumber *yesno = [NSNumber numberWithBool:(code == kMacPADResultNewVersion)];
    NSNumber *errorCode = [NSNumber numberWithInt:code];
    NSDictionary *userInfo = [NSDictionary dictionaryWithObjectsAndKeys:yesno, MacPADNewVersionAvailable,
                                                msg, MacPADErrorMessage, errorCode, MacPADErrorCode, nil];
    if (code == 0 || code == 5) {
        // Not an error
        [self performSelectorOnMainThread:@selector(returnSuccess:) withObject:userInfo waitUntilDone:NO];
    } else {
        // It's an error
        [self performSelectorOnMainThread:@selector(returnFailure:) withObject:userInfo waitUntilDone:NO];
    }
}

- (void)returnSuccess:(NSDictionary *)userInfo
{
    [[NSNotificationCenter defaultCenter] postNotificationName:MacPADCheckFinishedNotification
                                                        object:self userInfo:userInfo];
}

- (void)returnFailure:(NSDictionary *)userInfo
{
    [[NSNotificationCenter defaultCenter] postNotificationName:MacPADErrorOccurredNotification
                                                        object:self userInfo:userInfo];
}

- (void)processDictionary:(NSDictionary *)dict
{
    if (dict == nil) {
		DBLog(@"Update error: dict is nil");
        [self returnError:kMacPADResultInvalidURL message:@"Remote file or URL was invalid"];
        return;
    }
//    DBLog(@"HOLY MOLY");
    _newVersion = [[dict objectForKey:@"productVersion"] copy];
    if (_newVersion == nil) {
        // File is missing version information
        [self returnError:kMacPADResultBadSyntax message:@"Product version information missing"];
        return;
    }
    
    // Get release notes
    _releaseNotes = [[dict objectForKey:@"productReleaseNotes"] copy];
    
    // Get product page URL
    _productPageURL = [[dict objectForKey:@"productPageURL"] copy];
    
    // Get the first product download URL
    _productDownloadURLs = [[dict objectForKey:@"productDownloadURL"] copy];
    
    // Compare versions
    if ([self compareVersion:_newVersion toVersion:_currentVersion] == NSOrderedAscending) {
        // It's a new version
        [self returnError:kMacPADResultNewVersion message:@"New version available"];
    } else {
        [self returnError:kMacPADResultNoNewVersion message:@"No new version available"];
    }
    
    // We're done
}

- (void)dealloc
{
    // Unregister the delegate with the notification center
    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
    [nc removeObserver:_delegate name:MacPADErrorOccurredNotification object:self];
    [nc removeObserver:_delegate name:MacPADCheckFinishedNotification object:self];
    [nc removeObserver:self];
    
    // Release objects
    [_delegate release];
    [_fileHandle release];
    [_currentVersion release];
    [_buffer release];
    [_newVersion release];
    [_releaseNotes release];
    [_productPageURL release];
    [_productDownloadURLs release];
    
    [super dealloc];
}

- (NSComparisonResult)compareVersion:(NSString *)versionA toVersion:(NSString *)versionB
{
    NSArray *partsA = [self splitVersion:versionA];
    NSArray *partsB = [self splitVersion:versionB];
    
    NSString *partA, *partB;
    int i, n, typeA, typeB, intA, intB;
    
    n = MIN([partsA count], [partsB count]);
    for (i = 0; i < n; ++i) {
        partA = [partsA objectAtIndex:i];
        partB = [partsB objectAtIndex:i];
        
        typeA = [self getCharType:partA];
        typeB = [self getCharType:partB];
        
        // Compare types
        if (typeA == typeB) {
            // Same type; we can compare
            if (typeA == kNumberType) {
                intA = [partA intValue];
                intB = [partB intValue];
                if (intA > intB) {
                    return NSOrderedAscending;
                } else if (intA < intB) {
                    return NSOrderedDescending;
                }
            } else if (typeA == kStringType) {
                NSComparisonResult result = [partA compare:partB];
                if (result != NSOrderedSame) {
                    return result;
                }
            }
        } else {
            // Not the same type? Now we have to do some validity checking
            if (typeA != kStringType && typeB == kStringType) {
                // typeA wins
                return NSOrderedAscending;
            } else if (typeA == kStringType && typeB != kStringType) {
                // typeB wins
                return NSOrderedDescending;
            } else {
                // One is a number and the other is a period. The period is invalid
                if (typeA == kNumberType) {
                    return NSOrderedAscending;
                } else {
                    return NSOrderedDescending;
                }
            }
        }
    }
    // The versions are equal up to the point where they both still have parts
    // Lets check to see if one is larger than the other
    if ([partsA count] != [partsB count]) {
        // Yep. Lets get the next part of the larger
        // n holds the value we want
        NSString *missingPart;
        int missingType, shorterResult, largerResult;
        
        if ([partsA count] > [partsB count]) {
            missingPart = [partsA objectAtIndex:n];
            shorterResult = NSOrderedDescending;
            largerResult = NSOrderedAscending;
        } else {
            missingPart = [partsB objectAtIndex:n];
            shorterResult = NSOrderedAscending;
            largerResult = NSOrderedDescending;
        }
        
        missingType = [self getCharType:missingPart];
        // Check the type
        if (missingType == kStringType) {
            // It's a string. Shorter version wins
            return shorterResult;
        } else {
            // It's a number/period. Larger version wins
            return largerResult;
        }
    }
    
    // The 2 strings are identical
    return NSOrderedSame;
}

- (NSArray *)splitVersion:(NSString *)version
{
    NSString *character;
    NSMutableString *s;
    int i, n, oldType, newType;
    NSMutableArray *parts = [NSMutableArray array];
    if ([version length] == 0) {
        // Nothing to do here
        return parts;
    }
    s = [[[version substringToIndex:1] mutableCopy] autorelease];
    oldType = [self getCharType:s];
    n = [version length] - 1;
    for (i = 1; i <= n; ++i) {
        character = [version substringWithRange:NSMakeRange(i, 1)];
        newType = [self getCharType:character];
        if (oldType != newType || oldType == kPeriodType) {
            // We've reached a new segment
            [parts addObject:[s copy]];
            [s setString:character];
        } else {
            // Add character to string and continue
            [s appendString:character];
        }
        oldType = newType;
    }
    
    // Add the last part onto the array
    [parts addObject:[s copy]];
    return parts;
}

- (int)getCharType:(NSString *)character
{
    if ([character isEqualToString:@"."]) {
        return kPeriodType;
    } else if ([character isEqualToString:@"0"] || [character intValue] != 0) {
        return kNumberType;
    } else {
        return kStringType;
    }
}
@end