/* 
   EODatabaseContext.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 <eoaccess/common.h>

#include <Foundation/NSArray.h>
#include <Foundation/NSDictionary.h>
#include <Foundation/NSString.h>
#include <Foundation/NSException.h>
#include <Foundation/NSValue.h>
#include <Foundation/NSUtilities.h>
#include <Foundation/NSZone.h>

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

#include <eoaccess/EOAdaptor.h>
#include <eoaccess/EOAdaptorContext.h>
#include <eoaccess/EOModel.h>
#include <eoaccess/EOEntity.h>
#include <eoaccess/EOGenericRecord.h>

#include "EODatabase.h"
#include "EODatabaseContext.h"
#include "EODatabaseChannel.h"
#include "EOObjectUniquer.h"
#include "EOFault.h"

/*
 * Transaction scope
 */

typedef struct _EOTransactionScope {
    struct _EOTransactionScope*previous;
    EOObjectUniquer*		objectsDictionary;
    NSMutableArray*		objectsUpdated;
    NSMutableArray*		objectsDeleted;
    NSMutableArray*		objectsLocked;
} EOTransactionScope;

@implementation EODatabaseContext

/*
 * Initializing instances
 */

- initWithDatabase:(EODatabase*)aDatabase
{
    adaptorContext = [[[aDatabase adaptor] createAdaptorContext] retain];
    if (!aDatabase || !adaptorContext) {
	NSLog(@"EODatabaseContext could not create adaptor context");
	[self autorelease];
	return nil;
    }
    database = [aDatabase retain];
    channels = [[NSMutableArray alloc] init];
    transactionStackTop = NULL;
    transactionNestingLevel = 0;
    updateStrategy = EOUpdateWithOptimisticLocking;
    isKeepingSnapshots = YES;
    isUniquingObjects = [database uniquesObjects];
    [database contextDidInit:self];
    return self;
}

- (void)dealloc
{
    [database contextWillDealloc:self];
    [adaptorContext release];
    [database release];
    [channels release];
    while (transactionNestingLevel) {
	if (![self rollbackTransaction])
	    break;
    }
    while (transactionStackTop)
	[self privateRollbackTransaction];
    [super dealloc];
}

/*
 * Getting the database object
 */

- (EODatabase*)database
{
    return database;
}

/*
 * Getting the adaptor context
 */

- (EOAdaptorContext*)adaptorContext
{
    return adaptorContext;
}

/*
 * Finding open channels
 */

- (BOOL)hasBusyChannels
{
    int i;
    
    for (i = [channels count]-1; i >= 0; i--)
	if ([[[channels objectAtIndex:i] nonretainedObjectValue]
		isFetchInProgress])
	    return YES;
    return NO;
}

-(int)busyChannelsCount
{
  int i=0;
  int busyChannelsCount=0;
  for (i = [channels count]-1; i >= 0; i--)
	if ([[[channels objectAtIndex:i] nonretainedObjectValue]
		  isFetchInProgress])
	  busyChannelsCount++;
  return busyChannelsCount;
}

- (BOOL)hasOpenChannels
{
    int i;
    
    for (i = [channels count]-1; i >= 0; i--)
	if ([[[channels objectAtIndex:i] nonretainedObjectValue] isOpen])
	    return YES;
    return NO;
}

-(int)openChannelsCount
{
  int i=0;
  int openChannelsCount=0;
  for (i = [channels count]-1; i >= 0; i--)
	if ([[[channels objectAtIndex:i] nonretainedObjectValue] isOpen])
	  openChannelsCount++;
  return openChannelsCount;
}

- (NSArray*)channels
{
    int i, n;
    NSMutableArray* array = [NSMutableArray array];
    
    for (i=0, n=[channels count]; i < n; i++)
	[array addObject:[[channels objectAtIndex:i] nonretainedObjectValue]];
    
    return array;
}

- createChannel
{
    return [[[EODatabaseChannel alloc] initWithDatabaseContext:self] 
	autorelease];
}

- (void)channelDidInit:aChannel
{
    [channels addObject:[NSValue valueWithNonretainedObject:aChannel]];
}

