//
//  AppController.m
//  OpenMoko Flasher
//
//  Created by H. Nikolaus Schaller on 01.08.07.
//  Copyright 2007 Golden Delicious Computers GmbH&Co. KG. All rights reserved.
//  Licensed under GPLv2 - see www.fsf.org
//

#import "AppController.h"

@implementation NSString (Reverse)

- (NSComparisonResult) reverseCaseInsensitiveCompare:(NSString *)aString
{
	NSComparisonResult r=[self caseInsensitiveCompare:aString];
	return -r;	
}

@end

enum
{	// this must match the tag of the popup button values
	TYPE_UBOOT=1,
	TYPE_2,
	TYPE_KERNEL,
	TYPE_SPLASH,
	TYPE_ROOTFS
};

@implementation AppController

- (BOOL) applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication
{
	return YES;
}

- (NSString *) cachePath:(NSString *) package;
{ // path may be either a file or some http:// address
	NSMutableString *r=[package mutableCopy];
	NSString *str;
	[r replaceOccurrencesOfString:@"file://" withString:@"" options:0 range:NSMakeRange(0, [r length])];
	[r replaceOccurrencesOfString:@"http://" withString:@":" options:0 range:NSMakeRange(0, [r length])];
	[r replaceOccurrencesOfString:@"/" withString:@":" options:0 range:NSMakeRange(0, [r length])];
	str=[NSHomeDirectory() stringByAppendingFormat:@"/Library/Caches/OpenMoko Flasher/%@", r];
	[r autorelease];
#if 0
	NSLog(@"cachePath %@ -> %@", package, str);
#endif
	return str;
}

- (NSString *) urlPath:(NSString *) cacheFile;
{ // path may be either a file or some http:// address
	NSMutableString *r=[cacheFile mutableCopy];
	[r replaceOccurrencesOfString:@":" withString:@"/" options:0 range:NSMakeRange(1, [r length]-1)];
	if([r replaceOccurrencesOfString:@":" withString:@"http://" options:0 range:NSMakeRange(0, 1)] == 0)
		[r insertString:@"file://" atIndex:0];		// has no initial :
	[r autorelease];
#if 0
	NSLog(@"urlPath %@ -> %@", cacheFile, r);
#endif
	return r;
}

- (NSString *) serverFor:(NSString *) cacheFile;
{ // path may be either a file or some http:// address
	NSMutableArray *c=[[cacheFile componentsSeparatedByString:@"/"] mutableCopy];
	NSString *r;
	[c removeLastObject];
	r=[c componentsJoinedByString:@"/"];
	[c release];
#if 0
	NSLog(@"serverFor %@ -> %@", cacheFile, r);
#endif
	return r;
}

- (BOOL) downloading:(NSString *) pack;
{ // check if we are currently downloading this package
	NSEnumerator *e=[downloads objectEnumerator];
	NSURLDownload *d;
	while((d=[e nextObject]))
		{
		if([[[[d request] URL] absoluteString] isEqualToString:pack])
			return YES;
		}
	return NO;
}

- (BOOL) cached:(NSString *) pack;
{ // already cached?
	return [[NSFileManager defaultManager] fileExistsAtPath:[self cachePath:pack]];
}

- (void) loadCached;
{ // loads all from cache
	NSString *dir=[self cachePath:@""];
	NSArray *cached=[[NSFileManager defaultManager] directoryContentsAtPath:dir];
	NSEnumerator *e=[cached objectEnumerator];
	NSString *cacheFile;
	[packages removeAllObjects];
	while((cacheFile=[e nextObject]))
		{
		NSString *path;
		if([cacheFile hasPrefix:@"."])
			continue;	// skip
		if([cacheFile length] < 8)
			continue;	// too short (is some cache file)
		path=[self urlPath:cacheFile];
		[packages addObject:path];
		if([path hasPrefix:@"file:"])
			continue;	// don't add
		path=[self serverFor:path];
		if(![repositories containsObject:path])
			{ // add to known repositories
			[repositories addObject:path];
			[repositoryView reloadData];
			}
		}
	[self filter:nil];
}

- (void) updateButtons;
{
	int row=[table selectedRow];
	BOOL flag=row >= 0;
	NSString *pkg=(row >= 0 && row <[filtered count])?[filtered objectAtIndex:row]:nil;
	[loadButton setEnabled:flag && ![self cached:pkg] && ![self downloading:pkg]];
//	[stopButton setEnabled:flag && [self downloading:pkg]];
	[removeButton setEnabled:flag && ([self cached:pkg] || [self downloading:pkg])];	// can also stop a download
	[removeButton setTitle:[self downloading:pkg]?@"Cancel":@"Uncache"];
	[refreshButton setEnabled:[[repositoryView stringValue] hasPrefix:@"http://"]];
	[flashButton setEnabled:flag && [self cached:pkg] && ![self downloading:pkg] && !flasher];
	[progress setHidden:!(flasher || [downloads count] > 0)];
	if(![progress isHidden])
		[progress startAnimation:nil];
}

