// Wais.m
//
// Free software created 1 Feb 1992
// by Paul Burchard <burchard@math.utah.edu>.
// Incorporating:
/* 
   WIDE AREA INFORMATION SERVER SOFTWARE:
   No guarantees or restrictions.  See the readme file for the full standard
   disclaimer.

   This is part of the [NeXTstep] user-interface for the WAIS software.
   Do with it as you please.

   Version 0.82
   Wed Apr 24 1991

   jonathan@Think.COM

*/
//
// Here are the general utility hacks that didn't belong anywhere else...
// See Wais.h for more information.


#import "Wais.h"


// These global vars are needed by the WAIS library.
// We also use them for non-NeXTstep error messages.
FILE *logfile = stderr;
char *log_file_name = NULL;

// List of all Wais objects and their keys.
static id convertKeyToObject;
static id globalObjectList;

// Search path for objects---subclass should override.
static id globalFolderList;

// Global NeXTstep error message table for Wais classes.
static BOOL waisIsQuiet = NO;
static id waisStringTable = nil;

// Locks for thread collision protection.
#ifdef WAIS_THREAD_SUPPORT
static mutex_t waisTransactionMutex;
static mutex_t waisFileIOMutex;

// Port for callback to main thread.
port_t waisCallbackPort = PORT_NULL;
typedef struct
{
    msg_header_t hdr;
    msg_type_t type1;
    id callThis;
    msg_type_t type2;
    SEL performThis;
    msg_type_t type3;
    id withThis;
}
    _WaisCallback, *WaisCallback;
#endif



@implementation Wais



void waisCallbackHandler(WaisCallback msg, void *userData)
{
    [msg->callThis perform:msg->performThis with:msg->withThis];
    return;
}

+ initialize
{
    char *buf;
    const char *home, *folder;
    id folderList;
    
    if(self == [Wais class])
    {
    	// Grand unified indexed list of Wais objects.
	globalObjectList = [[List alloc] init];
	convertKeyToObject = [[HashTable alloc] initKeyDesc:"%" valueDesc:"@"];

#ifdef WAIS_THREAD_SUPPORT
	// Locks to prevent threads from colliding.
	waisTransactionMutex = mutex_alloc();
	waisFileIOMutex = mutex_alloc();

	// Callback port so other threads can ask main thread to
	// perform thread-unsafe actions.  This is done by registering
	// the port with the application; the application's main thread
	// picks up the requests via the AppKit event loop.  We give
	// the port a high priority since its cargo may be error messages.
	port_allocate(task_self(), &waisCallbackPort);
	DPSAddPort(waisCallbackPort, (DPSPortProc)waisCallbackHandler,
	    sizeof(_WaisCallback), NULL, NX_RUNMODALTHRESHOLD);
#endif
    }

    // Create subclass-specific list of folders in which
    // the Wais objects will be found.
    folderList = [[Storage alloc] initCount:0 
	elementSize:sizeof(NXAtom) description:"%"];
    if((home=NXHomeDirectory()) && [self defaultHomeFolder])
    {
	if(!(buf = s_malloc(strlen(home)
	    + strlen([self defaultHomeFolder]) + 1))) return nil;	    
	strcpy(buf, home);
	strcat(buf, [self defaultHomeFolder]);
	folder = NXUniqueString(buf);
	s_free(buf);
    }
    else folder = NXUniqueString("/");
    [folderList insert:(void *)&folder at:0];
    if(![self setFolderList:folderList]) return nil;
    return self;
}

+ folderList
{
    // Recommended subclass method.
    return globalFolderList;
}

+ setFolderList:aList
{
    // Recommended subclass method.
    if(![aList isKindOf:[Storage class]]) return nil;
    if([aList count] <= 0) return nil;
    if(globalFolderList) [globalFolderList free];
    globalFolderList = aList;
    return self;
}

+ (const char *)defaultHomeFolder
{
    // Recommended subclass method.
    return "/Library/WAIS";
}

- (const char *)valueForStringKey:(const char *)aKey
{
    if(!aKey) return 0;
    return (const char *)[infoFields valueForKey:(void *)aKey];
}

