// Copyright 1997-2000 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 "CDDBDatabase.h"
#import "CDDBPreferences.h"

#import <OmniNetworking/OmniNetworking.h>
#import <OmniFoundation/OFBundleRegistry.h>

NSString *CDDBServerHostNameKey     = @"hostname";
NSString *CDDBServerPortKey         = @"port";
NSString *CDDBServerDescriptionKey  = @"description";
NSString *CDDBServerProtocolKey     = @"protocol";
NSString *CDDBServerFormPathKey     = @"path";

NSString *CDDBDiscArtistKey        = @"artist";
NSString *CDDBDiscTitleKey         = @"discTitle";
NSString *CDDBExtendedDiscInfoKey  = @"extendedDiscInfo";
NSString *CDDBTrackInfoKey         = @"tracks";
NSString *CDDBTrackTitleKey        = @"trackTitle";
NSString *CDDBExtendedTrackInfoKey = @"extendedTrackInfo";

static NSCharacterSet *spaceCharacterSet = nil;

static BOOL         CDDBDebug = NO;
static unsigned int CDDBProtocolVersion = 4;


/******
 ******  This code is adapted from the cddb.howto
 ******/

int cddb_sum(int n)
{
    int	ret;

    /* For backward compatibility this algorithm must not change */

    ret = 0;
    while (n > 0) {
        ret = ret + (n % 10);
        n = n / 10;
    }
    return (ret);
}

NSString *cddb_discid(const CDDBTableOfContentsEntry *entries, int entryCount)
{
    int i, t = 0, n = 0;
    unsigned long signature;
    
    /* For backward compatibility this algorithm must not change */

    i = 0;
    while (i < entryCount) {
        n = n + cddb_sum((entries[i].minutes * 60) + entries[i].seconds);
        i++;
    }

    t = ((entries[entryCount].minutes * 60) + entries[entryCount].seconds) -
        ((entries[0].minutes * 60) + entries[0].seconds);

    signature = ((n % 0xff) << 24 | t << 8 | entryCount);

    return [NSString stringWithFormat: @"%08x", signature];
}

/******
 ******  end of cddb copied code
 ******/


@interface CDDBDatabase (PrivateAPI)
- (void) _commandError: (NSString *) commandDescription;
@end

@implementation CDDBDatabase

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

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

    spaceCharacterSet = [[NSCharacterSet characterSetWithCharactersInString: @" "] retain];
    CDDBDebug = [[OFUserDefaults sharedUserDefaults] boolForKey: @"CDDBDebug"];
    if (CDDBDebug)
        NSLog(@"CDDBDebug enabled");
}

- init;
{
    NSFileManager  *manager;
    NSDictionary   *attr;
    NSString       *userName;
    NSString       *appVersion;
    
    if (!(self = [super init]))
        return nil;

    // This is kind of a lame way to get the current logged in user (we are running as root).
    manager = [NSFileManager defaultManager];
    attr = [manager fileAttributesAtPath: @"/dev/console" traverseLink: NO];
    userName = [attr objectForKey: NSFileOwnerAccountName];

    // Format the hello string
    appVersion = [[OFBundleRegistry softwareVersionDictionary] objectForKey: @"OFSoftwareVersion"];
    if (!appVersion)
        appVersion = @"0.0";
    
    _helloString = [[NSString alloc] initWithFormat: @"%@ %@.%@ %@ %@",
        userName, [ONHost localHostname], [ONHost domainName],
        [[NSProcessInfo processInfo] processName], appVersion];

    return self;
}

- (void) dealloc;
{
    [self disconnect];
    [_lastReply release];
    [_helloString release];
    [super dealloc];
}

