/*
 *	File:		dbmAdaptor.m
 *
 *	Author:		Mike Henry
 *	Created:	24 Oct 1993
 *
 *	Description:	A simple adaptor using DBM
 *
 *	Copyright (c) Mike Henry, 1993. All Rights Reserved.
 */

#import "dbmAdaptor.h"

#import <appkit/Application.h>
#import <appkit/NXHelpPanel.h>
#import <appkit/Panel.h>
#import <appkit/nextstd.h>
#import <architecture/byte_order.h>
#import <dbkit/DBAttribute.h>
#import <dbkit/DBBinder.h>
#import <dbkit/DBDatabase.h>
#import <dbkit/DBEntity.h>
#import <dbkit/expressionValues.h>
#import <dbkit/DBRelationship.h>
#import <dbkit/DBQualifier.h>
#import <objc/NXBundle.h>
#import <objc/hashtable.h>

BOOL		yyError;
NXZone*		yyZone;
DBBinder*	yyBinder;
dbmAdaptor*	yyAdaptor;
const char*	yyInput;

extern		yyparse();

static const char* const dbmAdaptorVersion[] =
{
  "@(#) gdbmAdaptor",
  "@(#) Release 3.3 (v0.5)",
  "@(#) Copyright (c) Mike Henry 1995. All Rights Reserved.",
  "@(#) Send suggestions and bug reports to mike@enea.se"
};

#define NAME		(dbmAdaptorVersion[0]+5)
#define VERSION		(dbmAdaptorVersion[1]+5)
#define COPYRIGHT	(dbmAdaptorVersion[2]+5)
#define COMMENTS	(dbmAdaptorVersion[3]+5)

@implementation dbmAdaptor

- initForDatabase:(DBDatabase*)aDatabase
{
#ifdef GDBM
  fprintf(stderr, "*** This is %s, %s\n*** %s\n*** %s\n*** %s\n",
	  NAME, VERSION, COPYRIGHT, COMMENTS, gdbm_version);
#else
  fprintf(stderr, "*** This is %s, %s\n*** %s\n*** %s\n",
	  NAME, VERSION, COPYRIGHT, COMMENTS);
#endif
  
  bundle = [NXBundle bundleForClass:[self class]];
  if(helpPanel = [NXHelpPanel new])
    [helpPanel addSupplement:"Help" inPath:[bundle directory]];
  
  database = aDatabase;
  [database setName:NAME];

  return self;
}

