
#import <GNUstepBase/GSXML.h>

#import "Controller.h"
#import "TickerWindow.h"
#import "ColorAsRGBADictionary.h"
#import "RSSHandler.h"
#import "StringAdditions.h"
#import "TempFilename.h"
#import "ArrayAdditions.h"
#import "HeadlineView.h"
#import "DictionaryAdditions.h"
#import "TextWindow.h"
#import "FeedWindow.h"

@implementation TickerWindow

- (void)dealloc
{
    if(loadInProgress==YES){
	[urlHandle removeClient:self];
    }

    [attr release];
    
    TEST_RELEASE(url);
    TEST_RELEASE(urlHandle);
    TEST_RELEASE(feedData);
    TEST_RELEASE(articles);
    TEST_RELEASE(cache);

    [super dealloc];
}

static int _count = 0;
#define MAX_OFFSET     10
#define WINDOW_SHIFT   40
#define WINDOW_BASE_X 200
#define WINDOW_BASE_Y 400

- initWithAttributes:(NSDictionary *)attributes
{
    attr = 
	[[NSMutableDictionary alloc] 
	    initWithDictionary:attributes];

    NSFont 
	*font =
	[NSFont 
	    fontWithName:[attr objectForKey:ATTR_FONTNAME]
	    size:[attr floatForKey:ATTR_FONTSIZE]];

    NSRect frame;
    NSString *framestr = [attr objectForKey:ATTR_FRAME];
    if(framestr!=nil){
	frame = NSRectFromString(framestr);
    }
    else{
	frame.origin.x = WINDOW_BASE_X 
	    + (_count % MAX_OFFSET)*WINDOW_SHIFT;
	frame.origin.y = WINDOW_BASE_Y
	    + ((_count / MAX_OFFSET) % MAX_OFFSET)*WINDOW_SHIFT;
	frame.size.width = TICKER_MINWIDTH;
	frame.size.height = [font boundingRectForFont].size.height;

	_count++;
    }

    [super 
	initWithContentRect:frame
	styleMask:(NSResizableWindowMask | 
		   NSClosableWindowMask | NSTitledWindowMask)
	backing:NSBackingStoreRetained 
	defer:NO];

    refreshsecs = (long)60*(long)[[attr objectForKey:ATTR_REFRESH] intValue];
    ticksecs    = (long)[[attr objectForKey:ATTR_TICK] intValue];

    [self setMinSize:NSMakeSize(TICKER_MINWIDTH, frame.size.height)];
    [self setMaxSize:NSMakeSize(TICKER_MAXWIDTH, frame.size.height)];

    text = [[HeadlineView alloc] initWithFrame:frame];
    AUTORELEASE(text);

    [text setEditable:NO];
    [text setSelectable:NO];
    [text 
	setTextColor:
	    [NSColor colorFromRGBADictionary:[attr objectForKey:ATTR_FG]]];
    [text 
	setBackgroundColor:
	    [NSColor colorFromRGBADictionary:[attr objectForKey:ATTR_BG]]];
    [text setFont:font];
    [[text textContainer] setWidthTracksTextView:YES];

    [text setString:_(@"(loading)")];

    [self setContentView:text];

    [[NSFontManager sharedFontManager]
	setSelectedFont:font isMultiple:NO];

    [self setReleasedWhenClosed:YES];

    articles = nil; cache = nil;
    externalChoice = -1;

    feedData = nil; url = nil; urlHandle = nil; loadInProgress = NO;

    signalChange = NO;

    [self refresh];
    [self display];

    return self;
}

- (NSDictionary *)attributes
{
    NSRect frame = [self frame];
    [attr setObject:NSStringFromRect(frame) forKey:ATTR_FRAME];

    return attr;
}


- swapColors
{
    NSColor 
	*a = [text textColor], 
	*b = [text backgroundColor];

    [text setTextColor:b];
    [text setBackgroundColor:a];

    return self;
}