- (void) connect;
{
    ONTCPSocket    *tcpSocket;
    ONHost         *host;
    NSString       *protocol, *hostName;
    OFUserDefaults *defaults;
    unsigned int    port;

    if (CDDBDebug)
        NSLog(@"-[%@ %s] entered", isa, _cmd);

    defaults = [OFUserDefaults sharedUserDefaults];
    protocol = [defaults objectForKey: CDDBServerProtocolDefaultKey];
    _usingHTTP = [protocol isEqualToString: CDDDHTTPProtocolName];
    OBASSERT(_usingHTTP || [protocol isEqualToString: CDDDCDDBPProtocolName]);

    if (CDDBDebug)
        NSLog(@"CDDB: Using protocol = %@", protocol);

    if (_socketStream) {
        BOOL connectionStillValid;

        // HTTP connections are only good for one command
        OBASSERT(!_usingHTTP);
        
        NS_DURING {
            // Ask for the server version as a simple, non-state changing command
            [self sendCommand:@"ver"];
            connectionStillValid = [_lastReply length] > 0;
        } NS_HANDLER {
            connectionStillValid = NO;
        } NS_ENDHANDLER;
        if (connectionStillValid)
            return;
        [_socketStream release];
        [_lastReply release];
    }
    _lastReply = nil;

    tcpSocket = [ONTCPSocket tcpSocket];
    [tcpSocket setReadBufferSize:16 * 1024];
    _socketStream = [[ONSocketStream alloc] initWithSocket:tcpSocket];
    [_socketStream setStringEncoding: NSISOLatin1StringEncoding];

    if (_usingHTTP && [defaults boolForKey: CDDBHTTPUseProxyDefaultKey]) {
        // Connect to the proxy host rather than the real host.
        hostName = [defaults objectForKey: CDDBHTTPProxyHostNameDefaultKey];
        port = [defaults integerForKey: CDDBHTTPProxyPortDefaultKey];
    } else {
        // Connect directly to the main host
        hostName = [defaults objectForKey: CDDBServerHostNameDefaultKey];
        port = [defaults integerForKey: CDDBServerPortDefaultKey];
    }

    if (CDDBDebug)
        NSLog(@"CDDB: hostname = %@, port = %d", hostName, port);

    host = [ONHost hostForHostname: hostName];
    [tcpSocket connectToHost:host port: port];

    if (!_usingHTTP) {
        // Get the server version info
        [self readResponse];

        // Greet the server immediately
        if (![self sendCommandFormat: @"cddb hello %@", _helloString])
            [self _commandError: @"Error sending 'hello' command to CDDB server"]; 

        // Select a protocol version immediately
        if (![self sendCommandFormat: @"proto %d", CDDBProtocolVersion])
            [self _commandError: @"Error selecting protocol version on CDDB server"]; 
    }

    if (CDDBDebug)
        NSLog(@"-[%@ %s] exited", isa, _cmd);
}

- (void) disconnect;
{
    if (CDDBDebug)
        NSLog(@"-[%@ %s] entered", isa, _cmd);

    if (!_socketStream)
        return;
    NS_DURING {
        if (!_usingHTTP)
            [self sendCommand:@"QUIT"];
    } NS_HANDLER {
        // Too bad!
    } NS_ENDHANDLER;
    [_socketStream release];
    _socketStream = nil;

    if (CDDBDebug)
        NSLog(@"-[%@ %s] exited", isa, _cmd);
}

