/* 
 * ToDoInspector - controller for the ToDoList application
 *
 * You may freely copy, distribute and reuse the code in this example.
 * This code is provided AS IS without warranty of any kind, expressed 
 * or implied, as to its fitness for any particular use.
 *
 * Copyright 1995 Ralph Zazula (rzazula@next.com).  All Rights Reserved.
 *
 */

#import "ToDoController.h"
#import "ToDoItem.h"
#import "ToDoList.h"
#import "ToDoBrowserCell.h"
#import "ToDoInspector.h"
#import "_ToDoInspector.h"
#import "ToDoMatrix.h"
#import "ToDoServer.h"
#import "lock_file.h"
#import "ObjectError.h"

//static DPSTimedEntry updateEntry;

#define MATRIX [browser matrixInColumn:0]

@implementation ToDoController

void teUpdate(DPSTimedEntry tag, double now, void *self)
{
//	[(ToDoController *)self update];
}

+ initialize
{
	char path[MAXPATHLEN+1];
	struct stat st;
	static NXDefaultsVector Defaults = {
		{"ShowPrivate","NO"},
		{"ShowCompleted","NO"},
		{"ShowPending","YES"},
		{NULL}
	};
	
	if([self class] == [ToDoController class]) {
		[self setVersion:4];

		NXRegisterDefaults([NXApp appName], Defaults);
	
		//
		// create the path ~/Library/RZToDo
		//
		sprintf(path, "%s/Library/RZToDo", NXHomeDirectory());
		if(stat(path, &st) != 0) {
			if(!mkdir(path, 0700) || errno == EEXIST) {
			} else {
				NXRunAlertPanel("Library","Error creating folder: RZToDo in: %s/Library",
					0,0,0,NXHomeDirectory);
			}
		}
	}
	return self;
}

- appDidInit:sender
{
	char path[MAXPATHLEN+1];
	NXTypedStream *ts;
	id prototypeCell = [[ToDoBrowserCell alloc] init];
	const char *sendTypes[3];
	const char *returnTypes[3];
	sendTypes[0] = NXAsciiPboardType;
	sendTypes[1] = NXRTFPboardType;
	sendTypes[2] = NULL;
	returnTypes[0] = NXAsciiPboardType;
	returnTypes[1] = NXRTFPboardType;
	returnTypes[2] = NULL;
	
	[ObjectError setup];

	sprintf(path, TODO_FILE, NXHomeDirectory());
	
	if(lock_file(path) == LOCK_EXIST) {
		if(NXRunAlertPanel("Warning", "The ToDo List is locked!", 
			"Open Anyway", "Quit", NULL) != NX_ALERTDEFAULT) {
			[sender terminate:self];
		}
	}
		
	ts = NXOpenTypedStreamForFile(path, NX_READONLY);
	if(ts) {
		NX_DURING
		todoList = NXReadObject(ts);
		NXCloseTypedStream(ts);
		NX_HANDLER
		NX_ENDHANDLER
	}
	
	if(todoList) {
		if(![todoList isKindOf:[ToDoList class]]) {
			id newList = [[ToDoList alloc] init];
			[newList appendList:todoList];
			[todoList free];
			todoList = newList;
		}
	} else {
		todoList = [[ToDoList alloc] init];
	}

	[prototypeCell clearTabs];
	[prototypeCell addFixedTab:4];
	[prototypeCell addFixedTab:24];		
	[prototypeCell addFixedTab:-85];
	[browser setCellPrototype:prototypeCell];
		
	[splitView addSubview:browser];
	[splitView addSubview:bodyBox];
	[theText setDelegate:self];
			
	[browser setMatrixClass:[ToDoMatrix class]];
	[browser setDoubleAction:@selector(doubleClick:)];

	showPending = !strcmp(NXGetDefaultValue([NXApp appName], "ShowPending"), "YES");
	showCompleted = !strcmp(NXGetDefaultValue([NXApp appName], "ShowCompleted"), "YES");
	showPrivate = !strcmp(NXGetDefaultValue([NXApp appName], "ShowPrivate"), "YES");

	[self update];
	
	[window setFrameUsingName:"ToDoDocument"];
	[window setFrameAutosaveName:"ToDoDocument"];
	[window makeKeyAndOrderFront:nil];
	[NXApp setAutoupdate:YES];
	if([self selectedItems]) {
		[self displayItem:[[self selectedItems] lastObject]];
	}
	
//	updateEntry = DPSAddTimedEntry(1.0, teUpdate, self, NX_BASETHRESHOLD);

	/* start up the DO server */
	[[[ToDoServer alloc] init] _setController:self];
	
	/* services support */
	[NXApp registerServicesMenuSendTypes:sendTypes andReturnTypes:returnTypes];
	[[NXApp appListener] setServicesDelegate:self];

	return self;
}