- (NSArray *)prepareCommand:(NSString *)what error:(NSString **)errPtr
{
    NSString *cmdstr = [attr objectForKey:what];
    if(cmdstr==nil){
	*errPtr = 
	    [NSString stringWithFormat:_(@"(no command for %@)"), what];
	return nil;
    }

    cmdstr = [cmdstr stringByTrimmingWhitespaceAndNewlines];
    if(![cmdstr length]){
	*errPtr =
	    [NSString 
		stringWithFormat:_(@"(command for %@ empty: <%@>)"),
		what, [attr objectForKey:what]];
	return nil;
    }

    NSArray *allargs = [cmdstr stringToComponents];
    if(![allargs count]){
	*errPtr =
	    [NSString stringWithFormat:
			  _(@"(no components in command for %@)"), what];
	return nil;
    }

    return allargs;
}

- (NSString *)untag:(NSString *)source title:(NSString *)aTitle
{
    if(cache!=nil){
	NSString *cached = [cache objectForKey:source];
	if(cached!=nil){
	    // NSLog(@"cache hit %@", [self title]);
	    return cached;
	}
    }

    NSString *error;
    NSArray *allargs = [self prepareCommand:ATTR_UNTAG error:&error];
    if(allargs==nil){
	return error;
    }

    NSTask *cmdTask = [[NSTask alloc] init];
    // AUTORELEASE(cmdTask); (released after NSTaskDidTerminate notification)

    [cmdTask setLaunchPath:[allargs objectAtIndex:0]];

    NSMutableArray *args = 
	[NSMutableArray 
	    arrayWithArray:
		[allargs subarrayWithRange:NSMakeRange(1, [allargs count]-1)]];

    NSString *htmlfile = [[NSFileManager defaultManager] tempFilenameExt:@"html"];
    NSString *htmlstr =
	[NSString 
	    stringWithFormat:@"<HTML><HEAD><TITLE>%@</TITLE></HEAD>"
	    @"<BODY>\n%@\n</BODY></HTML>\n", aTitle, source];
    if([htmlstr writeToFile:htmlfile atomically:NO]==NO){
	return 
	    [NSString stringWithFormat:_(@"(couldn't write file %@)"), htmlfile];
    }
    [args addObject:htmlfile];

    [cmdTask setArguments:args];

    NSMutableDictionary *edict =
        [NSMutableDictionary 
	    dictionaryWithDictionary:
		[[NSProcessInfo processInfo] environment]];
    [edict setObject:htmlfile forKey:TICKER_TEMP_FILE];
    [edict setObject:[cmdTask description] forKey:TICKER_TASK_OBJECT];

    [cmdTask setEnvironment:edict];

    NSPipe *pipe = [NSPipe pipe];
    NSFileHandle *reader = [pipe fileHandleForReading];

    [cmdTask setStandardOutput:pipe];

    NSMutableData *data = [NSMutableData dataWithCapacity:1024];

    NS_DURING

    [cmdTask launch];
    while([cmdTask isRunning]==YES){
        [data appendData:[reader availableData]];
    }
    [data appendData:[reader readDataToEndOfFile]];

    NS_HANDLER

    NSString *emsg =
	[NSString stringWithFormat:_(@"(%@ exception: %@, %@)"),
		  ATTR_UNTAG,
                  [localException name],
                  [localException reason]];
    NS_VALUERETURN(emsg, id);

    NS_ENDHANDLER

    NSString
	*result =
	[[NSString alloc]
	    initWithBytes:[data bytes] length:[data length] 
	    encoding:NSASCIIStringEncoding];
    AUTORELEASE(result);

    if(cache!=nil && result!=nil && source!=nil){
	// NSLog(@"cache miss %@", [self title]);
	[cache setObject:result forKey:source];
    }
    
    return result;
}

