/*
   EODisplayGroup.m

   Copyright (C) 2004,2005 Free Software Foundation, Inc.

   Author: David Ayers <ayers@fsfe.org>

   This file is part of the GNUstep Database Library

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

   The GNUstep Database 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 
   Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public License 
   along with the GNUstep Database Library; see the file COPYING. If not, 
   write to the Free Software Foundation, Inc., 
   51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 
*/

#include "config.h"

#ifdef GNUSTEP
#include <Foundation/NSArray.h>
#include <Foundation/NSDictionary.h>
#include <Foundation/NSException.h>
#include <Foundation/NSNotification.h>
#include <Foundation/NSSet.h>
#include <Foundation/NSString.h>
#include <Foundation/NSValue.h>
#include <Foundation/NSScanner.h>

#include <AppKit/NSPanel.h>
#else
#include <Foundation/Foundation.h>
#include <AppKit/AppKit.h>
#endif

#include <GNUstepBase/GNUstep.h>
#include <EOAccess/EODatabaseDataSource.h>
#include <EOControl/EOClassDescription.h>
#include <EOControl/EODataSource.h>
#include <EOControl/EOEditingContext.h>
#include <EOControl/EOFetchSpecification.h>
#include <EOControl/EOKeyValueCoding.h>
#include <EOControl/EOObserver.h>
#include <EOControl/EOQualifier.h>
#include <EOControl/EOSortOrdering.h>

#include "EODisplayGroup.h"
#include "EODeprecated.h"
#include "EOAssociation.h"

#include <limits.h>

#define DG_SHOULD_CHANGE_SELECTION_TO_IDX \
     @selector(displayGroup:shouldChangeSelectionToIndexes:)
#define DG_DISPLAY_ARRAY_FOR_OBJECTS \
     @selector(displayGroup:displayArrayForObjects:)
#define DG_SHOULD_DISPLAY_ALERT \
     @selector(displayGroup:shouldDisplayAlertWithTitle:message:)
#define DG_DID_FETCH_OBJECTS \
     @selector(displayGroup:didFetchObjects:)
#define DG_CREATE_OBJECT_FAILED \
     @selector(displayGroup:createObjectFailedForDataSource:)
#define DG_SHOULD_INSERT_OBJECT \
     @selector(displayGroup:shouldInsertObject:atIndex:)
#define DG_DID_INSERT_OBJECT \
     @selector(displayGroup:didInsertObject:)
#define DG_DID_CHANGE_SELECTION \
     @selector(displayGroupDidChangeSelection:)
#define DG_DID_DELETE_OBJECT \
     @selector(displayGroup:didDeleteObject:)
#define DG_SHOULD_DELETE_OBJECT \
     @selector(displayGroup:shouldDeleteObject:)

/* undocumented notification */
NSString *EODisplayGroupWillFetchNotification = @"EODisplayGroupWillFetch";

@interface NSArray (private)
- (NSArray *)indexesForObjectsIdenticalTo: (NSArray *)array;
@end
@implementation NSArray (private)
- (NSArray *)indexesForObjectsIdenticalTo: (NSArray *)array
{
  NSUInteger idx, i, c = [array count];
  NSMutableArray *indices = (id)[NSMutableArray arrayWithCapacity: c];
  id object;
  NSNumber *number;

  for (i = 0; i < c; i++)
    {
      object = [array objectAtIndex: i];
      idx = [self indexOfObjectIdenticalTo: object];
      if (idx != NSNotFound)
	{
	  /* We should cache all these numbers.  */
	  number = [NSNumber numberWithUnsignedInteger: idx];
	  [indices addObject: number];
	}
    }
  return AUTORELEASE ([indices copy]);
}
@end

@interface EODisplayGroup (private)
- (void)_presentAlertWithTitle:(NSString *)title
		       message:(NSString *)message;
@end

@implementation EODisplayGroup (private)
- (void)_presentAlertWithTitle:(NSString *)title
		       message:(NSString *)message
{
  if (_delegate
      && [_delegate respondsToSelector: DG_SHOULD_DISPLAY_ALERT]
      && [_delegate displayGroup: self
		    shouldDisplayAlertWithTitle: title
		    message: message] == NO)
    {
      return;
    }
  NSRunAlertPanel(title, message, nil, nil, nil);
}

@end

/**
 * The EODisplayGoup keeps track of all enterprise objects from
 * a particular EODataSource to coordinate their internal state
 * with other objects such as UI elements and other EODisplayGroups.
 * Commonly the data source is a EODatabaseDataSource (EOAccess)
 * which manages the objects of a single entity for a specific
 * editing context.  The display group is connected to the UI elements
 * or other display groups via EOAssociations.  This framework is
 * responsible to update the enterprise objects when the contents
 * and state of the UI elements are changed and to update the UI
 * elements when the state of the enterprise objects are changed.
 */