- appWillTerminate:sender
{
	char path[MAXPATHLEN+1];

	if([window isDocEdited]) {
		switch(NXRunAlertPanel("Quit","Save changes to to-do list?", 
			"Yes", "No", "Cancel")) {
			case NX_ALERTDEFAULT :
				[self save:nil];
				break;
				
			case NX_ALERTALTERNATE :
				break;
				
			case NX_ALERTOTHER :
				return nil;
		}
	}
	
	sprintf(path, TODO_FILE, NXHomeDirectory());
	unlock_file(path);
	
	return self;
}

- info:sender
{
	if(!infoPanel) {
		[NXApp loadNibSection:"Info.nib" owner:self withNames:NO];
		[infoPanel setFrameUsingName:"ToDoInfo"];
		[infoPanel setFrameAutosaveName:"ToDoInfo"];
	}
	[infoPanel makeKeyAndOrderFront:self];
	return self;
}

- inspector:sender
{
	if(!inspector) {
		inspector = [[ToDoInspector alloc] init];
		[inspector _setController:self];
	}
	[[inspector window] orderFront:nil];
	return self;
}

- dirty:sender
{
	[window setDocEdited:YES];
	return self;
}

- clean:sender
{
	[window setDocEdited:NO];
	return self;
}

- disableEditing
{
	[subjectField setEditable:NO];
	[theText setEditable:NO];
	[dateField setEditable:NO];
	return self;
}

- enableEditing
{
	[subjectField setEditable:YES];
	[theText setEditable:YES];
	[dateField setEditable:YES];
	return self;
}

- (BOOL)showPending				{ return showPending; }
- (BOOL)showCompleted			{ return showCompleted; }
- (BOOL)showPrivate				{ return showPrivate; }
- setShowPending:(BOOL)flag	{ showPending = flag; return self; }
- setShowCompleted:(BOOL)flag	{ showCompleted = flag; return self; }
- setShowPrivate:(BOOL)flag	{ showPrivate = flag; return self; }

- save:sender
{
	char path[MAXPATHLEN+1];
	NXTypedStream *ts;
	BOOL failed = NO;
	
	sprintf(path, TODO_FILE, NXHomeDirectory());
	
	/* force saving of any edits in progress */
	if([window makeFirstResponder:window]) {
		[window endEditingFor:nil];
	}
	
	if(todoList) {
		NX_DURING
		ts = NXOpenTypedStreamForFile(path, NX_WRITEONLY);
		NXWriteRootObject(ts, todoList);
		NXCloseTypedStream(ts);
		NX_HANDLER
		failed = YES;
		NX_ENDHANDLER
		if(!failed) {
			[self clean:nil];
		} else {
			NXRunAlertPanel("Error","Save Failed!",NULL,NULL,NULL);
		}
	}
		
	return self;
}

- (long)dueDateFrom:(const char *)s
{
	struct tm newTime;
	long curTime;
	char *slash;
	const char *c;
		
	if(!s) {
		return 0;
	}
	
	c = s;
	curTime = time(NULL);
	newTime = *localtime(&curTime);
	newTime.tm_min++;

	newTime.tm_mon = atoi(c)-1;
	
	slash = index(c, '/');
	if(slash) {
		c = slash+1;
		newTime.tm_mday = atoi(c);
		slash = index(c, '/');
		if(slash) {
			c = slash+1;
			newTime.tm_year = atoi(c);
		}
	}
	
	return mktime(&newTime);
	
	/* cheesey parsing */
	
	if(sscanf(s, "%d/%d/%d", 
		&newTime.tm_mon, &newTime.tm_mday, &newTime.tm_year) != EOF) {
	
		
		return  mktime(&newTime);
	}
	
	return 0;
}

