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

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

#import "OWAddress.h"
#import "OWContentCache.h"
#import "OWContentType.h"
#import "OWDataStream.h"
#import "OWHeaderDictionary.h"
#import "OWObjectStream.h"
#import "OWPipeline.h"
#import "OWProcessor.h"
#import "OWSourceProcessor.h"
#import "OWURL.h"

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OWF/Processors.subproj/Protocols.subproj/OWNNTPSession.m,v 1.12 1999/03/30 21:26:35 kc Exp $")

static BOOL nntpDebug = NO;

static OWContentType *serverContent = nil;
static OWContentType *articleContent = nil;
static OWContentType *headerListContent = nil;
static NSString *defaultNNTPServerHostName = nil;
static unsigned int nntpServerPort;


static NSString *subjectHeader = @"Subject";
static NSString *messageIDHeader = @"Message-ID";
static NSString *fromHeader = @"From";

static NSArray *headersToRead = nil;

@interface OWNNTPSession (Private)
- (void)connect;
- (void)disconnect;

- (BOOL)readResponse;
- (BOOL)sendCommand:(NSString *)command;
- (BOOL)sendCommandFormat:(NSString *)aFormat, ...;

- (void)readHeader:(NSString *)headerName forMessagesInRange:(NSRange)range intoDictionary:(NSMutableDictionary *)messageHeaders;

- (void)readHeaders:(NSArray *)headers forMessagesInRange:(NSRange)range intoDictionary:(NSMutableDictionary *)messageHeaders;

- (void)getPost:(NSString *)post;
- (void)getGroup:(NSString *)group;
@end



@implementation OWNNTPSession

+ (NSString *)defaultNNTPServerHostName;
{
    return defaultNNTPServerHostName;
}

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

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

    serverContent = [[OWContentType contentTypeForString:@"Omni/OWNNTPSession"] retain];
    articleContent = [[OWContentType contentTypeForString:@"Omni/OWNewsArticle"] retain];
    headerListContent = [[OWContentType contentTypeForString:@"ObjectStream/OWNewsgroupHeaderList"] retain];

    headersToRead = [[NSArray alloc] initWithObjects:subjectHeader, messageIDHeader, fromHeader, nil];
}

+ (OWContentType *)newsServerType;
{
    return serverContent;
}

+ (OWContentType *)newsgroupArticleType;
{
    return articleContent;
}

+ (OWContentType *)newsgroupHeaderListType;
{
    return headerListContent;
}

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

+ (void)controllerDidInit:(NSNotification *)notification;
{
    [self readDefaults];
}

+ (void)readDefaults;
{
    OFUserDefaults *userDefaults;

    userDefaults = [OFUserDefaults sharedUserDefaults];

    // Don't look up the ONHost object until needed to avoid any problems with the lookup unless they actually matter.
    defaultNNTPServerHostName = [[userDefaults objectForKey:@"OWNNTPServerHost"] retain];
    nntpServerPort = [userDefaults integerForKey:@"OWNNTPServerPort"];
}

+ (OWNNTPSession *)nntpSessionForAddress:(OWAddress *)address;
{
    OWContentCache *cache;
    OWNNTPSession *session;
    
    OBPRECONDITION(address);

    // We will cache the session under its base address
    address = [address addressWithPath:@""];
    cache = [OWContentCache contentCacheForAddress:address];

    if (!(session = [cache contentOfType:serverContent])) {
        OWURL *url;
        NSString *server;

        url = [address url];
        server = [url netLocation];
        OBASSERT(server); // this should be ensured in OWNNTPProcessor

        session = [[self alloc] initWithNewsServer:server];
        [cache addContent:session];
        [session release];
    }

    return session;
}

- initWithNewsServer:(NSString *)aServerName;
{
    if (!(self = [super initWithName:@"News Server"]))
        return nil;

    serverLock = [[NSLock alloc] init];
    serverName = [aServerName copy];

    return self;
}

