#import "MyDocument.h"

static NSArray* gAvailableModes = nil;

static NSString* CopySymbol = nil;
static NSString* DeleteSymbol = nil;
static NSString* SetAttributesSymbol = nil;

static NSAttributedString* SuccessSymbol = nil;
static NSAttributedString* ErrorSymbol = nil;
static NSAttributedString* PendingSymbol = nil;
static NSAttributedString* NoSymbol = nil;
static NSAttributedString* CancelSymbol = nil;

static NSImage* FolderIcon = nil;
static NSImage* FileIcon = nil;

// Added in version 1.1:
// This singleton array contains all attributes supported by
// NSFileManager's changeFileAttributes:atPath
static NSArray* SupportedAttributes = nil;

#define SXOP_COPY [NSString stringWithFormat: @"%C", 0x2794]
#define SXOP_DELETE [NSString stringWithFormat: @"%C", 0x2704]
#define SXOP_SETATTR [NSString stringWithFormat: @"%C", 0x270d]

#define SXRES_SUCCESS [NSString stringWithFormat: @"%C", 0x2714]
#define SXRES_ERROR [NSString stringWithFormat: @"%C", 0x2718]
#define SXRES_PENDING [NSString stringWithFormat: @"%C", 0x261e]
#define SXRES_NONE @" "
#define SXRES_CANCEL [NSString stringWithFormat: @"%C", 0x2297]

@interface MyDocument (PrivateAPI)

// the workhorse
// this method performs the actual comparison of src and dst
// pass @"." as path to compare the whole content
// the op*** parameters decide what to do if a particular
// situation occurs (passing nil causes no action)
// the method exits immediately when userStop is YES
// setAttrInDst causes the attributes in destination
// to be set according to the attributes in the source
- (BOOL) traverseFolder:(NSString*)path 
			 relativeTo:(NSString*)src
			compareWith:(NSString*)dst
putOperationsInSchedule:(NSMutableArray*)currentSchedule
   opIfDoesntExistInDst:(NSString*)opIfDoesntExistInDst
	 opIfDifferentTypes:(NSString*)opIfDifferentTypes
		   opIfSrcNewer:(NSString*)opIfSrcNewer
		   opIfDstNewer:(NSString*)opIfDstNewer
		   setAttrInDst:(BOOL)setAttrInDst
			   srcColor:(NSColor*)srcColor
			   dstColor:(NSColor*)dstColor;

// executed in a separate thread
// depending on the project mode, executes
// traverseFolder:... once or twice
// (src vs dst and eventually dst vs src with different ops)
- (void) beginPreparationOfSchedule;

// check if the parameter is a folder (directory)
+ (BOOL) isFolder:(NSString*)path;

// called at the end of the "path is not a folder" alert sheet
- (void) pathNotFolderErrorEnd:(NSAlert*)alert returnCode:(int)rc contextInfo:(void*)ci;

// called at the end of the "src and dst are the same" alert sheet
- (void) sameFoldersErrorEnd:(NSAlert*)alert returnCode:(int)rc contextInfo:(void*)ci;

// show "path is not a folder" alert sheet
- (void) showPathIsNotFolderError:(NSString*)path;

// those are just convenience methods to simplify the interaction
// with an array controller in multi-threaded application
-(void)setScheduleStatus:(NSAttributedString*)newSymbol;
-(void)setSchedulePosition:(NSNumber*)newPosition;
-(void)setScheduleProgress:(NSNumber*)newProgress;

// executes the current schedule in a separate thread
// opens the progress sheet and updates the schedule
// array accordingly (using KVC, so GUI is updated automatically)
// creates the errors array if some operations fail
-(void)beginExecutionOfSchedule;

// returns an array containing supported attribute names
+(NSArray*) supportedAttributes;

// extract supported attributes from the given set of attributes
+(NSDictionary*) supportedAttributesFrom:(NSDictionary*)attr;

@end
///////////////////////////////////////////////////////////////////////

@implementation MyDocument

+ (void) initialize
{
	static BOOL initialized = NO;
	if (! initialized)
	{
		CopySymbol = [[NSString alloc] initWithString: SXOP_COPY];
		DeleteSymbol = [[NSString alloc] initWithString: SXOP_DELETE];
		SetAttributesSymbol = [[NSString alloc] initWithString: SXOP_SETATTR];
		SuccessSymbol = [[NSAttributedString alloc] initWithString: SXRES_SUCCESS
														attributes: [NSDictionary dictionaryWithObject: [NSColor greenColor] forKey: NSForegroundColorAttributeName]];
		ErrorSymbol = [[NSAttributedString alloc] initWithString: SXRES_ERROR
													  attributes: [NSDictionary dictionaryWithObject: [NSColor redColor] forKey: NSForegroundColorAttributeName]];
		PendingSymbol = [[NSAttributedString alloc] initWithString: SXRES_PENDING
														attributes: [NSDictionary dictionaryWithObject: [NSColor blueColor] forKey: NSForegroundColorAttributeName]];
		NoSymbol = [[NSAttributedString alloc] initWithString: SXRES_NONE
												   attributes: [NSDictionary dictionaryWithObject: [NSColor textColor] forKey: NSForegroundColorAttributeName]];
		CancelSymbol = [[NSAttributedString alloc] initWithString: SXRES_CANCEL
													   attributes: [NSDictionary dictionaryWithObject: [NSColor redColor] forKey: NSForegroundColorAttributeName]];
		FolderIcon = [[NSImage alloc] initWithContentsOfFile: [[NSBundle mainBundle] pathForImageResource:@"foldericon"]];
		FileIcon = [[NSImage alloc] initWithContentsOfFile: [[NSBundle mainBundle] pathForImageResource:@"fileicon"]];		
	}
}