/* This method is usually called by the DBDatabase object */
- (BOOL)connectUsingString:(const char*)aString
{
  int i, count;
  List* entities;
  char dbmInfoFile[MAXPATHLEN+1];

  if(isOpen)
    return NO;

  isOpen = YES;
  if(aString == 0)
    aString = NXCopyStringBufferFromZone("NoName.dbm", [self zone]);
  
  wrapper = (char*)NXCopyStringBufferFromZone(aString, [self zone]);
  if(mkdir(wrapper, 00755) == -1 && errno != EEXIST)
  {
    fprintf(stderr, "%s: Couldn't create database wrapper %s: %s.\n",
	    NAME, wrapper, sys_errlist[errno]);
    return NO;
  }

  strcpy(dbmInfoFile, wrapper);
  strcat(dbmInfoFile, "/dbmInfo");
#ifdef GDBM
  if((dbmInfo = gdbm_open(dbmInfoFile, 512, GDBM_WRCREAT, 00644, NULL)) == 0)
#else
  if((dbmInfo = dbm_open(dbmInfoFile, O_CREAT|O_RDWR, 00644)) == 0)
#endif
  {
    fprintf(stderr, "%s: Couldn't open dbmInfo file: %s.\n",
	    NAME, dbmInfoFile);
    return NO;
  }
  
  entities = [[List allocFromZone:[self zone]] init];
  [database getEntities:entities];
  count = [entities count];

  for(i = 0; i < count; i++)
  {
    char		entityDir[MAXPATHLEN+1];
    dbm_t		start = NXSwapHostLongLongToBig(0);
    datum		key, value;
    const char*		entityName = [[entities objectAt:i] expressionValue];

    value.dptr = (char*)&start;
    value.dsize = sizeof(dbm_t);
    key.dsize = strlen(entityName) + 5;
    NX_MALLOC(key.dptr, char, key.dsize);
    strcat(strcpy(key.dptr, entityName), SIZE_INFO);
#ifdef GDBM
    gdbm_store(dbmInfo, key, value, GDBM_INSERT);
    strcat(strcpy(key.dptr, entityName), ROW_INFO);
    gdbm_store(dbmInfo, key, value, GDBM_INSERT);
    strcat(strcpy(key.dptr, entityName), HOLE_INFO);
    gdbm_store(dbmInfo, key, value, GDBM_INSERT);
    strcat(strcpy(key.dptr, entityName), COUNT_INFO);
    gdbm_store(dbmInfo, key, value, GDBM_INSERT);
#else
    dbm_store(dbmInfo, key, value, DBM_INSERT);
    strcat(strcpy(key.dptr, entityName), ROW_INFO);
    dbm_store(dbmInfo, key, value, DBM_INSERT);
    strcat(strcpy(key.dptr, entityName), HOLE_INFO);
    dbm_store(dbmInfo, key, value, DBM_INSERT);
    strcat(strcpy(key.dptr, entityName), COUNT_INFO);
    dbm_store(dbmInfo, key, value, DBM_INSERT);
#endif
    NX_FREE(key.dptr);

    strcat(strcat(strcpy(entityDir, wrapper), "/"), entityName);

    if(mkdir(entityDir, 00755) == -1 && errno != EEXIST)
    {
      fprintf(stderr, "%s: Couldn't create wrapper for entity %s: %s.\n",
	      NAME, entityDir, sys_errlist[errno]);
#ifdef GDBM
      gdbm_close(dbmInfo);
#else
      dbm_close(dbmInfo);
#endif
      [entities free];
      return NO;
    }
  }

  [entities free];

  return YES;
}

- (BOOL)disconnectUsingString:(const char*)aString
{
  if(!isOpen)
    return NO;

#ifdef GDBM
  gdbm_close(dbmInfo);
#else
  dbm_close(dbmInfo);
#endif
  NXZoneFree([self zone], wrapper);
  wrapper = 0;
  
  isOpen = NO;
  return YES;
}

- (BOOL)isConnected
{
  return isOpen;
}

- (BOOL)deleteData:(DBBinder*)binder
{
  id		entity;
  int		i, count;
  BOOL		gotOne = NO;
  List*		list;
  datum		key;

  if(!isOpen)
    return NO;
  
  key = [self getKeyForBinder:binder];
  if(key.dptr == NULL)
    return NO;

  list = [[List allocFromZone:[self zone]] init];
  count = [[binder getProperties:list] count];
  entity = [[list lastObject] entity];
  
  for(i = 0; i < count; i++)
  {
    id    property = [list objectAt:i];
    DBM_FILE  db = [self openDBMFileForProperty:property];

    if(db == 0)
      continue;

    if(!gotOne && ![property isEntity]) {
      DBM_FILE dbi;
      char file[MAXPATHLEN+1];
      datum index;
      dbm_t arch_dep, nholes, currentSize;

      strcat(strcat(strcpy(file, wrapper), "/"), [entity expressionValue]);
#ifdef GDBM
      if((dbi = gdbm_open(file, 0, GDBM_WRCREAT, 00644, NULL)) == 0)
#else
      if((dbi = dbm_open(file, O_CREAT|O_RDWR, 00644)) == 0)
#endif
      {
	NX_FREE(key.dptr);
	[list free];
	return NO;
      }

      currentSize = [self get:SIZE_INFO for:entity];
      [self set:SIZE_INFO for:entity with:(currentSize-1)];

      nholes = [self get:HOLE_INFO for:entity];
      [self set:HOLE_INFO for:entity with:(nholes+1)];

      arch_dep = NXSwapHostLongLongToBig(nholes);
      index.dptr = (char*)&arch_dep;
      index.dsize = sizeof(dbm_t);
#ifdef GDBM
      gdbm_store(dbi, index, key, GDBM_INSERT);
      gdbm_close(dbi);
#else
      dbm_store(dbi, index, key, DBM_INSERT);
      dbm_close(dbi);
#endif

      gotOne = YES;
    } 

#ifdef GDBM
    if(gdbm_delete(db, key))
#else
    if(dbm_delete(db, key))
#endif
      fprintf(stderr,
	      "%s: Cannot delete data for key: %x.\n",
	      NAME,
	      (int)NXSwapBigLongLongToHost(*(dbm_t*)key.dptr));
    
#ifdef GDBM
    gdbm_close(db);
#else
    dbm_close(db);
#endif
  }

  NX_FREE(key.dptr);
  [list free];
  return YES;
}