- (void) awakeFromNib;
{
	[table setDoubleAction:@selector(doubleClick:)];
	[self tableViewSelectionDidChange:nil];
	packages=[[NSMutableArray alloc] initWithCapacity:100];
	moreInfo=[[NSMutableDictionary alloc] initWithCapacity:100];
	downloads=[[NSMutableArray alloc] initWithCapacity:5];
	repositories=[[[NSBundle mainBundle] objectForInfoDictionaryKey:@"Repositories"] mutableCopy];
	[repositoryView reloadData];
	[repositoryView setStringValue:[repositories objectAtIndex:1]];	// preselect first (should we remember by user defaults?)
#if 0
	NSLog(@"repositories=%@", repositories);
#endif
	[[NSFileManager defaultManager] createDirectoryAtPath:[self cachePath:@""] attributes:nil];
	[self loadCached];
	[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(readNotification:) name:NSFileHandleReadCompletionNotification object:nil];
}

- (void) applicationDidFinishLaunching:(NSNotification *)aNotification
{ // refresh initial repository
	[self performSelector:@selector(refresh:) withObject:nil afterDelay:0.1];
}

- (id) comboBox:(NSComboBox *)aComboBox objectValueForItemAtIndex:(int)index
{
	return [repositories objectAtIndex:index];
}

- (int) numberOfItemsInComboBox:(NSComboBox *)aComboBox
{
#if 0
	NSLog(@"items=%d", [repositories count]);
#endif
	return [repositories count];
}

- (IBAction) refresh:(id) sender;	// refresh list
{
	NSString *repos=[repositoryView stringValue];
	NSURL *url=[NSURL URLWithString:repos];
	NSError *err;
	NSString *str;
//	NSAttributedString *astr;
//	NSDictionary *attribs;
	if(!url)
			{
				[self updateButtons];
				[self filter:nil];
				return;
			}
	[progress setHidden:NO];
	[progress startAnimation:nil];
//	astr=[[NSAttributedString alloc] initWithURL:url options:nil documentAttributes:&attribs error:&err];
	str=[NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:&err];
	[self loadCached];
	if(!str)
		{
		NSRunAlertPanel(@"OpenMoko Flasher", @"Can't access %@ due to %@", @"Ok", nil, nil, url, err);
		}
	else
		{
		unsigned pos=0;
		unsigned len=[str length];
		while(YES)
			{
				// should be reworked to use NSScanner!
				
			NSRange f=[str rangeOfString:@"href=\"" options:NSCaseInsensitiveSearch range:NSMakeRange(pos, len-pos)];
			NSRange g;
			NSString *name;
			NSString *date;
			if(f.location == NSNotFound)
				break;	// no more locations
			f.location+=f.length;	// advance to start of href string
			g=[str rangeOfString:@"\">" options:NSCaseInsensitiveSearch range:NSMakeRange(f.location, len-f.location)];	// find closing location
			if(g.location == NSNotFound)
				break;	// some error
			name=[str substringWithRange:NSMakeRange(f.location, g.location-f.location)];
			if([[repositoryView stringValue] hasSuffix:@"/"])
				name=[[repositoryView stringValue] stringByAppendingString:name];
			else
				name=[[repositoryView stringValue] stringByAppendingFormat:@"/%@", name];	// make URL package name
//			NSLog(@"pkg=%@--", name);
			if([name hasSuffix:@".bin"] ||
			   [name hasSuffix:@".tgz"] ||
			   [name hasSuffix:@".gz"] ||
			   [name hasSuffix:@".jffs2"])
				{ // candidate
					if(![packages containsObject:name])
							{ // new
								[packages addObject:name];
								[moreInfo setObject:[NSMutableDictionary dictionaryWithCapacity:3] forKey:name];
							}
					g=[str rangeOfString:@"</a> " options:NSCaseInsensitiveSearch range:NSMakeRange(g.location, len-g.location)];	// find closing location
				if(g.location != NSNotFound)
						{
							int pos=g.location+g.length;
							while(pos < [str length] && [str characterAtIndex:pos] == ' ')
								pos++;
							date=[str substringWithRange:NSMakeRange(pos, 17)];
						}
				else
					date=@"?";
				// should convert to real NSDate for better sorting
				NSLog(@"add date=%@ name=%@", date, name);
				[[moreInfo objectForKey:name] setObject:date forKey:@"date"];
				}
			pos=g.location;
			}
		}
	[self updateButtons];
	[self filter:nil];
}