- (BOOL)sendCommand:(NSString *)command;
{
    if (!_socketStream)
        [self connect];
    
    [_lastReply release];
    _lastReply = nil;
    _lastReplyIntValue = 0;
    
    if (_usingHTTP) {
        NSString       *noSpacesString;
        NSString       *getString;
        OFUserDefaults *defaults;
        NSString       *reply;
        
        noSpacesString = [command stringByReplacingCharactersInSet: spaceCharacterSet
                                                        withString: @"+"];
        defaults = [OFUserDefaults sharedUserDefaults];
        if ([defaults boolForKey: CDDBHTTPUseProxyDefaultKey]) {
            getString = [NSString stringWithFormat: @"GET http://%@:%d%@?cmd=%@&hello=%@&proto=%d HTTP/1.0\n\n",
                [defaults objectForKey: CDDBServerHostNameDefaultKey],
                [defaults integerForKey: CDDBServerPortDefaultKey],
                [defaults objectForKey: CDDBHTTPFormPathDefaultKey],
                noSpacesString,
                [_helloString stringByReplacingCharactersInSet: spaceCharacterSet withString: @"+"],
                CDDBProtocolVersion];
        } else {
            getString = [NSString stringWithFormat: @"GET %@?cmd=%@&hello=%@&proto=%d HTTP/1.0\n\n",
                [defaults objectForKey: CDDBHTTPFormPathDefaultKey],
                noSpacesString,
                [_helloString stringByReplacingCharactersInSet: spaceCharacterSet withString: @"+"],
                CDDBProtocolVersion];
        }

        if (CDDBDebug)
            NSLog(@"CDDB Tx: %@", getString);
        [_socketStream writeString: getString];

        // Read the status return
        reply = [_socketStream readLine];
        if (CDDBDebug)
            NSLog(@"CDDB Rx: %@", reply);
        [_lastReply release];
        _lastReply = [reply retain];
        _lastReplyIntValue = [[[reply componentsSeparatedByString: @" "] objectAtIndex: 1] intValue];
        if (_lastReplyIntValue >= 400) {
            // An HTTP level error occured
            return NO;
        }
        
        // Read the returned headers.
        while ((reply = [_socketStream readLine])) {
            if ([reply isEqualToString: @""])
                break;

            if (CDDBDebug)
                NSLog(@"CDDB Rx: %@", reply);
        }

        
        // From here on out, the response looks just like what we would have gotten
        // from CDDBP (except possibly for some HTTP server specific errors).
    } else {
        if (CDDBDebug)
            NSLog(@"CDDB Tx: %@", command);

        [_socketStream writeString:command];
        [_socketStream writeString:@"\n"];
    }

    return [self readResponse];
}

- (BOOL)sendCommandFormat:(NSString *)aFormat, ...;
{
    NSString *commandString;
    BOOL success;
    va_list argList;

    va_start(argList, aFormat);
    commandString = [[NSString alloc] initWithFormat:aFormat arguments:argList];
    va_end(argList);
    success = [self sendCommand:commandString];
    [commandString release];
    return success;
}

- (BOOL)readResponse;
{
    NSString *reply;

    reply = [_socketStream readLine];
    if (CDDBDebug)
        NSLog(@"CDDB Rx: %@", reply);

    if (!reply)
        [NSException raise:@"Bad response" format:@"Invalid response from CDDB server"];

    [_lastReply release];
    _lastReply = [reply retain];
    _lastReplyIntValue = [reply intValue];
    return _lastReplyIntValue < 400;
}


- (NSArray *)readLinesFromServer
{
    NSMutableArray *linesArray;
    NSString *line;

    linesArray = [NSMutableArray array];

    line = [_socketStream readLine];
    while (line && ![line isEqualToString:@"."]) {
        // Unstuff the line *after* checking for the termination octet.
        if ([line hasPrefix:@".."] == YES)
            line = [line substringFromIndex:1];
        [linesArray addObject:line];
        line = [_socketStream readLine];
    }
    return linesArray;
}