- (id) initWithType:(NSString*) typeName error:(NSError**) outError
{
	self = [super initWithType: typeName error: outError];
	if (self)
	{
		project = [[NSMutableDictionary alloc] init];
		schedule = nil;
		errors = nil;
	}
	return self;
}

- (void) dealloc
{
	if (project)
		[project release];
	if (schedule)
		[schedule release];
	if (errors)
		[errors release];
	[super dealloc];
}

- (NSString *)windowNibName
{
    // Override returning the nib file name of the document
    // If you need to use a subclass of NSWindowController or if your document supports multiple NSWindowControllers, you should remove this method and override -makeWindowControllers instead.
    return @"MyDocument";
}

- (void)windowControllerDidLoadNib:(NSWindowController *) aController
{
    [super windowControllerDidLoadNib:aController];
    // Add any code here that needs to be executed once the windowController has loaded the document's window.
	[scheduleTable setDoubleAction:@selector(scheduleTableDoubleClicked:)];
	[scheduleTable setTarget:self];
}

- (BOOL)prepareSavePanel:(NSSavePanel *)savePanel
{
	// set the default save location to the synxer project folder
	NSString* folder = [[NSUserDefaults standardUserDefaults] stringForKey: @"ProjectFolder"];
	[[NSUserDefaults standardUserDefaults] setValue:folder forKey:@"NSNavLastRootDirectory"];	
	[[NSUserDefaults standardUserDefaults] setValue:folder forKey:@"NSNavLastCurrentDirectory"];	
	return [super prepareSavePanel:savePanel];
}

- (NSData *)dataRepresentationOfType:(NSString *)aType
{
    // Insert code here to write your document from the given data.  You can also choose to override -fileWrapperRepresentationOfType: or -writeToFile:ofType: instead.    
    // For applications targeted for Tiger or later systems, you should use the new Tiger API -dataOfType:error:.  In this case you can also choose to override -writeToURL:ofType:error:, -fileWrapperOfType:error:, or -writeToURL:ofType:forSaveOperation:originalContentsURL:error: instead.	
	return [NSKeyedArchiver archivedDataWithRootObject: project];
}

- (BOOL)saveToURL:(NSURL *)absoluteURL ofType:(NSString *)typeName forSaveOperation:(NSSaveOperationType)saveOperation error:(NSError **)outError
{
	BOOL result = [super saveToURL:absoluteURL ofType:typeName forSaveOperation:saveOperation error:outError];
	[[NSNotificationCenter defaultCenter] postNotificationName: @"SXProjectSaved" object: self]; 
	return result;
}

- (BOOL) readFromData:(NSData*)data ofType:(NSString*)aType error:(NSError**)error
{
	schedule = nil;
	errors = nil;
	NS_DURING
		project = [[NSKeyedUnarchiver unarchiveObjectWithData: data] retain];
	NS_HANDLER
		project = nil;
		if (error)
		{
			*error = [NSError errorWithDomain: NSCocoaErrorDomain
										 code: NSFileReadCorruptFileError 
									 userInfo: nil];			
		}
		NS_VALUERETURN(NO, BOOL);
	NS_ENDHANDLER
    return YES;
}

- (NSMutableDictionary*) project
{
	return project;
}

- (void) setProject: (NSMutableDictionary*) newProject
{
	if (newProject != project)
	{
		[newProject retain];
		if (project)
			[project autorelease];
		project = newProject;
	}
}


- (NSMutableArray*) schedule
{
	return schedule;
}

- (void) setSchedule:(NSMutableArray*)newSchedule
{
	if (schedule != newSchedule)
	{
		[newSchedule retain];
		[schedule release];
		schedule = newSchedule;
		[scheduleController setContent: schedule];
	}
}

- (NSMutableArray*) errors
{
	return errors;
}

- (void) setErrors:(NSMutableArray*)newErrors
{
	if (errors != newErrors)
	{
		[newErrors retain];
		[errors release];
		errors = newErrors;
		[errorsController setContent: errors];
	}
}

+ (NSArray*) availableModes
{
	if (! gAvailableModes)
	{
		gAvailableModes = [[NSArray arrayWithObjects:
			NSLocalizedString(@"Overwrite",@"[1]"),
			NSLocalizedString(@"Complement", @"[1]"),
			NSLocalizedString(@"Synchronize",@"[1]"),
			nil] retain];
	}
	return gAvailableModes;
}

