/* 
   EOController.m

   Copyright (C) 1996 Free Software Foundation, Inc.

   Author: Mircea Oancea <mircea@jupiter.elcom.pub.ro>
   Date: November 1996

   This file is part of the GNUstep Database Library.

   This library is free software; you can redistribute it and/or
   modify it under the terms of the GNU Library General Public
   License as published by the Free Software Foundation; either
   version 2 of the License, or (at your option) any later version.

   This library is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   Library General Public License for more details.

   You should have received a copy of the GNU Library General Public
   License along with this library; see the file COPYING.LIB.
   If not, write to the Free Software Foundation,
   59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/

#include <eoaccess/common.m>

#if NeXT_foundation_LIBRARY
typedef void NXTypedStream;
#endif

#include <eointerface/EOController.h>
#include <eointerface/EOAssociation.h>
#include <eointerface/EOQualifiedAssociation.h>
#include <eointerface/EOUtils.h>

#include "EOControllerPrivate.h"

/*
 * EOController implementation
 */

@implementation EOController

/*
 * Private class methods that keep all EOControllers list
 * used in grouping controllers for undo (see undo comments)
 */

static NSMutableArray*	allEOControllers = nil;

+ (void)initialize
{
    allEOControllers = [NSMutableArray new];
}

+ (void)_addController:ctrl
{
    [allEOControllers addObject:[NSValue valueWithNonretainedObject:ctrl]];
}

+ (void)_removeController:ctrl
{
    int i;

    for (i = [allEOControllers count]-1; i>=0; i--)
    {
	if (ctrl == [[allEOControllers objectAtIndex:i] 
		    nonretainedObjectValue])
	    [allEOControllers removeObjectAtIndex:i];
    }
}

+ (id)_allEOControllers
{
    return allEOControllers;
}

/*
 * Methods used to perform an operation deep in the controllers tree
 * they first ask associations destinations if they respond to the
 * given selector (if they do they are asked to perform it) and then
 * the next controller is asked to perform the operation
 * The selectors have one/two arg(s) which is the root controller 
 * (that one that initiated the operation) to prevent infinite recursion
 */

- (void)__recursePerform:(SEL)sel root:(id)root
{
    unsigned i, n = [associations count];
    for (i=0; i<n; i++) {
	id dst = [[associations objectAtIndex:i] destination];
	if ([dst respondsToSelector:sel] && dst != root)
	    [dst perform:sel withObject:root];
    }
    if (nextController && nextController != root)
	[nextController perform:sel withObject:root];
}

- (void)__recursePerform:(SEL)sel arg:(id)arg root:(id)root
{
    unsigned i, n = [associations count];
    for (i=0; i<n; i++) {
	id dst = [[associations objectAtIndex:i] destination];
	if ([dst respondsToSelector:sel] && dst != root)
		[dst perform:sel withObject:arg withObject:root];
    }
    if (nextController && nextController != root)
	[nextController perform:sel withObject:arg withObject:root];
}

- (id)__recursePerformUntilYES:(SEL)sel root:(id)root
{
    unsigned i, n = [associations count];
    id ret = (id)NO;
    for (i=0; i<n; i++) {
	id dst = [[associations objectAtIndex:i] destination];
	if ([dst respondsToSelector:sel] && dst != root) {
	    ret = [dst perform:sel withObject:root];
	    if (ret)
		return ret;
	}
    }
    if (nextController && nextController != root) {
	ret = [nextController perform:sel withObject:root];
	if (ret)
	    return ret;
    }
    return ret;
}

- (id)__recursePerformUntilNO:(SEL)sel root:(id)root
{
    unsigned i, n = [associations count];
    id ret = (id)YES;
    for (i=0; i<n; i++) {
	id dst = [[associations objectAtIndex:i] destination];
	if ([dst respondsToSelector:sel] && dst != root) {
	    ret = [dst perform:sel withObject:root];
	    if (!ret)
		return ret;
	}
    }
    if (nextController && nextController != root) {
	ret = [nextController perform:sel withObject:root];
	if (!ret)
	    return ret;
    }
    return ret;
}

/* 
 * Notify associations (and the delegate if selection has changed)
 */

- (void)__notifyAssociations:(SEL)sel
{
    if (!associationNotificationLock) 
	[associations makeObjectsPerform:sel];
}

- (void)__notifySelectionChanged
{
    [self __notifyAssociations:@selector(selectionDidChange)];
    if ([delegate respondsToSelector:
	@selector(controllerDidChangeSelection:)])
    {
	[delegate controllerDidChangeSelection:self];
    }
}

/* 
 * Initializing and destroying controllers
 */

- init
{
	return [self initWithDataSource:nil];
}

- initWithDataSource:(id <EODataSources>)aDataSource
{
    [super init];
    
    associations = [NSMutableArray new];
    selection = [NSMutableArray new];
    sortOrdering = [NSMutableArray new];
    objects = [NSMutableArray new];
    
    editStack = [NSMutableArray new];
    operationsStack = [NSMutableArray new];
    
    undoStack = [NSMutableArray new];
    undoMarkers = [NSMutableArray new];
    undoEnabled = YES;
    undoMarkEveryOperation = YES;
    undoFirstOp = YES;
    
    dataSource = [(id)aDataSource retain];
    [[self class] _addController:self];
    return self;
}

- (void)dealloc
{
    [(id)dataSource release];
    [associations release];
    [objects release];
    [selection release];
    [sortOrdering release];
    [editStack release];
    [operationsStack release];
    [operationsPosponedStack release];
    [undoStack release];
    [undoMarkers release];
    [undoGroup removeController:self];
    [undoGroup release];
    
    [[EOController class] _removeController:self];
    [super dealloc];
}

/* 
 * Managing associations 
 */	

- (void)addAssociation:(EOAssociation*)anAssociation
{
    [associations addObject:anAssociation];
}

- (void)removeAssociation:(EOAssociation*)anAssociation
{
    [associations removeObject:anAssociation];
}

- (NSArray*)associations
{
    return associations;
}

