/* 
   EOFaultResolver.m

   Copyright (C) 1996 Free Software Foundation, Inc.

   Author: Mircea Oancea <mircea@jupiter.elcom.pub.ro>
   Date: 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 <Foundation/NSObject.h>
#include <Foundation/NSUtilities.h>
#include <Foundation/NSArray.h>
#include <Foundation/NSDictionary.h>
#include <Foundation/NSString.h>
#include <Foundation/NSObjCRuntime.h>

#include <extensions/NSException.h>
#include <extensions/exceptions/GeneralExceptions.h>

#include <eoaccess/EOEntity.h>
#include <eoaccess/EOQualifier.h>

#include "EOFault.h"
#include "EOFaultResolver.h"
#include "EODatabaseContext.h"
#include "EODatabaseChannel.h"


BOOL __isGCEnabled(Class class_)
{
  Class gcObjectClass=[GCObject class];
  if ([class_ conformsToProtocol:@protocol(GarbageCollecting)])
	{
	  return YES;
	}
  else if ([class_ instancesRespondToSelector:@selector(gcIncrementRefCount)])
	return YES;
  else
	{
	  Class class;
	  for (class = class_; 
		   class != Nil;
		   class = class_get_super_class (class))
		{
		  if (class == gcObjectClass)
			return YES;
		  else if ([class instancesRespondToSelector:@selector(gcIncrementRefCount)])
			{
			  return YES;
			}
		  else if ([class instancesRespondToSelector:@selector(gcNextObject)])
			{
			  return YES;
			}
		  else if ([class conformsToProtocol:@protocol(GarbageCollecting )])
			{
			  return YES;
			}
		};
	};
  return NO;
};

@implementation EOFaultResolver

+ allocWithZone:(NSZone*)zone_
{
    id newObject = [super allocWithZone:zone_];
    ((EOFaultResolver*)newObject)->gcFlags.refCount = 0;
    return newObject;
}


- initWithDatabaseChannel:(EODatabaseChannel*)aChannel
  zone:(NSZone*)aZone  
  targetClass:(Class)aTargetClass 
  fault:aFault
{
    [super init];
    channel = [aChannel retain];
    targetClass = aTargetClass;
    fault = aFault;
    zone = aZone;
    faultReferences = 0;
	gcEnabled=__isGCEnabled(targetClass);
	if (gcEnabled)
	  faultReferences++;	  
    return self;
}

- (void)dealloc
{
    [channel release];
    channel=nil;
    [super dealloc];
}


/*
- retain
{
  if (gcEnabled)
	{
	  gcFlags.refCount++;
	  return self;
	}
  else
	{
	  return [super retain];
	};
}

- (unsigned int)retainCount
{
  if (gcEnabled)
	{
	  return gcFlags.refCount;
	}
  else
	{
	  return [super retainCount];
	};
}
*/
- gcSetNextObject:(id)anObject
{
  if (gcEnabled)
	  gcNextObject = anObject;
  return self;
}

- gcSetPreviousObject:(id)anObject
{
  if (gcEnabled)
	gcPreviousObject = anObject;
  return self;
}

- (id)gcNextObject
{
  if (gcEnabled)
	return gcNextObject;
  else
	return nil;
};
- (id)gcPreviousObject
{
  if (gcEnabled)
	return gcPreviousObject;
  else
	return nil;
}

-(BOOL)gcAlreadyVisited
{
  if (gcEnabled)
    return gcFlags.gcVisited;
  else
	return YES;
}

- (void)gcSetVisited:(BOOL)flag
{
  if (gcEnabled)
    gcFlags.gcVisited = flag;
}

- (void)gcDecrementRefCountOfContainedObjects
{
  if (gcEnabled)
	{
	  gcCountainedObjectRefCount--;
	};
}

- (BOOL)gcIncrementRefCountOfContainedObjects
{
  if (gcEnabled)
	{
	  if(gcFlags.gcVisited)
		return NO;
	  gcCountainedObjectRefCount++;
	  gcFlags.gcVisited = YES;
	  return YES;
	}
  else
	return NO;
}

- (BOOL)isGarbageCollectable
{
  return gcEnabled;
}

- (void)gcIncrementRefCount
{
  if (gcEnabled)
	//gcFlags.refCount++;
	faultReferences++;
}

- (void)gcDecrementRefCount
{
  if (gcEnabled)
	//gcFlags.refCount--;
	faultReferences--;
}

- (BOOL)beforeFault
{
    return NO;
}

- (BOOL)fault
{
    return NO;
}

- (BOOL)afterFault
{
  if (gcEnabled)
	{
	  [fault gcIncrementRefCount];
	  [fault gcSetNextObject:[self gcNextObject]];
	  [fault gcSetPreviousObject:[self gcPreviousObject]];
	  while(gcCountainedObjectRefCount-- > 0)
    	[fault gcIncrementRefCountOfContainedObjects];
	};
    return NO;
}

- (EODatabaseChannel*)databaseChannel
{
    return channel;
}

- (Class)targetClass;
{
    return targetClass;
}

- (NSDictionary*)primaryKey
{
    return nil;
}

- (EOEntity*)entity
{
    return nil;
}

- (EOQualifier*)qualifier
{
    return nil;
}

- (NSArray*)fetchOrder
{
    return nil;
}

@end /* EOFaultResolver */

@implementation EOArrayFault

- initWithQualifier:(EOQualifier*)aQualifier
  fetchOrder:(NSArray*)aFetchOrder 
  databaseChannel:(EODatabaseChannel*)aChannel 
  zone:(NSZone*)aZone  
  targetClass:(Class)aTargetClass 
  fault:aFault;
{
    [super initWithDatabaseChannel:aChannel zone:aZone 
    	targetClass:aTargetClass fault:aFault];
    qualifier = [aQualifier retain];
    fetchOrder = [aFetchOrder retain];
    return self;
}

