
/*
	Copyright 1993  Jeremy Slade.  All rights reserved.
*/

#import "Group.h"

#import "AtomList.h"
#import "DynamicItemSpec.h"
#import "DynamicItems.h"
#import "Folder.h"
#import "FolderController.h"
#import "FolderViewer.h"
#import "Globals.h"
#import "GroupBrowser.h"
#import "Inspector.h"
#import "ItemCell.h"
#import "MainController.h"

#import <string.h>
#import <sys/file.h>


@implementation Group


// -------------------------------------------------------------------------
//   Creating, intializing Methods
// -------------------------------------------------------------------------


+ initialize
/*
	Set the class version number
*/
{
	[self setVersion:Group_VERSION];
	return ( self );
}



+ new
{
	return ( [[Group alloc] init] );
}



- init
{
	return ( [self initCount:0] );
}



- initCount:(unsigned int)numSlots
/*
	Designated initializer.  Initialize list and set the default groupName
*/
{
	[super initCount:numSlots];
	
	[self setGroupName:"UNNAMED"];
	[self setKeyEquivalent:0];
	
	tag = 0;	// Assigned by the Folder
	
	restrictTypes = NO; // Allow all files by default
	
	// Set default drawInfo
	drawInfo.mode = IC_LARGE_BROWSE;
	drawInfo.actualImage = YES;
	drawInfo.lb_tridots = YES;
	drawInfo.lb_nameMode = IC_FILEANDPATH;
	drawInfo.lb_info1 = YES;
	drawInfo.lb_info2 = NO;
	drawInfo.sb_icon = YES;
	drawInfo.sb_nameMode = IC_FILEANDPATH;
	drawInfo.li_showName = YES;
	drawInfo.si_showName = YES;

	filter = NULL;
	defaultPath = NULL;
	
	typesList = [[AtomList alloc] initCount:0];
	
	dynamicItemSpecs = [[List alloc] initCount:0];
	dynamicUpdateInterval = 0.0;	// Off by default
	
	return ( self );
}



- free
/*
	Free the instance and any objects it allocates
*/
{
	if ( groupName ) NX_FREE ( groupName );
	if ( filter ) NX_FREE ( filter );
	if ( defaultPath ) NX_FREE ( defaultPath );
	
	[typesList free];
	[dynamicItemSpecs free];
	
	return ( [super free] );
}



// -------------------------------------------------------------------------
//   Naming the Group
// -------------------------------------------------------------------------


- setGroupName:(const char *)aString
/*
	Set the group's name to the specified string
*/
{
	if ( groupName ) {
		NX_FREE ( groupName );
		[self setChanged:YES]; // Only 'changed' if it already had a name
	}

	groupName = NXCopyStringBuffer ( aString );

	return ( self );
}



- (const char *)groupName
{
	return ( groupName );
}



// -------------------------------------------------------------------------
//   Group Tag
// -------------------------------------------------------------------------


- setTag:(int)aTag
/*
Called by the Group's Folder when it is assigned a unique tag identifying it within this Folder
*/
{
	if ( aTag != tag ) {
		int oldTag = tag;
		tag = aTag;
		if ( tag != oldTag ) [self setChanged:YES];
	}

	return ( self );
}



- (int)tag
{
	return ( tag );
}



// -------------------------------------------------------------------------
//   Setting general attributes
// -------------------------------------------------------------------------


- setKeyEquivalent:(unsigned short)charCode
/*
	Sets the key equivalent for selecting the group
*/
{
	if ( keyEquivalent != charCode ) {
		keyEquivalent = charCode;
		[self setChanged:YES];
	}
	
	return ( self );
}



- setDefaultPath:(const char *)fullPath
/*
	Sets the default path that this Group is associated with.  This path is the directory that the 'Add Items' panel will automatically be set to, and it is the path used for relative dynamicItems specifiers.
*/
{
	if ( defaultPath != fullPath || ( defaultPath && fullPath &&
		strcmp ( defaultPath, fullPath ) ) ) {
		
		if ( defaultPath ) NX_FREE ( defaultPath );
		defaultPath = fullPath ? NXCopyStringBuffer ( fullPath ) : NULL;
		[self makeObjectsPerform:@selector(resetFileStr)];
		[self setChanged:YES];
	}
	
	return ( self );
}