- (NSArray*)keys
{
    int i, n = [associations count];
    id *objs = Malloc(sizeof(id)*n);
    id array;
    
    for (i=0; i<n; i++)
	objs[i] = [[associations objectAtIndex:i] key];

    array = [[[NSArray alloc] initWithObjects:objs count:n] autorelease];
    Free(objs);
    return array;
}

- (void)disableAssociationNotification
{
    associationNotificationLock++;
}

- (void)reenableAssociationNotification
{
    if (associationNotificationLock-- < 0) 
    {
	NSLog(@"EOController: too many reenableAssociationNotification\n");
	associationNotificationLock = 0;
    }
}

/* 
 * Getting the objects 
 */

- (NSArray*)allObjects
{
    return objects;
}

/* 
 * Fetching objects 
 */

/*
 * Implementors note
 * "root" is used to prevent infinite recursion when deep performing
 *  	(see __recursePerform* methods)
 * Selection is changed if the new selection indexes are different
 * There are some problems regarding associations notifications and
 * selection. One is that controller should maintain positional selection
 * after a fetch and changes it (notifies its associations) only if is
 * selects first object after fetch (flag is set or previous selection is
 * out of bounds). The second is that it has to send fetch or _fetch:
 * to its detail controllers as documented. So I had to
 * add a flag "fetchInProgress" saying that the controller is inside a fetch,
 * and to modify EOQualified association to requalify detail data source on 
 * selection changes but not to do the fetch if its controller has 
 * "fetchInProgress" flag set - detail will receive _fetch: propagated 
 * recursively. 
 * One more issue is selection. I think it is better to maintain the objects
 * not the positions selected previously. However if positions change we have
 * to send selection did change (EOQualifiedAssociation will not requalify
 * or refetch because their master object and value are not changed).
 *
 */
- (BOOL)_fetch:root
{
    id   objs, nsel, tmp;
    BOOL flag;

    fetchInProgress = YES;

    [self endEditing];
    if ([delegate respondsToSelector:@selector(controllerWillFetch:)])
	if (![delegate controllerWillFetch:self]) { 
	    fetchInProgress = NO;
	    return NO;
	}
    if (![self isDiscardAllowed] || ![self _detailDiscardIfAllowed]) {
	fetchInProgress = NO;
	return NO;
    }
    [self discardEdits];
    [self discardOperations];
    [self releaseUndos];

    objs = [self selectedObjects];
    nsel = [[[NSMutableArray alloc] init] autorelease];

    tmp = [dataSource fetchObjects];
    [objects removeAllObjects];
    [objects addObjectsFromArray:tmp];
    [self _resort];

    if (!autoSelectFirst && [selection count]) {
	int i;
	for (i=[objs count]-1; i>=0; i--) {
	    id obj = [objs objectAtIndex:i];
	    if ([objects indexOfObjectIdenticalTo:obj] != NSNotFound)
		[nsel addObject:obj];
	}
    }
    if (![nsel count] && [objects count])
	[nsel addObject:[objects objectAtIndex:0]];
    
    flag = [self _setSelectionForObjects:nsel detailDiscard:NO];
	    
    if ([delegate respondsToSelector:@selector(controller:didFetchObjects:)])
	[delegate controller:self didFetchObjects:objects];
    
    [self __notifyAssociations:@selector(contentsDidChange)];

    if (flag)
	[self __notifySelectionChanged];

    flag = (BOOL)(int)[self __recursePerformUntilNO:_cmd root:root];

    fetchInProgress = NO;
    return flag;
}

- (BOOL)fetch
{
    return [self _fetch:self];
}

- (BOOL)fetchInProgress
{
    return fetchInProgress;
}

/* 
 * Managing the selection 
 * When one needs to change selection due to delete/insert/fetch he must do
 * in the following way: get the selected objects sel=[self selectedObjects]
 * alter selection sel, use flag=[self _setSelectionForObjects:sel 
 * detailDiscard:YES/NO], notify delegate for operation, notify 
 * contentChanged and if flag==YES notify selectionChanged
 */

static NSComparisonResult __compareIndex(id o1, id o2, void* context)
{
    int v1 = [o1 intValue];
    int v2 = [o2 intValue];
    
    return (v1==v2) ? (NSOrderedSame) : 
	(v1>v2 ? NSOrderedDescending : NSOrderedAscending);
}

- (BOOL)_setSelectionForObjects:(NSArray*)list detailDiscard:(BOOL)yn
{
    int i, n = [list count];
    id arry = [[NSMutableArray alloc] initWithCapacity:n];
    
    for (i=0; i<n; i++)
	[arry addObject:[NSNumber numberWithInt:
	    [objects indexOfObjectIdenticalTo:[list objectAtIndex:i]]]];
    [arry sortUsingFunction:(int (*)(id,id,void*))__compareIndex context:nil];
    
    if ([selection isEqual:arry])
	    return NO;
    if (yn) 
    {
	[self _detailDiscard];
	[self endEditing];
    }
    [selection removeAllObjects];
    [selection addObjectsFromArray:arry];

    [arry release];
    return YES;
}

- (BOOL)setSelectionIndexes:(NSArray*)aSelection
{
    if ([selection isEqual:aSelection])
	return YES;
    
    if (![self _detailDiscardIfAllowed])
	return NO;
    
    [self endEditing];
    [selection removeAllObjects];
    [selection addObjectsFromArray:aSelection];
    [selection sortUsingFunction:(int (*)(id,id,void*))__compareIndex 
	context:nil];
    
    [self __notifySelectionChanged];
    return YES;
}

- (NSArray*)selectionIndexes
{
    return selection;
}

- (NSArray*)selectedObjects
{
    int i, n = [selection count];
    id arry = [[[NSMutableArray alloc] initWithCapacity:n] autorelease];
    
    for (i=0; i<n; i++)
	[arry addObject:[objects objectAtIndex:
	    [[selection objectAtIndex:i] intValue]]];
    
    return arry;
}