- (void)dealloc;
{
    [self disconnect];
    [serverLock release];
    [serverName release];
    [controlSocketStream release];
    [super dealloc];
}

- (void)fetchForProcessor:(OWProcessor *)aProcessor inPipeline:(OWPipeline *)aPipeline;
{
    NSException *raisedException;

    // Only allow one process to access any one news server at a time.
    // This prevents us from fetching multiple articles at once, but (a) we shouldn't be fetching nearly as many of those as we do ftp or http and (b) this is easier than caching multiple connections.
    [serverLock lock];
    
    NS_DURING {
        OWAddress *address;
        NSString *path;
        
        nonretainedPipeline = aPipeline;
        nonretainedProcessor = aProcessor;
        [self connect];

        address = (OWAddress *)[aPipeline lastContent];
        path = [[address url] path];

        if ([path rangeOfString:@"@"].length == 1)
            [self getPost:path];
        else if ([path length])
            [self getGroup:path];
        else
            [NSException raise:@"Invalid news URL" format:@"You must supply a news group or a message ID."];
        
        raisedException = nil;
    } NS_HANDLER {
        raisedException = localException;
    } NS_ENDHANDLER;
    
    [serverLock unlock];
    
    nonretainedPipeline = nil;
    nonretainedProcessor = nil;

    [raisedException raise];
}

- (void)abortFetch;
{
    abortFetch = YES;
    [[controlSocketStream socket] abortSocket];
}

//
// OWContent protocol
//

- (OWContentType *)contentType;
{
    return serverContent;
}

//
// OWOptionalContent protocol
//

- (BOOL)shareable;
{
    return NO;
}

//
// Debugging
//

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

    debugDictionary = [super debugDictionary];

    if (controlSocketStream)
        [debugDictionary setObject:controlSocketStream forKey:@"controlSocketStream"];
    if (serverName)
        [debugDictionary setObject:serverName forKey:@"serverName"];
    if (lastReply)
        [debugDictionary setObject:lastReply forKey:@"lastReply"];
    if (lastReplyIntValue)
        [debugDictionary setObject:[NSNumber numberWithInt:lastReplyIntValue] forKey:@"lastReplyIntValue"];
    if (abortFetch)
        [debugDictionary setObject:[NSNumber numberWithBool:abortFetch] forKey:@"abortFetch"];

    return debugDictionary;
}

@end



@implementation OWNNTPSession (Private)

- (void)connect;
{
    ONTCPSocket *tcpSocket;
    ONHost *host;

    abortFetch = NO;
    if (controlSocketStream) {
        BOOL connectionStillValid;

        NS_DURING {
            [self sendCommand:@"STAT"];
            connectionStillValid = [lastReply length] > 0;
        } NS_HANDLER {
            connectionStillValid = NO;
        } NS_ENDHANDLER;
        if (connectionStillValid)
            return;
        [controlSocketStream release];
        [lastReply release];
    }
    lastReply = nil;

    tcpSocket = [ONTCPSocket tcpSocket];
    [tcpSocket setReadBufferSize:16 * 1024];
    controlSocketStream = [[ONSocketStream alloc] initWithSocket:tcpSocket];

    host = [ONHost hostForHostname:serverName];
    [tcpSocket connectToHost:host port:nntpServerPort];

    [self readResponse];
    [nonretainedProcessor setStatusFormat:@"Connected to %@", defaultNNTPServerHostName];
}

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

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

    linesArray = [NSMutableArray array];

    line = [controlSocketStream 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 = [controlSocketStream readLine];
    }
    return linesArray;
}

- (BOOL)readResponse;
{
    NSString *reply;

    if (abortFetch)
        [NSException raise:@"Fetch stopped" format:@"Fetch stopped"];

    reply = [controlSocketStream readLine];
    if (nntpDebug)
        NSLog(@"NNTP Rx...%@", reply);

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

    [lastReply release];
    lastReply = [reply retain];
    lastReplyIntValue = [reply intValue];
    return lastReplyIntValue < 400;
}