- setSortItems:(BOOL)flag
/*
	Sets whether the group will automatically sort itself when new items are added
*/
{
	if ( sortItems != flag ) {
		sortItems = flag;
		[self setChanged:YES];
	}
	
	return ( self );
}



- (unsigned short)keyEquivalent
{
	return ( keyEquivalent );
}



- (const char *)defaultPath
{
	return ( defaultPath );
}



- (BOOL)doesSortItems
{
	return ( sortItems );
}



// -------------------------------------------------------------------------
//   Setting drawing attributes
// -------------------------------------------------------------------------


- setDrawMode:(int)aMode
{
	int newMode;
	
	switch ( aMode ) {
		case IC_LARGE_BROWSE:
		case IC_SMALL_BROWSE:
			newMode = aMode;
			break;
		case IC_LARGE_ICON:	// Not Implemented
		case IC_SMALL_ICON:	// Not Implemented
		default:
			newMode = IC_LARGE_BROWSE;
			break;
	}
	
	if ( drawInfo.mode != newMode ) {
		drawInfo.mode = newMode;
		[folder setNeedsLoadBrowser:YES];
		[self setChanged:YES];
	}
	
	return ( self );
}



- (int)drawMode
{
	return ( drawInfo.mode );
}



- setDrawActualImage:(BOOL)flag
{
	if ( drawInfo.actualImage != flag ) {
		drawInfo.actualImage = flag;
		[self setChanged:YES];
		[folder setNeedsLoadBrowser:YES];
	}
	return ( self );
}



- (BOOL)doesDrawActualImage
{
	return ( drawInfo.actualImage );
}



- setDrawTriDots:(BOOL)flag
{
	if ( drawInfo.lb_tridots != flag ) {
		drawInfo.lb_tridots = flag;
		[self setChanged:YES];
		[folder setNeedsLoadBrowser:YES];
	}
	return ( self );
}



- (BOOL)doesDrawTriDots
{
	return ( drawInfo.lb_tridots );
}



- setDrawInfoLine1:(BOOL)flag
{
	if ( drawInfo.lb_info1 != flag ) {
		drawInfo.lb_info1 = flag;
		[self setChanged:YES];
		[folder setNeedsLoadBrowser:YES];
	}
	return ( self );
}



- (BOOL)doesDrawInfoLine1
{
	return ( drawInfo.lb_info1 );
}



- setDrawInfoLine2:(BOOL)flag
{
	if ( drawInfo.lb_info2 != flag ) {
		drawInfo.lb_info2 = flag;
		[self setChanged:YES];
		[folder setNeedsLoadBrowser:YES];
	}
	return ( self );
}



- (BOOL)doesDrawInfoLine2
{
	return ( drawInfo.lb_info2 );
}



- setLBNameMode:(int)mode
{
	int newMode = IC_NAME_UNKNOWN;
	
	switch ( mode ) {
		case IC_FULLPATH:
		case IC_FILEANDPATH:
		case IC_FILEONLY:
		case IC_RELATIVE:
		case IC_NOPATH:
			newMode = mode;
			break;
		case IC_NAME_UNKNOWN:
		default:
			newMode = IC_FILEANDPATH;
			break;
	}
	
	if ( drawInfo.lb_nameMode != newMode ) {
		drawInfo.lb_nameMode = newMode;
		[self makeObjectsPerform:@selector(resetFileStr)];
		[folder setNeedsLoadBrowser:YES];
		[self setChanged:YES];
	}
	
	return ( self );
}



- (int)lbNameMode
{
	return ( drawInfo.lb_nameMode );
}



- setDrawSmallIcons:(BOOL)flag
{
	if ( drawInfo.sb_icon != flag ) {
		drawInfo.sb_icon = flag;
		[self setChanged:YES];
		[folder setNeedsLoadBrowser:YES];
	}
	return ( self );
}



- (BOOL)doesDrawSmallIcons
{
	return ( drawInfo.sb_icon );
}