@implementation EODisplayGroup

NSArray *emptyArray;
NSDictionary *emptyDictionary;
+ (void)initialize
{
  if (emptyArray == nil)
    {
      emptyArray = [NSArray new];
      emptyDictionary = [NSDictionary new];
    }
}

static NSString *_globalDefaultStringMatchOperator = @"caseInsensitiveLike";
+ (NSString *)globalDefaultStringMatchOperator
{
  return _globalDefaultStringMatchOperator;
}
+ (void)setGlobalDefaultStringMatchOperator: (NSString *)operator
{
  ASSIGNCOPY(_globalDefaultStringMatchOperator, operator);
}

static BOOL _globalDefaultForValidatesChangesImmediately = NO;
+ (BOOL)globalDefaultForValidatesChangesImmediately
{
  return _globalDefaultForValidatesChangesImmediately;
}
+ (void)setGlobalDefaultForValidatesChangesImmediately: (BOOL)flag
{
  _globalDefaultForValidatesChangesImmediately = flag ? YES : NO;
}

- (id)init
{
  if ((self = [super init]))
    {
      _allObjects = [NSMutableArray new];
      _displayedObjects = [NSMutableArray new];

      _selection = emptyArray;
      _observerNotificationBeginProxy 
	= [[EOObserverProxy alloc] initWithTarget: self
				   action:
				     @selector(_beginObserverNotification:)
				   priority: EOObserverPriorityFirst];
      [EOObserverCenter addObserver: _observerNotificationBeginProxy
      			  forObject: self];
      
      _observerNotificationEndProxy
	= [[EOObserverProxy alloc] initWithTarget: self
				   action:
				     @selector(_endObserverNotification:)
				   priority: EOObserverPrioritySixth];
      [EOObserverCenter addObserver: _observerNotificationEndProxy
      			  forObject: self];

      _insertedObjectDefaultValues = emptyDictionary;

      _queryMatch = [NSMutableDictionary new];
      _queryMin = [NSMutableDictionary new];
      _queryMax = [NSMutableDictionary new];
      _queryOperator = [NSMutableDictionary new];

      _defaultStringMatchOperator 
	= [[self class] globalDefaultStringMatchOperator];
      _defaultStringMatchFormat = @"%@*";

      _queryBindings = [NSMutableDictionary new];

      _flags.selectsFirstObjectAfterFetch = YES;
      _flags._initialized = YES;
    }
  return self;
}

- (void)dealloc
{
  EOEditingContext *context = [_dataSource editingContext];

  [[NSNotificationCenter defaultCenter] removeObserver:self];
  [context removeEditor:self];
  if ([context messageHandler] == self)
    [context setMessageHandler:nil];

  DESTROY(_dataSource);
  if (_allObjects != emptyArray)
    DESTROY(_allObjects);
  DESTROY(_displayedObjects);
  if (_selection != emptyArray)
    DESTROY(_selection);
  DESTROY(_sortOrdering);
  DESTROY(_qualifier);
  DESTROY(_localKeys);
  DESTROY(_selectedObjects);
  [EOObserverCenter removeObserver:_observerNotificationBeginProxy
			 forObject:self];
  [EOObserverCenter removeObserver:_observerNotificationEndProxy
			 forObject:self];
  DESTROY(_observerNotificationBeginProxy);
  DESTROY(_observerNotificationEndProxy);
  if (_insertedObjectDefaultValues != emptyDictionary)
    DESTROY(_insertedObjectDefaultValues);
  DESTROY(_savedAllObjects);
  DESTROY(_queryMatch);
  DESTROY(_queryMin);
  DESTROY(_queryMax);
  DESTROY(_queryOperator);
  DESTROY(_defaultStringMatchOperator);
  DESTROY(_defaultStringMatchFormat);
  DESTROY(_queryBindings);
  DESTROY(_editingAssociation);

  [super dealloc];
}

- (id) initWithCoder:(NSCoder *)decoder
{
  int tmpI;

  self = [self init]; 
  [self setDataSource:[decoder decodeObject]]; 
  _delegate = [decoder decodeObject]; 
  ASSIGN(_sortOrdering, [decoder decodeObject]);
  ASSIGN(_qualifier, [decoder decodeObject]);
  ASSIGN(_localKeys, [decoder decodeObject]);
  /* encode _query*, _defaultStringMatch* ?? */
  [decoder decodeValueOfObjCType: @encode(int) at: &tmpI];
  _flags.selectsFirstObjectAfterFetch = tmpI; 
  [decoder decodeValueOfObjCType: @encode(int) at: &tmpI];
  _flags.autoFetch = tmpI;
  return self;
}

- (void) awakeFromNib
{
  if (_flags.autoFetch)
    {
      [self fetch];
    }
}