- (void)channelWillDealloc:aChannel
{
    int i;
    
    for (i = [channels count]-1; i >= 0; i--)
	if ([[channels objectAtIndex:i] nonretainedObjectValue] == aChannel) {
	    [channels removeObjectAtIndex:i];
	    break;
	}
}

/*
 * Controlling transactions
 */

- (BOOL)beginTransaction
{
    if ([adaptorContext transactionNestingLevel] != transactionNestingLevel)
	THROW([[InternalInconsistencyException alloc] initWithFormat:
	    @"EODatabaseContext:%x:transaction nesting levels do not match: "
	    @"database has %d, adaptor has %d, "
	    @"in [EODatabaseContext beginTransaction]",
	    self, transactionNestingLevel, 
	    [adaptorContext transactionNestingLevel]]);

    if (![adaptorContext beginTransaction])
	return NO;
    [self privateBeginTransaction];
    return YES;
}

- (BOOL)commitTransaction
{
    if (!transactionNestingLevel)
	THROW([[InternalInconsistencyException alloc] initWithFormat:
	    @"EODatabaseContext:%x: No transaction in progress "
	    @"in [EODatabaseContext commitTransaction]",
	    self]);
    if ([adaptorContext transactionNestingLevel] != transactionNestingLevel)
	THROW([[InternalInconsistencyException alloc] initWithFormat:
	    @"EODatabaseContext:%x:transaction nesting levels do not match: "
	    @"database has %d, adaptor has %d, "
	    @"in [EODatabaseContext commitTransaction]",
	    self, transactionNestingLevel, 
	    [adaptorContext transactionNestingLevel]]);

    if (![adaptorContext commitTransaction])
	return NO;
    [self privateCommitTransaction];
    return YES;
}

- (BOOL)rollbackTransaction
{
    if (!transactionNestingLevel)
	THROW([[InternalInconsistencyException alloc] initWithFormat:
	    @"EODatabaseContext:%x: No transaction in progress "
	    @"in [EODatabaseContext rollbackTransaction]",
	    self]);
    if ([adaptorContext transactionNestingLevel] != transactionNestingLevel)
	THROW([[InternalInconsistencyException alloc] initWithFormat:
	    @"EODatabaseContext:%x:transaction nesting levels do not match: "
	    @"database has %d, adaptor has %d, "
	    @"in [EODatabaseContext rollbackTransaction]",
	    self, transactionNestingLevel, 
	    [adaptorContext transactionNestingLevel]]);

    if (![adaptorContext rollbackTransaction])
	return NO;
    [self privateRollbackTransaction];
    return YES;
}

/*
 * Notifying of other transactions
 */

- (void)transactionDidBegin
{
    [adaptorContext transactionDidBegin];
    [self privateBeginTransaction];
}

- (void)transactionDidCommit
{
    if (!transactionNestingLevel)
	THROW([[InternalInconsistencyException alloc] initWithFormat:
	    @"EODatabaseContext:%x: No transaction in progress "
	    @"in [EODatabaseContext transactionDidCommit]",
	    self]);

    [adaptorContext transactionDidCommit];
    [self privateCommitTransaction];
}

- (void)transactionDidRollback
{
    if (!transactionNestingLevel)
	THROW([[InternalInconsistencyException alloc] initWithFormat:
	    @"EODatabaseContext:%x: No transaction in progress "
	    @"in [EODatabaseContext transactionDidRollback]",
	    self]);

    [adaptorContext transactionDidRollback];
    [self privateRollbackTransaction];
}

/*
 * Nesting transactions
 */

- (BOOL)canNestTransactions
{
    return [adaptorContext canNestTransactions];
}

- (unsigned)transactionNestingLevel
{
    return transactionNestingLevel;
}

/*
 * Setting the update strategy
 */

- (void)setUpdateStrategy:(EOUpdateStrategy)aStrategy
{
    if ([self transactionNestingLevel])
	THROW([[InvalidArgumentException alloc] initWithFormat:
	    @"EODatabaseContext:%x: Cannot change update strategy "
	    @"when context has a transaction open, "
	    @"in [EODatabaseContext setUpdateStrategy]",
	    self]);
    updateStrategy = aStrategy;
    isKeepingSnapshots = (updateStrategy == EOUpdateWithNoLocking) ? NO : YES;
    isUniquingObjects = [database uniquesObjects];
}