- (BOOL)updateData:(DBBinder*)binder
{
  BOOL bool;
  datum key;

  if(!isOpen)
    return NO;

  key = [self getKeyForBinder:binder];
  bool = [self storeForKey:key withBinder:binder];
  NX_FREE(key.dptr);

  return bool;
}

- (BOOL)insertData:(DBBinder*)binder
{
  id		entity;
  List*		list;
  datum		key;
  dbm_t		size, nholes;
  
  if(!isOpen)
    return NO;

  list = [binder getProperties:[[List allocFromZone:[self zone]] init]];
  entity = [[list lastObject] entity];

  size = [self get:SIZE_INFO for:entity];
  nholes = [self get:HOLE_INFO for:entity];

  if(nholes > 0)
  {
    datum index;
    DBM_FILE dbh;
    char holeFile[MAXPATHLEN+1];

    strcat(strcat(strcpy(holeFile, wrapper), "/"), [entity expressionValue]);
#ifdef GDBM
    if((dbh = gdbm_open(holeFile, 0, GDBM_WRCREAT, 00644, NULL)) == 0)
#else
    if((dbh = dbm_open(holeFile, O_CREAT|O_RDWR, 00644)) == 0)
#endif
    {
      fprintf(stderr, "%s: Couldn't open hole database %s.\n", NAME, holeFile);
      [list free];
      return NO;
    }

#ifdef GDBM
    index = gdbm_firstkey(dbh);
#else
    index = dbm_firstkey(dbh);
#endif
    if(index.dptr == 0)
    {
      fprintf(stderr, "%s: Couldn't find any holes.\n", NAME);
#ifdef GDBM
      gdbm_close(dbh);
#else
      dbm_close(dbh);
#endif
      [list free];
      return NO;
    }

#ifdef GDBM
    key = gdbm_fetch(dbh, index);
#else
    key = dbm_fetch(dbh, index);
#endif

    if(key.dptr == 0)
    {
      fprintf(stderr, "%s: No rows for hole.\n", NAME);
#ifdef GDBM
      free(index.dptr);
      gdbm_close(dbh);
#else
      dbm_close(dbh);
#endif
      [list free];
      return NO;
    }

    if([self storeForKey:key withBinder:binder] == NO)
    {
      fprintf(stderr, "%s: Couldn't store new row for hole.\n", NAME);
#ifdef GDBM
      free(index.dptr);
      gdbm_close(dbh);
#else
      dbm_close(dbh);
#endif
      [list free];
      return NO;
    }

    [self set:HOLE_INFO for:entity with:(nholes-1)];
#ifdef GDBM
    if(gdbm_delete(dbh, index) != 0)
#else
    if(dbm_delete(dbh, index) != 0)
#endif
    {
      fprintf(stderr, "%s: Couldn't delete used hole.\n", NAME);
#ifdef GDBM
      free(key.dptr);
      free(index.dptr);
      gdbm_close(dbh);
#else
      dbm_close(dbh);
#endif
      [list free];
      return NO;
    }

#ifdef GDBM
    free(key.dptr);
    free(index.dptr);
    gdbm_close(dbh);
#else
    dbm_close(dbh);
#endif
  }
  else
  {
    dbm_t arch_dep = NXSwapHostLongLongToBig(size);
    
    key.dptr = (char*)&arch_dep;
    key.dsize = sizeof(dbm_t);
    
    if([self storeForKey:key withBinder:binder] == NO)
    {
      fprintf(stderr, "%s: Couldn't store new row for key.\n", NAME);
      [list free];
      return NO;
    }
    
    [self set:SIZE_INFO for:entity with:(size+1)];
  }

  [list free];
  return YES;
}