- new:sender
{
	id item = [[ToDoItem alloc] init];
	
	/* force saving of any edits in progress */
	if([window makeFirstResponder:window]) {
		[window endEditingFor:nil];
	}

	if(!todoList) {
		todoList = [[ToDoList alloc] init];
	}
	[item setSubject:"New Item"];
	[todoList addObject:item];
	[self dirty:self];
	[MATRIX clearSelectedCell];
	[self update];
	[self selectItem:item];
	[self displayItem:item];
	[subjectField selectText:self];
	
   return self;
}


- clear:sender
{
	[[browser window] disableFlushWindow];
	[subjectField setStringValue:""];
	[dateField setStringValue:""];
//	[theText selectAll:nil];
//	[theText clear:nil];
	[theText setText:""];
	[[[browser window] reenableFlushWindow] flushWindow];
   return self;
}

- doubleClick:sender
{
	[subjectField selectText:self];
   return self;
}

- modify:sender
{
	id itemList = [self selectedItems];
	id item;
	
	if(!itemList) {
		NXBeep();
		return nil;
	}
	
	if([itemList count] > 1) {
		NXBeep();
		return nil;
	}
	
	item = [itemList lastObject];
	
	[item setSubject:[subjectField stringValue]];
	[item setDueDate:[self dueDateFrom:[dateField stringValue]]];
	[item setDataFromText:theText];
	[self dirty:self];
	[self update];
   return self;
}

- remove:sender
{
	id itemList = [self selectedItems];
	int i;

	if(!itemList) {
		NXBeep();
		return nil;
	}
	
	if(NXRunAlertPanel("Delete","Really delete selected item(s)?",
		"Delete","Cancel",NULL) == NX_ALERTALTERNATE) {
		return self;
	}
		
	for(i=0; i<[itemList count]; i++) {
		[[todoList removeObject:[itemList objectAt:i]] free];
	}
	
	[MATRIX clearSelectedCell];
	[self clear:nil];
	[self dirty:self];
	[self update];
	
   return self;
}

- timestamp:sender
/* insert a time stamp */
{
	char buf[28];
	time_t t = time(NULL);
	NXSelPt selBegin, selEnd;
	id itemList;
	
	/* only valid for a single selection */
	itemList = [self selectedItems];
	
	if(!itemList || [itemList count] > 1) {
		NXBeep();
		return nil;
	}

	sprintf(buf, "[%s", ctime(&t));
	buf[25] = ']';		
	buf[26] = '\n';		
	buf[27] = '\0';
	
	/* check to see if the text has a selection */
	[theText getSel:&selBegin :&selEnd];
	if(selBegin.cp < 0) {
		/* no selection, make one */
		int textlength = [theText textLength];
		[theText setSel:textlength :textlength];
	} else if(selBegin.cp != selEnd.cp) {
		/* something is selected, don't nuke it! */
		[theText setSel:selEnd.cp :selEnd.cp];
	}
	[theText replaceSel:buf];
	/* buggy Text doesn't send textDidChange: after the above... */
	[self textDidChange:theText];
		
	return self;
}

- displayItem:(ToDoItem *)item
{
	char *data;
	int len;
	NXStream *stream;

	if(item) {
		[[browser window] disableFlushWindow];
		[subjectField setStringValue:[item subject]];
		[dateField setStringValue:[item asciiDueDate]];
		[item getData:&data length:&len];
		if(len) {
			stream = NXOpenMemory(NULL, 0, NX_READWRITE);
			NXWrite(stream, data, len);
			NXSeek(stream, 0, NX_FROMSTART);
			[theText readRichText:stream];
			NXCloseMemory(stream, NX_FREEBUFFER);
		} else {
			[theText setText:""];
		}
		[[[browser window] reenableFlushWindow] flushWindow];
	} else {
		[self clear:nil];
	}
	return self;
}

- selectItem:(ToDoItem *)item
{
	id cellList = nil;
	id cell;
	int i;
	
	if(item) {
		cellList = [MATRIX cellList];
		for(i=0; i<[cellList count]; i++) {
			cell = [cellList objectAt:i];
			if([cell item] == item) {
				[MATRIX selectCell:cell];
				[MATRIX scrollCellToVisible:i:0];
				break;
			}
		}
	} else {
		[MATRIX clearSelectedCell];
	}
	
	return self;
}