- (EOUpdateStrategy)updateStrategy
{
    return updateStrategy;
}

- (BOOL)keepsSnapshots
{
    return isKeepingSnapshots;
}

/*
 * Processing transactions internally
 */

- (void)privateBeginTransaction
{
    NSZone* zone = [self zone];
    EOTransactionScope* newScope = 
	    NSZoneMalloc(zone, sizeof(EOTransactionScope));
    
    newScope->previous = transactionNestingLevel ? transactionStackTop : NULL;
    newScope->objectsDictionary = [[EOObjectUniquer allocWithZone:zone] init];
    newScope->objectsUpdated = [[NSMutableArray allocWithZone:zone] init];
    newScope->objectsDeleted = [[NSMutableArray allocWithZone:zone] init];
    newScope->objectsLocked = [[NSMutableArray allocWithZone:zone] init];

    transactionStackTop = newScope;
    transactionNestingLevel++;
    
    if (transactionNestingLevel == 1) {
	isUniquingObjects = [database uniquesObjects];
    }
}

- (void)privateCommitTransaction
{
    EOTransactionScope* newScope = transactionStackTop;
    transactionStackTop = newScope->previous;
    transactionNestingLevel--;
    
    // In nested transaction fold updated and deleted objects 
    // into the parent transaction; locked objects are forgotten
    // deleted objects are deleted form the parent transaction
    if (transactionNestingLevel) {
	// Keep updated objects
	[transactionStackTop->objectsUpdated 
	    addObjectsFromArray:newScope->objectsUpdated];
	// Keep deleted objects
	[transactionStackTop->objectsDeleted 
	    addObjectsFromArray:newScope->objectsDeleted];
	// Register objects in parent transaction scope
	[newScope->objectsDictionary 
	    transferTo:transactionStackTop->objectsDictionary 
	    objects:YES andSnapshots:YES];
    }
    // If this was the first transaction then fold the changes 
    // into the database; locked and updateted objects are forgotten
    else {
	int i, n;
	
	for (i = 0, n = [newScope->objectsDeleted count]; i < n; i++)
	    [database forgetObject:
		[newScope->objectsDeleted objectAtIndex:i]];
	// Register objects into the database
	if (isUniquingObjects || [database keepsSnapshots]) {
	    [newScope->objectsDictionary 
		transferTo:[database objectUniquer]
		objects:isUniquingObjects
		andSnapshots:[database keepsSnapshots]];
	}
    }
    
    // Kill transaction scope
    [newScope->objectsDictionary release];
    [newScope->objectsUpdated release];
    [newScope->objectsDeleted release];
    [newScope->objectsLocked release];
    NSZoneFree([self zone], newScope);
}

- (void)privateRollbackTransaction
{
    EOTransactionScope* newScope = transactionStackTop;
    transactionStackTop = newScope->previous;
    transactionNestingLevel--;
    
    // Forget snapshots, updated, deleted and locked objects
    // in current transaction
    
    [newScope->objectsDictionary release];
    [newScope->objectsUpdated release];
    [newScope->objectsDeleted release];
    [newScope->objectsLocked release];
    NSZoneFree([self zone], newScope);
}

/*
 * Handle Objects
 */

- (void)forgetObject:anObj
{
    EOTransactionScope* scope;

    if (!transactionNestingLevel)
	THROW([[InternalInconsistencyException alloc] initWithFormat:
	    @"EODatabaseContext:%x: No transaction in progress "
	    @"when trying to forget object, "
	    @"in [EODatabaseContext forgetObject]",
	    self]);
    if (!anObj)
	THROW([[InvalidArgumentException alloc] initWithFormat:
	    @"EODatabaseContext:%x: Cannot forget null object, "
	    @"in [EODatabaseContext forgetObject]",
	    self]);
    if ([EOFault isFault:anObj])
	THROW([[InvalidArgumentException alloc] initWithFormat:
	    @"EODatabaseContext:%x: Cannot forget forget a fault object, "
	    @"in [EODatabaseContext forgetObject]",
	    self]);
    
    [transactionStackTop->objectsDeleted addObject:anObj];

    for (scope = transactionStackTop; scope; scope = scope->previous) {
	[scope->objectsDictionary forgetObject:anObj];
    }
}