- (BOOL)clearSelection
{
    return [self setSelectionIndexes:[NSArray array]];
}

- (BOOL)selectNext
{
    int index, count;
    id newsel;

    if ((count = [objects count])) {
	if ([selection count])
	    index = [[selection objectAtIndex:0] intValue] + 1;
	else
	    index = 0;
	index = (index < count) ? index : 0;
	newsel = [NSArray arrayWithObjects:
		    [NSNumber numberWithInt:index], nil];
    }
    else {
	newsel = [NSArray array];
    }
    return [self setSelectionIndexes:newsel];
}

- (BOOL)selectPrevious
    {
    int index, count;
    id newsel;
    
    if ((count = [objects count])) {
	if ([selection count])
	    index = [[selection objectAtIndex:0] intValue] - 1;
	else
	    index = count-1;
	index = index<0 ? 0 : index;
	newsel=[NSArray arrayWithObjects:
		[NSNumber numberWithInt:index],nil];
    }
    else {
	newsel = [NSArray array];
    }
    return [self setSelectionIndexes:newsel];
    }

- (void)setSelectsFirstObjectAfterFetch:(BOOL)yn
{
    autoSelectFirst = yn;
}

- (BOOL)selectsFirstObjectAfterFetch
{
    return autoSelectFirst;
}

/* 
 * Editing objects 
 */

- (void)_addEdits:edits object:obj markUndo:(BOOL)yn
{
    [editStack addObject:[[[_EOOperation alloc] 
	initOperation:_EOUpdate onObject:obj new:edits old:nil] autorelease]];
    [self _addUndo:[[[_EOOperation alloc] 
	    initOperation:_EOUpdate onObject:obj 
		new:[[edits copy] autorelease]
		old:[[[NSDictionary alloc]
		initWithDictionary:[obj valuesForKeys:[edits allKeys]]]
			autorelease]] autorelease] 
	mark:yn];
}

- (void)associationDidEdit:(EOAssociation*)association
{
    id key = [association key];
    id val = [association value];
    id obj, dict;
    
    if (![objects count] || ![selection count])
	return;
    
    obj = [objects objectAtIndex:[[selection objectAtIndex:0] intValue]];
    
    dict = [[[NSMutableDictionary alloc] init] autorelease];
    [dict setObject:val forKey:key];
    [self _addEdits:dict object:obj markUndo:YES];
    
    if ([delegate respondsToSelector:
		@selector(controller:association:didEditObject:key:value:)])
	[delegate controller:self association:association
		didEditObject:obj key:key value:val];
    
    if (autoSaveToObjects)
	[self saveToObjects];
}

- (void)setValues:(NSDictionary*)newValues forObject:obj
{
    id dict = [[NSMutableDictionary alloc] initWithDictionary:newValues];
    [self _addEdits:dict object:obj markUndo:YES];
    [dict release];
    if (autoSaveToObjects)
	[self saveToObjects];
}

- (void)endEditing
{
    [self __notifyAssociations:@selector(endEditing)];
}

/* 
 * Sorting objects 
 */

- (void)_resort
{
    if ([delegate respondsToSelector:@selector(controller:sortObjects:)])
    {
	    [delegate controller:self sortObjects:objects];
    }
    else
    {
	    [objects sortUsingKeyOrderArray:[self sortOrdering]];
    }
}

- (void)resort
{
    [self endEditing];
    [self _resort];
    [self redisplay];
}

- (void)setSortOrdering:(NSArray*)keySortOrderArray
{
    if (keySortOrderArray == sortOrdering)
	return;
    [sortOrdering removeAllObjects];
    [sortOrdering addObjectsFromArray:keySortOrderArray];
}

- (NSArray*)sortOrdering
{
    return sortOrdering;
}

/* 
 * Deleting objects 
 */

- (BOOL)_deleteObject:obj atIndex:(int)index markUndo:(BOOL)yn
{
    [operationsStack addObject:[[[_EOOperation alloc] 
	initOperation:_EODelete onObject:obj index:index] autorelease]];
    if (yn)
	[self _addUndo:[[[_EOOperation alloc] 
	    initOperation:_EODelete onObject:obj index:index] autorelease] 
	    mark:yn];	
    [objects removeObjectIdenticalTo:obj];

    if ([self savesToDataSourceAutomatically])
	if (![self saveToDataSource])
	    return NO;
    return YES;
}

- (BOOL)_deleteObjectIndexes:indx
{
    int i,n,k;
    id sel = [self selectedObjects];
    BOOL ret = YES;
    BOOL flag;
    id nextsel, todelete;
    
    // next object selected if selection becomes empty
    k = [indx count];
    todelete = [[NSMutableArray alloc] initWithCapacity:k];
    for (i=0; i<k; i++)
	[todelete addObject:[objects objectAtIndex:
	    [[indx objectAtIndex:i] intValue]]];
    nextsel = nil;
    k = [selection count];
    n = [objects count];
    if (k)
    {
	for (i = [[selection objectAtIndex:0] intValue]+1; i<n; i++)
	{
	    id obj = [objects objectAtIndex:i];
	    if ([todelete indexOfObjectIdenticalTo:obj]==NSNotFound)
	    {
		nextsel = obj;
		break;
	    }
	}
	if (!nextsel)
	{
	    for (i = [[selection objectAtIndex:0] intValue]-1; i>=0; i--)
	    {
		id obj = [objects objectAtIndex:i];
		if ([todelete indexOfObjectIdenticalTo:obj]==NSNotFound)
		{
		    nextsel = obj;
		    break;
		}
	    }
	}
    }
    [todelete release];
    
    // check if dataSource can delete object
    if (![dataSource canDelete])
    {
	EOAlertPanel(
		@"Data source cannot delete objects !", 
		@"Ok");
	return NO;
    }
    [self endEditing];
    
    // delete objects
    n=[indx count];
    for (k=0, i=n-1; i>=0; i--) 
    {
	int idx = [[indx objectAtIndex:i] intValue];
	id obj = [objects objectAtIndex:idx];
	BOOL doit = YES;
    
	if ([sel indexOfObjectIdenticalTo:obj] != NSNotFound)
	{
	    [sel removeObjectIdenticalTo:obj];
	}
	if (idx >= [objects count])
	    [NSException raise:NSRangeException
		format:@"EOController[%p]: delete at index %d from %d",
		self, idx, [objects count]];
    
	if ([delegate respondsToSelector:
		@selector(controller:willDeleteObject:)])
	{
	    if (![delegate controller:self willDeleteObject:obj])
		    doit = ret = NO;
	}
	
	if (doit)
	{
	    [obj retain];
	    if (![self _deleteObject:obj atIndex:idx markUndo:(k++)?NO:YES])
		doit = ret = NO;
	    if (doit)
	    {
		if ([delegate respondsToSelector:
			@selector(controller:didDeleteObject:)])
		{
		    [delegate controller:self didDeleteObject:obj];
		}	
	    }
	    [obj release];
	}
    }
    
    if (![sel count] && nextsel)
	[sel addObject:nextsel];
    flag = [self _setSelectionForObjects:sel detailDiscard:YES];
    [self __notifyAssociations:@selector(contentsDidChange)];
    if (flag)
	[self __notifySelectionChanged];
    return YES;
}