- (BOOL)sendCommand:(NSString *)command;
{
    if (abortFetch)
        [NSException raise:@"Fetch stopped" format:@"Fetch stopped"];

    if (nntpDebug) {
            NSLog(@"NNTP Tx...%@", command);
    }

    [controlSocketStream writeString:command];
    [controlSocketStream writeString:@"\r\n"];

#warning TJW: Handle disconnection here?  We cannot transparently reconnect if we are in a group.  Maybe we should cache which group we want to be in so that we can reset it if necessary?
    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;
}

- (void)getPost:(NSString *)post;
{
    OWDataStream *outputDataStream;
    NSAutoreleasePool *autoreleasePool;
    
    if (![self sendCommandFormat:@"ARTICLE <%@>", post]) {
        [NSException raise:@"Selecting article failed" format:@"Failed selecting article %@: %@", post, lastReply];
    }
    outputDataStream = [[[OWDataStream alloc] init] autorelease];
    [outputDataStream setContentType:articleContent];
    [nonretainedPipeline addSourceContent:outputDataStream];
    [nonretainedPipeline cacheContent];
    [nonretainedPipeline startProcessingContent];
    autoreleasePool = [[NSAutoreleasePool alloc] init];

    NS_DURING {
        NSData *data;
        const char *ptr, *bytes, *end;
        unsigned int dataBytesRead, bytesInThisPool;
        unsigned int subsetFromOffset;
        BOOL writeLastPart;

        bytesInThisPool = 0;
        dataBytesRead = 0;
        [nonretainedProcessor processedBytes:dataBytesRead ofBytes:0];

        while ((data = [controlSocketStream readData])) {
            unsigned int dataLength;
            NSRange range;

            dataLength = [data length];
            dataBytesRead += dataLength;
            bytesInThisPool += dataLength;
            [nonretainedProcessor processedBytes:dataBytesRead ofBytes:0];
            bytes = [data bytes];
            end = bytes + dataLength;
            subsetFromOffset = 0;
            writeLastPart = YES;
            for (ptr = bytes; ptr < end; ptr++) {
                if (*ptr == '\n') {
                    if (++ptr < end && *ptr == '.') {
                        if (++ptr < end && *ptr == '\r') {
                            // end of message reached
                            range.location = subsetFromOffset;
                            range.length = (ptr - bytes) - range.location - 2;
                            [outputDataStream writeData:[data subdataWithRange:range]];
                            writeLastPart = NO;
                            break;
                        } else {
                            // doubled period at beginning of a line
                            range.location = subsetFromOffset;
                            range.length = (ptr - bytes) - range.location - 2;
                            subsetFromOffset = ptr - bytes;
                            [outputDataStream writeData:[data subdataWithRange:range]];
                        }
                    }
                }
            }
            if (subsetFromOffset) {
                range.location = subsetFromOffset;
                range.length = dataLength - range.location;
                [outputDataStream writeData:[data subdataWithRange:range]];
            } else if (writeLastPart) {
                [outputDataStream writeData:data];
            }
            if (bytesInThisPool > 64 * 1024) {
                [autoreleasePool release];
                autoreleasePool = [[NSAutoreleasePool alloc] init];
                bytesInThisPool = 0;
            }
            if (!writeLastPart) {
                [outputDataStream dataEnd];
                break;
            }
        }
    } NS_HANDLER {
        [outputDataStream dataAbort];
        [localException retain];
        [autoreleasePool release];
        [[localException autorelease] raise];
        autoreleasePool = nil;
    } NS_ENDHANDLER;
}