- (IBAction) add:(id) sender;		// manually add file to cache
{
	NSOpenPanel *o=[NSOpenPanel openPanel];
	NSString *pack;
	if([o runModalForDirectory:nil file:nil types:[NSArray arrayWithObjects:@"jffs2", @"bin", @"gz", nil]] != NSFileHandlingPanelOKButton)
		return;	// ignore
	pack=[@"file://" stringByAppendingString:[[o filename] lastPathComponent]];
	if([[NSFileManager defaultManager] fileExistsAtPath:[self cachePath:pack]])
		NSRunAlertPanel(@"OpenMoko Flasher", @"Already exists in cache: %@", @"Ok", nil, nil, [o filename]);
	else if(![[NSFileManager defaultManager] copyPath:[o filename] toPath:[self cachePath:pack] handler:nil])
		NSRunAlertPanel(@"OpenMoko Flasher", @"Can't add %@", @"Ok", nil, nil, [o filename]);
	else
		{
		if(![packages containsObject:pack])
			[packages addObject:pack];	// cache as file:name
		}
	[self filter:nil];	// reload
}

- (IBAction) remove:(id) sender;		// remove from cache
{
	NSString *path;
	NSString *package;
	int row=[table selectedRow];
	NSEnumerator *e=[downloads objectEnumerator];
	NSURLDownload *d;
	if(row < 0)
		return;
	package=[filtered objectAtIndex:row];
	path=[self cachePath:package];
	while((d=[e nextObject]))
		{
		NSURL *url=[[d request] URL];
		if([[url absoluteString] isEqualToString:package])
			{
			[d cancel];
#if 1	// cancel should have deleted the file?
			[[NSFileManager defaultManager] removeFileAtPath:path handler:nil];
#endif
			[downloads removeObject:d];
			NSLog(@"cancelled %@", d);
			NSLog(@"downloads %@", downloads);
			[self filter:nil];	// show result
			return;
			}
		}
	if(![self cached:package])
		return;	// not cached
	if(NSRunAlertPanel(@"OpenMoko Flasher", @"Do you really want to delete %@ from the cache?", @"Ok", @"Cancel", nil, package) != NSAlertDefaultReturn)
		return;
	[[NSFileManager defaultManager] removeFileAtPath:path handler:nil];
	[self filter:nil];	// show result
}

- (IBAction) load:(id) sender;		// load selected to cache
{
	NSString *path;
	NSString *package;
	int row=[table selectedRow];
	if(row < 0)
		return;
	package=[filtered objectAtIndex:row];
	path=[self cachePath:package];
	if(![self cached:package] || ![self downloading:package])
		{ // does not exist
		NSURL *url=[NSURL URLWithString:package];
		NSURLRequest *request=[NSURLRequest requestWithURL:url];
		NSURLDownload *download=[[NSURLDownload alloc] initWithRequest:request delegate:self];
		NSLog(@"url=%@", url);
		NSLog(@"request=%@", request);
		[download setDestination:path allowOverwrite:YES];
		[download setDeletesFileUponFailure:YES];
		[downloads addObject:download];	// add to list
		NSLog(@"downloads=%@", downloads);
		[download release];
		[self filter:nil];	// needs a refresh to show downloading status
		}
}

- (void) readNotification:(NSNotification *) n;
{
	NSFileHandle *f=[n object];
	NSData *d=[[n userInfo] objectForKey:NSFileHandleNotificationDataItem];
	NSAttributedString *astr;
	NSString *str;
	if([d length] == 0)
		{
		NSLog(@"flashing done");
		[flasher waitUntilExit];
		if([flasher terminationStatus] == 0)
			{ // flashed ok
			[[NSUserDefaults standardUserDefaults] setObject:[currentFile lastPathComponent] forKey:[[region selectedItem] title]];
			}
		else
			NSRunAlertPanel(@"Access OpenMoko", @"Flashing failed. Try to unplug/replug the USB connection. And, try twice.", @"Ok", nil, nil);
		[table reloadData];	// update status
		[flasher release];
		flasher=nil;
		[currentFile release];
		[self updateButtons];	// stop progress indicator
		return;	// done
		}
	NSLog(@"received %d bytes", [d length]);
	str=[[NSString alloc] initWithData:d encoding:NSUTF8StringEncoding];
	astr=[[NSAttributedString alloc] initWithString:str];
	[str release];
	[[logView textStorage] appendAttributedString:astr];
	[astr release];
	[logView scrollRangeToVisible:NSMakeRange([[logView string] length], 0)];	// scroll to bottom
	[[logView window] display];
	[f readInBackgroundAndNotify];	// continue
}