- (BOOL)deleteObjectAtIndex:(unsigned)anIndex
{
    return [self _deleteObjectIndexes:[NSArray arrayWithObject:
	    [NSNumber numberWithInt:anIndex]]];
}

- (BOOL)deleteSelection
{
    return [self _deleteObjectIndexes:[[selection copy] autorelease]];
}

/* 
 * Inserting objects 
 */

- (BOOL)_insertObject:obj atIndex:(int)index markUndo:(BOOL)yn
{
    [objects insertObject:obj atIndex:index];
    [operationsStack addObject:[[[_EOOperation alloc] 
	initOperation:_EOInsert onObject:obj index:index] autorelease]];
    if (yn)
	[self _addUndo:[[[_EOOperation alloc] 
	    initOperation:_EOInsert onObject:obj index:index] autorelease] 
	    mark:yn];

    if ([self savesToDataSourceAutomatically])
	if (![self saveToDataSource])
	    return NO;
    return YES;
}

- insertObject:obj atIndex:(unsigned)index
{
    BOOL flag;
    
    [self endEditing];
    
    if  ((!objects && index) || (index > [objects count]))
	[NSException raise:NSRangeException
	    format:@"EOController[%p]: insertObject at index %d from %d",
	    self, index, [objects count]];
    
    if ([delegate respondsToSelector:
	@selector(controller:willInsertObject:atIndex:)])
    {
	if (![delegate controller:self willInsertObject:obj atIndex:index])
	    return nil;
    }
    
    [obj retain];
    
    if (![self _insertObject:obj atIndex:index markUndo:YES])
    {
	[obj release];
	return nil;
    }
    
    [obj release];
    
    flag = [self _setSelectionForObjects:[NSArray arrayWithObject:obj]
	detailDiscard:YES];
    
    if ([delegate respondsToSelector:@selector(controller:didInsertObject:)])
	[delegate controller:self didInsertObject:obj];
    
    [self __notifyAssociations:@selector(contentsDidChange)];
    
    if (flag)
	[self __notifySelectionChanged];
    
    return obj;
}

- insertObjectAtIndex:(unsigned)anIndex
{
	id obj;

	obj = [dataSource createObject];
	if (!obj) 
	{
		if ([delegate respondsToSelector:
			@selector(controller:createObjectFailedForDataSource:)])
			[delegate controller:self 
				createObjectFailedForDataSource:dataSource];
		else 
		EOAlertPanel(
			@"Data source could not create object !", 
			@"Ok");
		return nil;
	}
	return [self insertObject:obj atIndex:anIndex];
}

/* 
 * Discarding edits/operations and isDiscardAllowed
 */

- (void)_discardEdits:root
{
	[self discardEdits];
	return [self __recursePerform:_cmd root:root];
}

- (void)discardEdits
{
	[editStack removeAllObjects];
	[self __notifyAssociations:@selector(discardEdits)];
}

- (void)_discardOperations:root
{
	[self discardOperations];
	return [self __recursePerform:_cmd root:root];
}

- (void)discardOperations
{
	[operationsStack removeAllObjects];
}

- (BOOL)_detailDiscardIfAllowed
{
	if (!(BOOL)(int)[self 
		__recursePerformUntilNO:@selector(_isDiscardAllowed:) root:self])
		return NO;
	[self __recursePerform:@selector(_discardEdits:) root:self];
	[self __recursePerform:@selector(_discardOperations:) root:self];
	return YES;
}

- (BOOL)_detailDiscard
{
	[self __recursePerformUntilNO:@selector(_isDiscardAllowed:) root:self];
	[self __recursePerform:@selector(_discardEdits:) root:self];
	[self __recursePerform:@selector(_discardOperations:) root:self];
	return YES;
}

- (BOOL)_isDiscardAllowed:root
{
	if (![self isDiscardAllowed])
		return NO;
	return (BOOL)(int)[self __recursePerformUntilNO:_cmd root:self];
}

- (BOOL)isDiscardAllowed
{
    [self _reduceEdits];
    if ([editStack count])
    {
	if ([delegate respondsToSelector:
			@selector(controllerWillDiscardEdits:)])
	{
	    if (![delegate controllerWillDiscardEdits:self])
		    return NO;
	}
	else
	{
	    switch(EOAlertPanel(
		    @"Edits are unsaved !", 
		    @"Save", @"Discard", @"Cancel"))
	    {
		case 0:
			[self saveToObjects];
			break;
		case 1:
			[self discardEdits];
			break;
		case 2:
			return NO;
			break;
		default:
			[self saveToObjects];
	    }
	}
    }
    [self _reduceOperations];
    if ([operationsStack count])
    {
	if ([delegate respondsToSelector:
			@selector(controllerWillDiscardOperations:)])
	{
		if (![delegate controllerWillDiscardOperations:self])
			return NO;
	}
	else
	{
	    switch(EOAlertPanel(
		    @"Objects are not saved in database !", 
		    @"Save", @"Discard", @"Cancel"))
	    {
		case 0:
			[self saveToDataSource];
			break;
		case 1:
			[self discardOperations];
			break;
		case 2:
			return NO;
			break;
		default:
			[self saveToDataSource];
	    }
	}
    }
    return YES;
}