- (void)encodeWithCoder: (NSCoder *)encoder
{
  int tmpI;
  
  [encoder encodeObject: _dataSource]; 
  [encoder encodeObject: _delegate];
  [encoder encodeObject: _sortOrdering];
  [encoder encodeObject: _qualifier];
  [encoder encodeObject: _localKeys];
  tmpI = _flags.selectsFirstObjectAfterFetch;
  [encoder encodeValueOfObjCType: @encode(int) at: &tmpI];
  tmpI = _flags.autoFetch;
  [encoder encodeValueOfObjCType: @encode(int) at: &tmpI];

}

- (BOOL)fetchesOnLoad
{
  return _flags.autoFetch;
}
- (void)setFetchesOnLoad: (BOOL)flag
{
  _flags.autoFetch = flag ? YES : NO;
}

- (BOOL)selectsFirstObjectAfterFetch
{
  return _flags.selectsFirstObjectAfterFetch;
}
- (void)setSelectsFirstObjectAfterFetch: (BOOL)flag
{
  _flags.selectsFirstObjectAfterFetch = flag ? YES : NO;
}

- (BOOL)validatesChangesImmediately
{
  return _flags.validateImmediately;
}
- (void)setValidatesChangesImmediately: (BOOL)flag
{
  _flags.validateImmediately = flag ? YES : NO;
}

- (BOOL)usesOptimisticRefresh
{
  return _flags.optimisticRefresh;
}
- (void)setUsesOptimisticRefresh: (BOOL)flag
{
  _flags.optimisticRefresh = flag ? YES : NO;
}

- (NSDictionary *)queryBindingValues
{
  return AUTORELEASE([_queryBindings copy]);
}
- (void)setQueryBindingValues: (NSDictionary *)values
{
  ASSIGN(_queryBindings, [values mutableCopyWithZone: [self zone]]);
}

- (NSDictionary *)queryOperatorValues
{
  return AUTORELEASE([_queryOperator copy]);
}
- (void)setQueryOperatorValues: (NSDictionary *)values
{
  ASSIGN(_queryOperator,
	 AUTORELEASE([values mutableCopyWithZone: [self zone]]));
}

- (NSString *)defaultStringMatchFormat
{
  return _defaultStringMatchFormat;
}
- (void)setDefaultStringMatchFormat: (NSString *)format
{
  ASSIGNCOPY(_defaultStringMatchFormat, format);
}

- (NSString *)defaultStringMatchOperator
{
  return _defaultStringMatchOperator;
}
- (void)setDefaultStringMatchOperator: (NSString *)operator
{
  ASSIGNCOPY(_defaultStringMatchOperator, operator);
}

- (EODataSource *)dataSource
{
  return _dataSource;
}

- (void)setDataSource: (EODataSource *)dataSource
{
  if (_dataSource != dataSource)
    {
      EOEditingContext *context;
      NSNotificationCenter *center;

      center = [NSNotificationCenter defaultCenter];

      if (_dataSource
	  && (context = [_dataSource editingContext]))
	{
	  [context removeEditor: self];

	  if ([context messageHandler] == self)
	    {
	      [context setMessageHandler: nil];
	    }
	  [center removeObserver: self
		  name: EOObjectsChangedInEditingContextNotification
		  object: context];

	  [center removeObserver: self
		  name: EOObjectsChangedInStoreNotification
		  object: context];
	}

      [self setObjectArray: nil];
      ASSIGN(_dataSource, dataSource);
      if ((context = [_dataSource editingContext]))
	{
	  [context addEditor: self];

	  if ([context messageHandler] == nil)
	    {
	      [context setMessageHandler: self];
	    }

	  [center addObserver: self
		  selector: @selector(objectsInvalidatedInEditingContext:)
		  name: EOInvalidatedAllObjectsInStoreNotification
		  object: context];

	  [center addObserver: self
		  selector: @selector(objectsChangedInEditingContext:)
		  name: EOObjectsChangedInEditingContextNotification
		  object: context];
	}
      if (_delegate
	  && [_delegate respondsToSelector: 
			  @selector(displayGroupDidChangeDataSource:)])
	{
	  [_delegate displayGroupDidChangeDataSource: self];
	}
    }
}

- (EOQualifier *)qualifier
{
  return _qualifier;
}
- (void)setQualifier: (EOQualifier *)qualifier
{
  ASSIGN(_qualifier, qualifier);
}

- (NSArray *)sortOrderings
{
  return _sortOrdering;
}
- (void)setSortOrderings: (NSArray *)orderings
{
  ASSIGNCOPY(_sortOrdering, orderings);
}