- (void) dealloc
{
    [qualifier autorelease];
    [fetchOrder autorelease];
    [super dealloc];
}

- (EOEntity*)entity
{
    return [qualifier entity];
}

- (EOQualifier*)qualifier
{
    return qualifier;
}

- (NSArray*)fetchOrder
{
    return fetchOrder;
}

- (BOOL)fault
{
    id anObj;
    BOOL tran;
	BOOL channelWasOpened=YES;

    [EOFault clearFault:fault];
    [(id)fault init];
    

    if ([channel isFetchInProgress])
	THROW([[InvalidArgumentException alloc]
		initWithFormat:@"attempt to fault with busy channel: %@",
		[self description]]);

	// Because we may want to close channel between 'primary' fetch and fault objects fetch (usefull for web application !)
    if (![channel isOpen])
	  {
		channelWasOpened=NO;
		if (![channel openChannel])
		  {
			THROW([[InvalidArgumentException alloc]
					initWithFormat:@"Cannot open channel to resolve fault: %@",
					[self description]]);
		  };
	  };

    tran = [[channel databaseContext] transactionNestingLevel] != 0;
    if (!tran)
	if (![[channel databaseContext] beginTransaction])
	    return NO;
    
    if (![channel selectObjectsDescribedByQualifier:qualifier
	    fetchOrder:fetchOrder]) {
	if (!tran)
	    [[channel databaseContext] rollbackTransaction];
	return NO;
    }
    
    while ((anObj = [channel fetchWithZone:zone])) {
	[(id)fault addObject:anObj];
    }
    
    [channel cancelFetch];

    if (!tran)
	  if (![[channel databaseContext] commitTransaction])
		{
		  if (!channelWasOpened)
			[channel closeChannel];
		  return NO;
		};

	if (!channelWasOpened)
		[channel closeChannel];
    return YES;
}

- (NSString*)description
{
    return [NSString stringWithFormat:
	@"<Array fault 0x%x (qualifier=%@, order=%@, channel=%@)>",
	fault,
	[qualifier description], 
	[fetchOrder description], 
	[channel description]];
}

@end /* EOArrayFault */

@implementation EOObjectFault

- initWithPrimaryKey:(NSDictionary*)aKey
  entity:(EOEntity*)anEntity 
  databaseChannel:(EODatabaseChannel*)aChannel 
  zone:(NSZone*)aZone  
  targetClass:(Class)aTargetClass 
  fault:aFault
{
    [super initWithDatabaseChannel:aChannel zone:aZone 
    	targetClass:aTargetClass fault:aFault];
    entity = [anEntity retain];
    primaryKey = [aKey retain];
    return self;
}

- (void) dealloc
{
    [entity autorelease];
    [primaryKey autorelease];
    [super dealloc];
}

- (NSDictionary*)primaryKey
{
    return primaryKey;
}

- (EOEntity*)entity
{
    return entity;
}

- (BOOL)fault
{
    EOQualifier* qualifier;
    BOOL tran;
    id anObj = nil;
	BOOL channelWasOpened=YES;
    
    if ([channel isFetchInProgress])
	THROW([[InvalidArgumentException alloc]
		initWithFormat:@"attempt to fault with busy channel: %@",
		[self description]]);
    
    qualifier = [EOQualifier qualifierForPrimaryKey:primaryKey entity:entity];
    if (!qualifier)
	THROW([[InvalidArgumentException alloc]
		initWithFormat:@"could not build qualifier for fault: %@",
		[self description]]);
    
	// Because we may want to close channel between 'primary' fetch and fault objects fetch (usefull for web application !)
    if (![channel isOpen])
	  {
		channelWasOpened=NO;
		if (![channel openChannel])
		  {
			THROW([[InvalidArgumentException alloc]
					initWithFormat:@"Cannot open channel to resolve fault: %@",
					[self description]]);
		  };
	  };

    tran = [[channel databaseContext] transactionNestingLevel] != 0;
    if (!tran)
	if (![[channel databaseContext] beginTransaction])
	    goto done;
    
    if (![channel selectObjectsDescribedByQualifier:qualifier
	    fetchOrder:nil]) {
	if (!tran)
	    [[channel databaseContext] rollbackTransaction];
	goto done;
    }

    // Fetch the object
    anObj = [channel fetchWithZone:zone];

    // The fetch failed!
    if (!anObj)
      {
	[channel cancelFetch];
	if (!tran)
	  [[channel databaseContext] rollbackTransaction];
	goto done;
      }

    // Make sure we only fetched one object
    if ([channel fetchWithZone:zone]) {
	anObj = nil;
    }
    
    [channel cancelFetch];
    
    if (!tran)
	if (![[channel databaseContext] commitTransaction])
	    anObj = nil;

done:
	if (!channelWasOpened)
	  [channel closeChannel];
    if (anObj != fault)
	  {
		if ([EOFault isFault:fault])
		  [EOFault clearFault:fault];
		[(id)fault unableToFaultWithPrimaryKey:primaryKey 
			 entity:entity databaseChannel:channel];
		return NO;
	  }

	return YES;
}

- (NSString*)description
{
    return [NSString stringWithFormat:
	@"<Object fault 0x%x (class=%@, entity=%@, key=%@, channel=%@)>",
	fault,
	NSStringFromClass(targetClass), 
	[entity name], 
	[primaryKey description], 
	[channel description]];
}

@end /* EOObjectFault */