- (const char *)insertStringKey:(const char *)aKey value:(const char *)aValue
{
    const char *saveKey, *saveValue;
    
    if(!aKey) return 0;
    if(!aValue) { [infoFields removeKey:(void *)aKey]; return 0; }
    saveKey = NXUniqueString(aKey); saveValue = NXUniqueString(aValue);
    return (const char *)[infoFields
    	insertKey:(void *)saveKey value:(void *)saveValue];
}

+ waisObjectList
{
    return globalObjectList;
}

+ objectForCompleteKey:(const char *)aKey
{
    id found;
    BOOL quiet;

    // Is this a known, loaded object, of right type?
    if(aKey[0] != '/') return nil;
    if(found = (id)[convertKeyToObject valueForKey:(void *)aKey])
    {
    	if([found isKindOf:[self class]]) return found;
	else return nil;
    }
    
    // If unknown, try to load it as a file.
    // (Suppress error msgs---just testing.)
    found = [[[self class] alloc] initKey:aKey];
    quiet = [[self class] isQuiet]; [[self class] setQuiet:YES];
    if([found readWaisFile]) { [[self class] setQuiet:quiet]; return found; }
    [[self class] setQuiet:quiet]; [found free];
    return nil;
}

+ objectForKey:(const char *)aKey
{
    id found, folderList;
    const char **folders;
    char *fullKey;
    int f, len, nfold;
        
    // If complete key, just look up.
    if(!aKey) return nil;
    if(aKey[0] == '/')
    	return [[self class] objectForCompleteKey:aKey];
    
    // If partial key, try prepending all folders in folderList.
    if(!(folderList=[[self class] folderList])
    	|| (nfold=[folderList count])<=0) return nil;
    if(!(folders=(const char **)[folderList elementAt:0]))
    	return nil;
    for(f=0; f<nfold; f++)
    {
    	if(!folders[f]) continue;
	len = strlen(folders[f]);
	fullKey = s_malloc(len + strlen(aKey) + 2);
	strcpy(fullKey, folders[f]);
	if(fullKey[len-1] != '/') strcat(fullKey, "/");
	strcat(fullKey, aKey);
	if(found = [[self class] objectForCompleteKey:fullKey])
	    { s_free(fullKey); return found; }
	s_free(fullKey);	
    }
    
    // Not found.
    return nil;
}

- (const char *)key
{
    return key;
}

// Note: in index, this simply overwrites any previous object with same key.
// Is this what we want?

- setKey:(const char *)aKey
{
    id folderList;
    const char **folders;
    char *buf;
    int len;
    
    // Make sure it's in the list.
    [globalObjectList addObjectIfAbsent:self];
    
    // Remove object from index if new key is NULL.
    if(!aKey)
    {
	if(key && self==(id)[convertKeyToObject valueForKey:(void *)key])
	    [convertKeyToObject removeKey:key];
	key = 0;
	return self;
    }
   
    // If full key, unique it and index it (removing any old entry).
    if(aKey[0] == '/')
    {
	if(key && self==(id)[convertKeyToObject valueForKey:(void *)key])
	    [convertKeyToObject removeKey:key];
	key = NXUniqueString(aKey);
	[convertKeyToObject insertKey:(void *)key value:(void*)self];
	return self;
    }
    
    // If partial key, first create full key using default folder.
    if(!(folderList=[[self class] folderList]) || [folderList count]<=0)
    	return nil;
    if(!(folders=(const char **)[folderList elementAt:0]))
    	return nil;
    if(!folders[0]) return nil;
    len = strlen(folders[0]);
    buf = s_malloc(len + strlen(aKey) + 2);
    strcpy(buf, folders[0]);
    if(buf[len-1] != '/') strcat(buf, "/");
    strcat(buf, aKey);
    
    // Now index it (removing any old entry).
    if(key && self==(id)[convertKeyToObject valueForKey:(void *)key])
    	[convertKeyToObject removeKey:key];
    key = NXUniqueString(buf);
    s_free(buf);
    [convertKeyToObject insertKey:(void *)key value:(void*)self];
    return self;
}

- initKey:(const char *)aKey
{
    [super init];
    key = 0;
    [self setKey:aKey];
    infoFields = [[HashTable alloc] initKeyDesc:"%" valueDesc:"%"];
    return self;
}

- free
{
    // Remove self from indices.
    [convertKeyToObject removeKey:key];
    [globalObjectList removeObject:self];

    // Free instance vars (but don't free key---it's an NXAtom).
    [infoFields free];
    return [super free];
}