- (NSDictionary *) readDiscWithSignature: (NSString *) signature
                                category: (NSString *) category;
{
    NSArray                  *responses;
    unsigned int              responseIndex, responseCount;
    NSString                 *response;
    NSMutableDictionary      *dict;
    NSMutableArray           *trackInfo;

    if (_usingHTTP)
        // Every command must be in a different connection when using HTTP
        [self connect];

    if (![self sendCommandFormat: @"cddb read %@ %@", category, signature])
        [NSException raise: @"CDDBDatabaseInternalError"
                    format: @"Attempted to read non-existant category/signature %@ %@", category, signature];

    dict   = [NSMutableDictionary dictionary];
    trackInfo = [[NSMutableArray alloc] init];
    [dict setObject: trackInfo forKey: CDDBTrackInfoKey];
    [trackInfo release];
    
    responses = [self readLinesFromServer];
    responseCount = [responses count];
    for (responseIndex = 0; responseIndex < responseCount; responseIndex++) {
        NSRange   equalsRange;
        NSString *key, *value;
        
        response = [responses objectAtIndex: responseIndex];
        if ([response hasPrefix: @"#"])
            continue;

        equalsRange = [response rangeOfString: @"="];
        if (!equalsRange.length)
            // This shouldn't happen, but we'll be careful
            continue;

        key = [response substringToIndex: equalsRange.location];
        value = [response substringFromIndex: equalsRange.location + equalsRange.length];

        if ([key isEqualToString: @"DTITLE"]) {
            NSRange   slashRange;
            NSString *artist, *title;
            
            slashRange = [value rangeOfString: @"/"];
            if (slashRange.length) {
                // The value contains at least one '/'.  We'll assume that it contains no others
                // and that the first part is the artist and the second the disc title (as per
                // the CDDB file format spec).
                artist = [[value substringToIndex: slashRange.location] stringByRemovingSurroundingWhitespace];
                title  = [[value substringFromIndex: slashRange.location+1] stringByRemovingSurroundingWhitespace];
            } else {
                // The CDDB file format spec says that in this case the disc title and artist
                // name should be assumed to be the same thing.
                artist = value;
                title  = value;
                
            }

            [dict setObject: artist forKey: CDDBDiscArtistKey];
            [dict setObject: title  forKey: CDDBDiscTitleKey];
            continue;
        }

        if ([key isEqualToString: @"EXTD"] && [value length]) {
            NSMutableString *extendedDiscInfo;

            extendedDiscInfo = [dict objectForKey: CDDBExtendedDiscInfoKey];
            if (!extendedDiscInfo) {
                extendedDiscInfo = [[NSMutableString alloc] init];
                [dict setObject: extendedDiscInfo forKey: CDDBExtendedDiscInfoKey];
                [extendedDiscInfo release];
            }

            // This should already contain newlines.
            [extendedDiscInfo appendString: value];
        }

        if ([key hasPrefix: @"TTITLE"]) {
            unsigned int         index, trackCount;
            NSMutableString     *title;
            NSMutableDictionary *trackDict = nil;;
            
            index = [[key substringFromIndex: 6] intValue];
            trackCount = [trackInfo count];
            if (index > trackCount) {
                [self disconnect];
                [NSException raise: @"CDDBDatabaseInternalError"
                            format: @"Tracks returned out of order from CDDB server."];
            } else if (index == trackCount) {
                trackDict = [[NSMutableDictionary alloc] init];
                [trackInfo addObject: trackDict];
                [trackDict release];
            } else if (index < trackCount) {
                trackDict = [trackInfo objectAtIndex: index];
            }

            title = [trackDict objectForKey: CDDBTrackTitleKey];
            if (title)
                [title appendString: value];
            else {
                title = [value mutableCopy];
                [trackDict setObject: title forKey: CDDBTrackTitleKey];
                [title release];
            }
            
            continue;
        }

        if ([key hasPrefix: @"EXTT"] && [value length]) {
            unsigned int         index;
            NSMutableString     *extendedTrackInfo;
            NSMutableDictionary *trackDict;
            
            index = [[key substringFromIndex: 4] intValue];
            if (index >= [trackInfo count]) {
                [self disconnect];
                [NSException raise: @"CDDBDatabaseInternalError"
                            format: @"Extended info returned on a track that hasn't yet been received from CDDB server."];
            }
            trackDict = [trackInfo objectAtIndex: index];

            extendedTrackInfo = [trackDict objectForKey: CDDBExtendedTrackInfoKey];
            if (!extendedTrackInfo) {
                extendedTrackInfo = [[NSMutableString alloc] init];
                [trackDict setObject: extendedTrackInfo forKey: CDDBExtendedTrackInfoKey];
                [extendedTrackInfo release];
            }

            // This should already contain newlines.
            [extendedTrackInfo appendString: value];
            continue;
        }

        // Unknown key -- ignore
    }

    if (_usingHTTP)
        // Every command must be in a different connection when using HTTP
        [self disconnect];
    
    return dict;
}