/* 
 * Saving edits to objects 
 */

- (void)_reduceEdits
{	
    int i, n;
    
    for (i=1, n=[editStack count]; n && i<n; i++)
    {
	int j;
	id op = [editStack objectAtIndex:i];
	id obj = [op object];

	for (j=0; j<i; j++)
	{
	    id nop = [editStack objectAtIndex:j];
	    if (obj == [nop object])
	    {
		[[nop newvalues] addEntriesFromDictionary:[op newvalues]];
		[editStack removeObjectAtIndex:i];
		i--; n--;
		break;
	    }
	}
    }
}

- (NSDictionary*)_coerceEdits:(NSDictionary*)edits
{
    id key;
    id keys = [edits keyEnumerator];
    id dict = [[[NSMutableDictionary alloc] 
	    initWithCapacity:[edits count]] autorelease];
    while ((key=[keys nextObject]))
    {
	id val = [dataSource coerceValue:[edits objectForKey:key] forKey:key];
	if (val)
	    [dict setObject:val forKey:key];
    }
    return dict;
}

- (BOOL)_updateObject:obj
{
    [operationsStack addObject:[[[_EOOperation alloc] 
	    initOperation:_EOUpdate 
	    onObject:obj
	    index:0] autorelease]];
    if ([self savesToDataSourceAutomatically])
	    if (![self saveToDataSource])
		    return NO;
    return YES;
}

- (BOOL)_saveToObjects:root
{
    [self endEditing];
    [self _reduceEdits];
    
    while ([editStack count])
    {
	id op = [editStack objectAtIndex:0];
	id obj = [op object];
	id edt = [op newvalues];
	
	if ([delegate respondsToSelector:
		@selector(controller:willSaveEdits:toObject:)])
	{
	    edt = [delegate controller:self willSaveEdits:edt toObject:obj];
	    if (!edt)
		return NO;
	}
	edt = [self _coerceEdits:edt]; 
	[obj takeValuesFromDictionary:edt];
	[self _updateObject:obj];
	[editStack removeObjectAtIndex:0];
	if ([delegate respondsToSelector:
		@selector(controller:didSaveToObject:)])
	{
	    [delegate controller:self didSaveToObject:obj];
	}	
    }
    [self __notifyAssociations:@selector(contentsDidChange)];
    [self __notifyAssociations:@selector(discardEdits)];
    return (BOOL)(int)[self __recursePerformUntilNO:_cmd root:root];
}

- (BOOL)saveToObjects
{
    return [self _saveToObjects:self];
}

- (BOOL)hasChangesForObjects
{
    return [editStack count] ? YES : NO;
}

- (BOOL)setSavesToObjectsAutomatically:(BOOL)yn
{
    autoSaveToObjects = yn;
    return [self hasChangesForObjects];
}

- (BOOL)savesToObjectsAutomatically
{
    return autoSaveToObjects;
}

/* 
 * Saving to the data source 
 */

/* reduce operation bitmap based on _EOUpdate=0, _EOInsert=1, _EODelete=2 */

static char reduceTable[3][3] = {
	{'U','I','@'}, 	// _EOUpdate & (_EOUpdate, _EOInsert, _EODelete)
	{'I','I','*'},	// _EOInsert & (_EOUpdate, _EOInsert, _EODelete)
	{'D','*','D'}	// _EODelete & (_EOUpdate, _EOInsert, _EODelete)
};

- (void)_reduceOperations
{
    int i, n;
    
    for (i=1, n=[operationsStack count]; n && i<n; i++)
    {
	int j;
	id cop = [operationsStack objectAtIndex:i];
	id obj = [cop object];

	for (j=i-1; j>=0; j--)
	{
	    id nop = [operationsStack objectAtIndex:j];
	    if (obj == [nop object])
	    {				
		switch (reduceTable[[cop operation]][[nop operation]])
		{
		    case '*':
			    [operationsStack removeObjectAtIndex:j];
			    i--; n--;
			    [operationsStack removeObjectAtIndex:i];
			    i--; n--;
			    break;
		    case 'U':
			    [cop setOperation:_EOUpdate];
			    [operationsStack removeObjectAtIndex:i];
			    i--; n--;
			    break;
		    case 'I':
			    [cop setOperation:_EOInsert];
			    [operationsStack removeObjectAtIndex:i];
			    i--; n--;
			    break;
		    case 'D':
			    [cop setOperation:_EODelete];
			    [operationsStack removeObjectAtIndex:i];
			    i--; n--;
			    break;
		    case '@': 
			    /* no reduce */
			    break;
		    default: [NSException raise:NSGenericException
			    format:@"EOController[%p] _reduce inconsitency",
			    self];
		}
		break;
	    }
	}
    }
}