- (EOQualifier *) _qualifierForKey:(NSString *)key
		 value:(id)val
		 defaultOperator:(char)defaultOp
{
  NSString *op;
  SEL selector = NULL;
  NSString *fmt = nil;
  
  EOClassDescription *classDesc = [_dataSource classDescriptionForObjects];
   
  [[classDesc validateValue:&val forKey:key] raise]; 
  op = [_queryOperator objectForKey:key];
  if (op == nil)
    {
      switch (defaultOp)
	{
	  case '=':
	    if ([val isKindOfClass:[NSString class]])
	      {
	        op = _defaultStringMatchOperator; 
		fmt = _defaultStringMatchFormat;
	      }
            else
	      {
	        selector = EOQualifierOperatorEqual;
	      }
	    break;
	  case '>':
	    selector = EOQualifierOperatorGreaterThanOrEqualTo;
	   break;
	  case '<':
	    selector = EOQualifierOperatorLessThanOrEqualTo;
	   break;

	}
    }
  if (op) 
    selector = [EOKeyValueQualifier operatorSelectorForString:op];

  if (fmt)
    val = [NSString stringWithFormat:fmt, val];
  
  return [EOKeyValueQualifier
             	qualifierWithKey:key
                   operatorSelector:selector
                   value: val];
}

- (EOQualifier *)qualifierFromQueryValues
{
  NSMutableArray *quals = [NSMutableArray array];
  int i, c, j;
  id dicts[3];
  char ops[3] = { '=', '<', '>' };
  dicts[0] = _queryMatch;
  dicts[1] = _queryMax;
  dicts[2] = _queryMin;
  for (j = 0; j < 3; j++)
    {   
      NSArray *keys = [dicts[j] allKeys];

      for (i = 0, c = [keys count]; i < c; i++)
        { 
	  NSString *key = [keys objectAtIndex:i];
	  id val = [dicts[j] objectForKey:key];
          
	  [quals addObject:[self _qualifierForKey:key
		  			value:val
					defaultOperator:ops[j]]];
        }
    }
  return [EOAndQualifier qualifierWithQualifierArray:quals];
}

- (NSDictionary *)equalToQueryValues
{
  return AUTORELEASE([_queryMatch copy]);
}

- (void)setEqualToQueryValues: (NSDictionary *)values
{
  ASSIGN(_queryMatch,
         AUTORELEASE([values mutableCopyWithZone: [self zone]]));
}

- (NSDictionary *)greaterThanQueryValues
{
  return AUTORELEASE([_queryMin copy]);
}
- (void)setGreaterThanQueryValues: (NSDictionary *)values
{
  ASSIGN(_queryMin,
         AUTORELEASE([values mutableCopyWithZone: [self zone]]));
}

- (NSDictionary *)lessThanQueryValues
{
  return AUTORELEASE([_queryMax copy]);
}
- (void)setLessThanQueryValues: (NSDictionary *)values
{
  ASSIGN(_queryMax,
         AUTORELEASE([values mutableCopyWithZone: [self zone]]));
}

- (void)qualifyDisplayGroup
{
  [self setQualifier:[self qualifierFromQueryValues]];
  [self updateDisplayedObjects];
  _flags.queryMode = NO;
}

- (void)qualifyDataSource
{
  /* only works with EODatabaseDataSource ?? */
  [[(EODatabaseDataSource *)_dataSource fetchSpecification]
	  			setQualifier:[self qualifierFromQueryValues]];
  _flags.queryMode = NO;
  [self fetch];
}

- (BOOL)inQueryMode
{
  return _flags.queryMode;
}
- (void)setInQueryMode: (BOOL)flag
{
  _flags.queryMode = flag ? YES : NO;
}

- (BOOL)fetch
{
  BOOL flag = YES;
  
  if (_dataSource
      && (flag = [self endEditing]))
    {
      if (_delegate
	  && [_delegate respondsToSelector:
			 @selector(displayGroupShouldFetch:)])
	{
	  flag = [_delegate displayGroupShouldFetch: self];
	}

      if (flag)
	{
	  NSNotificationCenter *center;
	  NSArray *objects;

	  center = [NSNotificationCenter defaultCenter];
	  [center postNotificationName: EODisplayGroupWillFetchNotification
		   object: self];
	  
	  if ([_dataSource respondsToSelector:
			     @selector(setQualifierBindings:)])
	    {
	      [_dataSource setQualifierBindings: _queryBindings];
	    }

	  objects = [_dataSource fetchObjects];
	  [self setObjectArray: objects];
	  
	  if (_delegate
	      && [_delegate respondsToSelector: DG_DID_FETCH_OBJECTS])
	    {
	      [_delegate displayGroup: self
			 didFetchObjects: objects];
	    }

	  flag = objects ? YES : NO;
	}
    }
  
  return flag;
}

- (NSArray *)allObjects
{
  return AUTORELEASE([[NSArray alloc] initWithArray:_allObjects copyItems:NO]);
}