- singleClick:sender
{
	id itemList = [self selectedItems];
	id item;
	
	if(!itemList || ([itemList count] > 1) || ![itemList count]) {
		[self clear:self];
		[self disableEditing];
		return self;
	}
	
	item = [itemList lastObject];
	
	if(!item) {
		return self;
	}
	
	[self enableEditing];
	[self displayItem:item];
	
	/* 
	 * selecting the subject text field here makes things like services->mail
	 * mail the text field contents instead of the ToDoItem...  Don't do it
	 * if you get services implemented.
	 */
//	[subjectField selectText:self];
	
   return self;
}

- update
/*
 * redisplay the browser
 */
{
	id itemList = [self selectedItems];
	id cellList;
	int i,j;
	id cell;
	NXSize cellSize;
	BOOL madeSelection = NO;
	
	[[browser window] disableFlushWindow];

	[browser loadColumnZero];
	[[browser matrixInColumn:0] getCellSize:&cellSize];
	cellSize.height = 18;
	[[browser matrixInColumn:0] setCellSize:&cellSize];
	[[browser matrixInColumn:0] sizeToCells];
	
	cellList = [MATRIX cellList];
	for(i=0; i<[itemList count]; i++) {
		for(j=0; j<[cellList count]; j++) {
			cell = [cellList objectAt:j];
			if([cell item] == [itemList objectAt:i]) {
				[MATRIX selectCell:cell];
				[MATRIX scrollCellToVisible:j:0];
				madeSelection = YES;
			}
		}
	}
	[browser display];
	[[[browser window] reenableFlushWindow] flushWindow];

	if(!madeSelection) {
		[self clear:nil];
	}

	return self;
}

/*** as the browser's delegate ***/

- _fillCell:theCell forItem:(ToDoItem *)item
{
	char	font,color;
	
	if([item isPrivate]) {
		color = FONT_DKGRAY;
	} else {
		color = FONT_BLACK;
	}
	if([item isCompleted]) {
		font = FONT_ITALIC;
	} else {
		font = FONT_BOLD;
	}
	
	switch([item type]) {
		case TODO_TYPE_NORMAL :
			[theCell setImageNamed:"i-2do-item" at:0];
			break;
		case TODO_TYPE_APPOINTMENT :
			[theCell setImageNamed:"Appointment" at:0];
			break;
		case TODO_TYPE_LOWPRIORITY :
			[theCell setImageNamed:"LowPriority" at:0];
			break;
		case TODO_TYPE_HIGHPRIORITY :
			[theCell setImageNamed:"HighPriority" at:0];
			break;
	}
	
	[theCell setText:[item subject] at:1 font:font color:color];
	if([item isCompleted]) {
		[theCell setText:"Fin: %s" at:2 color:color, [item asciiCompletedDate]];
	} else {
		if([item type] == TODO_TYPE_APPOINTMENT) {
			[theCell setText:"On: %s" at:2 color:color, [item asciiDueDate]];
		} else {
			[theCell setText:"Due: %s" at:2 color:color, [item asciiDueDate]];
		}
	}
	[theCell setItem:item];
	
	return self;
}

- (int)browser:sender fillMatrix:matrix inColumn:(int)column
{
	int	i, count = 0;
	ToDoItem *item;
	id		theCell = nil;
	
	[todoList makeObjectsPerform:@selector(adjustPriority)];
	[todoList sort];
	
	/* Set the matrix to have the right number of cells. */
	[matrix renewRows:0 cols:1];

	/*
	 * For each cell set its value, set whether it is a leaf
	 * or not and mark it loaded.
	 */
	for (i=0; i<[todoList count]; i++) {
		item = [todoList objectAt:i];
		if((([item isPrivate] && showPrivate) || ![item isPrivate]) &&
			([item isCompleted] ? showCompleted : showPending)) {
			[matrix addRow];
			theCell = [matrix cellAt:count :0];
			[self _fillCell:theCell forItem:item];
			[theCell setLeaf:YES];
			[theCell setLoaded:YES];
			[theCell setTag:i];
			count++;
		}
	}
		
	/* Return the number of rows. */
	return count;
}

/*** as the window's delegate ***/

- windowWillClose:sender
{
	[NXApp terminate:sender];
	return nil;
}