- (short)readWaisStruct:(const char *)structName
    forElement:(const char *)elementName
    fromFile:(FILE *)file
    withDecoder:(WaisDecoder)theDecoder
{
    short check_result;
    static char read_buf[READ_BUF_SIZE];
    char field_name[STRINGSIZE+MAX_SYMBOL_SIZE];
    WaisDecoder decoder;

    if(!structName || !theDecoder || !file) return FALSE;
    [Wais lockFileIO];
    if(feof(file)) { [Wais unlockFileIO]; return END_OF_STRUCT_OR_LIST; }
    check_result = CheckStartOfStruct(structName+1, file);
    if(check_result==FALSE || check_result==END_OF_STRUCT_OR_LIST)
    	{ [Wais unlockFileIO]; return check_result; }

    while((check_result=ReadSymbol(field_name, file, MAX_SYMBOL_SIZE))!=FALSE
    	&& check_result!=END_OF_STRUCT_OR_LIST
	&& !feof(file))
    {
    	for(decoder=theDecoder; decoder->name; decoder++)
	    if(0 == strcmp(field_name, decoder->name))
	    {
	    	switch(decoder->elementType)
		{
		case W_STRUCT:
		    [Wais unlockFileIO]; 
		    check_result = [self readWaisStruct:decoder->structName
		    	forElement:decoder->name fromFile:file 
		    	withDecoder:decoder->subDecoder];
		    if(check_result==FALSE
		    	|| check_result==END_OF_STRUCT_OR_LIST)
		    	return check_result;
		    [Wais lockFileIO]; 
		    break;
		
		case W_LIST:
		    if((check_result=ReadStartOfList(file))==FALSE
		    	|| check_result==END_OF_STRUCT_OR_LIST)
			{ [Wais unlockFileIO]; return check_result; }
		    [Wais unlockFileIO]; 
		    do check_result = [self 
			readWaisStruct:decoder->structName
			forElement:decoder->name fromFile:file
			withDecoder:decoder->subDecoder];
		    while(check_result!=FALSE
		    	&& check_result!=END_OF_STRUCT_OR_LIST);
		    if(check_result==FALSE) return check_result;
		    [Wais lockFileIO]; 
		    break;
		
		case W_FIELD:
		    if(!decoder->reader)
		    	{ SkipObject(file); break; }
		    switch(decoder->readArgs)
		    {
		    case 1:
			check_result = decoder->reader(file);
			break;
		    case 2:
			check_result = decoder->reader(read_buf, file);
			break;
		    case 3:
			check_result = decoder->reader(read_buf, file, 
			    MIN(READ_BUF_SIZE, decoder->maxBufSize));
			break;
		    default:
			[Wais unlockFileIO]; return FALSE;
		    }
		    if(check_result==FALSE
		    	|| check_result==END_OF_STRUCT_OR_LIST)
			{ [Wais unlockFileIO]; return check_result; }
		    if(decoder->readArgs > 1)
			[self insertStringKey:field_name value:read_buf];
		    break;
		
		default:
		    [Wais unlockFileIO]; return FALSE;
		}
	    break;
	    }
	if(!decoder->name) SkipObject(file);
    }
    [Wais unlockFileIO];
    return TRUE;
}

// Subclass method.
+ (const char *)fileStructName
{
    return ":null";
}

// Subclass method.
+ (WaisDecoder)fileStructDecoder
{
    return NULL;
}

- readWaisFile
{
    FILE *file;
    short check_result;
    
    // Try to open file specified by key.
    if(!key) return nil;
    [Wais lockFileIO];
    if(!(file = fopen(key, "r")))
    {
    	[Wais unlockFileIO];
	ErrorMsg([[self class] errorTitle], "Can't read %s file: %s.",
	    [[self class] fileStructName], key);
	return nil;
    }
    [Wais unlockFileIO];
    
    // Read using decoder.
    check_result = [self readWaisStruct:[[self class] fileStructName]
    	forElement:[[self class] fileStructName]
    	fromFile:file
	withDecoder:[[self class] fileStructDecoder]];
    if(check_result == FALSE)
    {
    	[Wais lockFileIO]; fclose(file); [Wais unlockFileIO]; 
	ErrorMsg([[self class] errorTitle], "Bad %s file format: %s.",
	    [[self class] fileStructName], key);
	return nil;
    }
    [Wais lockFileIO]; fclose(file); [Wais unlockFileIO]; 
    return self;
}