- (NSArray *) serverList;
{
    NSArray             *responses;
    NSMutableArray      *servers;
    NSMutableDictionary *server;
    unsigned int         responseCount;

    if (CDDBDebug)
        NSLog(@"-[%@ %s] entered", isa, _cmd);

    if (![self sendCommand: @"sites"])
        [self _commandError: @"Error obtaining CDDB server list"];

    // The returned lines look like:
    // 	hostname protocol port path latitude longitude description

    responses = [self readLinesFromServer];
    if (CDDBDebug)
        NSLog(@"CDDB Rx multiple: %@", responses);

    responseCount = [responses count];
    
    servers = [NSMutableArray array];
    while (responseCount--) {
        NSString  *response, *protocol;
        NSString  *hostname = nil, *port = nil, *description = nil, *path = nil;
        NSScanner *scanner;
        
        response = [responses objectAtIndex: responseCount];
        scanner = [NSScanner scannerWithString: response];
        [scanner setCharactersToBeSkipped: nil];

        if (CDDBDebug)
            NSLog(@"response = %@", response);

        // hostname
        if (![scanner scanUpToString: @" " intoString: &hostname])
            continue;
        if (![scanner scanString: @" " intoString: NULL])
            continue;
        if (CDDBDebug)
            NSLog(@"hostname = %@", hostname);
        
        // protocol -- currently we only handle 'cddbp' and 'http'
        if (![scanner scanUpToString: @" " intoString: &protocol])
            continue;
        if (![scanner scanString: @" " intoString: NULL])
            continue;
        if (CDDBDebug)
            NSLog(@"protocol = %@", protocol);
        if (![protocol isEqualToString: CDDDCDDBPProtocolName] && ![protocol isEqualToString: CDDDHTTPProtocolName])
            continue;
        
        // port
        if (![scanner scanUpToString: @" " intoString: &port])
            continue;
        if (![scanner scanString: @" " intoString: NULL])
            continue;
        if (CDDBDebug)
            NSLog(@"port = %@", port);

        // path
        if (![scanner scanUpToString: @" " intoString: &path])
            continue;
        if (![scanner scanString: @" " intoString: NULL])
            continue;

        // lattitue
        if (![scanner scanUpToString: @" " intoString: NULL])
            continue;
        if (![scanner scanString: @" " intoString: NULL])
            continue;

        // longitude
        if (![scanner scanUpToString: @" " intoString: NULL])
            continue;
        if (![scanner scanString: @" " intoString: NULL])
            continue;

        // description
        if (![scanner scanUpToString: @"\n" intoString: &description])
            continue;
        if (CDDBDebug)
            NSLog(@"description = %@", description);

        if (!hostname || !port || !description)
            continue;

        server = [[NSMutableDictionary alloc] init];
        [server setObject: hostname forKey: CDDBServerHostNameKey];
        [server setObject: port forKey: CDDBServerPortKey];
        [server setObject: description forKey: CDDBServerDescriptionKey];
        [server setObject: protocol forKey: CDDBServerProtocolKey];
        [server setObject: path forKey: CDDBServerFormPathKey];

        [servers addObject: server];
        [server release];
    }

    if (CDDBDebug)
        NSLog(@"-[%@ %s] exited", isa, _cmd);
    return servers;
}

- (NSArray *) categories;
{
    NSArray *categories;

    if (CDDBDebug)
        NSLog(@"-[%@ %s] entered", isa, _cmd);

    if (![self sendCommand: @"cddb lscat"])
        [self _commandError: @"Error obtaining CDDB category list"];

    categories = [self readLinesFromServer];

    if (CDDBDebug)
        NSLog(@"categories = %@", categories);
    if (CDDBDebug)
        NSLog(@"-[%@ %s] exited", isa, _cmd);

    return categories;
}

- (NSDictionary *) statistics;
{
    NSArray             *responses;
    unsigned int         responseIndex;
    NSMutableDictionary *stats;
    
    if (CDDBDebug)
        NSLog(@"-[%@ %s] entered", isa, _cmd);

    if (![self sendCommand: @"stat"])
        [self _commandError: @"Error obtaining CDDB statistics"];

    responses = [self readLinesFromServer];

    // The lines returned from the server that have interesting data
    // are of the form '   key: value'.
    stats = [NSMutableDictionary dictionary];

    responseIndex = [responses count];
    while (responseIndex--) {
        NSString   *response;
        NSArray    *components;
        NSString   *key, *value;

        response = [responses objectAtIndex: responseIndex];
        components = [response componentsSeparatedByString: @":"];
        if ([components count] != 2)
            continue;

        value = [[components objectAtIndex: 1] stringByRemovingSurroundingWhitespace];
        if (![value length])
            // there is no key -- this is just a heading
            continue;

        key = [[components objectAtIndex: 0] stringByRemovingSurroundingWhitespace];

        [stats setObject: value forKey: key];
    }

        
    if (CDDBDebug)
        NSLog(@"stats = %@", stats);
    if (CDDBDebug)
        NSLog(@"-[%@ %s] exited", isa, _cmd);

    return stats;
}