- (void)setObjectArray: (NSArray *)objects
{
  if (objects == nil)
    {
      objects = emptyArray;
    }
  
  ASSIGN(_allObjects,
	 AUTORELEASE([objects mutableCopyWithZone: [self zone]]));

  [self updateDisplayedObjects];

  [self selectObjectsIdenticalTo:[self selectedObjects] 
	   selectFirstOnNoMatch:_flags.selectsFirstObjectAfterFetch];
 

  [self redisplay];
}

- (NSArray *)displayedObjects
{
  return AUTORELEASE([_displayedObjects copy]);
}

- (void)redisplay
{
  /* TODO: Check this again! */
  _flags.didChangeContents = YES;
//  [EOObserverCenter notifyObserversObjectWillChange: nil];
  [self willChange];
}

- (void)updateDisplayedObjects
{
  NSArray *oldSelection = [self selectedObjects];
  volatile NSArray *displayedObjects = [self allObjects];

  if (_delegate 
      && [_delegate respondsToSelector: DG_DISPLAY_ARRAY_FOR_OBJECTS])
    {
      displayedObjects = [_delegate displayGroup: self
		     	    displayArrayForObjects: (id)displayedObjects];
    }
  
  NS_DURING
    {
      displayedObjects 
	= [(id)displayedObjects filteredArrayUsingQualifier: _qualifier];
      displayedObjects
	= [(id)displayedObjects sortedArrayUsingKeyOrderArray: _sortOrdering];
    }
  NS_HANDLER
    {
      [self _presentAlertWithTitle: 
	      @"Exception during sort or filter operatation."
	    message: [localException reason]];
    }
  NS_ENDHANDLER;

  ASSIGN(_displayedObjects,
	 AUTORELEASE([(id)displayedObjects mutableCopyWithZone:[self zone]]));

  [self selectObjectsIdenticalTo: oldSelection
	selectFirstOnNoMatch: NO];

  [self redisplay];
}

- (NSArray *)selectionIndexes
{
  return _selection;
}

- (BOOL)setSelectionIndexes: (NSArray *)selection
{

  if ([self endEditing] && selection)
    {
      if (_delegate
	  && [_delegate respondsToSelector: DG_SHOULD_CHANGE_SELECTION_TO_IDX]
	  && [_delegate displayGroup: self
			shouldChangeSelectionToIndexes: selection] == NO)

	{
	  return NO;
	}
      else
	{
	  NSNumber *number;
	  NSArray *newSelection;
	  NSMutableArray *newObjects;
	  id object;
	  unsigned c, i, count, index;
	  
	  count = [_displayedObjects count];
	  c = [selection count];
	  newObjects = (id)[NSMutableArray arrayWithCapacity: c];
	  
	  for (i = 0; i < c; i++)
	    {
	      number = [selection objectAtIndex: i];
	      index = [number unsignedIntValue];
	      object = (index < count)
		       ? [_displayedObjects objectAtIndex: index]
		       : nil;
	      if (object != nil)
		{
		  [newObjects addObject: object];
		}
	    }
	  
	  if ([_selectedObjects isEqual:newObjects] == NO
	      || [_selection isEqual:selection] == NO)
	    {
	      ASSIGNCOPY(_selectedObjects, newObjects);
	      newSelection = [_displayedObjects
	 	     	        indexesForObjectsIdenticalTo: _selectedObjects];
	      /* don't release emptyArray */
	      (_selection == emptyArray) ? _selection = RETAIN(newSelection)
		  		         : ASSIGN(_selection, newSelection);
	      _flags.didChangeSelection = YES;
	      if ([_delegate respondsToSelector: DG_DID_CHANGE_SELECTION])
	        {
	          [_delegate displayGroupDidChangeSelection:self];
	        }
	      
	      [self willChange];
	    }
	  return YES;
	}
    }
  
  return NO;
}

- (BOOL)selectObject: (id)object
{
  NSArray *array;
  if (object) 
    {
      array = [NSArray arrayWithObject: object];
    }
  else
    {
      array = [NSArray array];
    }
  return [self selectObjectsIdenticalTo: array];
}
- (BOOL)selectObjectsIdenticalTo: (NSArray *)selection
{
  NSArray *indices;
  indices = [_displayedObjects indexesForObjectsIdenticalTo: selection];
  if ([selection count] && ![indices count]) return NO;
  return [self setSelectionIndexes: indices];
}

- (BOOL)selectObjectsIdenticalTo: (NSArray *)selection 
	    selectFirstOnNoMatch: (BOOL)flag
{
  BOOL selected = [self selectObjectsIdenticalTo: selection];

  if (!selected)
    {
      unsigned c = [_displayedObjects count];
      if (flag && c != 0)
        {
	  id object = [_displayedObjects objectAtIndex: 0];
          selected = [self selectObject: object];
	}
#if 0 
      // this really doesn't seem like it belongs here.
      else if (c) 
        {
	  [self setSelectionIndexes:_selection]; 
	}
#endif
      else
	{
	  [self clearSelection];
	}
    }
  
  return selected;
}