- (EODataSourceFailureResponse)_dataSourceOperation:(_EOControllerOperation)op
  onObject:(id)obj
{
    EODataSourceOperationDelegateResponse rsp = EOPerformDataSourceOperation;
    
    switch(op)
    {
	case _EOUpdate:
	    if ([delegate respondsToSelector:
		    @selector(controller:willUpdateObject:inDataSource:)])
	    {
		rsp = [delegate controller:self willUpdateObject:obj
			inDataSource:dataSource];
	    }
	    if (rsp==EOPerformDataSourceOperation) 
	    {
		if ([dataSource updateObject:obj])
		{
		    if ([delegate respondsToSelector:
			  @selector(controller:didUpdateObject:inDataSource:)])
		    {
			    [delegate controller:self 
				    didUpdateObject:obj 
				    inDataSource:dataSource];
		    }
		}
		else
		{
		    if ([delegate respondsToSelector:
		    @selector(controller:failedToUpdateObject:inDataSource:)])
		    {
			rsp = [delegate controller:self 
				failedToUpdateObject:obj 
				inDataSource:dataSource];
		    }
		    else
		    {
			rsp =  EOAlertPanel(
				@"Unable to update object in data source",
				@"Continue", @"Rollback") ?
				EORollbackDataSourceOperation :
				EOContinueDataSourceOperation;
		    }
		}
	    }
	    break;
    
	case _EOInsert:
	    if ([delegate respondsToSelector:
		    @selector(controller:willInsertObject:inDataSource:)])
	    {
		rsp = [delegate controller:self willInsertObject:obj
			inDataSource:dataSource];
	    }
	    if (rsp==EOPerformDataSourceOperation)
	    {
		if ([dataSource insertObject:obj])
		{
		    if ([delegate respondsToSelector:
			  @selector(controller:didInsertObject:inDataSource:)])
		    {
			[delegate controller:self
				didInsertObject:obj
				inDataSource:dataSource];
		    }
		}
		else
		{
		    if ([delegate respondsToSelector:
		    @selector(controller:failedToInsertObject:inDataSource:)])
		    {
			rsp = [delegate controller:self 
				failedToInsertObject:obj 
				inDataSource:dataSource];
		    }
		    else
		    {
			rsp =  EOAlertPanel(
				@"Unable to insert object in data source",
				@"Continue", @"Rollback") ?
				EORollbackDataSourceOperation :
				EOContinueDataSourceOperation;
		    }
		}
	    }
	    break;
    
	case _EODelete:
	    if ([delegate respondsToSelector:
		    @selector(controller:willDeleteObject:inDataSource:)])
	    {
		rsp = [delegate controller:self willDeleteObject:obj
			inDataSource:dataSource];
	    }
	    if (rsp==EOPerformDataSourceOperation)
	    { 
		if ([dataSource deleteObject:obj])
		{
		    if ([delegate respondsToSelector:
			  @selector(controller:didDeleteObject:inDataSource:)])
		    {
			[delegate controller:self
				didDeleteObject:obj
				inDataSource:dataSource];
		    }
	    }
	    else
	    {
		    if ([delegate respondsToSelector:
		    @selector(controller:failedToDeleteObject:inDataSource:)])
		    {
			rsp = [delegate controller:self 
				failedToDeleteObject:obj 
				inDataSource:dataSource];
		    }
		    else
		    {
			rsp =  EOAlertPanel(
				@"Unable to delete object in data source",
				@"Continue", @"Rollback") ?
				EORollbackDataSourceOperation :
				EOContinueDataSourceOperation;
		    }
		}
	    }
	    break;
		
	case _EORollBack:
	    if (![(id)dataSource respondsToSelector:@selector(rollback)])
		    return EONoDataSourceFailure;
	    if ([delegate respondsToSelector:
		    @selector(controller:willRollbackDataSource:)])
	    {
		[delegate controller:self willRollbackDataSource:dataSource];
	    }
	    [(id)dataSource rollback];
	    if ([delegate respondsToSelector:
		    @selector(controller:didRollbackDataSource:)])
	    {
		[delegate controller:self didRollbackDataSource:dataSource];
	    }
	    return EONoDataSourceFailure;
    
	default:
		[NSException raise:NSGenericException
		format:@"EOController[%p] internal inconsitency", self];
    }
    switch (rsp)
    {
	case EOContinueDataSourceOperation:
		return EOContinueDataSourceFailureResponse;
	case EODiscardDataSourceOperation:
		return EONoDataSourceFailure;
	case EORollbackDataSourceOperation:
		return EORollbackDataSourceFailureResponse;
	case EOPerformDataSourceOperation:
		return EONoDataSourceFailure;
    }
    return EONoDataSourceFailure;
}

- (BOOL)_saveToDataSourceRollback:(id)root
{
    [operationsPosponedStack release];
    operationsPosponedStack = nil;
    [self _dataSourceOperation:_EORollBack onObject:nil];
    [self __recursePerform:_cmd root:root];
    return YES;
}

- (BOOL)_saveToDataSourceCommit:(id)root
{
    if (![dataSource saveObjects])
    {
	if ([delegate respondsToSelector:
		@selector(controller:saveObjectsFailedForDataSource:)])
	    [delegate controller:self
		    saveObjectsFailedForDataSource:dataSource];
	else
	    EOAlertPanel(@"DataSource could not save objects !", @"Ok");
	[self _saveToDataSourceRollback:self];
	return NO;
    }
    [operationsStack removeAllObjects];
    [operationsStack addObjectsFromArray:operationsPosponedStack];
    [operationsPosponedStack release];
    operationsPosponedStack = nil;
    if ([delegate respondsToSelector:
	@selector(controllerDidSaveToDataSource:)])
    {
	[delegate controllerDidSaveToDataSource:self];
    }
    return (BOOL)(int)[self __recursePerformUntilNO:_cmd root:root];
}

- (BOOL)_saveToDataSourcePrepare:(id)root
{
    int i,n;
    
    if (!dataSource)
	return YES;
    if ([delegate respondsToSelector:
	@selector(controllerWillSaveToDataSource:)])
    {
	if (![delegate controllerWillSaveToDataSource:dataSource])
		return NO;
    }
    [self _reduceOperations];
    operationsPosponedStack = [[NSMutableArray alloc] init];
    
    n = [operationsStack count];
    for (i=0; i<n; i++)
    {
	id op = [operationsStack objectAtIndex:i];
	id obj = [op object];
	EODataSourceFailureResponse err = EONoDataSourceFailure;
	
	if ([obj respondsToSelector:@selector(prepareForDataSource:)])
	{
	    if (![obj prepareForDataSource])
	    {
		if ([delegate respondsToSelector:@selector
			(controller:object:failedToPrepareForDataSource:)])
		    err = [delegate controller:self object:obj
			    failedToPrepareForDataSource:dataSource];
		else
		    err = EORollbackDataSourceFailureResponse;
	    }
	}
	if (!err)
		err = [self _dataSourceOperation:[op operation] onObject:obj];
	if (err == EORollbackDataSourceFailureResponse)
		return NO;
	if (err == EOContinueDataSourceFailureResponse)
		[operationsPosponedStack addObject:op];
    }
    return (BOOL)(int)[self __recursePerformUntilNO:_cmd root:root];
}