/* This method gets called when doing a "fetchAllRecords" at the IB level */  
- (BOOL)selectData:(DBBinder*)binder
{
  id		entity;
  List*		list;
  
  if(!isOpen)
    return NO;

  list = [[List allocFromZone:[self zone]] init];
  entity = [[[binder getProperties:list] lastObject] entity];

  [self set:ROW_INFO for:entity with:0];
  [self set:COUNT_INFO for:entity with:0];

  [list free];
  
  return YES;
}

- (BOOL)fetchData:(DBBinder*)binder
{
  id		entity;
  int		i, count;
  List*		list;
  dbm_t		found = 0, currentRow = 0;

  if(!isOpen)
  {
    fprintf(stderr, "%s: fetch:  Database not open.\n", NAME);
    return NO;
  }
  
  list = [[List allocFromZone:[self zone]] init];
  count = [[binder getProperties:list] count];
  entity = [[list lastObject] entity];

  if([binder qualifier] && ![[binder qualifier] isEmpty])
  {
    dbm_t row = [self get:ROW_INFO for:entity];
    dbm_t size = [self get:SIZE_INFO for:entity];
    const char* qual = [[binder qualifier] expressionValue];
    
    while(![self evaluateString:qual using:binder])
      if(row < size)
	[self set:ROW_INFO for:entity with:++row];
      else
      {
	[list free];
	return NO;
      }
  }

  for(i = 0; i < count; i++)
  {
    id property = [list objectAt:i];
    id value = [binder valueForProperty:property];
    DBM_FILE db = [self openDBMFileForProperty:property];
    datum key, data = {0, 0};
    dbm_t arch_dep;
    
    if(db == 0)
    {
      fprintf(stderr, "%s: Couldn't open entity: %s.\n",
	      NAME, [entity expressionValue]);
      [list free];
      return NO;
    }
    
    found = [self get:COUNT_INFO for:entity];
    currentRow = [self get:ROW_INFO for:entity];
    arch_dep = NXSwapHostLongLongToBig(currentRow);
    
    key.dptr = (char*)&arch_dep;
    key.dsize = sizeof(dbm_t);
    
#ifdef GDBM
    data = gdbm_fetch(db, key);
#else
    data = dbm_fetch(db, key);
#endif
    
    if(data.dptr == 0)
    {
#ifdef GDBM
      gdbm_close(db);
#else
      dbm_close(db);
#endif
      
      if((([self get:HOLE_INFO for:entity] - found) > 0) &&
	 ([self get:SIZE_INFO for:entity] > currentRow))
      {
	[self set:COUNT_INFO for:entity with:(found+1)];
	[self set:ROW_INFO for:entity with:(currentRow+1)];
	
	[list free];
	return [self fetchData:binder];
      }

      [list free];
      return NO;
    }
    
    // Eeek!! This switch should be replaced by a dynamically bound method;
    if(data.dsize == 0)
      [value setNull];
    else
      switch([[property propertyType] objcType][0])
      {
      case '*':
	[value setStringValue:data.dptr];
	break;
      case 'i':
	[value setIntValue:NXSwapBigIntToHost(*(int*)data.dptr)];
	break;
      case 'f':
	[value setFloatValue:NXSwapBigFloatToHost(*(float*)data.dptr)];
	break;
      case 'd':
	[value setDoubleValue:NXSwapBigDoubleToHost(*(double*)data.dptr)];
	break;
      case '@':
      {
	NXStream* stream = NXOpenMemory(data.dptr, data.dsize, NX_READONLY);
	[value setObjectValue:NXReadObject(stream)];
	NXCloseMemory(stream, NX_FREEBUFFER);
      }
	break;
      default:
	[value setNull];
	break;
      }
    
#ifdef GDBM
    free(data.dptr);
    gdbm_close(db);
#else
    dbm_close(db);
#endif
  }
  
  [self set:ROW_INFO for:entity with:(currentRow+1)];
  
  [list free];
  return YES;
}