// Recommended subclass method.
+ (BOOL)checkFileName:(const char *)fileName
{
    return YES;
}

// User should free the returned List when done.
+ loadFolder:(const char *)folderName
{
    int len;
    id w, wlist;
    char filename[MAX_FILENAME_LEN];
    DIR *dirp;
    struct dirent *dp;
    
    // Open directory and loop through its files.
    if(!folderName) return nil;
    [Wais lockFileIO];
    if(!(dirp = opendir(folderName)))
    {
	[Wais unlockFileIO];
    	ErrorMsg([[self class] errorTitle],
	    "Can't open folder %s.", folderName);
	return nil;
    }
    wlist = [[List alloc] init];
    while(dp=readdir(dirp))
    {
    	// Check if file name has correct extension.
	if(![[self class] checkFileName:dp->d_name]) continue;
	    
	// If yes, then read it in and put into return list.
	strcpy(filename, folderName);
	len = strlen(filename);
	if(filename[len-1] != '/') strcat(filename, "/");
	strcat(filename, dp->d_name);
	w = [[[self class] alloc] initKey:filename];
	[Wais unlockFileIO];
	if([w readWaisFile]) [wlist addObject:w];
	else [w free];
	[Wais lockFileIO];	
    }
    closedir(dirp);
    [Wais unlockFileIO];
    if([wlist count] <= 0) { [wlist free]; return nil; }
    return wlist;
}

- (short)writeWaisStruct:(const char *)structName
    forElement:(const char *)elementName
    toFile:(FILE *)file
    withDecoder:(WaisDecoder)theDecoder
{
    short check_result;
    const char *field_value;
    WaisDecoder decoder;
    
    if(!structName || !theDecoder || !file) return NO;
    [Wais lockFileIO];
    WriteStartOfStruct(structName+1, file);
    WriteNewline(file);

    for(decoder=theDecoder; decoder->name; decoder++)
	switch(decoder->elementType)
	{
	case W_STRUCT:
	    WriteSymbol(decoder->name, file);
	    WriteNewline(file);
	    [Wais unlockFileIO];
	    check_result = [self writeWaisStruct:decoder->structName
		forElement:decoder->name toFile:file 
		withDecoder:decoder->subDecoder];
	    if(check_result==FALSE) return check_result;
	    [Wais lockFileIO];
	    break;
		
	case W_LIST:
	    WriteSymbol(decoder->name, file);
	    WriteNewline(file);
	    WriteStartOfList(file);
	    [Wais unlockFileIO];
	    do check_result = [self 
		writeWaisStruct:decoder->structName
		forElement:decoder->name toFile:file
		withDecoder:decoder->subDecoder];
	    while(check_result!=FALSE && check_result!=END_OF_STRUCT_OR_LIST);
	    if(check_result==FALSE) return check_result;
	    [Wais lockFileIO];
	    WriteNewline(file);
	    WriteEndOfList(file);
	    WriteNewline(file);
	    break;
		
	case W_FIELD:
	    // Don't save empty fields.
	    if(!(field_value=[self valueForStringKey:decoder->name])) break;
	    if(!field_value[0]) break;
	    if(!decoder->writer) break;
	    WriteSymbol(decoder->name, file);
	    switch(decoder->writeArgs)
	    {
	    case 1:
		check_result = decoder->writer(file);
		break;
	    case 2:
		check_result = decoder->writer(field_value, file);
		break;
	    default:
		[Wais unlockFileIO]; return FALSE;
	    }
	    //!!! The return values of the WriteXXX() routines from
	    //!!! the WAIS ir library cannot be trusted.  They actually
	    //!!! use return values from fprintf(), which are undefined!
	    check_result = TRUE;
	    
	    if(check_result==FALSE)
	    	{ [Wais unlockFileIO]; return check_result; }
	    WriteNewline(file);
	    break;

	default:
	    [Wais unlockFileIO]; return FALSE;
	}
	
    WriteEndOfStruct(file);
    WriteNewline(file);
    [Wais unlockFileIO];
    return TRUE;
}