- (BOOL)selectNext
{
  id selObj = [self selectedObject];
  unsigned idx;
  
  if (selObj == nil) return NO;

  idx = [[self displayedObjects]
	  	indexOfObjectIdenticalTo:[self selectedObject]];
  
  if (idx == UINT_MAX) return NO;

  return [self setSelectionIndexes:[NSArray arrayWithObject:[NSNumber numberWithUnsignedInt:++idx]]];
}
- (BOOL)selectPrevious
{
  id selObj = [self selectedObject];
  unsigned idx;
  
  if (selObj == nil) return NO;

  idx = [[self displayedObjects]
	  	indexOfObjectIdenticalTo:[self selectedObject]];
  
  if (idx == 0) return NO;

  return [self setSelectionIndexes:[NSArray arrayWithObject:[NSNumber numberWithUnsignedInt:--idx]]];
}

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

- (NSArray *)selectedObjects
{
  return AUTORELEASE([_selectedObjects copy]);
}

- (void)setSelectedObjects: (NSArray *)objects
{
  ASSIGNCOPY(_selectedObjects, objects);
}

- (id)selectedObject
{
  id object = nil;

  if ([_selectedObjects count])
    {
      object = [_selectedObjects objectAtIndex: 0];
    }
    
  return object;
}

- (void)setSelectedObject: (id)object
{
  if (object)
    {
      [self selectObject: object];
    }
  else
    {
      [self clearSelection];
    }
}

- (id)insertObjectAtIndex: (unsigned)index
{
  id object = nil;
  if ([self endEditing])
    {
      object = [_dataSource createObject];
      if (object == nil)
	{
	  if (_delegate
	      && [_delegate respondsToSelector: DG_CREATE_OBJECT_FAILED])
	    {
	      [_delegate displayGroup: self
			 createObjectFailedForDataSource: _dataSource];
	    }
	  else
	    {
	      [self _presentAlertWithTitle: @"EODisplayGroup"
		    message: @"Data source did not provide new object. "];
	    }
	}
      else
	{
	  NSArray *defaultValueKeys = [_insertedObjectDefaultValues allKeys];
	  unsigned i, c = [defaultValueKeys count];
	  NSString *key;
	  id value;

	  /* We cannot use -takeValuesFromDictionary because
	     we need to call -takeValue:forKeyPath:.  */
	  for (i = 0; i < c; i++)
	    {
	      key = [defaultValueKeys objectAtIndex: i];
	      value = [_insertedObjectDefaultValues valueForKeyPath: key];
	      [object smartTakeValue: value forKeyPath: key];
	    }
	  [self insertObject: object atIndex: index];
	}
    }
  return object;
}

- (void)insertObject: (id)object atIndex: (unsigned)index
{
  if ([self endEditing])
    {
      unsigned c = [_displayedObjects count];
      if (c < index)
	{
	  [NSException raise: NSRangeException
		       format: @"-[%@ %@]: Index %d is out of range %d", 
		       NSStringFromClass([self class]),
		       NSStringFromSelector(_cmd),
		       index, c];
	}
      if (_delegate == nil
	  || [_delegate respondsToSelector: DG_SHOULD_INSERT_OBJECT] == NO
	  || [_delegate displayGroup: self
			shouldInsertObject: object
			atIndex: index])
	{
	  NS_DURING
	    {
	      [_dataSource insertObject: object];
	    }
	  NS_HANDLER
	    {
	      [self _presentAlertWithTitle: @"EODisplayGroup insertion error"
		    message: [localException reason]];
	      return;
	    }
	  NS_ENDHANDLER;
	  
	  /* It is safe to use the index for _allObjects but it seems
	     strange. OTOH _allObjects should probably be viewed as set.  */
	  [_allObjects insertObject: object atIndex: index];
	  [_displayedObjects insertObject: object atIndex: index];
	  
	  if (_delegate
	      && [_delegate respondsToSelector: DG_DID_INSERT_OBJECT])
	    {
	      [_delegate displayGroup: self didInsertObject: object];
	    }

	  [self selectObject: object];
	  [self redisplay];
	}
    }
}

- (NSDictionary *)insertedObjectDefaultValues
{
  return _insertedObjectDefaultValues;
}
- (void)setInsertedObjectDefaultValues: (NSDictionary *)values
{
   (_insertedObjectDefaultValues == emptyDictionary) ? _insertedObjectDefaultValues = [values copy] : ASSIGNCOPY(_insertedObjectDefaultValues, values);
}