- (NSArray*) availableModes
{
	return [[self class] availableModes];
}

- (BOOL) traverseFolder:(NSString*)path 
			 relativeTo:(NSString*)src
			compareWith:(NSString*)dst
putOperationsInSchedule:(NSMutableArray*)currentSchedule
   opIfDoesntExistInDst:(NSString*)opIfDoesntExistInDst
	 opIfDifferentTypes:(NSString*)opIfDifferentTypes
		   opIfSrcNewer:(NSString*)opIfSrcNewer
		   opIfDstNewer:(NSString*)opIfDstNewer
		   setAttrInDst:(BOOL)setAttrInDst
			   srcColor:(NSColor*)srcColor
			   dstColor:(NSColor*)dstColor
{
	NSFileManager* fm = [NSFileManager defaultManager];
	NSString* fullPath = [src stringByAppendingPathComponent: path];
	NSDirectoryEnumerator* de = [fm enumeratorAtPath: fullPath];
	if (! de)
		return NO;
	NSString* subpath;
	NSString* omitPrefix = nil;
	NSRange r;
	NSDictionary* attrSrc;
	NSDictionary* attrDst;
	BOOL dstExists;
	NSString* srcFullPath;
	NSString* dstFullPath;
	NSString* operation;
	int srcType;
	int dstType;
	BOOL dstLocked;
	float ComparisonTolerance = [[[NSUserDefaults standardUserDefaults] valueForKey:@"ComparisonTolerance"] floatValue];
	
	while (((subpath=[de nextObject]) != nil) && (!userStop))
	{
		if (omitPrefix != nil)
		{
			r = [subpath rangeOfString: omitPrefix];
			if (r.location == 0)
				continue;
			[omitPrefix release];
			omitPrefix = nil;
		}
		
		// prepare absolute paths for src and dst
		srcFullPath = [src stringByAppendingPathComponent:subpath];
		dstFullPath = [dst stringByAppendingPathComponent:subpath];
		
		// check if dst exists
		// we can't use just fileExistsAtPath: because it traverses symlinks
		// while fileExistsAtPath:isDirectory: does not
		dstExists = [fm fileExistsAtPath:dstFullPath isDirectory:NULL];
		
		// get attributes for src and dst (if dst exists)
		attrSrc = [fm fileAttributesAtPath:srcFullPath traverseLink:NO];
		if (dstExists)
		{
			attrDst = [fm fileAttributesAtPath:dstFullPath traverseLink:NO];
		}
		else
			attrDst = nil;
		
		// determine the source type (file, directory, symlink)
		if ([[attrSrc objectForKey:NSFileType] isEqualTo:NSFileTypeDirectory])
			srcType = SXTYPE_FOLDER;
		else if ([[attrSrc objectForKey:NSFileType] isEqualTo:NSFileTypeSymbolicLink])
			srcType = SXTYPE_LINK;
		else
			srcType = SXTYPE_FILE;
		
		// determine the destination type (file, directory, symlink)
		if (dstExists)
		{
			if ([[attrDst objectForKey:NSFileType] isEqualTo:NSFileTypeDirectory])
				dstType = SXTYPE_FOLDER;
			else if ([[attrDst objectForKey:NSFileType] isEqualTo:NSFileTypeSymbolicLink])
				dstType = SXTYPE_LINK;
			else
				dstType = SXTYPE_FILE;			
			dstLocked = [[attrDst objectForKey:NSFileImmutable] boolValue];
		}
		else
		{
			dstType = SXTYPE_NONEXISTING;
			dstLocked = NO;
		}

		operation = nil;

		// 1. dst doesn't exist
		if (!dstExists)
		{
			operation = opIfDoesntExistInDst;
		}
		else
		{
			// 2. dst exists but is of different type than src
			if (srcType != dstType)
			{
				operation = opIfDifferentTypes;
			}
			else
			{
				// 3. dst exists and is of the same type as src
				NSDate* srcDate;
				NSDate* dstDate;
				srcDate = [attrSrc objectForKey:NSFileModificationDate];
				dstDate = [attrDst objectForKey:NSFileModificationDate];
				NSComparisonResult srcVsDstDate = [srcDate compare:dstDate];
				NSTimeInterval dateDifference = [srcDate timeIntervalSinceDate:dstDate];

				// if symbolic links, just check if they're the same
				// comparing dates is meaningless, but in case of
				// synchronization we have to decide which way to copy
				if (srcType == SXTYPE_LINK)
				{
					if (![fm contentsEqualAtPath:srcFullPath andPath:dstFullPath])
					{
						// check creation dates
						if (srcVsDstDate == NSOrderedAscending)
							operation = opIfDstNewer;
						else
							operation = opIfSrcNewer;
					}					
				}
				// files: check the date difference with the given
				// tolerance (usually 1s)
				else if (srcType == SXTYPE_FILE)
				{
					if (dateDifference < -ComparisonTolerance)
						operation = opIfDstNewer;						
					else if (dateDifference > ComparisonTolerance)
						operation = opIfSrcNewer;						
				} // end of decision about files/directories				
			} // end of handler when src and dst types are the same			
		} // end of handler when dst exists
				
		// Prepare the item of the schedule
		if (operation)
		{
			NSAttributedString* coloredSrc;
			NSAttributedString* coloredDst;
			coloredSrc = [[NSAttributedString alloc] initWithString:srcFullPath attributes:[NSDictionary dictionaryWithObject:srcColor forKey:NSForegroundColorAttributeName]];
			coloredDst = [[NSAttributedString alloc] initWithString:dstFullPath attributes:[NSDictionary dictionaryWithObject:srcColor forKey:NSForegroundColorAttributeName]];
			NSMutableDictionary* newItem = [[NSMutableDictionary alloc] init];
			[newItem setObject:operation forKey:@"operation"];
			[newItem setObject:attrSrc forKey:@"attributes"];			
			[newItem setObject:coloredSrc forKey:@"src"];
			[newItem setObject:coloredDst forKey:@"dst"];
			[newItem setObject:NoSymbol forKey:@"status"];
			[newItem setObject:(srcType == SXTYPE_FOLDER ? FolderIcon : FileIcon) forKey:@"icon"];
			// if overwriting, delete the dst first (mandatory, file manager will fail
			// to copy otherwise)
			if ((operation == CopySymbol) && dstExists)
				[newItem setObject:[NSNumber numberWithBool:YES] forKey:@"deleteFirst"];
			else
				[newItem setObject:[NSNumber numberWithBool:NO] forKey:@"deleteFirst"];
			// set attributes after copying, except symlinks - setting attribute
			// of a symlink traverses the link and changes the attributes of
			// the linked object
			if ((operation == CopySymbol) && srcType != SXTYPE_LINK)
				[newItem setObject:[NSNumber numberWithBool:YES] forKey:@"setAttributes"];
			else
				[newItem setObject:[NSNumber numberWithBool:NO] forKey:@"setAttributes"];			
			[newItem setObject:[NSNumber numberWithBool:dstLocked] forKey:@"locked"];
			[currentSchedule addObject:newItem];
			[coloredSrc release];
			[coloredDst release];
			[newItem release];
			
			[operationCountField setIntValue:[operationCountField intValue]+1];
			[operationCountField performSelectorOnMainThread:@selector(displayIfNeeded) withObject:nil waitUntilDone:NO];			
		}
		
		// If we are copying or deleting a directory, don't check its content
		// 'cause it will be copied recursively anyway
		if ((srcType == SXTYPE_FOLDER) && ((operation == CopySymbol) || (operation == DeleteSymbol)))
		{
			// don't look inside this directory
			omitPrefix = [subpath copy];
		}
	}
	return (! userStop);
}