- (void)readHeader:(NSString *)headerName forMessagesInRange:(NSRange)range intoDictionary:(NSMutableDictionary *)messageHeaders;
{
    NSString *line;
    NSAutoreleasePool *pool;
    NSException *raisedException;
    unsigned int count = 0;
    
    // This isn't necessarily supported on all news servers, I think.  It is VASTLY faster than using 'HEAD' though.  All modern servers seem to have it.
    if (![self sendCommandFormat:@"XHDR %@ %d-%d", headerName, range.location, range.location + range.length])
        [NSException raise:@"Gathering headers failed" format:@"Failed gathering header %@: %@", headerName, lastReply];

    pool = [[NSAutoreleasePool alloc] init];

    NS_DURING {
        // Put the results into a dictionary keyed off of message number.  XHDR will respond with '(none)' if the message exists but there is no such header for that message.  Not sure what we should do in that case.  For now, we'll just assume the value is really '(none)'.
        while ((line = [controlSocketStream readLine])) {
            NSRange spaceRange;
            NSString *value;
            NSString *messageNumber;
            OWHeaderDictionary *headers;

            if ([line isEqualToString:@"."])
                break;

            // Find the first space after the message number
            spaceRange = [line rangeOfString:@" "];
            if (!spaceRange.length)
                [NSException raise:@"Bad response" format:@"Invalid response from HTTP server: %@", line];

            messageNumber = [line substringToIndex:spaceRange.location];
            value = [line substringFromIndex:spaceRange.location + spaceRange.length];

            // Keep a per-messageNumber dictionary of header values
            headers = [messageHeaders objectForKey:messageNumber];
            if (!headers) {
                headers = [[OWHeaderDictionary alloc] init];
                [messageHeaders setObject:headers forKey:messageNumber];
                [headers release];
            }

            [headers addString:value forKey:headerName];

            // Clean out the pool every once in a while
            count++;
            if (count >= 200) {
                [pool release];
                pool = [[NSAutoreleasePool alloc] init];
            }
        }

        raisedException = nil;
    } NS_HANDLER {
        raisedException = [localException retain];
    } NS_ENDHANDLER;

    [pool release];
    [[raisedException autorelease] raise];
}

- (void)readHeaders:(NSArray *)headers forMessagesInRange:(NSRange)range intoDictionary:(NSMutableDictionary *)messageHeaders;
{
    unsigned int headerIndex;

    headerIndex = [headers count];
    while (headerIndex--)
        [self readHeader:[headers objectAtIndex:headerIndex] forMessagesInRange:range intoDictionary:messageHeaders];
}


- (void)getGroup:(NSString *)group;
{
    OWObjectStream *outputObjectStream;
    NSMutableDictionary *messageHeaders;
    NSArray *replyArray;
    unsigned int messageNumber;
    NSRange messageRange;
    
    if (![self sendCommandFormat:@"GROUP %@", group])
        [NSException raise:@"Selecting newsgroup failed" format:@"Failed selecting group %@: %@", group, lastReply];

    outputObjectStream = [[[OWObjectStream alloc] init] autorelease];
    [outputObjectStream setContentType:headerListContent];
    [nonretainedPipeline addSourceContent:outputObjectStream];
    [nonretainedPipeline cacheContent];
    [nonretainedPipeline startProcessingContent];
    
    replyArray = [lastReply componentsSeparatedByString:@" "];
    if ([replyArray count] < 4)
        [NSException raise:@"Bad response" format:@"Invalid response from NNTP server: %@", lastReply];

    messageRange.location = [[replyArray objectAtIndex:2] intValue];
    messageRange.length = [[replyArray objectAtIndex:3] intValue] - messageRange.location;

    messageHeaders = [NSMutableDictionary dictionary];
    [self readHeaders:headersToRead forMessagesInRange:messageRange intoDictionary:messageHeaders];

    for (messageNumber = messageRange.location; messageNumber <= NSMaxRange(messageRange); messageNumber++) {
        NSString *messageNumberString;
        OWHeaderDictionary *headers;

        messageNumberString = [[NSString alloc] initWithFormat:@"%d", messageNumber];
        headers = [messageHeaders objectForKey:messageNumberString];
        [messageNumberString release];

        if (headers)
            [outputObjectStream writeObject:headers];
    }
    
    [outputObjectStream dataEnd];
}

@end