static int _compareByNumericStatEntry(id obj1, id obj2, void *context)
{
    int stat1, stat2;

    stat1 = [[(NSDictionary *)context objectForKey: obj1] intValue];
    stat2 = [[(NSDictionary *)context objectForKey: obj2] intValue];

    // Biggest numbers first, not last
    return stat2 - stat1;
}

- (NSArray *) categoriesOrderedByStatistic;
{
    NSArray      *categories, *orderedCategories;
    NSDictionary *statistics;

    if (CDDBDebug)
        NSLog(@"-[%@ %s] entered", isa, _cmd);

    categories = [self categories];
    statistics = [self statistics];

    orderedCategories = [categories sortedArrayUsingFunction: _compareByNumericStatEntry context: statistics];

    if (CDDBDebug)
        NSLog(@"orderedCategories = %@", orderedCategories);
    if (CDDBDebug)
        NSLog(@"-[%@ %s] exited", isa, _cmd);

    return orderedCategories;
}

- (NSArray *) sendQueryForEntries: (const CDDBTableOfContentsEntry *) entries
                       entryCount: (unsigned int) entryCount
                  durationSeconds: (unsigned int) durationSeconds;
{
    NSMutableString          *times;
    unsigned int              entryIndex;
    NSArray                  *responses;
    NSString                 *category, *signature;
    
    times = [NSMutableString string];

    for (entryIndex = 0; entryIndex < entryCount; entryIndex++)
        [times appendFormat: @" %d", entries[entryIndex].logicalBlockAddress];

    signature = cddb_discid(entries, entryCount);
    if (![self sendCommandFormat: @"cddb query %@ %d%@ %d", signature, entryCount, times, durationSeconds])
        [self _commandError: @"Error obtaining disc information"];

    if (_lastReplyIntValue == 200) {
        // Exact match -- we could look in _lastReply for category and disc information
        // but since it will be repeated in the 'read' command we won't.  We *do* need to
        // get the category.  The return line will look like:
        //
        // 200 rock a70cea0d Clan Of Xymox / Hiden Faces
        //
        category = [[_lastReply componentsSeparatedByString: @" "] objectAtIndex: 1];

        if (_usingHTTP)
            // Every command must be in a different connection when using HTTP
            [self disconnect];

        return [NSArray arrayWithObject: [self readDiscWithSignature: signature category: category]];
    } else if (_lastReplyIntValue == 211 || _lastReplyIntValue == 210) {
        // A list of exact or inexact matches -- read response lines that look like:
        //
        // rock 940c180c River City People / Say Something Good
        //
        responses = [self readLinesFromServer];
        if (CDDBDebug)
            NSLog(@"CDDB Rx multiple: %@", responses);
        
        if ([responses count] != 1) {
            NSLog(@"Didn't receive exactly one response from CDDB server = %@", responses);
        }
        if ([responses count] > 0) {
            NSString *response;
            NSArray  *words;

            response = [responses objectAtIndex: 0];
            words = [response componentsSeparatedByString: @" "];
            
            category = [words objectAtIndex: 0];
            
            // Get the signature that they want
            signature = [words objectAtIndex: 1];

            if (_usingHTTP)
                // Every command must be in a different connection when using HTTP
                [self disconnect];

            return [NSArray arrayWithObject: [self readDiscWithSignature: signature category: category]];
        }
    }
    

    
    return nil;
}


@end


@implementation CDDBDatabase (PrivateAPI)

- (void) _commandError: (NSString *) commandDescription;
{
    [NSException raise: @"CDDBDatabaseCommunicationError"
                format: @"%@%@",
        commandDescription,
        (_lastReply ? [NSString stringWithFormat: @":\n%@", _lastReply] : @".")];
}

@end