- (void) beginPreparationOfSchedule
{
	NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
	NSMutableArray* newSchedule = [NSMutableArray new];
	BOOL pres;
	int mode;
	
	mode = [[project objectForKey:@"mode"] intValue];
		
	if (mode == SXMODE_OVERWRITE)
	{
		// process src and overwrite dst if any difference occurs
		pres = [self traverseFolder:@"."
						 relativeTo:[project objectForKey:@"srcFolder"]
						compareWith:[project objectForKey:@"dstFolder"]
			putOperationsInSchedule:newSchedule
			   opIfDoesntExistInDst:CopySymbol
				 opIfDifferentTypes:CopySymbol
					   opIfSrcNewer:CopySymbol
					   opIfDstNewer:CopySymbol
					   setAttrInDst: YES
						   srcColor:[NSColor colorWithCalibratedRed:0.0f green:0.5f blue:0.25f alpha:1.0]
						   dstColor:[NSColor redColor]
			];
		if (pres)
		{
			// process dst and remove any objects not existsing in src
			// don't worry about any differences in existing files -
			// the previous step handled that already
			pres = [self traverseFolder:@"."
							 relativeTo:[project objectForKey:@"dstFolder"]
							compareWith:[project objectForKey:@"srcFolder"]
				putOperationsInSchedule:newSchedule
				   opIfDoesntExistInDst:DeleteSymbol
					 opIfDifferentTypes:nil
						   opIfSrcNewer:nil
						   opIfDstNewer:nil
						   setAttrInDst:NO
							   srcColor:[NSColor redColor]
							   dstColor:[NSColor colorWithCalibratedRed:0.0f green:0.5f blue:0.25f alpha:1.0]
				];
		}		
	}
	else if (mode == SXMODE_COMPLEMENT)
	{
		// process only src, copy all objects not existing in dst
		// and overwrite only older objects
		// if object types differ, leave them as they are
		pres = [self traverseFolder:@"."
						 relativeTo:[project objectForKey:@"srcFolder"]
						compareWith:[project objectForKey:@"dstFolder"]
			putOperationsInSchedule:newSchedule
			   opIfDoesntExistInDst:CopySymbol
				 opIfDifferentTypes:nil
					   opIfSrcNewer:CopySymbol
					   opIfDstNewer:nil
					   setAttrInDst:NO
						   srcColor:[NSColor colorWithCalibratedRed:0.0f green:0.5f blue:0.25f alpha:1.0]
						   dstColor:[NSColor redColor]
			];
	}
	else if (mode == SXMODE_SYNCHRONIZE)
	{
		// process src, copy any newer objects to dst
		pres = [self traverseFolder:@"."
						 relativeTo:[project objectForKey:@"srcFolder"]
						compareWith:[project objectForKey:@"dstFolder"]
			putOperationsInSchedule:newSchedule
			   opIfDoesntExistInDst:CopySymbol
				 opIfDifferentTypes:nil
					   opIfSrcNewer:CopySymbol
					   opIfDstNewer:nil
					   setAttrInDst:NO
						   srcColor:[NSColor colorWithCalibratedRed:0.0f green:0.5f blue:0.25f alpha:1.0]
						   dstColor:[NSColor redColor]
			];
		if (pres)
		{
			// process dst, copy any newer objects to src
			pres = [self traverseFolder:@"."
							 relativeTo:[project objectForKey:@"dstFolder"]
							compareWith:[project objectForKey:@"srcFolder"]
				putOperationsInSchedule:newSchedule
				   opIfDoesntExistInDst:CopySymbol
					 opIfDifferentTypes:nil
						   opIfSrcNewer:CopySymbol
						   opIfDstNewer:nil
						   setAttrInDst:NO
							   srcColor:[NSColor redColor]
							   dstColor:[NSColor colorWithCalibratedRed:0.0f green:0.5f blue:0.25f alpha:1.0]
				];
		}
	}
	else
		pres = NO;
	
	if (pres)
	{
		[self setSchedule: newSchedule];
		[documentTabs selectTabViewItemWithIdentifier: @"2"];
	}
	else
	{
		[self setSchedule: nil];
	}
	[newSchedule release];
	[preparingScheduleIndicator stopAnimation: self];
	[preparingSchedulePanel orderOut: self];
	[NSApp endSheet: preparingSchedulePanel];
	scheduleRunning = NO;
	[pool release];
}