- writeWaisFile
{
    FILE *file;
    short check_result;
    
    // Try to create file specified by key.
    if(!key) return nil;
    [Wais lockFileIO];
    if(!(file = fopen(key, "w")))
    {
	[Wais unlockFileIO];
    	ErrorMsg([[self class] errorTitle], "Can't create %s file %s.",
	    [[self class] fileStructName], key);
	return nil;
    }
    [Wais unlockFileIO];
    
    // Write using decoder.
    check_result = [self writeWaisStruct:[[self class] fileStructName]
    	forElement:[[self class] fileStructName]
    	toFile:file
	withDecoder:[[self class] fileStructDecoder]];
    if(check_result == FALSE)
    {
	[Wais lockFileIO]; fclose(file); [Wais unlockFileIO];
    	ErrorMsg([[self class] errorTitle], "Error writing %s file %s.",
	    [[self class] fileStructName], key);
	return nil;
    }
    [Wais lockFileIO]; fclose(file); [Wais unlockFileIO];
    return self;
}

+ lockTransaction
{
#ifdef WAIS_THREAD_SUPPORT
    mutex_lock(waisTransactionMutex);
#endif
    return self;
}

+ unlockTransaction
{
#ifdef WAIS_THREAD_SUPPORT
    mutex_unlock(waisTransactionMutex);
    cthread_yield();
#endif
    return self;
}

+ lockFileIO
{
#ifdef WAIS_THREAD_SUPPORT
    mutex_lock(waisFileIOMutex);
#endif
    return self;
}

+ unlockFileIO
{
#ifdef WAIS_THREAD_SUPPORT
    mutex_unlock(waisFileIOMutex);
    cthread_yield();
#endif
    return self;
}

+ waisNewLocks
{
    // Locks to prevent threads from colliding.
    //!!! We don't try to recycle old locks.
    waisTransactionMutex = mutex_alloc();
    waisFileIOMutex = mutex_alloc();
    return self;
}

+ callback:anObject perform:(SEL)aSelector with:anArgument
{
#ifdef WAIS_THREAD_SUPPORT
    _WaisCallback msg =
    {
    	// Note we are just sending object id's and method SEL's.
    	{ 0, 0, sizeof(_WaisCallback), 0, PORT_NULL, waisCallbackPort, 0},
	{ MSG_TYPE_INTEGER_32, 32, 1, TRUE, FALSE, FALSE },
	anObject,
	{ MSG_TYPE_INTEGER_32, 32, 1, TRUE, FALSE, FALSE },
	aSelector,
	{ MSG_TYPE_INTEGER_32, 32, 1, TRUE, FALSE, FALSE },
	anArgument
    };
    msg_send(&(msg.hdr), 0, 10.0/*timeout*/);
#else
    [anObject perform:aSelector with:anArgument];
#endif
    return self;
}

+ (port_t)callbackPort
{
    return waisCallbackPort;
}

+ setStringTable:aTable
{
    waisStringTable = aTable;
    return self;
}

+ setQuiet:(BOOL)yn
{
    waisIsQuiet = yn;
    return self;
}

+ (BOOL)isQuiet
{
    return waisIsQuiet;
}

+ (const char *)errorTitle
{
    // Recommended subclass method.
    return "WAIS Error!";
}

// For internal use only!  Use ErrorMsg() function instead.
+ popAlertPanel:errorBuf
{
    char *title, *errmsg;
    int title_len;
    
    if(!errorBuf) return nil;
    title = (char *)[errorBuf elementAt:0];
    title_len = strlen(title);
    errmsg = (char *)[errorBuf elementAt:title_len+1];
    NXRunAlertPanel(title, errmsg,
	[waisStringTable valueForStringKey:"OK"], NULL, NULL);
    [errorBuf free];
    return self;
}



@end



// -------- UTILITY ROUTINES FOR ERRORS AND WAIS FORMATTED I/O ----------