- (BOOL)deleteObjectAtIndex: (unsigned)index
{
  id object = [_displayedObjects objectAtIndex:index];
  if (!_delegate
      || ([_delegate respondsToSelector:DG_SHOULD_DELETE_OBJECT]
      	  && [_delegate displayGroup:self shouldDeleteObject:object]))
    {
      NS_DURING
	[_dataSource deleteObject:object];	      
        if ([_delegate respondsToSelector:DG_DID_DELETE_OBJECT])
	  {
	    [_delegate displayGroup:self didDeleteObject:object];
	  }
	[_displayedObjects removeObjectAtIndex:index];
	[_allObjects removeObject:object];
	NS_VALUERETURN(YES, BOOL);
      NS_HANDLER
        return NO;
      NS_ENDHANDLER
    }
	    
  return NO;
}
- (BOOL)deleteSelection
{
  BOOL flag = YES;
  NSArray *sel = [self selectionIndexes];
  int c = [sel count];

  if (c == 0)
    {
      return YES;
    }

  if ((flag = [self endEditing]))
    {
      int i;

      [self redisplay];
      for (i = 0; i < c && flag; i++)
        {
	  unsigned int index = [[sel objectAtIndex:i] unsignedIntValue];
	  id selection = [self selectedObjects];
	  flag = [self deleteObjectAtIndex:index];
	  [self selectObjectsIdenticalTo:selection selectFirstOnNoMatch:NO];
	}
    }
  return flag;
}

- (NSArray *)localKeys
{
  return _localKeys;
}
- (void)setLocalKeys: (NSArray *)keys
{
  ASSIGNCOPY(_localKeys, keys);
}

- (id)delegate
{
  return _delegate;
}
- (void)setDelegate: (id)delegate
{
  _delegate = delegate;
}

- (NSArray *)observingAssociations
{
  /* not sure if this should be using an EOQualifier somehow */
  NSMutableArray *oa = [[NSMutableArray alloc] init]; 
  NSArray *observers = [EOObserverCenter observersForObject: self]; 
  int i, count;
  
  count = [observers count];
  for (i = 0; i < count; i ++)
    {
      id currentObject = [observers objectAtIndex:i];
      if ([currentObject isKindOfClass:[EOAssociation class]])
        [oa addObject: currentObject];
    }
  
  return AUTORELEASE(oa);
}

- (EOAssociation *)editingAssociation
{
  return _editingAssociation;
}
- (BOOL)endEditing
{
  return _editingAssociation ? [_editingAssociation endEditing] : YES;
}

@end

@implementation EODisplayGroup (EODisplayGroupTargetAction)
/* TODO: check for return value handling and exception handling.  */
- (void)selectNext: (id)sender
{
  [self selectNext];
}
- (void)selectPrevious: (id)sender
{
  [self selectPrevious];
}

- (void)fetch: (id)sender
{
  [self fetch];
  [self redisplay];
}

- (void)insert: (id)sender
{
  unsigned idx = 0;
  NSArray *selections = [self selectionIndexes];

  if ([selections count])
    {
      NSNumber *index = [selections objectAtIndex: 0];
      idx = [index unsignedIntValue];
    }

  [self insertObjectAtIndex: idx];
}
- (void)delete: (id)sender
{
  [self deleteSelection];
}

- (void)qualifyDataSource: (id)sender
{
  [self qualifyDataSource];
}
- (void)qualifyDisplayGroup: (id)sender
{
  [self qualifyDisplayGroup];
}

- (void)enterQueryMode: (id)sender
{
  [self setInQueryMode: YES];
}

@end

@implementation EODisplayGroup (EOAssociationInteraction)

- (BOOL)selectionChanged
{
  return _flags.didChangeSelection;
}
- (BOOL)contentsChanged
{
  return _flags.didChangeContents;
}
- (int)updatedObjectIndex
{
  return -1;
}

- (id)valueForObject: (id)object key: (NSString *)key
{
  return [object valueForKeyPath: key];
}
- (id)selectedObjectValueForKey: (NSString *)key
{
  return [self valueForObject: [self selectedObject] key: key];
}

- (id)valueForObjectAtIndex: (unsigned)index key: (NSString *)key
{
  return [self valueForObject: [_displayedObjects objectAtIndex: index] 
  			  key: key];
}

- (NSMutableDictionary *)_queryDictForOperator:(NSString *)op
{
  if ([op isEqual:@"<"])
    return _queryMax;
  
  if ([op isEqual:@">"])
    return _queryMin; 
  
  if ([op isEqual:@"="])
    return _queryMatch;

  if ([op isEqual:@"Op"])
    return _queryOperator;
  
  return nil;
}