- setSBNameMode:(int)mode
{
	int newMode = IC_NAME_UNKNOWN;
	
	switch ( mode ) {
		case IC_FULLPATH:
		case IC_FILEANDPATH:
		case IC_FILEONLY:
		case IC_RELATIVE:
		case IC_NOPATH:
			newMode = mode;
			break;
		case IC_NAME_UNKNOWN:
		default:
			newMode = IC_FILEANDPATH;
			break;
	}
	
	if ( drawInfo.sb_nameMode != newMode ) {
		drawInfo.sb_nameMode = newMode;
		[self makeObjectsPerform:@selector(resetFileStr)];
		[folder setNeedsLoadBrowser:YES];
		[self setChanged:YES];
	}
	
	return ( self );
}



- (int)sbNameMode
{
	return ( drawInfo.sb_nameMode );
}



- setLIDrawName:(BOOL)flag
{
	if ( drawInfo.li_showName != flag ) {
		drawInfo.li_showName = flag;
		[self setChanged:YES];
		[folder setNeedsLoadBrowser:YES];
	}
	return ( self );
}



- (BOOL)liDoesDrawName
{
	return ( drawInfo.li_showName );
}



- setSIDrawName:(BOOL)flag
{
	if ( drawInfo.si_showName != flag ) {
		drawInfo.si_showName = flag;
		[self setChanged:YES];
		[folder setNeedsLoadBrowser:YES];
	}
	return ( self );
}



- (BOOL)siDoesDrawName
{
	return ( drawInfo.si_showName );
}



- getDrawInfo:(DrawInfo *)info
{
	*info = drawInfo;
	return ( self );
}



// -------------------------------------------------------------------------
//   Allowed File Types
// -------------------------------------------------------------------------


- addAllowedType:(const char *)anExt
/*
	Add a new file type (e.g. .h, .m, .snd) to the list of allowed types for this group
*/
{
	NXAtom a = NXUniqueString ( anExt );
	if ( [typesList indexOfAtom:a] == NX_NOT_IN_LIST  ) {
		[typesList addAtomAlphabetically:a];
		restrictTypes = YES; // Enable restrictions
		[self setChanged:YES];
	}
	return ( self );
}



- removeAllowedType:(const char *)anExt
/*
	Remove the specified type from the typesList
*/
{
	if ( [typesList indexOfAtom:NXUniqueString(anExt)] != NX_NOT_IN_LIST ) {
		[typesList removeAtom:NXUniqueString(anExt)];
		[self setChanged:YES];
	}
	return ( self );
}



- setRestrictTypes:(BOOL)flag
/*
	Set the restrictTypes flag
*/
{
	if ( restrictTypes != flag ) {
		restrictTypes = flag;
		[self setChanged:YES];
	}
	return ( self );
}



- (const char **)allowedTypes
/*
	Returns a NULL-terminated array of strings containing the allowed file types for this group.  This array is rebuilt each time this method is called.  The caller shouldn't keep the returned array around, as it will be freed
*/
{
	static const char **allowedTypes = NULL;
	int i, count = 0;
	
	if ( allowedTypes ) { // Free previously allocated memory
		NX_FREE ( allowedTypes );
		allowedTypes = NULL;
	}
	
	// Allocate memory for array
	// Array must be big enough for all types + NULL pointer
	if ( !NX_MALLOC ( allowedTypes, char *, [typesList count]+1 ) ) {
		[self debug:10 :"Unable to allocate allowedTypes array!\n"];
		return ( NULL );
	}
	
	// Add all the types to the array
	count = [typesList count];
	for ( i=0; i<count; i++ ) {
		allowedTypes[i] = [typesList atomAt:i];
	}
	
	// Add NULL pointer at end of array
	allowedTypes[count] = NULL;
	return ( allowedTypes );
}



- (BOOL)doesRestrictTypes
/*
	Returns YES if file type restrictions are currently active
*/
{
	return ( (!restrictTypes || ![typesList count]) ? NO : YES );
}



- (BOOL)isAllowedType:(const char *)path
/*
	Checks if path is one of the types in the typesList
*/
{
	const char *ext;
	NXAtom atom;
	
	if ( ![self doesRestrictTypes] ) // No Restrictions, all types allowed
		return ( YES );
		
	ext = rindex ( path, '.' ); // Find last '.'
	if ( ext ) ext++;
		else ext = path;
	atom = NXUniqueString ( ext );
	
	return ( [typesList indexOfAtom:atom] != NX_NOT_IN_LIST );
}