void ErrorMsg(const char *title, const char *format, ...)
{
    va_list args;
    id errorBuf;
    int title_len;
    
    if(waisIsQuiet) return;
    
    // Create buffer for passing error title/message to callback.
    errorBuf = [[Storage alloc] initCount:0 elementSize:sizeof(char)
	description:"c"];
    [errorBuf setNumSlots:CHARS_PER_PAGE];//!!!
    
    va_start(args, format);
    
    if(!errorBuf || !waisStringTable
    	|| ![waisStringTable valueForStringKey:format]
    	|| ![waisStringTable valueForStringKey:title])
    {
        // Can't use AppKit---enter message in log file instead.
	[errorBuf free];
	[Wais lockFileIO];
	fprintf(logfile, "%s---", title);
    	vfprintf(logfile, format, args);
	fprintf(logfile, "\n");
	fflush(logfile);
	[Wais unlockFileIO];
    }
    else
    {
    	// Pop up alert panel with AppKit.
	// Use callback since AppKit is not thread safe.
	// +popAlertPanel: will free errorBuf when it's done with it.
    	sprintf((char *)[errorBuf elementAt:0],
	    "%s", [waisStringTable valueForStringKey:title]);
	title_len = strlen((char *)[errorBuf elementAt:0]);
    	vsprintf((char *)[errorBuf elementAt:(title_len+1)],
	    [waisStringTable valueForStringKey:format], args);
	[Wais callback:[Wais class]
	    perform:@selector(popAlertPanel:) with:errorBuf];
    }
    va_end(args);
    return;
}


long ReadLongS(char *buffer, FILE *file)
{
    long val, rtn;
    
    rtn = ReadLong(file, &val);
    if(!rtn || rtn==END_OF_STRUCT_OR_LIST) return(rtn);
    sprintf(buffer, "%ld", val);
    return(rtn);
}

long WriteLongS(char *buffer, FILE *file)
{
    long val;
    
    if(1 != sscanf(buffer, " %ld ", &val)) return FALSE;
    WriteLong(val, file);
    return TRUE;//!!!
}

long ReadDoubleS(char *buffer, FILE *file)
{
    double val;
    long rtn;
    
    rtn = ReadDouble(file, &val);
    if(!rtn || rtn==END_OF_STRUCT_OR_LIST) return(rtn);
    sprintf(buffer, "%g", val);
    return(rtn);
}

long WriteDoubleS(char *buffer, FILE *file)
{
    double val;
    
    if(1 != sscanf(buffer, " %le ", &val)) return FALSE;
    WriteDouble(val, file);
    return TRUE;//!!!
}

long ReadListX(FILE *file)
{
    long rtn;
    
    // Ignore list.
    rtn = ReadStartOfList(file);
    if(!rtn) return(rtn);
    while(getc(file) != ')');
    return(rtn);
}

void read_subfield(const char *source, char *key, char *value, int value_size)
{
  char ch;
  long position = 0;  /* position in value */
  const char *pos =strstr(source, key); /* address into source */

  value[0] = '\0';		/* initialize to nothing */

  if(NULL == pos)
    return;

  pos = pos + strlen(key);
  ch = *pos;
  /* skip leading quotes and spaces */
  while((ch == '\"') || (ch == ' ')) {
    pos++; ch = *pos;
  }
  for(position = 0; pos < source + strlen(source); pos++){
    if((ch = *pos) == ' ') {
      value[position] = '\0';
      return;
    }
    value[position] = ch;
    position++;
    if(position >= value_size){
      value[value_size - 1] = '\0';
      return;
    }
  }
  value[position] = '\0';
}

/* right now this hacks out the ^Q/S too.  I'll do better later. --j */
void replace_controlM(char *buffer, long *length)
{
  char *here, *there, c;
  long i, newlength;

  here = there = buffer;
  for(newlength = 0, i = 0; i < *length; i++) {
    c = *here;
    switch (c) {
    case 0:
      *there = 0;
      *length = newlength;
      return;
    case '\r':
      *there = '\n';
      newlength++;
      here++; there++;
      break;
    case 19:
    case 17:
      here++;
      break;
    default:
      *there = *here;
      newlength++;
      here++; there++;
    }
  }
  *length = newlength;
}

any* copy_any(any *thing)
{
  int i;
  any* result;

  result = NULL;

  if(thing != NULL) {
    if((result = (any*)s_malloc(sizeof(any))) != NULL) {
      result->bytes = NULL;
      result->size = thing->size;
      if((result->bytes = s_malloc(thing->size)) != NULL) {
	for(i = 0; i < thing->size; i++)
	  result->bytes[i] = thing->bytes[i];
      }
    }
  }
  return result;
}