- (void)fetchDone:(DBBinder*)binder
{
  id		entity;
  List*		list;
  dbm_t		row, size;

  list = [binder getProperties:[[List allocFromZone:[self zone]] init]];
  entity = [[list lastObject] entity];

  row = [self get:ROW_INFO for:entity]; 
  size = [self get:SIZE_INFO for:entity];

  [self set:SIZE_INFO for:entity with:(size>row?size:row)];

  [list free];
}

- (BOOL)evaluateString:(const char*)string using:(DBBinder*)binder
{
  yyAdaptor	= self;
  yyBinder	= binder;
  yyError	= NO;
  yyInput	= string;
  yyZone	= [self zone];

  return !(yyparse() == 1 || yyError);
}

- (const char*)connectionName
{
  return NAME;
}

- (const char*)currentLoginString
{
  return wrapper;
}

- enumerateEntities:(List*)aList
{
  [aList empty];

  return nil;
}

- enumerateProperties:(List*)aList forEntity:(id<DBEntities>)anEntity
{
  [aList empty];

  return nil;
}

@end

@implementation dbmAdaptor(Private)

- (DBM_FILE)openDBMFileForProperty:(id<DBProperties, DBExpressionValues>)property
{
  char file[MAXPATHLEN+1];

  if(property == nil)
    return 0;

  strcpy(file, wrapper);
  strcat(file, "/");
  strcat(file, [(id<DBEntities>)[property entity] expressionValue]);
  strcat(file, "/");
  strcat(file, [property expressionValue]);
  
#ifdef GDBM
  return gdbm_open(file, 0, GDBM_WRCREAT, 00644, NULL);
#else
  return dbm_open(file, O_CREAT|O_RDWR, 00644);
#endif
}