- (BOOL)saveToDataSource
{
    [self endEditing];
    if (![self _saveToDataSourcePrepare:self])
    {
	[self _saveToDataSourceRollback:self];
	return NO;
    }
    return [self _saveToDataSourceCommit:self];
}

- (BOOL)hasChangesForDataSource
{
    return [operationsStack count] ? YES : NO;
}

- (BOOL)setSavesToDataSourceAutomatically:(BOOL)yn
{
    autoSaveToDataSource = yn;
    return [self hasChangesForDataSource];
}

- (BOOL)savesToDataSourceAutomatically
{
    return autoSaveToDataSource;
}

/* 
 * Controlling undo 
 * This is a little bit ugly. Controller grouping works 
 * like this: every group of controllers linked in master-detail-next
 * has one common _EOUndoGroup object which maintains a stack of controllers.
 * Sending undo to a controller implies that _undo is sent to the last
 * controller in the group which acctualy modified something -- this is 
 * what the user would expect. By setting undoFirstOp in markUndo
 * add pushing the controller in _addUndo is undoFirstOp is set.
 * The problem comes when we need to set the undoGroup. We do this by
 * searching for a top-level master, recursive aske it and descendants if
 * they have an undo group and setting it and descendants the found undo 
 * group or a new one.
 */

- (void)_addUndo:(id)op mark:(BOOL)yn
{
    if (!undoEnabled)
	return;

    if (!undoGroup)
	[[self class] setUndoGroupForController:self];

    if (yn && undoMarkEveryOperation)
	[self markUndo];

    if (undoFirstOp) 
    {
	[undoGroup pushController:self];
	undoFirstOp = NO;
    }
    [undoStack addObject:op];

    // debug undo
    // NSLog(@"AFTER ADDUNDO\nMarkers: %@\nStack: %@\n",
    //	[undoMarkers description], [undoStack description]);
}

- (void)_undo
{
    id ops;
    int i, n;
    id sel;
    BOOL modified = NO;
    
    if (!undoEnabled)
	return;
    
    [self saveToObjects];
    
    // get operations in current mark
    if ([undoMarkers count]) 
    {
	i = [[undoMarkers lastObject] intValue];
	[undoMarkers removeLastObject];
    }
    else
	return;
    
    // check the delegate
    if ([delegate respondsToSelector:@selector(controllerWillUndo:)])
	if (![delegate controllerWillUndo:self])
		return;
    
    // debug undo
    // NSLog(@"BEFORE UNDO:\nMarkers: %@\nStack: %@\n",
    //	[undoMarkers description], [undoStack description]);
    
    n = [undoStack count];
    ops = [NSMutableArray arrayWithCapacity:n-i+1];
    while (i < [undoStack count]) 
    {
	[ops addObject:[undoStack objectAtIndex:i]];
	[undoStack removeObjectAtIndex:i];
    }
    if (undoMaxMarks)
	undoUsedMarks--;
    
    // selection list
    sel = [self selectedObjects];
    
    // undo each entry
    for (i=[ops count]-1; i>=0; i--)
    {
	id operation = [ops objectAtIndex:i];
	id obj = [operation object];
	int index = [operation index];
	id edits = [operation oldvalues];
	int selindex;
    
	// ask the delegate for permision
	if ([delegate respondsToSelector:
	    @selector(controller:willUndoObject:)]) 
	{
	    if (![delegate controller:self willUndoObject:obj])
	    {
		    continue;
	    }
	}
	// undo operation
	switch ([operation operation])
	{
	    case _EOUpdate:
		edits = [self _coerceEdits:edits];
		[obj takeValuesFromDictionary:edits];
		[self _updateObject:obj];
		modified = YES;
		break;

	    case _EOInsert:
		// select previous object if selection goes empty
		[sel removeObjectIdenticalTo:obj];
		if (![sel count])
		{
		    // TODO - if sel becomes empty select something
		    selindex = [objects indexOfObjectIdenticalTo:obj];
		    if (selindex == 0)
			selindex = [objects count]-1 ? 0 : NSNotFound;
		    else
			selindex--;
		    if (selindex != NSNotFound)
			[sel addObject:[objects objectAtIndex:selindex]];
		}
		[self _deleteObject:obj atIndex:index markUndo:NO];
		modified = YES;
		break;

	    case _EODelete:
		[self _insertObject:obj atIndex:index markUndo:NO];
		modified = YES;
		break;
		    
	    default:;
	}
	// remove from undo ops
	[ops removeObjectAtIndex:i];
	// notify delegate
	if ([delegate respondsToSelector:
			@selector(controller:didUndoObject:)])
	{
		[delegate controller:self didUndoObject:obj];
	}
    }
    
    // restack left operations in current mark
    [undoStack addObjectsFromArray:ops];
    
    // debug undo
    // NSLog(@"AFTER UNDO:\nMarkers: %@\nStack: %@\n",
    //	[undoMarkers description], [undoStack description]);
    
    // notify delegate
    if ([delegate respondsToSelector:@selector(controllerDidUndo:)])
	[delegate controllerDidUndo:self];
	
    // content changed
    if (modified)
    {
	BOOL flag = [self _setSelectionForObjects:sel detailDiscard:YES];
	[self __notifyAssociations:@selector(contentsDidChange)];
	if (flag)
	    [self __notifySelectionChanged];
    }
}