- (int) packageType:(NSString *) file
{
	if([file hasPrefix:@"u-boot-"] && [file hasSuffix:@".bin"])
		return TYPE_UBOOT;
	if([file hasSuffix:@"-u-boot.bin"])
		return TYPE_UBOOT;
	if([file hasSuffix:@"u-boot_env"])
		return TYPE_2;
	if([file hasPrefix:@"uImage-"] && [file hasSuffix:@".bin"])
		return TYPE_KERNEL;
	if([file hasSuffix:@".uImage.bin"])
		return TYPE_KERNEL;
	if([file hasSuffix:@".splash.gz"])
		return TYPE_SPLASH;
	if([file hasSuffix:@".jffs2"])
		return TYPE_ROOTFS;
	return NSNotFound;
}

- (IBAction) flash:(id) sender;		// flash selected file from cache
{
	NSBundle *b=[NSBundle mainBundle];
	NSString *path;
	NSString *package;
	NSPipe *pipe;
	int type;
	NSString *mappedType=nil;
	int row=[table selectedRow];
	if(row < 0)
		return;
	if([self downloading:package])
		return;
	package=[filtered objectAtIndex:row];
	type=[self packageType:[package lastPathComponent]];
	switch(type)
		{
			case TYPE_UBOOT: mappedType=@"u-boot"; break;
			case TYPE_2: mappedType=@"2"; break;
			case TYPE_KERNEL: mappedType=@"kernel"; break;
			case TYPE_SPLASH: mappedType=@"splash"; break;
			case TYPE_ROOTFS: mappedType=@"rootfs"; break;
			default:
				return;	// unknown type
		}
	path=[self cachePath:package];
	if(!path)
		return;	// can't load
	currentFile=[path retain];
#if 1
	{
	NSString *cmd=[NSString stringWithFormat:@"export DYLD_LIBRARY_PATH=\"%@:${DYLD_LIBRARY_PATH}\"; '%@' -a %@ -R -D '%@'", 
			[b resourcePath], [b pathForAuxiliaryExecutable:@"dfu-util"], mappedType, path];
	NSLog(@"\n%@", cmd);
	}
#endif
	if(NSRunAlertPanel(@"Access OpenMoko", @"Do you really want to flash a new file?\nThis may damage your device!\n\nTo turn on Flashing mode, press the AUX buton while switching on the OpenMoko. Leave the device in the BOOT menu. If it fails, unplug/replug USB. And try again (twice is better than once).", @"Ok", @"Cancel", nil) != NSOKButton)
		return;
	flasher=[[NSTask alloc] init];
#if 0
	[flasher setLaunchPath:@"/bin/echo"];
#else
	[flasher setLaunchPath:[b pathForAuxiliaryExecutable:@"dfu-util"]];
#endif
	[flasher setArguments:[NSArray arrayWithObjects:
//		@"dfu-util",
		@"-a",
		mappedType,
		@"-R",
		@"-D",
		path,
		nil]];
	[flasher setEnvironment:[NSDictionary dictionaryWithObjectsAndKeys:
		[b resourcePath], @"DYLD_LIBRARY_PATH",
		nil]];
#if 1
	NSLog(@"%@ %@ %@", [flasher launchPath], [flasher arguments], [flasher environment]);
	// write virtual command(s) to log window
#endif
	pipe=[NSPipe pipe];
	[flasher setStandardOutput:pipe];
	[flasher setStandardError:pipe];
	[[pipe fileHandleForReading] readInBackgroundAndNotify];
	[flasher launch];
	[self updateButtons];	// start progress indicator
}

// should be effective for every change!!!

- (IBAction) didchange:(id) sender;
{
	// check if it really did change...
	if(sender == repositoryView)
			{
				[self refresh:nil];
			}
	else
			{
				[self filter:nil];	// update filter
				[self updateButtons];
			}
}

- (IBAction) filter:(id) sender;	// change filter
{
	[filtered release];	// clear cache
	filtered=nil;
	[table reloadData];
}

- (IBAction) go:(id) sender;
{
	[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:[repositoryView stringValue]]];
	[sender setState:NSOffState];
}