// -------------------------------------------------------------------------
//   Adding items to the group
// -------------------------------------------------------------------------


- addItem:(const char *)path
/*
	Add the item to the group if it doesn't already exists there, and if it is an allowed type
*/
{
	id	newItem;

	if ( [self itemExists:path] || ![self isAllowedType:path] )
		return ( nil );

	if ( (newItem = [self itemMatching:path]) ) {
		switch ( NXRunAlertPanel ( "Alert",
			"There is already an item called %s in this group.  Do you want to replace the existing one?",
			"Replace", "Add anyway", "Cancel", [newItem filename] ) ) {
			case NX_ALERTDEFAULT:
				// Replace the existing item with the new path
				[newItem setPath:path];
				return ( self );
				
			case NX_ALERTOTHER:
			default:
				// Cancel
				return ( nil );
		}
	}
	
	newItem = [[ItemCell alloc] initPath:path];
	[newItem setGroup:self];

	[self addObject:newItem];
	[self setChanged:YES];
	
	if ( [folder currentGroup] == self ) [folder setNeedsLoadBrowser:YES];
	return ( self );
}



- (BOOL)itemExists:(const char *)path
/*
	checks all the items in the group for one that matches path
*/
{
	int i, count;
	
	count=[self count];
	for ( i=0; i<count; i++ )
		if ( !strcmp ( [[self objectAt:i] path], path ) )
			return ( YES ); // Found a match
			
	return ( NO );
}



- itemMatching:(const char *)path
/*
	Finds and returns the item (if it exists) that has the same filename as path--just the last component of the path.
*/
{
	const char *filename;
	int i, count;
	
	filename = rindex ( path, '/' );
	if ( filename ) filename++; else filename = path;
	
	count = [self count];
	for ( i=0; i<count; i++ ) {
		if ( !strcmp ( [[self objectAt:i] filename], filename ) )
			return ( [self objectAt:i] ); // Found a match
	}
	
	return ( nil );
}



- cleanUp:sender
/*
	Filter items in group, removing all that aren't currently allowed types
*/
{
	int i, count, selCount;
	BOOL oldVerify = verifyActions;
	id item;
	
	if ( ![self doesRestrictTypes] )
		return ( self );
	
	// Select all items to be removed
	count = [self count];
	selCount = 0;
	for ( i=0; i<count; i++ ) {
		if ( ![self isAllowedType:[(item=[self objectAt:i]) path]]
			|| access ( [item path], F_OK ) == -1 ) {
			// Item will be tagged for removal if it is not an allowed
			// type or if the file it references does not exist.
			[item setState:1]; // Select for removal
			selCount++;
		} else
			[item setState:0]; // Allowed type, won't be removed
		[[[folder viewer] browser] scrollRowToVisible:i];
		NXPing();
	}
	
	if ( !verifyActions || NXRunAlertPanel ( "Clean Up",
		"Remove %i restricted items from the group %s ?",
		"Remove", "Cancel", NULL, selCount, groupName ) == NX_ALERTDEFAULT ) {
		// Remove the selected items by calling removeSelectedItems:
		verifyActions = NO; // Temporarily disable verifcation
		[self removeSelectedItems:self];
		verifyActions = oldVerify; // Restore old state
	}
	
	return ( self );
}



// -------------------------------------------------------------------------
//   Sorting Items
// -------------------------------------------------------------------------


static int compareItems ( ItemCell **item1, ItemCell **item2 )
/*
	Compares the items for sorting, based on their filenames ( the last component of the item's path ).  Comparison is NOT case-sensitive.  This functions is used by the qsort() routine.
*/
{
	int compare;
	compare = NXOrderStrings( [*item1 filename], [*item2 filename], NO, -1, NULL );
	return ( compare );
}



