/* 
   EODatabaseDataSource.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/NSHashTable.h>

#include <eoaccess/EOModel.h>
#include <eoaccess/EOEntity.h>
#include <eoaccess/EOAttribute.h>
#include <eoaccess/EORelationship.h>
#include <eoaccess/EOQualifier.h>
#include <eoaccess/EOGenericRecord.h>
#include <eoaccess/EONull.h>

#include <eoaccess/EOAdaptor.h>
#include <eoaccess/EOAdaptorContext.h>
#include <eoaccess/EOAdaptorChannel.h>

#include <eoaccess/EODatabase.h>
#include <eoaccess/EODatabaseContext.h>
#include <eoaccess/EODatabaseChannel.h>

#include "EODatabaseDataSource.h"
#include "EODetailDatabaseDataSource.h"

@interface EODatabaseDataSource(Private)
- (BOOL)beginAutoTransaction;
- (BOOL)commitAutoTransaction;
@end

// EODatabaseDataSource implementation

@implementation EODatabaseDataSource

// Database objects rendesvous 

typedef struct {
    int refCount;
    id databaseObject;
    NSMapTable* dict;
} RendesvousEntry;

static NSString* nullKeyName = @"-- null key --";
#define RVKEY(key) (key ? key:nullKeyName)

static NSMapTable* rendesvousDictionary;

+ (void)initialize
{
    // THREAD - use of global variable
    rendesvousDictionary = NSCreateMapTable(NSObjectMapKeyCallBacks, 
	NSOwnedPointerMapValueCallBacks, 1);
}

+ (EODatabaseChannel*)databaseChannelWithDatabaseName:(NSString*)aDatabaseName 
  contextName:(NSString*)aContextName 
  channelName:(NSString*)aChannelName
{
    RendesvousEntry *entry;
    
    // Database
    entry = (RendesvousEntry*)NSMapGet(rendesvousDictionary,
						    RVKEY(aDatabaseName));
    if (!entry)
    	return nil;
    
    // Context
    entry = (RendesvousEntry*)NSMapGet(entry->dict, RVKEY(aContextName));
    if (!entry)
    	return nil;
    
    // Channel
    entry = (RendesvousEntry*)NSMapGet(entry->dict, RVKEY(aChannelName));
    if (!entry)
    	return nil;
    
    return entry->databaseObject;
}

+ (void)releaseObjectsWithDatabaseName:(NSString *)aDatabaseName 
  contextName:(NSString *)aContextName 
  channelName:(NSString *)aChannelName
{
    RendesvousEntry *entry1, *entry2, *entry3;
    
    // Database
    entry1 = (RendesvousEntry*)NSMapGet(rendesvousDictionary, 
						    RVKEY(aDatabaseName));
    if (!entry1)
    	return;
    
    // Context
    entry2 = (RendesvousEntry*)NSMapGet(entry1->dict, RVKEY(aContextName));
    if (!entry2)
    	return;
    
    // Channel
    entry3 = (RendesvousEntry*)NSMapGet(entry2->dict, RVKEY(aChannelName));
    if (!entry3)
    	return;
    
    // Release channel
    if (--(entry3->refCount) <= 0) {
	[entry3->databaseObject release];
	NSMapRemove(entry2->dict, RVKEY(aChannelName));
    }
    else 
	return;
    
    // Release context
    if (--(entry2->refCount) <= 0) {
	[entry2->databaseObject release];
	NSFreeMapTable(entry2->dict);
	NSMapRemove(entry1->dict, RVKEY(aContextName));
    }
    else 
	return;
    
    // Release database
    if (--(entry1->refCount) <= 0) {
	[entry1->databaseObject release];
	NSFreeMapTable(entry1->dict);
	NSMapRemove(rendesvousDictionary, RVKEY(aDatabaseName));
    }
    
    return;
}

+ (void)registerChannel:(EODatabaseChannel *)channel 
  forRendezvousWithDatabaseName:(NSString *)aDatabaseName 
  contextName:(NSString *)aContextName 
  channelName:(NSString *)aChannelName
{
    RendesvousEntry *dbEntry, *ctxEntry, *chnEntry;
    id context = [channel databaseContext];
    id database = [context database];
    
    if (!channel || !database || !context)
    	return;
    
    // Database
    dbEntry = (RendesvousEntry*)NSMapGet(rendesvousDictionary, 
						    RVKEY(aDatabaseName));
    if (!dbEntry) {
    	dbEntry = (RendesvousEntry*)malloc(sizeof(RendesvousEntry));
	dbEntry->refCount = 1;
	dbEntry->databaseObject = [database retain];
	dbEntry->dict = NSCreateMapTable(NSObjectMapKeyCallBacks, 
	    NSOwnedPointerMapValueCallBacks, 1);
	NSMapInsert(rendesvousDictionary, RVKEY(aDatabaseName), dbEntry);
    } 
    else if (dbEntry->databaseObject != database) {
	[dbEntry->databaseObject autorelease];
	dbEntry->databaseObject = [database retain];
    }
    
    // Context
    ctxEntry = (RendesvousEntry*)NSMapGet(dbEntry->dict, RVKEY(aContextName));
    if (!ctxEntry) {
    	ctxEntry = (RendesvousEntry*)malloc(sizeof(RendesvousEntry));
	ctxEntry->refCount = 1;
	ctxEntry->databaseObject = [context retain];
	ctxEntry->dict = NSCreateMapTable(NSObjectMapKeyCallBacks, 
	    NSOwnedPointerMapValueCallBacks, 1);
	NSMapInsert(dbEntry->dict, RVKEY(aContextName), ctxEntry);
    }
    else if (ctxEntry->databaseObject != context) {
	[ctxEntry->databaseObject autorelease];
	ctxEntry->databaseObject = [context retain];
    }
    
    // Channel
    chnEntry = (RendesvousEntry*)NSMapGet(ctxEntry->dict, RVKEY(aChannelName));
    if (!chnEntry) {
    	chnEntry = (RendesvousEntry*)malloc(sizeof(RendesvousEntry));
	chnEntry->refCount = 1;
	chnEntry->databaseObject = [channel retain];
	chnEntry->dict = NULL;
	NSMapInsert(ctxEntry->dict, RVKEY(aChannelName), chnEntry);
    }
    else if (chnEntry->databaseObject != channel) {
	[chnEntry->databaseObject autorelease];
	chnEntry->databaseObject = [channel retain];
    }
}

+ (EODatabaseChannel*)channelForRendezvousWithModelName:(NSString*)aModelName
  databaseName:(NSString*)aDatabaseName 
  contextName:(NSString*)aContextName 
  channelName:(NSString*)aChannelName
{
    RendesvousEntry *dbEntry, *ctxEntry, *chnEntry;
    id database, context, channel, adaptor, model;
    
    // Database
    dbEntry = (RendesvousEntry*)NSMapGet(rendesvousDictionary, 
						    RVKEY(aDatabaseName));
    if (!dbEntry) {
	model = [[EOModel alloc] initWithContentsOfFile:
			[EOModel findPathForModelNamed:aModelName]];
	// CHECK - model is ok
	adaptor = [EOAdaptor adaptorWithModel:model];
	// CHECK - adaptor is ok
	database = [[EODatabase alloc] initWithAdaptor:adaptor];
	// CHECK - database is ok
	
    	dbEntry = (RendesvousEntry*)malloc(sizeof(RendesvousEntry));
	dbEntry->refCount = 1;
	dbEntry->databaseObject = [database retain];
	dbEntry->dict = NSCreateMapTable(NSObjectMapKeyCallBacks, 
	    NSOwnedPointerMapValueCallBacks, 1);
	NSMapInsert(rendesvousDictionary, RVKEY(aDatabaseName), dbEntry);
    }
    else
	database = dbEntry->databaseObject;
    
    // Context
    ctxEntry = (RendesvousEntry*)NSMapGet(dbEntry->dict, 
    						RVKEY(aContextName));
    if (!ctxEntry) {
	// TODO - use database createDatabaseContext
	context = [[EODatabaseContext alloc] initWithDatabase:database];
	
    	ctxEntry = (RendesvousEntry*)malloc(sizeof(RendesvousEntry));
	ctxEntry->refCount = 1;
	ctxEntry->databaseObject = [context retain];
	ctxEntry->dict = NSCreateMapTable(NSObjectMapKeyCallBacks, 
	    NSOwnedPointerMapValueCallBacks, 1);
	NSMapInsert(dbEntry->dict, RVKEY(aContextName), ctxEntry);
    }
    else 
    	context = ctxEntry->databaseObject;	   
    
    // Channel
    chnEntry = (RendesvousEntry*)NSMapGet(ctxEntry->dict, RVKEY(aChannelName));
    if (!chnEntry) {
	channel = [[EODatabaseChannel alloc] initWithDatabaseContext:context];
	
    	chnEntry = (RendesvousEntry*)malloc(sizeof(RendesvousEntry));
	chnEntry->refCount = 1;
	chnEntry->databaseObject = [channel retain];
	chnEntry->dict = NULL;
	NSMapInsert(ctxEntry->dict, RVKEY(aChannelName), chnEntry);
    }
    else
	channel = chnEntry->databaseObject;
    
    return channel;
}

// Initializing instances

- initWithDatabaseChannel:(EODatabaseChannel*)aChannel
  entity:(EOEntity*)anEntity
{
    if (!aChannel || !anEntity) {
	// WARN - no channel/entity
    	[self dealloc];
	return nil;
    }

    databaseChannel = [aChannel retain];
    qualifier = [[anEntity qualifier] retain];
    auxiliaryQualifier = nil;
    fetchOrder = nil;

    isInitializedInRendesvous = NO;
    isConnected = [aChannel isOpen];
    isFetchEnabled = YES;
    isAutoTransactionEnabled = YES;
    isInAutoTransaction = NO;
    
    return self;
}

- (void)dealloc
{
    if (isInitializedInRendesvous) {
    	[[self class] releaseObjectsWithDatabaseName:dataBaseName
		contextName:dataBaseContextName
		channelName:dataBaseChannelName];
	[dataBaseName release];
	[dataBaseContextName release];
	[dataBaseChannelName release];
    }
    [databaseChannel release];
    [qualifier release];
    [auxiliaryQualifier release];
    [fetchOrder release];
    [super dealloc];
}

- initWithDatabaseChannel:(EODatabaseChannel *)aChannel
  entityNamed:(NSString *)entityName
{
    EOEntity* entity = [[[[[aChannel adaptorChannel] 
    	adaptorContext] adaptor] model] entityNamed:entityName];
    
    if (!aChannel || !entity) {
	// WARN - no channel/entity
    	[self dealloc];
	return nil;
    }
    
    return [self initWithDatabaseChannel:aChannel entity:entity];
}

- initWithModelName:(NSString *)modelName
  entityName:(NSString *)entityName
{
    return [self initWithModelName:modelName
    	entityName:entityName
	databaseName:nil
	contextName:nil
	channelName:nil];
}

- initWithModelName:(NSString *)modelName
  entityName:(NSString *)entityName
  databaseName:(NSString *)databaseName
  contextName:(NSString *)contextName
  channelName:(NSString *)channelName
{    
    EODatabaseChannel* aChannel;
    
    aChannel = [[self class] channelForRendezvousWithModelName:modelName
	databaseName:databaseName 
	contextName:contextName 
	channelName:channelName];

    if ([self initWithDatabaseChannel:aChannel entityNamed:entityName]) {
	dataBaseName = [databaseName retain];
	dataBaseContextName = [contextName retain];
	dataBaseChannelName = [channelName retain];
	isInitializedInRendesvous = YES;
	return self;
    }
    else 
	return nil;
}

// Getting the database channel

- (EODatabaseChannel*)databaseChannel
{
    return databaseChannel;
}

// Getting the root entity	

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

// Setting automatic transaction control

- (void)setBeginsTransactionsAutomatically:(BOOL)flag
{
    isAutoTransactionEnabled = flag;
}

- (BOOL)beginsTransactionsAutomatically
{
    return isAutoTransactionEnabled;
}

// Setting the qualifier

- (void)setQualifier:(EOQualifier*)aQualifier
{
    [qualifier autorelease];
    qualifier = [aQualifier retain];
}

- (EOQualifier*)qualifier
{
    return qualifier;
}

- (void)setAuxiliaryQualifier:(EOQualifier*)aQualifier
{
    [auxiliaryQualifier autorelease];
    auxiliaryQualifier = [aQualifier retain];
}

- (EOQualifier*)auxiliaryQualifier
{
    return auxiliaryQualifier;
}

- (EOQualifier*)qualifierForFetch
{
    if (auxiliaryQualifier) {
	EOQualifier* fetchQualifier = [qualifier copy];
	[fetchQualifier conjoinWithQualifier:auxiliaryQualifier];
	return [fetchQualifier autorelease];
    }
    return qualifier;
}

// Enabling fetching

- (void)setFetchEnabled:(BOOL)flag
{
    isFetchEnabled = flag;
}

- (BOOL)isFetchEnabled
{
    return isFetchEnabled;
}

// Setting the fetch order

- (void)setFetchOrder:(NSArray*)aFetchOrder
{
    [fetchOrder autorelease];
    fetchOrder = [aFetchOrder retain];
}

- (NSArray*)fetchOrder
{
    return fetchOrder;
}

// EODataSources methods

- (NSArray *)keys
{
    return [[qualifier entity] classPropertyNames];
}

- createObject
{
    return [databaseChannel initializedObjectForRow:nil 
	entity:[qualifier entity] zone:[self zone]];
}

- (BOOL)insertObject:object
{
    if (![self beginAutoTransaction] || [[qualifier entity] isReadOnly])
	return NO;
    return [databaseChannel insertObject:object];
}

- (BOOL)deleteObject:object
{
    if (![self beginAutoTransaction] || [[qualifier entity] isReadOnly])
	return NO;
    return [databaseChannel deleteObject:object];
}

- (BOOL)updateObject:object
{
    if (![self beginAutoTransaction] || [[qualifier entity] isReadOnly])
	return NO;
    return [databaseChannel updateObject:object];
}

- (NSArray *)fetchObjects
{
    NSMutableArray* objects = nil;
    
    if (!isFetchEnabled || ![self beginAutoTransaction])
	return nil;
    if ([databaseChannel selectObjectsDescribedByQualifier:
	    [self qualifierForFetch] fetchOrder:fetchOrder]) {
	id obj;
	NSZone* zone = [self zone];
	objects = [[[NSMutableArray allocWithZone:zone] init] autorelease];
	
	do {
	    obj = [databaseChannel fetchWithZone:zone];
	    if (obj)
		[objects addObject:obj];
	} while (obj);
	[databaseChannel cancelFetch];
    }
    [self commitAutoTransaction];
    return objects;
}

- (BOOL)saveObjects
{
    return [self commitAutoTransaction];
}

- (BOOL)canDelete
{
    return [[qualifier entity] isReadOnly] ? NO : YES;
}

- coerceValue:value forKey: (NSString *)key
{
    EOEntity* entity = [qualifier entity];
    EOAttribute* attr = [entity attributeNamed:key];
    
    // TODO - call public conversion function
    return attr ? [attr convertValueToModel:value] : value;
}

// EOQualifiableDataSources methods

- (void)qualifyWithRelationshipKey:(NSString *)key ofObject:sourceObject
{
    EOEntity* entity;
    EORelationship* relation;
    
    [qualifier release];
    qualifier = nil;
    [self setFetchEnabled:NO];
    
    if (!sourceObject)
    	return;
    
    if ([sourceObject respondsToSelector:@selector(entity)])
    	entity = [sourceObject entity];
    else
	entity = [[[[[databaseChannel adaptorChannel] adaptorContext] adaptor]
	    model] entityForObject:sourceObject];
    if (!entity)
	return;
    
    relation = [entity relationshipNamed:key];
    if (!relation) {
	// WARN - no relationship named for entity
    	return;
    }
    
    qualifier = [EOQualifier qualifierForObject:sourceObject
    	relationship:relation];
    if (!qualifier)
    	return;
    
    [self setFetchEnabled:YES];
}

// EOMasterDataSources methods

- (id <EOQualifiableDataSources>)dataSourceQualifiedByKey:(NSString *)key
{
    EOEntity* myEntity = [qualifier entity];
    EORelationship* relation = [myEntity relationshipNamed:key];
    EOEntity* destEntity = [relation destinationEntity];
    
    if (!relation) {
    	// WARN - no relationshhip named
	return nil;
    }
    
    if (!destEntity) {
    	// WARN - no destinatin entity
	return nil;
    }
    
    return [[[EODetailDatabaseDataSource allocWithZone:[self zone]]
    	initWithMasterDataSource:self entity:destEntity]
	autorelease];
}

// EORollbackDataSources methods

- (void)rollback
{
    if (isInAutoTransaction)
	[[databaseChannel databaseContext] rollbackTransaction];
    isInAutoTransaction = NO;
}

@end /* EODatabaseDataSource */

// EODatabaseDataSource private methods

@implementation EODatabaseDataSource(Private)

- (BOOL)beginAutoTransaction
{
    if (isInAutoTransaction)
    	return YES;
    
    if (!isAutoTransactionEnabled)
	// CHECK - that the database context has a transaction started
    	return YES;
    
    if (![databaseChannel isOpen]) {
	EOAdaptor* adaptor = [[[databaseChannel adaptorChannel]
		adaptorContext] adaptor];
	if ([adaptor hasValidConnectionDictionary] == NO)
	    [NSException raise:nil format:@"Set the connection dictionary before!"];
	if ([databaseChannel openChannel] == NO)
		return NO;
    }
    
    if ([[databaseChannel databaseContext] transactionNestingLevel])
	return YES;

    if (![[databaseChannel databaseContext] beginTransaction])
	return NO;
    
    isInAutoTransaction = YES;
    return YES;
}

- (BOOL)commitAutoTransaction
{
    if (!isInAutoTransaction)
    	return YES;
    
    isInAutoTransaction = NO;
    return [[databaseChannel databaseContext] commitTransaction];
}

@end