- objectForPrimaryKey:(NSDictionary*)aKey
  entity:(EOEntity*)anEntity
{
    EOTransactionScope* scope;
    id anObj;
    
    if (!isUniquingObjects || !aKey || !anEntity)
	return nil;
    
    aKey = [anEntity primaryKeyForRow:aKey];
    if (!aKey)
	return nil;
    
    for (scope = transactionStackTop; scope; scope = scope->previous) {
	anObj = [scope->objectsDictionary
	    objectForPrimaryKey:aKey 
	    entity:anEntity];
	if (anObj)
	    return anObj;
    }
    
    anObj = [database objectForPrimaryKey:aKey entity:anEntity];
    return anObj;
}

- (void)recordObject:anObj
  primaryKey:(NSDictionary*)aKey 
  entity:(EOEntity*)anEntity
  snapshot:(NSDictionary*)snapshot
{
    if (!transactionNestingLevel)
	THROW([[InternalInconsistencyException alloc] initWithFormat:
	    @"EODatabaseContext:%x: No transaction in progress "
	    @"when trying to record object, "
	    @"in [EODatabaseContext recordObject:primaryKey:entity:snapshot:]",
	    self]);
    if (!anObj)
	THROW([[InvalidArgumentException alloc] initWithFormat:
	    @"EODatabaseContext:%x: Cannot record null object, "
	    @"in [EODatabaseContext recordObject:primaryKey:entity:snapshot:]",
	    self]);
    if (!anEntity && isUniquingObjects)
	THROW([[InvalidArgumentException alloc] initWithFormat:
	    @"EODatabaseContext:%x: Cannot record object with null entity "
	    @"when uniquing objects, "
	    @"in [EODatabaseContext recordObject:primaryKey:entity:snapshot:]",
	    self]);
    aKey = [anEntity primaryKeyForRow:aKey];
    if (!aKey && isUniquingObjects)
	THROW([[InvalidArgumentException alloc] initWithFormat:
	    @"EODatabaseContext:%x: Cannot record object with null key "
	    @"when uniquing objects, "
	    @"in [EODatabaseContext recordObject:primaryKey:entity:snapshot:]",
	    self]);
    if (!snapshot && isKeepingSnapshots && ![EOFault isFault:anObj])
	THROW([[InvalidArgumentException alloc] initWithFormat:
	    @"EODatabaseContext:%x: Cannot record object with null snapshot "
	    @"when keeping snapshots, "
	    @"in [EODatabaseContext recordObject:primaryKey:entity:snapshot:]",
	    self]);
    
    if (isKeepingSnapshots || isUniquingObjects)
	[transactionStackTop->objectsDictionary 
	    recordObject:anObj 
	    primaryKey: isUniquingObjects ? aKey : nil
	    entity: isUniquingObjects ? anEntity :nil
	    snapshot: isKeepingSnapshots ? snapshot : nil];
}

- (void)recordObject:anObj
  primaryKey:(NSDictionary*)key 
  snapshot:(NSDictionary*)snapshot
{
    EOEntity* entity;

    if ([anObj respondsToSelector:@selector(entity)])
	entity = [anObj entity];
    else
	entity = [[[database adaptor] model] entityForObject:anObj];
    
    [self recordObject:anObj primaryKey:key entity:entity snapshot:snapshot];
}

- (NSDictionary*)snapshotForObject:anObj
{
    EOTransactionScope* scope;
    EOUniquerRecord* rec;
    
    if (!isKeepingSnapshots)
	return nil;
    
    for (scope = transactionStackTop; scope; scope = scope->previous) {
	rec = [scope->objectsDictionary recordForObject:anObj];
	if (rec)
	    return rec->snapshot;
    }
    
    rec = [[database objectUniquer] recordForObject:anObj];
    if (rec)
	return rec->snapshot;
    
    return nil;
}