- browse:(GenericArticle *)art text:(BOOL)textFlag
{
    NSString *alert =
	[NSString 
	    stringWithString:
		(textFlag==YES ?
		 _(@"Browse article text") : _(@"Follow article link"))];

    NSString *error;
    NSArray *allargs = [self prepareCommand:ATTR_BROWSE error:&error];
    if(allargs==nil){
	NSRunAlertPanel(alert, error, _(@"Ok"), nil, nil);
	return self;
    }

    NSTask *cmdTask = [[NSTask alloc] init];
    // AUTORELEASE(cmdTask); (released after NSTaskDidTerminate notification)

    [cmdTask setLaunchPath:[allargs objectAtIndex:0]];

    NSMutableArray *args = 
	[NSMutableArray 
	    arrayWithArray:
		[allargs subarrayWithRange:NSMakeRange(1, [allargs count]-1)]];

    NSMutableDictionary *edict =
	[NSMutableDictionary 
	    dictionaryWithDictionary:
		[[NSProcessInfo processInfo] environment]];

    NSString *lastarg;
    if(textFlag==YES){
	NSString *htmlfile = [[NSFileManager defaultManager] tempFilenameExt:@"html"];
	NSString *htmlstr =
	    [NSString 
		stringWithFormat:@"<HTML><HEAD><TITLE>%@</TITLE></HEAD>"
		@"<BODY>\n%@\n</BODY></HTML>\n", [art title], [art desc]];
	if([htmlstr writeToFile:htmlfile atomically:NO]==NO){
	    NSRunAlertPanel(alert, _(@"(couldn't write file %@)"), 
			    _(@"Ok"), nil, nil,
			    htmlfile);
	    return self;
	}
	lastarg = htmlfile;

	[edict setObject:htmlfile forKey:TICKER_TEMP_FILE];
    }
    else{
	lastarg = [art link];
    }

    [args addObject:lastarg];
    [cmdTask setArguments:args];

    [edict setObject:[cmdTask description] forKey:TICKER_TASK_OBJECT];
    [cmdTask setEnvironment:edict];

    NS_DURING
    [cmdTask launch];
    NS_HANDLER
    NSRunAlertPanel(alert, _(@"(%@ exception: %@, %@)"),
		    _(@"Ok"), nil, nil,
		    ATTR_BROWSE,
		    [localException name],
		    [localException reason]);
    NS_ENDHANDLER

    return self;
}


- setErrorFormat:(NSString *)fmt arg:(NSString *)arg
{
    [text 
	setString:
	    [NSString 
		stringWithFormat:fmt, arg]];
    return self;
}

- (NSString *)loadtitle
{
    NSString *name = [attr objectForKey:ATTR_NAME];
    if(name==nil){
	name = [attr objectForKey:ATTR_URL];
    }

    return
	[NSString
	    stringWithFormat:_(@"Loading %@: %@"),
	    name, [self description]];
}


- (BOOL)loadInProgress
{
    return loadInProgress;
}

- refresh
{
    if(loadInProgress==YES){
	return self;
    }

    [self setTitle:[self loadtitle]];

    NSString *urlstr = [attr objectForKey:ATTR_URL];
    url = [[NSURL alloc] initWithString:urlstr];

    if(url==nil || 
       [url scheme]==nil || [url host]==nil || [url path]==nil){
	[self setErrorFormat:_(@"(error: the url \"%@\" is not valid)") 
	      arg:urlstr];
	return self;
    }

    if(![[NSHost hostWithName:[url host]] names]){
	[self setErrorFormat:_(@"(error: host \"%@\" unknown)")
	      arg:[url host]];	
	return self;
    }

    loadInProgress = YES;

    urlHandle = 
	[[[NSURLHandle URLHandleClassForURL:url] alloc] 
	    initWithURL:url cached:NO];

    NSString *prxflag = [attr objectForKey:ATTR_PRXUSE];
    if(prxflag!=nil && [prxflag isEqualToString:@"On"]==YES){
	if([urlHandle
	       writeProperty:
		   [attr objectForKey:ATTR_PRXHOST]
	       forKey:GSHTTPPropertyProxyHostKey]==NO){
	    NSLog(@"couldn't write host property %@ for %@",
		  [attr objectForKey:ATTR_PRXHOST],
		  [attr objectForKey:ATTR_URL]);
	}
	if([urlHandle
	       writeProperty:
		   [attr objectForKey:ATTR_PRXPORT]
	       forKey:GSHTTPPropertyProxyPortKey]==NO){
	    NSLog(@"couldn't write port property %@ for %@",
                  [attr objectForKey:ATTR_PRXPORT],
                  [attr objectForKey:ATTR_URL]);
        }
    }

    [urlHandle addClient:self];
    [urlHandle loadInBackground];

    return self;
}