- (BOOL) validateMenuItem:(id <NSMenuItem>)menuItem
{
	NSString *action=NSStringFromSelector([menuItem action]);
	if([action isEqualToString:@"load:"])
		return [loadButton isEnabled];
	if([action isEqualToString:@"flash:"])
		return [flashButton isEnabled];
	return YES;
}

// table callbacks

- (IBAction) doubleClick:(id) sender;
{
	int row=[sender clickedRow];
	NSString *pack;
	if(row < 0)
		return;
	pack=[filtered objectAtIndex:row];
	if([self cached:pack])
		[[NSWorkspace sharedWorkspace] selectFile:[self cachePath:pack] inFileViewerRootedAtPath:@""];
}

- (int) numberOfRowsInTableView:(NSTableView *)aTableView
{
	if(!filtered)
		{
		int tag=[[region selectedItem] tag];
		NSEnumerator *e=[packages objectEnumerator];
		NSString *package;
		NSString *repos=[repositoryView stringValue];
		if([repos hasSuffix:@"/"])
			repos=[repos substringToIndex:[repos length]-1];
		filtered=[[NSMutableArray alloc] initWithCapacity:100];
		while((package=[e nextObject]))
			{
			NSString *loc=[self serverFor:package];
			NSString *file=[package lastPathComponent];
			if([repos hasPrefix:@"http://"] && ![loc isEqualToString:repos])
				{
#if 0
				NSLog(@"no match: %@ - %@", loc, repos);
#endif
				continue;	// filter by repository
				}
			if(tag != 0 && [self packageType:file] != tag)
				continue;	// didn't pass filter
			[filtered addObject:package];	// did go through filter
			}
		[filtered sortUsingSelector:@selector(reverseCaseInsensitiveCompare:)];
		[self updateButtons];	// may change button status
		}
	return [filtered count];
}

- (id) tableView:(NSTableView *)aTableView objectValueForTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex
{
	NSString *ident=[aTableColumn identifier];
	if([ident isEqualToString:@"status"])
		{ // get status
		NSString *name=[filtered objectAtIndex:rowIndex];
		NSString *current=[[NSUserDefaults standardUserDefaults] objectForKey:[[region selectedItem] title]];
		if([current isEqualToString:name])
			return @"Flashed";
		if([self downloading:name])
			return @"Loading";
		if([self cached:name])
			return @"Cached";
		return @"";
		}
	if([ident isEqualToString:@"type"])
		{
		switch([self packageType:[[filtered objectAtIndex:rowIndex] lastPathComponent]])
			{
			case TYPE_UBOOT:	return @"BootLD";
			case TYPE_KERNEL: return @"Kernel";
			case TYPE_SPLASH: return @"Splash";
			case TYPE_ROOTFS: return @"RootFS";
			default: @"?";
			}
		}
	if([ident isEqualToString:@"package"])
		{
		if([[repositoryView stringValue] hasPrefix:@"http://"])
			return [[filtered objectAtIndex:rowIndex] lastPathComponent];	// show filename only
		return [filtered objectAtIndex:rowIndex];	// show full path
		}
	if([ident isEqualToString:@"date"])
		{
		NSString *package=[filtered objectAtIndex:rowIndex];
		NSDictionary *more=[moreInfo objectForKey:package];
		return [more objectForKey:@"date"];
		}
	if([ident isEqualToString:@"size"])
		{
		NSString *package=[filtered objectAtIndex:rowIndex];
		if([self cached:package])
			{ // get cached file size
			NSString *path=[self cachePath:package];
			NSDictionary *stat=[[NSFileManager defaultManager] fileAttributesAtPath:path traverseLink:YES];
			if(stat)
				{ // get (real) file size
				return [[stat objectForKey:NSFileSize] description];
				}
			}
		return @"?";
		}
	return @"?";
}

- (void) tableViewSelectionDidChange:(NSNotification *)aNotification
{
	[self updateButtons];
}

- (void) downloadDidFinish:(NSURLDownload *)download
{
	NSLog(@"download finished %@", download);
	[downloads removeObject:download];
	[self filter:nil];
}

- (void)download:(NSURLDownload *)download didReceiveDataOfLength:(unsigned)length
{ // show new file length
	static NSDate *last;
	if(!last || [[NSDate date] timeIntervalSinceDate:last] > 0.2)
		{ // refresh
		[table reloadData];
		[last release];
		last=[[NSDate alloc] init];
		}
}

- (void) download:(NSURLDownload *)download didFailWithError:(NSError *)error
{
	NSLog(@"download error %@ for %@", error, download);
	[downloads removeObject:download];
	[self filter:nil];
}

@end