- (BOOL)setValue: (id)value forObject: (id)object key: (NSString *)key
{
  SEL didSetValue = @selector(displayGroup:didSetValue:forObject:key:);
  NSException *exception = nil;
  
  if ([key hasPrefix:@"@query"])
    {
      NSString *oper = [NSString string];
      NSScanner *scn = [NSScanner scannerWithString:key];
      NSMutableDictionary *_queryDict = nil;
      
      [scn setScanLocation:6];
      
      if ([scn scanUpToString:@"." intoString:&oper]
          && [scn scanString:@"." intoString:NULL]
          && [scn scanLocation] != [key length])
        {
          NSString *realKey = [key substringFromIndex:[scn scanLocation]];
	  _queryDict = [self _queryDictForOperator:oper];
	  [_queryDict setObject:value forKey:realKey];
        }
        if (!_queryDict)
          [[NSException exceptionWithName:NSInvalidArgumentException
			reason:@"Invalid query operator, expected '<', '>', '=',or 'Op'"
			 userInfo:nil] raise];
	return _queryDict != nil;
    }
  else
    { 
  
      NS_DURING
        {
          [object takeValue:value forKeyPath:key];
        } 
      NS_HANDLER
         /* -userInfo likely contains useful information... */
          NSLog(@"Exception in %@ name:%@ reason:%@ userInfo:%@", NSStringFromSelector(_cmd), [localException name], [localException reason], [localException userInfo]); 
          return NO;
      NS_ENDHANDLER

      exception = [object validateValue: &value forKey: key];

      if (exception && _flags.validateImmediately)
        {
          [self _presentAlertWithTitle:@"Validation error"
	      		   message:[exception reason]];
          return NO;
        }
      else if ([_delegate respondsToSelector:didSetValue])
        {
          [_delegate displayGroup: self 
		  didSetValue: value
		    forObject: object
			  key: key];
        }
    }    
  return YES;
}

- (BOOL)setSelectedObjectValue: (id)value forKey: (NSString *)key
{
  return [self setValue: value forObject: [self selectedObject]  key: key];
}

- (BOOL)setValue: (id)value forObjectAtIndex: (unsigned)index 
	     key: (NSString *)key
{
  if ([_displayedObjects count] > index)
    return [self setValue: value 
  	      forObject: [_displayedObjects objectAtIndex: index]
	            key: key];
  return NO;
}

- (BOOL)enabledToSetSelectedObjectValueForKey:(NSString *)key
{
  return [self selectedObject] || [key hasPrefix: @"@query"] || _flags.queryMode;
}

- (BOOL)association: (EOAssociation *)association 
failedToValidateValue: (NSString *)value
	     forKey: (NSString *)key 
	     object: (id)object
   errorDescription: (NSString *)description
{
  [self _presentAlertWithTitle:@"Validation error" message:description];
  return NO;
}

- (void)associationDidBeginEditing: (EOAssociation *)association
{
  ASSIGN(_editingAssociation,association);
}

- (void)associationDidEndEditing: (EOAssociation *)association
{
  ASSIGN(_editingAssociation,nil);
}

@end

@implementation EODisplayGroup (EOEditors)
/* FIXME GDL2 currently never calls this.. */
- (BOOL)editorHasChangesForEditingContext: (EOEditingContext *)editingContext
{
  /* check */
  [self endEditing];
  return _flags.didChangeContents;
}
- (void)editingContextWillSaveChanges: (EOEditingContext *)editingContext
{
  [self endEditing];
}
@end

@implementation EODisplayGroup (EOMessageHandlers)
- (void)editingContext: (EOEditingContext *)editingContext
   presentErrorMessage: (NSString *)message
{
}
@end

@implementation EODisplayGroup (EODeprecated)
- (void)setSortOrdering: (NSArray *)sortOrderings
{
  [self setSortOrderings: sortOrderings];
}
- (NSArray *)sortOrdering
{
  return [self sortOrderings];
}
@end

@implementation EODisplayGroup (notifications)
- (void)objectsInvalidatedInEditingContext: (NSNotification *)notif
{
}

- (void)objectsChangedInEditingContext: (NSNotification *)notif
{
/*
  // hmmm this doesn't work because of relationships.... (WHY?)
  NSDictionary *userInfo = [notif userInfo];
  NSArray *upd = [userInfo objectForKey:EOUpdatedKey];
  NSArray *ins = [userInfo objectForKey:EOInsertedKey];
  NSArray *del = [userInfo objectForKey:EODeletedKey];
  _flags.didChangeContents = [_allObjects firstObjectCommonWithArray:upd]
	  		     || [_allObjects firstObjectCommonWithArray:ins]
  			     || [_allObjects firstObjectCommonWithArray:del];
*/
  /* FIXME this doesn't seem correct.
   * display groups/data sources can share editing contexts.
   * which will lead to spurious updates 
   */ 
  _flags.didChangeContents = YES;
  [self willChange];
}

@end

@implementation EODisplayGroup (GDL2Private)
- (void) _beginObserverNotification:(id)sender
{
  /* FIXME what goes here?? */
}

- (void) _endObserverNotification:(id)sender
{
  _flags.didChangeContents = NO;
  _flags.didChangeSelection = NO;
  [EOObserverCenter notifyObserversObjectWillChange:nil];
}
@end