+ (BOOL) isFolder:(NSString*)path
{
	BOOL exists, isdir;
	exists = [[NSFileManager defaultManager] fileExistsAtPath:path isDirectory:&isdir];
	return (exists && isdir);
}

- (void) pathNotFolderErrorEnd:(NSAlert*)alert returnCode:(int)rc contextInfo:(void*)ci
{
	scheduleRunning = NO;
	[alert release];
}

- (void) sameFoldersErrorEnd:(NSAlert*)alert returnCode:(int)rc contextInfo:(void*)ci
{
	scheduleRunning = NO;
	[alert release];
}

- (void) showPathIsNotFolderError:(NSString*)path
{
	NSAlert* a = [[NSAlert alertWithMessageText: NSLocalizedString(@"Invalid folder",@"[1]")
								  defaultButton: NSLocalizedString(@"OK",@"[1]")
								alternateButton: nil
									otherButton: nil
					  informativeTextWithFormat: NSLocalizedString(@"The path %@ does not point to a valid folder (directory). Please check your project settings and try again.",@"[1]"), path
		] retain];
	[a beginSheetModalForWindow: [self windowForSheet]
				  modalDelegate: self
				 didEndSelector: @selector(pathNotFolderErrorEnd:returnCode:contextInfo:)
					contextInfo: NULL];
}

- (IBAction) prepareScheduleForCurrentProject:(id)sender
{
	if (scheduleRunning)
		return;	
	userStop = NO;
	scheduleRunning = YES;
	
	if (![[self class] isFolder:[project objectForKey:@"srcFolder"]])
	{
		[self showPathIsNotFolderError:[project objectForKey:@"srcFolder"]];		
		return;
	}
	
	if (![[self class] isFolder:[project objectForKey:@"dstFolder"]])
	{
		[self showPathIsNotFolderError:[project objectForKey:@"dstFolder"]];
		return;
	}
	if ([[project objectForKey:@"srcFolder"] isEqual:[project objectForKey:@"dstFolder"]])
	{
		NSAlert* a = [[NSAlert alertWithMessageText:NSLocalizedString(@"Invalid folders",@"[1]")
									  defaultButton:NSLocalizedString(@"OK",@"[1]")
									alternateButton:nil
										otherButton:nil
						  informativeTextWithFormat:NSLocalizedString(@"The source and destination folders are the same. It is not possible to synchronize a folder with itself.",@"[1]")
			] retain];
		[a beginSheetModalForWindow:[self windowForSheet]
					  modalDelegate:self
					 didEndSelector:@selector(sameFoldersErrorEnd:returnCode:contextInfo:)
						contextInfo:nil];
		return;		
	}
	
	[operationCountField setIntValue:0];
	
	[NSApp beginSheet: preparingSchedulePanel
	   modalForWindow: [self windowForSheet] 
		modalDelegate: self
	   didEndSelector: nil
		  contextInfo: nil];
	[preparingScheduleIndicator startAnimation: self];
	[NSApplication detachDrawingThread: @selector(beginPreparationOfSchedule) 
							  toTarget: self
							withObject: nil];
}