- sortItems
/*
	Sort the items in currentGroup alphabetically by filename ( last component of path ).  Uses qsort() to do this.  Qsort() calls our comparison function, compareItems(), and the directly reorders them in the dataPtr array.
*/
{	
	if ( [self doesSortItems] || needsSort  ) {
		// Qsort()'s parameters are:
		//	1. pointer to beginning of data -- in this case, dataPtr
		//	2. number of elements to be sorted -- numElements
		//	3. size of the element -- sizeof ( id * )
		//	4. the comparison function -- compareItems() 
		qsort ( dataPtr, numElements, sizeof(id *), compareItems );
		
		[self setChanged:YES];
		[folder setNeedsLoadBrowser:YES];
	}
	
	return ( self );
}



- explicitSortItems:sender
/*
	Sort the items in the current group, overriding the setting of the sortItems preference
*/
{
	needsSort = YES;
	[self sortItems];
	return ( self );
}



// -------------------------------------------------------------------------
//   Making Selection
// -------------------------------------------------------------------------


- selectItem:sender
/*
	Called when an item is selected in browser
*/
{
	if ( [inspector inspectorMode] == INSPECT_ITEM )
		[inspector inspect:[[[folder viewer] browser] selection]];
	return ( self );
}



- selectAll:sender
{
	[[[folder viewer] browser] selectAll:self];
	[folder showSelf:self];
	
	if ( [inspector inspectorMode] == INSPECT_ITEM )
		[inspector inspect:[[[folder viewer] browser] selection]];
	return ( self );
}



- selection
{
	return ( [[[folder viewer] browser] selection] );
}


- selectionList
{
	return ( [[[folder viewer] browser] selectionList] );
}


- (int)selectionCount
{
	return ( [[[folder viewer] browser] selectionCount] );
}



// -------------------------------------------------------------------------
//   Acting on Items
// -------------------------------------------------------------------------

- launchSelectedItems:sender
{
	int i, count;
	char *s;
	
	for ( i=0, count=[self count]; i<count; i++ ) {
		if ( [[self objectAt:i] state] ) {
			// Check if the item is a Folder
			//// FIX: Item should have method to do this ( do the OOP thing )
			s = rindex ( [[self objectAt:i] filename], '.' );
			if ( s ) s++;
				else s = (char *)[[self objectAt:i] filename];
			if ( !strcmp ( s, FOLDER_EXT ) ) {
				[folderController openFolder:[[self objectAt:i] path]];
			} else {
				[NXApp deactivateSelf];
				[[self objectAt:i] launch];
			}
		}
	}
	
	[folder showSelf:self];
	return ( self );
}


- removeSelectedItems:sender
/*
	Remove all the selected items from currentGroup
*/
{
	int i, count;
	BOOL removedSomething = NO;
	
	for ( count=[self count], i=count-1; i>=0; i-- ) {
		if ( [[self objectAt:i] state] ) {
			// Remove the item
			[[self removeObjectAt:i] free];
			removedSomething = YES;
		}
	}
	
	if ( removedSomething ) {
		[folder setNeedsLoadBrowser:YES];
		[self setChanged:YES];	
		return ( self );
	} else
		return ( nil );
}


//// FIX: Change to copySelection to Pbaord.
//// Write selection as NXFilenamePboardType
- copyItemToPboard:sender
/*
	Copy the path of the selected item into the pasteboard
*/
{
	id	pboard = [Pasteboard newName:NXGeneralPboard];
	const char *const types[2] = { NXAsciiPboardType, NULL };
	id item;
	
	if ( !(item = [[[folder viewer] browser] selectedCell]) )
		return ( nil );

	[pboard declareTypes:types num:1 owner:nil];
	[pboard writeType:NXAsciiPboardType
		data:[item path]
		length:strlen ( [item path] )];
		
	return ( self );
}



// -------------------------------------------------------------------------
//   Folder
// -------------------------------------------------------------------------


- setFolder:aFolder
{
	folder = aFolder;
	[self setTag:[folder tagFor:self]];
	return ( self );
}



- folder
{
	return ( folder );
}



- setChanged:(BOOL)flag
{
	isChanged = flag;
	
	if ( autoSave && isChanged ) {
		[self writeSelf];
		isChanged = NO;
	}
		
	if ( isChanged ) [folder groupChanged:self];
		else [folder groupSaved:self];
	
	return ( self );
}



- (BOOL)isChanged
{
	return ( [folder isChanged] );
}