- (NSDictionary*)primaryKeyForObject:anObj
{
    EOTransactionScope* scope;
    EOUniquerRecord* rec;
    
    if (![database uniquesObjects])
	return nil;
    
    for (scope = transactionStackTop; scope; scope = scope->previous) {
	rec = [scope->objectsDictionary recordForObject:anObj];
	if (rec)
	    return rec->pkey;
    }
    
    rec = [[database objectUniquer] recordForObject:anObj];
    if (rec)
	return rec->pkey;
    return nil;
}

- (void)primaryKey:(NSDictionary**)aKey
  andSnapshot:(NSDictionary**)aSnapshot
  forObject:anObj
{
    EOTransactionScope* scope;
    EOUniquerRecord* rec;

    if (!isKeepingSnapshots && ![database uniquesObjects]) {
	*aKey = *aSnapshot = nil;
	return;
    }
    
    for (scope = transactionStackTop; scope; scope = scope->previous) {
	rec = [scope->objectsDictionary recordForObject:anObj];
	if (rec) {
	    if (aKey)
		*aKey = rec->pkey;
	    if (aSnapshot)
		*aSnapshot = rec->snapshot;
	    return;
	}
    }
    
    rec = [[database objectUniquer] recordForObject:anObj];
    if (rec) {
	if (aKey)
	    *aKey = rec->pkey;
	if (aSnapshot)
	    *aSnapshot = rec->snapshot;
	return;
    }
    
    if (aKey)
	*aKey = nil;
    if (aSnapshot)
	*aSnapshot = nil;
}

- (void)recordLockedObject:anObj
{
    if (!transactionNestingLevel)
	THROW([[InternalInconsistencyException alloc] initWithFormat:
	    @"EODatabaseContext:%x: No transaction in progress "
	    @"when trying to record locked object, "
	    @"in [EODatabaseContext recordLockedObject:]",
	    self]);
    if (!anObj)
	THROW([[InvalidArgumentException alloc] initWithFormat:
	    @"EODatabaseContext:%x: Cannot record null object as locked, "
	    @"in [EODatabaseContext recordLockedObject:]",
	    self]);
    if ([EOFault isFault:anObj])
	THROW([[InvalidArgumentException alloc] initWithFormat:
	    @"EODatabaseContext:%x: Cannot record a fault object as locked, "
	    @"in [EODatabaseContext recordLockedObject:]",
	    self]);
    
    [transactionStackTop->objectsLocked addObject:anObj];
}

- (BOOL)isObjectLocked:anObj
{
    EOTransactionScope* scope;
    
    for (scope = transactionStackTop; scope; scope = scope->previous) {
	if ([scope->objectsLocked indexOfObjectIdenticalTo:anObj]!=NSNotFound)
	    return YES;
    }
    return NO;
}

- (void)recordUpdatedObject:anObj
{
    if (!transactionNestingLevel)
	THROW([[InternalInconsistencyException alloc] initWithFormat:
	    @"EODatabaseContext:%x: No transaction in progress "
	    @"when trying to record updated object, "
	    @"in [EODatabaseContext recordUpdatedObject:]",
	    self, anObj]);
    if (!anObj)
	THROW([[InvalidArgumentException alloc] initWithFormat:
	    @"EODatabaseContext:%x: Cannot record null object as updatetd, "
	    @"in [EODatabaseContext recordUpdatedObject:]",
	    self]);
    if ([EOFault isFault:anObj])
	THROW([[InvalidArgumentException alloc] initWithFormat:
	    @"EODatabaseContext:%x: Cannot record fault object as updated, "
	    @"in [EODatabaseContext recordUpdatedObject:]",
	    self]);
    
    [transactionStackTop->objectsUpdated addObject:anObj];
}

- (BOOL)isObjectUpdated:anObj
{
    EOTransactionScope* scope;
    
    for (scope = transactionStackTop; scope; scope = scope->previous) {
	if(([scope->objectsUpdated indexOfObjectIdenticalTo:anObj]!=NSNotFound)
	||([scope->objectsDeleted indexOfObjectIdenticalTo:anObj]!=NSNotFound))
	    return YES;
    }
    return NO;
}

@end /* EODatabaseContext */