- (void)URLHandleResourceDidBeginLoading:(NSURLHandle *)sender
{
    feedData = [[NSMutableData alloc] initWithCapacity:URL_RESOURCE_CAPACITY];
}   


- (void)URLHandle:(NSURLHandle *)sender resourceDataDidBecomeAvailable:(NSData *)newBytes
{
    [feedData appendData:newBytes];
}

- (void)URLHandle:(NSURLHandle *)sender resourceDidFailLoadingWithReason:(NSString *)reason
{
    [self setErrorFormat:_(@"(load failed: %@)") arg:reason];

    loadInProgress = NO;

    TEST_AUTORELEASE(feedData);
    feedData = nil;

    AUTORELEASE(url);
    url = nil;

    AUTORELEASE(urlHandle);
    urlHandle = nil;
}

- (void)URLHandleResourceDidCancelLoading:(NSURLHandle *)sender
{
    [text setString:_(@"(load canceled)")];

    loadInProgress = NO;

    TEST_AUTORELEASE(feedData);
    feedData = nil;

    AUTORELEASE(url);
    url = nil;

    AUTORELEASE(urlHandle);
    urlHandle = nil;
}

- (void)URLHandleResourceDidFinishLoading:(NSURLHandle *)sender
{
    RSSHandler *handler = [RSSHandler handler];
    GSXMLParser *parser = 
	[GSXMLParser 
	    parserWithSAXHandler:handler withData:feedData];

    loadInProgress = NO;

    TEST_AUTORELEASE(feedData);
    feedData = nil;

    AUTORELEASE(url);
    url = nil;

    AUTORELEASE(urlHandle);
    urlHandle = nil;

    [parser keepBlanks:NO];
    [parser substituteEntities:NO];

    if([parser parse]==NO){
	NSMutableArray *msg = [handler messages];
	NSString *msgstr;
	if(![msg count]){
	    msgstr = _(@"unknown error");
	}
	else{
	    msgstr = [msg componentsJoinedByString:@" "];
	}

	[self setErrorFormat:_(@"(parse error: %@)") arg:msgstr];
	return;
    }

    [Controller debugAllocation];

    NSMutableArray *newArticles = 
	[NSMutableArray 
	    arrayWithCapacity:
		(articles==nil ? TICKER_DEFAULT_CAP : [articles count])];

    RSSNode *root = [handler root], *rssOrAtom = nil;
    if((rssOrAtom=[root childNamed:@"rss"])!=nil ||
       (rssOrAtom=[root childNamed:@"RDF"])!=nil ||
       (rssOrAtom=[root childNamed:@"rdf"])){
	BOOL isRSS = [[rssOrAtom name] isEqualToString:@"rss"];

	RSSNode *channel;
	if((channel=[rssOrAtom childNamed:@"channel"])==nil){
	    [self setErrorFormat:_(@"(channel expected)") arg:nil];
	    return;
	}

	NSString *newTitle = [self description];
	RSSNode *title, *copyright, *rights;

	if((title=[channel childNamed:@"title"])!=nil){
	    newTitle = [title value];
	    if((copyright=[channel childNamed:@"copyright"])!=nil){
		newTitle = 
		    [NSString stringWithFormat:@"%@ %@", newTitle, [copyright value]];
	    }
	    if((rights=[channel childNamed:@"rights"])!=nil){
		newTitle = 
		    [NSString stringWithFormat:@"%@ %@", newTitle, [rights value]];
	    }
	    [self setTitle:[newTitle stringByUnescapingHTML]];
	}

	NSString *globalAuthor = nil;
	RSSNode *author;

	if((author=[channel childNamed:CHAN_FIELD_ATHRMANAG])!=nil){
	    globalAuthor = [author value];
	}
	else if((author=[channel childNamed:CHAN_FIELD_ATHRWEBMSTR])!=nil){
	    globalAuthor = [author value];
	}

	RSSNode *item;
	NSEnumerator *en = 
	    [[(isRSS==YES ? channel : rssOrAtom) children] objectEnumerator];
	while((item=[en nextObject])!=nil){
	    if([[item name] isEqualToString:@"item"]==YES){
		RSSArticle *art = [RSSArticle newWithNode:item inWindow:self];
		
		if([art author]==nil && globalAuthor!=nil){
		    [art setAuthor:globalAuthor];
		}   

		[newArticles addObject:art];
		// GSPrintf(stdout, @"%@", [item description]);
	    }
	}
    }
    else if((rssOrAtom=[root childNamed:@"feed"])!=nil){
	NSString *newTitle = [self description];
	RSSNode *title, *copyright, *rights;

	if((title=[rssOrAtom childNamed:@"title"])!=nil){
	    newTitle = [title value];
	    if((copyright=[rssOrAtom childNamed:@"copyright"])!=nil){
		newTitle = 
		    [NSString stringWithFormat:@"%@ %@", newTitle, [copyright value]];
	    }
	    if((rights=[rssOrAtom childNamed:@"rights"])!=nil){
		newTitle = 
		    [NSString stringWithFormat:@"%@ %@", newTitle, [rights value]];
	    }
	    [self setTitle:[newTitle stringByUnescapingHTML]];
	}

	NSString *globalAuthor = nil;
	RSSNode *author = [rssOrAtom childNamed:@"author"];
	if(author!=nil){
	    globalAuthor = [GenericArticle authorFromAtomAuthor:author];
	}

	RSSNode *entry;
	NSEnumerator *en = [[rssOrAtom children] objectEnumerator];
	while((entry=[en nextObject])!=nil){
	    if([[entry name] isEqualToString:@"entry"]==YES){
		AtomArticle *art = [AtomArticle newWithNode:entry inWindow:self];

		if([art author]==nil && globalAuthor!=nil){
		    [art setAuthor:globalAuthor];
		}

		[newArticles addObject:art];
		// GSPrintf(stdout, @"%@", [entry description]);
	    }
	}
    }
    else{
	RSSNode *what = root;
	NSArray *children = [root children];
	if(children!=nil && [children count]>0){
	    what = [children objectAtIndex:0];
	}

	NSMutableString *desc = 
	    [NSMutableString stringWithString:[what description]];
	[desc
	    replaceOccurrencesOfString:@"\n"
	    withString:@" "
	    options:NSLiteralSearch
	    range:NSMakeRange(0, [desc length])];

	#define PREFIX_LENGTH 64
	int len = [desc length];
	int prefix = (len>=PREFIX_LENGTH ? PREFIX_LENGTH : len);

	[self setErrorFormat:_(@"(rss or atom feed expected, got \"%@\")") 
	      arg:[desc substringToIndex:prefix]];
	return;
    }


    GenericArticle *genart;
    NSDate *now = [NSDate date];
    NSEnumerator *en = [newArticles objectEnumerator];
    int apos = 0;

    while((genart=[en nextObject])!=nil){
	if([genart date]==nil){
	    NSDate *artDate =
		[now 
		    addTimeInterval:
			(NSTimeInterval)-ART_NODATE_SECS*apos];
	    [genart setDate:artDate];
	}
	apos++;
    }
    
    [newArticles sortUsingSelector:@selector(compareByDate:)];
    // GSPrintf(stdout, @"%@\n", [newArticles mapSelector:@selector(date)]);

    BOOL swapped = signalChange;
    if(articles==nil){
	signalChange = NO;
    }
    else{
	NSArray 
	    *guid1 = 
	    [[articles mapSelector:@selector(uniqueid)]
		sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)],
	    *guid2 = 
	    [[newArticles mapSelector:@selector(uniqueid)]
		sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
	signalChange = ([guid1 isEqualToArray:guid2]==YES ? NO : YES);
    }

    ASSIGN(articles, newArticles);

    [[NSNotificationCenter defaultCenter]
	postNotification:
	    [NSNotification
		notificationWithName:TickerDidUpdateNotification
		object:self]];

    int artcount = [articles count];
    if(cache==nil && artcount>0){
	cache = [[Cache alloc] initWithCapacity:artcount];
    }

    if(signalChange==YES){
	NSBeep();
    }
    if(swapped!=signalChange){
	[self swapColors];
    }

    if(artcount>0){
	current = 0;
	[text 
	    setString:
		[[articles objectAtIndex:current++] title]];
    }
    else{
	[text setString:_(@"(no articles found)")];
    }
    [text display];

    return;
}