- selectedItems
{
	static id itemList = nil;
	id cellList = nil;
	int i;
	
	
	cellList = [MATRIX getSelectedCells:nil];
	
	if(cellList ? [cellList count] : 0) {
		if(itemList) {
			[itemList empty];
		} else {
			itemList = [[List alloc] initCount:[cellList count]];
		}
		for(i=0; i<[cellList count]; i++) {
			[itemList addObject:[[cellList objectAt:i] item]];
		}
		[cellList free];
	} else {
		return nil;
	}
	
	return itemList;
}

/*** for the DO server ***/

- toDoList 		{ return todoList; }

- addItem:(id <ToDoItems>)anItem
{
	[todoList addObject:[[ToDoItem alloc] initFromItem:anItem]];
	[self dirty:self];
	[self update];
	return self;
}

- removeItem:anItem
{
	return self;
}
		
/*** as the SplitView's delegate ***/

#define SPLITVIEWSIZE 80.0

- splitView:sender getMinY:(NXCoord *)minY maxY:(NXCoord *)maxY ofSubviewAt:(int)offset
{
	*minY = SPLITVIEWSIZE;
	*maxY -= SPLITVIEWSIZE - 20;
	if ( *maxY < SPLITVIEWSIZE  - 20)
		*maxY = SPLITVIEWSIZE - 20;
	return self;
}

- splitView:sender resizeSubviews:(const NXSize *)oldSize
{
    NXRect lower, upper;
    float delta;
    
    [[sender window] disableDisplay];
    [sender adjustSubviews];
    [browser getFrame:&upper];
    [bodyBox getFrame:&lower];
    if (upper.size.height < SPLITVIEWSIZE) {
        delta = SPLITVIEWSIZE - upper.size.height;
        upper.size.height=SPLITVIEWSIZE;
        lower.size.height-=delta;
        [browser setFrame:&upper];
        [bodyBox setFrame:&lower];
        }
	 
    [[sender window] reenableDisplay];
    [[sender window] display];

    return self;
}

- splitViewDidResizeSubviews:sender
{
	NXRect upper;
    [browser getFrame:&upper];
    if (floor((upper.size.height + upper.origin.y)/2) != 
	 	(upper.size.height + upper.origin.y)/2) {
    		upper.size.height--;
		[browser setFrame:&upper];
	}
	return self;
}

/*** as the text delegate for the UI ***/

static BOOL _changed = NO;

- textDidChange:sender
{
	if(([sender superview] == subjectField) || ([sender superview] == dateField) ||
		(sender == theText)) {
		_changed = YES;
		[self dirty:self];
	}
	return self;
}

- (BOOL)textWillEnd:sender
{
	id itemList = nil;
	id item;
	BOOL dateChanged = NO;
	
	if(!_changed) {
		return NO;
	}

	itemList = [self selectedItems];
	
	if(!itemList || [itemList count] > 1) {
		return NO;
	}
		
	item = [itemList lastObject];
	
	if([sender superview] == subjectField) {
		[item setSubject:[subjectField stringValue]];
	} else if ([sender superview] == dateField) {
		[item setDueDate:[self dueDateFrom:[dateField stringValue]]];
		dateChanged = YES;
	} else if (sender == theText) {
		[item setDataFromText:theText];
	}
	
	if(!dateChanged) {
		/* no reordering */
		[self _fillCell:[MATRIX selectedCell] forItem:item];
		[browser display];
	} else {
		/* call update in case new date causes re-ordering */
		[self update];
	}
	
	_changed = NO;
	
	return NO;
}

/* services support */

extern BOOL IncludesType(const NXAtom *types, NXAtom type);

- addItem:(id)pasteboard userData:(const char *)userData error:(char **)msg
{
	[self pasteFromPasteboard:pasteboard];
	return self;
}

- validRequestorForSendType:(NXAtom)sendType andReturnType:(NXAtom)returnType
 {
	id itemList = nil;
 	NXAtom validSendTypes[] = {NXAsciiPboardType, NXRTFPboardType, NULL};		

	itemList = [self selectedItems];
	
	if(!itemList || ![itemList count]) {
		return NO;
	}

	if(!returnType || !*returnType) {
		/* no return type, only good for valid send type */
		if(IncludesType(validSendTypes, sendType)) {
			return self;
		}
	}
	return nil;
}

- (BOOL)writeSelectionToPasteboard:(Pasteboard *)pboard types:(NXAtom *)types
{
	return ([self copyToPasteboard:pboard] ? YES : NO);
}

@end