- setNeedsShow:(BOOL)flag
{
	return ( [folder setNeedsShow:flag] );
}


- (BOOL)needsShow
{
	return ( [folder needsShow] );
}



// -------------------------------------------------------------------------
//   Archiving
// -------------------------------------------------------------------------


- awake
{
	[super awake];
	
	// Make sure all the items' group is set to self
	[self makeObjectsPerform:@selector(setGroup:) with:self];

	// Start dynamic updating
	[self startDynamicUpdate];

	return ( self );
}



- read:(NXTypedStream *)stream
{
	unsigned int count, i;
	int versionNumber;
	
	[super read:stream];
	
	// Read versionNumber
	versionNumber = NXTypedStreamClassVersion ( stream, [[self class] name] );
	
	if ( versionNumber <= Group_VERSION ) { // Up thru current version

		// Unarchive all of the items in the list -- see write: for details 
		// Read the number of items in the list and set the capacity
		// of the array
		NXReadType ( stream, "i", &count );
		[self setAvailableCapacity:count];
		numElements = count;
		for ( i=0; i<count; i++ ) dataPtr[i] = NXReadObject ( stream );

		NXReadTypes ( stream, "*iscc",
			&groupName,
			&tag,
			&keyEquivalent,
			&restrictTypes, &sortItems );
		NXReadTypes ( stream, DrawInfoTypeString, &drawInfo );
		NXReadTypes ( stream, "**f",
			&filter,
			&defaultPath,
			&dynamicUpdateInterval );
		typesList = NXReadObject ( stream );
		dynamicItemSpecs = NXReadObject ( stream );
	}
	
	else {	// Unrecognized archive version
		[self errMsg:"Group: unrecognized version %i of archived object!\n",
			versionNumber];
	}
	
	return ( self );
}



- write:(NXTypedStream *)stream
/*
	Overrides standard archiving behavior of List to deal with Dynamic Items.  We must make sure that the archived count is the number of static items (non-dynamic), since DynamicItems don't get archived.
*/
{
	unsigned int i, count;

	// First, set numElements to 0 to make [super write:] think there is nothing to write
	count = numElements;
	numElements = 0;
	
	// Now let super do it's thing....
	[super write:stream];
	
	// Restore numElements
	numElements = count;
	
	// Archive all of the items ourselves
	count -= [self countDynamicItems]; // actual num. of items being archived
	NXWriteType ( stream, "i", &count );
	for ( i=0; i<numElements; i++ )
		if ( ![dataPtr[i] isDynamic] ) NXWriteObject ( stream, dataPtr[i] );

	// Write our instance variables
	NXWriteTypes ( stream, "*iscc",
		&groupName,
		&tag,
		&keyEquivalent,
		&restrictTypes, &sortItems );
	NXWriteTypes ( stream, DrawInfoTypeString, &drawInfo );
	NXWriteTypes ( stream, "**f",
		&filter,
		&defaultPath,
		&dynamicUpdateInterval );
	NXWriteObject ( stream, typesList );
	NXWriteObject ( stream, dynamicItemSpecs );
		
	return ( self );
}



- writeSelf
/*
	Archive self to a typed stream
*/
{
	NXTypedStream *stream;
	char path[MAXPATHLEN+1];
	
	if ( ![folder filename] )
		return ( self );
		
	strcpy ( path, [self getGroupFilename] );
	
	if ( !(stream = NXOpenTypedStreamForFile ( path, NX_WRITEONLY )) ) {
		[self errMsg:"Group: unable to create output stream to archive.\n"];
		return ( self ); // Unable to open typed stream
	}
		
	NXWriteRootObject ( stream, self );
	NXCloseTypedStream ( stream );
	
	[self setChanged:NO];
	return ( self );
}



- (const char *)getGroupFilename
/*
	Return the filename that this group will archive to/unarchive from
*/
{
	static char path[MAXPATHLEN+1] = "";
	
	if ( !folder ) {
		[self errMsg:"Group: Unowned group %s!\n", groupName];
		return ( NULL );
	}
	
	if ( !tag ) [self setTag:[folder tagFor:self]];
	
	sprintf ( path, "%s/Group%05i.%s", [folder filename], tag, GROUP_EXT );
	return ( path );
}



@end