- (datum)getKeyForBinder:(DBBinder*)binder
{
  datum data, key, val;
  List* list = [[List allocFromZone:[self zone]] init];
  int i, count = [[binder getProperties:list] count];
  
  for(i = 0; i < count; i++)
  {
    id prop = [list objectAt:i];
    
    if([prop isKey])
    {
      DBM_FILE db = [self openDBMFileForProperty:prop];
      
      if(db == 0)
	continue;

      [self convertDBValue:[binder valueForProperty:prop] toDatum:&val];
      
#ifdef GDBM
      for(key = gdbm_firstkey(db); key.dptr; key = gdbm_nextkey(db, key))
      {
	data = gdbm_fetch(db, key);
#else
      for(key = dbm_firstkey(db); key.dptr; key = dbm_nextkey(db))
      {
	data = dbm_fetch(db, key);
#endif
	if(val.dsize == data.dsize &&
	   bcmp(val.dptr, data.dptr, val.dsize) == 0)
	  break;
	NX_FREE(val.dptr);
	val.dptr = 0;
	val.dsize = 0;
      }
      
#ifdef GDBM
      gdbm_close(db);
#else
      dbm_close(db);
#endif
      
      if(key.dptr)
	break;
    }
  }

  NX_FREE(val.dptr);
  [list free];

  if(key.dptr)
  {
    char* tmp;

    NX_MALLOC(tmp, char, key.dsize);
    bcopy(key.dptr, tmp, key.dsize);
#ifdef GDBM
    free(key.dptr);
#endif
    key.dptr = tmp;
  }

  return key;
}

- (BOOL)storeForKey:(datum)key withBinder:(DBBinder*)binder
{
  int		i;
  int		count;
  List*		list;
  
  if(key.dptr == 0)
  {
    fprintf(stderr, "%s: Invalid key.\n", NAME);
    return NO;
  }

  list = [[List allocFromZone:[self zone]] init];
  count = [[binder getProperties:list] count];

  for(i = 0; i < count; i++)
  {
    datum data;
    id prop = [list objectAt:i];
    id value = [binder valueForProperty:prop];
    DBM_FILE db = [self openDBMFileForProperty:prop];
    
    if(db == 0)
    {
      fprintf(stderr, "%s: Couldn't open entity: %s.\n",
	      NAME, [(id<DBEntities>)[prop entity] expressionValue]);
      [list free];
      return NO;
    }
    
    [self convertDBValue:value toDatum:&data];

#ifdef GDBM
    if(gdbm_store(db, key, data, GDBM_REPLACE) != 0)
#else
    if(dbm_store(db, key, data, DBM_REPLACE) != 0)
#endif
      fprintf(stderr,
	      "%s: Cannot store data for key: %x.\n",
	      NAME, 
	      (int)NXSwapBigLongLongToHost(*(dbm_t*)key.dptr));
    
    NX_FREE(data.dptr);
#ifdef GDBM
    gdbm_close(db);
#else
    dbm_close(db);
#endif
  }

  [list free];
  return YES;
}

- (void)convertDBValue:(DBValue*)value toDatum:(datum*)data
{
  datum nullDatum = {0, 0};
  *data = nullDatum;
  
  if([value isNull])
    return;

  // Eeek!! This switch should be replaced by a dynamically bound method;
  switch([[value valueType] objcType][0])
  {
  case 'i':
    data->dptr = (char*)malloc(sizeof(int));
    *(int*)data->dptr = NXSwapHostIntToBig([value intValue]);
    data->dsize = sizeof(int);
    
    break;
  case 'f':
    data->dptr = (char*)malloc(sizeof(float));
    *(float*)data->dptr = NXSwapHostFloatToBig([value floatValue]);
    data->dsize = sizeof(float);
    
    break;
  case 'd':
    data->dptr = (char*)malloc(sizeof(double));
    *(double*)data->dptr = NXSwapHostDoubleToBig([value doubleValue]);
    data->dsize = sizeof(double);
    
    break;
  case '*':
  {
    const char* str = [value stringValue];
    
    data->dptr = (char*)malloc((str ? strlen(str) + 1 : 1)*sizeof(char));
    strcpy(data->dptr, str ? str : "");
    data->dsize = (strlen(data->dptr) + 1) * sizeof(char);
  }
    break;
  case '@':
  {
    int		maxLength;
    NXStream*	stream = NXOpenMemory(NULL, 0, NX_READWRITE);
    
    NXWriteObject(stream, [value objectValue]);
    fprintf(stderr, "Object: \"%s\"\n", [[value objectValue] name]);
    NXGetMemoryBuffer(stream, &data->dptr, &data->dsize, &maxLength);
    NXCloseMemory(stream, NX_FREEBUFFER);
  }
    break;
  default:
    break;
  }
}

- (dbm_t)get:(const char*)info for:entity 
{
  dbm_t arch_dep;
  datum key, data;

  key.dsize = strlen([entity expressionValue]) + strlen(info) + sizeof(char);
  NX_MALLOC(key.dptr, char, key.dsize);
  strcat(strcpy(key.dptr, [entity expressionValue]), info);
#ifdef GDBM
  data = gdbm_fetch(dbmInfo, key);
#else
  data = dbm_fetch(dbmInfo, key);
#endif
  NX_FREE(key.dptr);

  if(data.dptr == 0)
    return 0;

  arch_dep = NXSwapBigLongLongToHost(*(dbm_t*)data.dptr);
#ifdef GDBM
  free(data.dptr);
#endif;
  return arch_dep;
}

- set:(const char*)info for:entity with:(dbm_t)value
{
  datum key, data;
  dbm_t arch_dep = NXSwapHostLongLongToBig(value);

  key.dsize = strlen([entity expressionValue]) + strlen(info) + sizeof(char);
  NX_MALLOC(key.dptr, char, key.dsize);
  strcat(strcpy(key.dptr, [entity expressionValue]), info);
  data.dptr = (char*)&arch_dep;
  data.dsize = sizeof(dbm_t);
#ifdef GDBM
  gdbm_store(dbmInfo, key, data, GDBM_REPLACE);
#else
  dbm_store(dbmInfo, key, data, DBM_REPLACE);
#endif
  NX_FREE(key.dptr);

  return self;
}

@end