- tick
{
    if(signalChange==YES){
	signalChange = NO;
	[self swapColors];
    }

    int acount = [articles count];
    if(acount){
	if(current==acount){
	    current = 0;
	}
	[text 
	    setString:
		[[articles objectAtIndex:current++] title]];
    }
    [text display];

    return self;
}

- (unsigned long)refreshsecs
{
    return refreshsecs;
}

- (unsigned long)ticksecs
{
    return ticksecs;
}

- (GenericArticle *)currentArticle
{
    int what = current-1;

    if(externalChoice!=-1){
	what = externalChoice;
	externalChoice = -1;
    }

    if(articles==nil || what>=[articles count]){
	return nil;
    }

    return [articles objectAtIndex:what];
}

- (int)currentArticleIndex
{
    int what = current-1;

    if(externalChoice!=-1){
	what = externalChoice;
	externalChoice = -1;
    }

    return what;
}


- (NSArray *)articles
{
    return articles;
}

- setExternalChoice:(int)choice
{
    externalChoice = choice;
    return self;
}

- articleText:(id)sender
{
    GenericArticle *art = [self currentArticle];
    if(art==nil){
	NSBeep();
	return self;
    }

    TextWindow *win =
	[[TextWindow alloc]
	    initWithTickerWindow:self 
	    article:art
	    number:[articles indexOfObject:art]
	    attributes:attr];
    [win orderFrontRegardless];
    [win makeMainWindow];

    return self;
}

- articleBrowse:(id)sender
{
    GenericArticle *art = [self currentArticle];
    if(art==nil){
	NSBeep();
	return self;
    }

    [self browse:art text:YES];

    return self;
}

- articleLink:(id)sender
{
    GenericArticle *art = [self currentArticle];
    if(art==nil || [art link]==nil){
	NSBeep();
	return self;
    }

    [self browse:art text:NO];

    return self;
}

- articleShowAll:(id)sender
{
    if(articles==nil || ![articles count]){
	NSBeep();
	return self;
    }

    FeedWindow *feed = 
	[[FeedWindow alloc] initWithTickerWindow:self];
    [feed orderFrontRegardless];
    [feed makeMainWindow];

    return self;
}

- articleUpdate:(id)sender
{
    if(loadInProgress==YES){
	NSBeep();
	return self;
    }

    [self refresh];
    return self;
}


- (BOOL)validateMenuItem:(id <NSMenuItem>)menuItem
{
    GenericArticle *art = [self currentArticle];
    int tag = [menuItem tag];
    if(art==nil || (tag==MENU_LINK && [art link]==nil)){
	return NO;
    }

    return YES;
}

@end