- (void)markUndo
{
    [self endEditing];
    
    if (undoMaxMarks && undoUsedMarks >= undoMaxMarks) 
    {
	int i, m, n;
	
	if ([undoMarkers count]) 
	{
	    m = [[undoMarkers objectAtIndex:0] intValue];
	    [undoMarkers removeObjectAtIndex:0];
	}
	else
	    m = [undoStack count];
	n = m;
	for (i=0; i<m; i++)
	    [undoStack removeObjectAtIndex:i];
	m = [undoMarkers count];
	for (i=0; i<m; i++) 
	{
	    [undoMarkers replaceObjectAtIndex:i 
		withObject:[NSNumber numberWithInt:
		    [[undoMarkers objectAtIndex:i] intValue]-n]];
	}
    }
    [undoMarkers addObject:[NSNumber numberWithInt:[undoStack count]]];
    undoFirstOp = YES;
    if (undoMaxMarks)
	undoUsedMarks++;
}

- (void)undo
{
    [self endEditing];
    [[undoGroup popController] _undo];
}

- (void)releaseUndos
{
    [self endEditing];
    [undoStack removeAllObjects];
    [undoMarkers removeAllObjects];
    [undoGroup removeController:self];
}

- (BOOL)hasUndos
{
    return [undoMarkers count] || [undoStack count] ? YES : NO;
}

- (void)setUndoEnabled:(BOOL)yn
{
    undoEnabled = yn;
}

- (BOOL)isUndoEnabled
{
    return undoEnabled;
}

- (void)setMarksEveryOperation:(BOOL)yn
{
    undoMarkEveryOperation = yn;
}

- (BOOL)marksEveryOperation
{
    return undoMarkEveryOperation;
}

- (void)setMaximumUndoMarks:(unsigned int)max
{
    undoMaxMarks = max;
}

- (unsigned int)maximumUndoMarks
{
    return undoMaxMarks;
}

/* 
 * Redisplaying the user interface 
 */

- (void)_redisplay:root
{
    [self __notifyAssociations:@selector(contentsDidChange)];
    [self __notifyAssociations:@selector(selectionDidChange)];
    [self __recursePerform:_cmd root:root];
}

- (void)redisplay
{
    [self _redisplay:self];
}

/* 
 * Chaining controllers 
 */

- (void)setNextController:(EOController*)aController
{
    nextController = aController;
}

- (EOController*)nextController
{
    return nextController;
}

/* 
 * Setting the data source 
 */

- (void)setDataSource:(id <EODataSources>)aDataSource
{
    [(id)dataSource autorelease];
    dataSource = [(id)aDataSource retain];
    
    [self releaseUndos];
    [self discardEdits];
    [self discardOperations];
    [self clearSelection];

    [objects removeAllObjects];
    
    if ([delegate respondsToSelector: 
	@selector(controller:didChangeDataSource:)])
    {
	[delegate controller:self didChangeDataSource:dataSource];
    }
}

- (id <EODataSources>)dataSource
{
    return dataSource;
}

/* 
 * Setting the delegate 
 */

- (void)setDelegate:aDelegate
{
    delegate = aDelegate;
}

- delegate
{
    return delegate;
}

/* 
 * Action methods 
 */

- delete:sender
{
    return [self deleteSelection] ? self : nil;
}

- discardEdits:sender
{
    [self discardEdits];
    return self;
}

- discardOperations:sender
{
    [self discardOperations];
    return self;
}

- insert:sender
{
    unsigned index = [selection count];
    if (index)
	index = [[selection objectAtIndex:index-1] unsignedIntValue]+1;
    else
	index = [objects count];
    return [self insertObjectAtIndex:index] ? self : nil;
}

- fetch:sender
{
    return [self fetch] ? self : nil;
}

- saveToDataSource:sender
{
    return [self saveToDataSource] ? self : nil;
}

- saveToObjects:sender
{
    return [self saveToObjects] ? self : nil;
}

- selectNext:sender
{
    if ([self selectNext]) 
    {
	[self redisplay];
	return (id)YES;
    }
    return (id)NO;
}

- selectPrevious:sender
{
    if ([self selectPrevious]) 
    {
	[self redisplay];
	return (id)YES;
    }
    return (id)NO;
}

- markUndo:sender
{
    [self undo];
    return self;
}

- undo:sender
{
    [self undo];
    return self;
}

@end /* EOController */

/*
 * EOControllerUndoExtensions
 */

@implementation EOController(EOControllerUndoExtensions)

- (id)undoGroup
{
    return undoGroup;
}

- (void)setUndoGroup:grp
{
    [undoGroup autorelease];
    undoGroup = [grp retain];
}

- (void)_setUndoGroup:ctrl root:root
{
    [self setUndoGroup:ctrl];
    [self __recursePerform:_cmd arg:ctrl root:root];
}

- (id)_askUndoGroupForController:root
{
    return undoGroup ? undoGroup :
	[self __recursePerformUntilYES:_cmd root:root];
}

+ (id)_topLevelControllerForController:ctrl
{
    id all = [self _allEOControllers];
    id top = ctrl;
    
    while (ctrl) 
    {
	int i;
	
	again:
	for (i = [all count]-1; i>=0; i--) 
	{
	    int j;
	    id list = [[[all objectAtIndex:i] 
		    nonretainedObjectValue] associations];
	    
	    for (j=[list count]-1; j>=0; j--)
	    {
		id obj = [[list objectAtIndex:j] destination];
		if (ctrl == obj) {
		    top = ctrl = [[all objectAtIndex:i] 
			    nonretainedObjectValue];
		    goto again;
		}
	    }
	}
	ctrl = nil;
    }
    return top;
}

+ (void)setUndoGroupForController:(EOController*)ctrl
{
    id grp;
    grp  = [[self _topLevelControllerForController:ctrl] 
	    _askUndoGroupForController:ctrl];
    if (!grp)
	    grp = [[[_EOUndoGroup alloc] init] autorelease];
    [ctrl _setUndoGroup:grp root:ctrl];
}

@end

/*
 * EOAssociationClasses protocol implementation
 */

@implementation EOController(EOAssociationClasses)
- (Class)associationClass
{
    return [EOQualifiedAssociation class];
}
@end