- (IBAction) stopPreparingSchedule: (id) sender
{
	userStop = YES;
}

- (IBAction) stopExecutingSchedule: (id) sender
{
	userStop = YES;
}

+ (NSString*) selectFolder
{
	NSOpenPanel* openPanel;
	NSString* selectedPath;
	NSString* actualPath;
	openPanel = [NSOpenPanel openPanel];
	[openPanel setCanChooseFiles: NO];
	[openPanel setCanChooseDirectories: YES];
	[openPanel setResolvesAliases: YES];
	if ([openPanel runModal] == NSOKButton)
	{
		selectedPath = [openPanel filename];
		actualPath = [[NSFileManager defaultManager] pathContentOfSymbolicLinkAtPath:selectedPath];
		return (actualPath == nil ? selectedPath : actualPath);
	}
	else
		return nil;
}

- (IBAction) chooseSrcFolder:(id)sender
{
	NSString* folder = [[self class] selectFolder];
	if (folder)
	{
		[self setValue: folder forKeyPath: @"project.srcFolder"];
	}
}

- (IBAction) chooseDstFolder:(id)sender
{
	NSString* folder = [[self class] selectFolder];
	if (folder)
	{
		[self setValue: folder forKeyPath: @"project.dstFolder"];
	}
	
}

- (IBAction) scheduleTableDoubleClicked:(id)sender
{
	NSAttributedString* currentItemStatus;
	currentItemStatus = [[scheduleController selection] valueForKey:@"status"];
	if ([currentItemStatus isEqualTo: NoSymbol])
	{
		[[scheduleController selection] setValue:CancelSymbol forKey:@"status"];
	}
	else
	{
		[[scheduleController selection] setValue:NoSymbol forKey:@"status"];
	}
	
}

-(void)setScheduleStatus:(NSAttributedString*)newSymbol
{
	[[scheduleController selection] setValue:newSymbol forKey:@"status"];
}

-(void)setSchedulePosition:(NSNumber*)newPosition
{
	[scheduleController setSelectionIndex:[newPosition intValue]];
	
	// bugfix in version 1.1:
	// Leopard requires explicite scrolling of NSTableView
	// in Tiger it was enough to use the controller (as above)
	[scheduleTable scrollRowToVisible:[newPosition intValue]];
}

-(void)setScheduleProgress:(NSNumber*)newProgress
{
	[executingScheduleIndicator setDoubleValue:[newProgress doubleValue]];	
}


-(void)beginExecutionOfSchedule
{
	NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
	NSMutableArray* newErrors;
	newErrors = [[NSMutableArray alloc] init];
	NSFileManager* fm = [NSFileManager defaultManager];
	NSString* src;
	NSString* dst;
	NSString* op;
	NSDictionary* attr;
	BOOL delete;
	BOOL overwriteLocked;
	BOOL setAttr;
	BOOL locked;
	int itemCnt, currentItem;
	currentItem = 0;
	itemCnt = [schedule count];
	NSString* errorStr;
	SEL setSelectionSelector = @selector(setSchedulePosition:);
	SEL setStatusSelector = @selector(setScheduleStatus:);
	SEL setProgressSelector = @selector(setScheduleProgress:);
	NSMutableDictionary *dstAttr;
	
	overwriteLocked = [[NSUserDefaults standardUserDefaults] boolForKey:@"OverwriteLocked"];
	
	while (currentItem < itemCnt && !userStop)
	{
		errorStr = nil;
		
		// set the selection in the schedule controller
		// do it on the main thread
		[self performSelectorOnMainThread:setSelectionSelector 
							   withObject:[NSNumber numberWithInt:currentItem] 
							waitUntilDone:YES];
		//[scheduleController setSelectionIndex:currentItem];
		
		// get the current operation
		op = [[scheduleController selection] valueForKey:@"operation"];
				
		// if already done, in progress or canceled, skip this operation
		if (![[[scheduleController selection] valueForKey:@"status"] isEqualTo:NoSymbol])
		{			
			currentItem++;
			[completedOperationsField setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Completed %d out of %d operations in the schedule.",@"[1]"),currentItem,itemCnt]];
			[executingScheduleIndicator setDoubleValue: currentItem];
			continue;			
		}
		// show the "operation pending" symbol
		[self performSelectorOnMainThread:setStatusSelector
							   withObject:PendingSymbol
							waitUntilDone:YES];
		//[[scheduleController selection] setValue:PendingSymbol forKey:@"status"];

		// retrieve the operation parameters from the dictionary
		// use bindings, because selection is a proxy object
		delete = [[[scheduleController selection] valueForKey:@"deleteFirst"] boolValue];
		setAttr = [[[scheduleController selection] valueForKey:@"setAttributes"] boolValue];
		src = [[[scheduleController selection] valueForKey:@"src"] string];
		dst = [[[scheduleController selection] valueForKey:@"dst"] string];
		attr = [[scheduleController selection] valueForKey:@"attributes"];
		locked = [[[scheduleController selection] valueForKey:@"locked"] boolValue];
		
		// copy operation
		if ([op isEqualToString:CopySymbol])
		{
			if (overwriteLocked && locked)
			{
				// try to unlock first
				dstAttr = [[fm fileAttributesAtPath:dst traverseLink:NO] mutableCopy];
				[dstAttr setObject:[NSNumber numberWithBool:NO] forKey:NSFileImmutable];
				[fm changeFileAttributes:dstAttr atPath:dst];
				
				// we don't check the results - there's nothing we can do anyway
			}
			
			// delete before copying?
			if (delete)
			{
				if (![fm removeFileAtPath:dst handler:nil])
				{
					errorStr = [NSString stringWithFormat:
						NSLocalizedString(@"Could not delete %@ before copying %@. Check the permissions in the target file system or remove the write protection.",@"[1]"),
						dst, src
						];
				}
			}
			if (!errorStr)
			{
				// now copy
				if (![fm copyPath:src toPath:dst handler:nil])
				{
					errorStr = [NSString stringWithFormat:
						NSLocalizedString(@"Could not copy %@ to %@. Check the amount of free space in the target file system and make sure you have sufficient permissions.",@"[1]"),
						src, [dst stringByDeletingLastPathComponent]];
				}
				else
				{
					// set attributes after copying
					
					// bugfix in version 1.1:
					// now only the attributes supported by
					// NSFileManager's changeFileAttributes:atPath:
					// are set
					
					if (setAttr)
					{
						if (![fm changeFileAttributes:[[self class] supportedAttributesFrom:attr] atPath:dst])
						{
							errorStr = [NSString stringWithFormat:
								NSLocalizedString(@"Could not set the attributes of %@. Check the permissions and ownership in the target file system.",@"[1]"),
								dst
								];
							NSLog(@"Path: %@, attributes: %@\n",dst,attr);
						}						
					}
				}
			}
		}
		// delete operation
		else if ([op isEqualToString:DeleteSymbol])
		{
			if (![fm removeFileAtPath:src handler:nil])
			{
				errorStr = [NSString stringWithFormat:
					NSLocalizedString(@"Could not delete %@. Check the permissions in the target file system or remove the write protection.",@"[1]"),
					src
					];				
			}
		}
		// set attributes operation
		else if ([op isEqualToString:SetAttributesSymbol])
		{
			if (![fm changeFileAttributes:attr atPath:dst])
			{
				errorStr = [NSString stringWithFormat:
					NSLocalizedString(@"Could not set attributes of %@. Check the permissions in the target file system or remove the write protection.",@"[1]"),
					src
					];								
			}
		}
		
		// if error occured, add an item to the errors array		
		if (errorStr)
		{
			NSMutableDictionary* errorItem = [NSMutableDictionary new];
			[errorItem setObject:errorStr forKey:@"description"];
			[errorItem setObject:op forKey:@"operation"];
			[errorItem setObject:[[scheduleController selection] valueForKey:@"icon"] forKey:@"icon"];
			[newErrors addObject:errorItem];
			[errorItem release];
			[self performSelectorOnMainThread:setStatusSelector
								   withObject:ErrorSymbol
								waitUntilDone:YES];
			//[[scheduleController selection] setValue:ErrorSymbol forKey:@"status"];
		}
		else
		{
			[self performSelectorOnMainThread:setStatusSelector
								   withObject:SuccessSymbol
								waitUntilDone:YES];
			//[[scheduleController selection] setValue:SuccessSymbol forKey:@"status"];
		}
		
		currentItem++;
		// update the progress sheet
		[completedOperationsField setStringValue:[NSString stringWithFormat:NSLocalizedString(@"Completed %d out of %d operations in the schedule.",@"[1]"),currentItem,itemCnt]];
		// update the progress indicator in the main thread using a wrapper
		[self performSelectorOnMainThread:setProgressSelector
							   withObject:[NSNumber numberWithInt:currentItem]
							waitUntilDone:NO];
	}

	// set the new errors array
	[self setValue:newErrors forKey:@"errors"];
	[newErrors release];
	// close the progress sheet
	[executingSchedulePanel orderOut: self];
	[NSApp endSheet: executingSchedulePanel];
	// if there are any errors, show the errors tab
//	if ([newErrors count]>0)
//	{
//		[documentTabs selectTabViewItemWithIdentifier:@"3"];
//	}
	// always show the errors tab, so that the user can close
	// the project by hitting Return
	[documentTabs selectTabViewItemWithIdentifier:@"3"];
	scheduleRunning = NO;
	[pool release];
}

- (void) emptyScheduleErrorEnd:(NSAlert*)alert returnCode:(int)rc contextInfo:(void*)ci
{
	scheduleRunning = NO;
	[alert release];
}

- (IBAction) executeScheduleForCurrentProject:(id)sender
{
	if (scheduleRunning)
		return;	
	userStop = NO;
	scheduleRunning = YES;
	
	// don't execute an empty schedule
	if ([schedule count] == 0)
	{
		NSAlert* a = [[NSAlert alertWithMessageText: NSLocalizedString(@"Empty schedule",@"[1]")
									  defaultButton: NSLocalizedString(@"OK",@"[1]")
									alternateButton: nil
										otherButton: nil
						  informativeTextWithFormat: NSLocalizedString(@"The schedule is empty. Either there are no differences between the source and destination folder, or you forgot to prepare the schedule.",@"[1]")
			] retain];
		[a beginSheetModalForWindow: [self windowForSheet]
					  modalDelegate: self
					 didEndSelector: @selector(emptyScheduleErrorEnd:returnCode:contextInfo:)
						contextInfo: NULL];
		
	}
	
	// show the progress sheet
	[NSApp beginSheet: executingSchedulePanel
	   modalForWindow: [self windowForSheet] 
		modalDelegate: self
	   didEndSelector: nil
		  contextInfo: nil];
	[executingScheduleIndicator setMaxValue:[schedule count]];
	[executingScheduleIndicator setDoubleValue:0.0];
	// execute the schedule in a separate thread
	[NSApplication detachDrawingThread: @selector(beginExecutionOfSchedule) 
							  toTarget: self
							withObject: nil];	
}

- (IBAction) closeProject:(id)sender
{
	[self close];
}

+(NSArray*) supportedAttributes
{
	if (SupportedAttributes == nil)
	{
		SupportedAttributes = [NSArray arrayWithObjects:
			NSFileBusy, 
			NSFileCreationDate, 
			NSFileExtensionHidden, 
			NSFileGroupOwnerAccountID, 
			NSFileGroupOwnerAccountName, 
			NSFileHFSCreatorCode, 
			NSFileHFSTypeCode, 
			NSFileImmutable, 
			NSFileModificationDate, 
			NSFileOwnerAccountID, 
			NSFileOwnerAccountName, 
			NSFilePosixPermissions,
			nil];
	}
	return SupportedAttributes;
}

+(NSDictionary*) supportedAttributesFrom:(NSDictionary*)attr
{
	NSArray* sa = [self supportedAttributes];
	NSEnumerator* en = [sa objectEnumerator];
	NSMutableDictionary* res = [NSMutableDictionary new];
	id attrName;
	id attrValue;
	while ((attrName = [en nextObject]) != nil)
	{
		attrValue = [attr objectForKey:attrName];
		if (attrValue != nil)
		{
			[res setObject:attrValue forKey:attrName];
		}
	}
	return [res autorelease];
}

// TODO:
// 1. Set the default save location to the projects folder.
//    DONE [2007-05-16]
//    Had to do some dirty tricks and set NSNavLastRootDirectory to
//    fool the save panel.
// 2. Try to do any bindings-related operations only on the main thread.
//    DONE [2007-05-15,16]
//    Read-only operations remain in worker threads. Updates are executed
//    in the main thread, using wrapper methods of MyDocument.
// 3. Check why sometimes after sync there are some objects with different
//    attributes (ttomek -> ttomek2, sometimes poczta or one of its
//    subfolders are scheduled again for synchronization)
//    CAUSE: When copying new files to a folder, the modification date
//           in dst is changed, but the attributes of the dst folder
//           were set before the copying.
//    SOLUTION: For many reasons, the folder attributes are not compared
//              now.
//    DONE [2007-05-15]
// 4. The execution progess indicator should be moved in the main thread
//    (wrapper + performSelectorOnMainThread:::)
//    DONE [2007-05-16]
// 5. Add preferences panel, use the user-defined modification date
//    tolerance instead of the hard-coded 1 second.
//    Also, let the user define the default folder for project files.
//    DONE [2007-05-16]
//    The tolerance is taken into account in traverseFolder:::::...
// 6. Check releasing of the top-level objects.
// 7. Source objects in the schedule should be displayed in green,
//    while destination in red.
//    DONE [2007-05-18]
// 8. Always show the errors tab (so that Return will close project).
//    DONE [2007-05-18]
// 9. Disable the Execute schedule button when the schedule is empty.
//    DONE [2007-05-18]
//    Used bindings (Enabled -> array controller @count)
// 10. Design new icons.
//     DONE [2007-05-19]
// 11. Correct rescanFolder to consider only .synxer files.
//     DONE [2007-05-19]
// 12. Ask for confirmation before deleting a project.
//     DONE [2007-05-19]
// 13. Add automatic version checking.
//     DONE [2007-05-21]
// 14. Prepare help.
//     DONE [2007-05-25]
//     English version completed on 2007-05-21.
//     Polish version completed on 2007-05-25.
// 15. Add the "Factory defaults" button to the preferences panel.
//     DONE [2007-05-21]
// 16. Make the last column in schedule and errors tabs wider.
//     DONE [2007-05-21]



@end
