// WaisQuestion.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

*/
//

#import "WaisQuestion.h"

// Search path for questions.
static id questionFolderList;

// Maximum number of search results (unless instance's searchLimit is higher).
static int globalSearchLimit = SEARCH_LIMIT_DEFAULT;

// Error panel title.
static char *errorTitle = "WAIS Question Error!";

// Decoders for structured WAIS files.

_WaisDecoder waisRectDecoder[] = 
{
    { ":left",		W_FIELD,0,0,	ReadLongS,2,	WriteLongS,2 },
    { ":right",		W_FIELD,0,0,	ReadLongS,2,	WriteLongS,2 },
    { ":top",		W_FIELD,0,0,	ReadLongS,2,	WriteLongS,2 },
    { ":bottom",	W_FIELD,0,0,	ReadLongS,2,	WriteLongS,2 },
    { NULL }
};

_WaisDecoder waisQuestionDecoder[] = 
{
    { ":version",	W_FIELD,0,0,	ReadLongS,2,	WriteLongS,2 },
    { ":seed-words",	W_FIELD,0,0,	ReadString,3,	WriteString,2,
    					MAX_SYMBOL_SIZE },
    { ":relevant-documents",W_LIST,
    	":document-id",	waisDocumentIDDecoder },
    { ":sources",	W_LIST,
    	":source-id",	waisSourceIDDecoder },
    { ":result-documents",W_LIST,
    	":document-id",	waisDocumentIDDecoder },
    { ":view",		W_FIELD,0,0,	ReadLongS,2,	WriteLongS,2 },
    { ":window-size",	W_STRUCT,
    	":rect",	waisRectDecoder },
    { NULL }
};


@implementation WaisQuestion

+ folderList
{
    return questionFolderList;
}

+ setFolderList:aList
{
    questionFolderList = aList;
    return self;
}

+ (const char *)defaultHomeFolder
{
    return "/Library/WAIS/questions";
}

+ (const char *)fileStructName
{
    return ":question";
}

+ (WaisDecoder)fileStructDecoder
{
    return waisQuestionDecoder;
}

+ (const char *)errorTitle
{
    return errorTitle;
}

+ (BOOL)checkFileName:(const char *)fileName
{
    if(!fileName) return NO;
    if(strlen(fileName) <= strlen(W_Q_EXT)) return NO;
    if(!strstr(fileName, W_Q_EXT)) return NO;
    if(0 != strcmp(W_Q_EXT, strstr(fileName, W_Q_EXT))) return NO;
    return YES;
}

- initKey:(const char *)aKey
{
    [super initKey:aKey];
    sourceList = [[List alloc] init];
    relevantList = [[List alloc] init];
    resultList = [[List alloc] init];
    scoreList = [[Storage alloc] initCount:0 elementSize:sizeof(float) description:"f"];
    searchLimit = 1;
    listCounter = 0;
    return self;
}

- free
{
    [sourceList free];
    [relevantList free];
    [resultList free];
    [scoreList free];
    return [super free];
}

- (float)scoreForDocument:waisDocument
{
    int index;
    float *value;
    
    if(![waisDocument isKindOf:[WaisDocument class]]) return 0.0;
    if((index=[resultList indexOf:waisDocument]) == NX_NOT_IN_LIST) return 0.0;
    if(!(value=(float *)[scoreList elementAt:index])) return 0.0;
    return *value;
}

- setScoresFromResults
{
    int r, nresult;
    long max_score, this_score;
    float *score_array;
    const char *score;

    // Compute and normalize scores.
    nresult = [resultList count];
    [scoreList setNumSlots:nresult];
    score_array = (float *)[scoreList elementAt:0];
    if(!score_array) return nil;
    max_score = 0;
    for(r=0; r<nresult; r++)
    	if((score=[[resultList objectAt:r] valueForStringKey:":score"])
	    && max_score<(this_score=atol(score)))
	    max_score = this_score;
    if(max_score <= 0)
    {
    	[scoreList setNumSlots:0];
	ErrorMsg(errorTitle,
	    "Found no documents with positive matching scores.");
	return nil;
    }
    for(r=0; r<nresult; r++)
    	if(score=[[resultList objectAt:r] valueForStringKey:":score"])
    	    score_array[r] = ((float)atol(score))/((float)max_score);
	else score_array[r] = 0.0;
    return self;
}

- search
{
    int i, s, d, r, nrelevant, nsource, nretrieve, nresult;
    long request_buffer_length, limit, startat, endat;
    id source, doc;
    const char *database, *theType, *score;
    char buf[MAX_SYMBOL_SIZE];
    static char request[MAX_MESSAGE_LEN], response[MAX_MESSAGE_LEN];
    DocObj **Doco;
    DocID *docid;
    SearchResponseAPDU *interp_response;
    WAISDocumentHeader *header; 
    diagnosticRecord **diag;

    // Start results fresh.
    [resultList empty];
    [scoreList setNumSlots:0];

    // The DocObj list Doco formats the relevant documents for the WAIS lib.
    Doco = (DocObj**)s_malloc(([[self relevantList] count]+1)*sizeof(char*));
    nrelevant = [[self relevantList] count];
    for(d=0; d<nrelevant; d++)
    {
    	doc = [[self relevantList] objectAt:d];
	if((docid=[doc waisDocID]) && docid->originalLocalID)
	{
	    theType = [doc valueForStringKey:":type"];
	    if(!theType) theType = "TEXT";
	    if([doc valueForStringKey:":start"])
	    	startat = atol([doc valueForStringKey:":start"]);
	    else startat = (-1);
	    if([doc valueForStringKey:":end"])
	    	endat = atol([doc valueForStringKey:":end"]);
	    else endat = (-1);
	    if(startat >= 0) Doco[d] = makeDocObjUsingLines(
		anyFromDocID(docid), theType, startat, endat);
	    else Doco[d] = makeDocObjUsingWholeDocument(
	    	anyFromDocID(docid), theType);
	}
    }
    Doco[d] = NULL;

    // Any sources or key words?
    if([[self sourceList] count] <= 0)
    	{ ErrorMsg(errorTitle, "No sources to search."); return nil; }
    if(![self keywords])
    	{ ErrorMsg(errorTitle, "No key words for search."); return nil; }
	
    // Apportion search limit equally over the sources.
    nsource = [[self sourceList] count];
    limit = ((searchLimit>globalSearchLimit) ? searchLimit : globalSearchLimit)
    	/ nsource;

    // Loop over sources.
    for(s=0; s<nsource; s++)
    {
	// Make sure source is valid and connected.
    	source = [[self sourceList] objectAt:s];
	[source setConnected:YES];
	if(![source isConnected])
	{
	    ErrorMsg(errorTitle, "Can't connect to source %s.", [source key]);
	    continue;
	}
	if(!(database = [source valueForStringKey:":database-name"]))
	{
	    ErrorMsg(errorTitle, "Bad source %s.", [source key]);
	    continue;
	}
	
	// Lock transaction to prevent conflicts between threads.
	[Wais lockTransaction];

	// Create request message.
	request_buffer_length = [source bufferLength];
	if(!generate_search_apdu(request + HEADER_LENGTH,
	    &request_buffer_length, [self keywords], database, Doco, limit))
	{
	    [Wais unlockTransaction];
	    ErrorMsg(errorTitle,
	    	"Buffer overflow: request too large for source %s.",
		[source key]);
	    continue;
	}
	
	// Send request message.
	if(!interpret_message(request, MAX_MESSAGE_LEN - request_buffer_length, 
	    response, MAX_MESSAGE_LEN, [source connection], false))
	{
	    [Wais unlockTransaction];
	    ErrorMsg(errorTitle, 
	    	"Warning: no information returned from source %s.",
		[source key]);
	    continue;
	}
	
	// Decode search response.
	// Transaction is done; unlock.
	if(!readSearchResponseAPDU(&interp_response, response + HEADER_LENGTH))
	{
	    [Wais unlockTransaction];
	    ErrorMsg(errorTitle, 
	    	"Source %s returned bad search response.",
		[source key]);
	    continue;
	}
	[Wais unlockTransaction];
	if(interp_response
	    && (WAISSearchResponse *)interp_response
	    	->DatabaseDiagnosticRecords 
	    && (diag = ((WAISSearchResponse *)interp_response
	    	->DatabaseDiagnosticRecords)->Diagnostics)
	    )
	    for(i=0; diag[i]; i++) if(diag[i]->ADDINFO)
	    	ErrorMsg(errorTitle, "Search diagnostics: %s, %s",
		    diag[i]->DIAG, diag[i]->ADDINFO);

	// Build sorted result list with info in DocHeaders of search response.
	nretrieve = interp_response->NumberOfRecordsReturned;
	if(nretrieve==0
	    || !interp_response->DatabaseDiagnosticRecords
	    || !((WAISSearchResponse*)interp_response
	    	->DatabaseDiagnosticRecords)->DocHeaders)
		    continue;
	for(d=0; d<nretrieve; d++) if(header=((WAISSearchResponse*)
	    interp_response->DatabaseDiagnosticRecords)->DocHeaders[d])
	{
	    // Create Wais document.
	    //!!! Note that this will mask out any previous doc by this name.
	    //!!! Should select type based on prefs.
	    if(header->Types) theType = header->Types[0];
	    else theType = NULL;
	    if(!(doc = [[WaisDocument alloc] initKey:NULL])
	    	|| ![doc setFromSource:source]
		|| ![doc setWaisDocIDFromAny:header->DocumentID])
	    {
	    	ErrorMsg(errorTitle, "Can't form document for headline %s.",
		    header->Headline);
		continue;
	    }
	    [doc insertStringKey:":headline" value:header->Headline];
	    [doc insertStringKey:":type" value:theType];
	    if(![doc setKeyFromInfo])
	    {
	    	ErrorMsg(errorTitle, "Can't form document for headline %s.",
		    header->Headline);
		continue;
	    }

	    // Place in result list...keep raw scores descending.
	    nresult = [resultList count];
	    for(r=0; r<nresult; r++) if(!(score=[[resultList objectAt:r]
		valueForStringKey:":score"]) || header->Score>atol(score))
		break;
	    [resultList insertObject:doc at:r];
	    
	    // Fill in other info fields from header.
	    // We ignore header->Source.
	    if(header->OriginCity)
	    	[doc insertStringKey:":origin-city" value:header->OriginCity];
	    sprintf(buf, "%d", header->Score);
	    [doc insertStringKey:":score" value:buf];
	    sprintf(buf, "%ld", header->Lines);
	    [doc insertStringKey:":number-of-lines" value:buf];
	    sprintf(buf, "%ld", header->DocumentLength);
	    [doc insertStringKey:":number-of-bytes" value:buf];
	    sprintf(buf, "%ld", header->BestMatch);
	    [doc insertStringKey:":best-line" value:buf];
	    [doc insertStringKey:":start" value:"-1"];
	    [doc insertStringKey:":end" value:"-1"];
	}
    }
    
    // Compute and normalize scores.
    if([resultList count] <= 0)
    {
	ErrorMsg(errorTitle, "Found no documents matching the question.");
	return nil;
    }
    if(![self setScoresFromResults])
    {
    	[resultList empty]; [scoreList setNumSlots:0];
	ErrorMsg(errorTitle, "Bad document scores.");
	return nil;
    }
    return resultList;
}

- resultList
{
    return resultList;
}

- (const char *)keywords
{
    return [self valueForStringKey:":seed-words"];
}

- setKeywords:(const char *)theText
{
    [self insertStringKey:":seed-words" value:theText];
    return self;
}

- addSource:waisSource
{
    if(![waisSource isKindOf:[WaisSource class]]) return nil;
    return [sourceList addObjectIfAbsent:waisSource];
}

- removeSource:waisSource
{
    return [sourceList removeObject:waisSource];
}

- clearSources
{
    [sourceList empty];
    return self;
}

- sourceList
{
    return sourceList;
}

- addRelevantDocument:waisDocument
{
    if(![waisDocument isKindOf:[WaisDocument class]]) return nil;
    return [relevantList addObjectIfAbsent:waisDocument];
}

- removeRelevantDocument:waisDocument
{
    return [relevantList removeObject:waisDocument];
}

- clearRelevantDocuments
{
    [relevantList empty];
    return self;
}

- relevantList
{
    return relevantList;
}

+ setSearchLimit:(int)maxDocs
{
    if(maxDocs <= 0) return nil;
    globalSearchLimit = maxDocs;
    return self;
}

+ (int)searchLimit
{
    return globalSearchLimit;
}

- setSearchLimit:(int)maxDocs
{
    if(maxDocs <= 0) return nil;
    searchLimit = maxDocs;
    return self;
}

- (int)searchLimit
{
    return searchLimit;
}

- (short)readWaisStruct:(const char *)structName
    forElement:(const char *)elementName
    fromFile:(FILE *)file
    withDecoder:(WaisDecoder)theDecoder
{
    id subObj, inList, origObj;
    short check_result;
    
    // Check if need additional subobject in a list to capture new data.
    subObj = nil;
    if(0 == strcmp(elementName, ":relevant-documents"))
    	{ inList = relevantList; subObj = [[WaisDocument alloc] initKey:NULL];}
    else if(0 == strcmp(elementName, ":result-documents"))
    	{ inList = resultList; subObj = [[WaisDocument alloc] initKey:NULL];}
    else if(0 == strcmp(elementName, ":sources"))
    	{ inList = sourceList; subObj = [[WaisSource alloc] initKey:NULL];}
    else
    {
    	// If not, and done reading full question record, update score list.
    	check_result = [super readWaisStruct:structName
	    forElement:elementName fromFile:file withDecoder:theDecoder];
	if(check_result == FALSE) return check_result;
	if(![self setScoresFromResults]) return FALSE;
	return check_result;
    }
    
    // Read into subobject.
    if(!subObj || !inList) return FALSE;
    check_result = [subObj readWaisStruct:structName
	forElement:elementName fromFile:file withDecoder:theDecoder];
    if(check_result==FALSE || check_result==END_OF_STRUCT_OR_LIST)
	{ [subObj free]; return check_result; }
    if(inList == sourceList)
    {
    	// Convert source-id's into sources.
    	origObj = subObj;
	if(!(subObj = [WaisSource objectForKey:[origObj
	    valueForStringKey:":filename"]]))
	{
	    ErrorMsg(errorTitle, "Unknown source %s.",
	    	[origObj valueForStringKey:":filename"]);
	    [origObj free]; return TRUE;
	}
	[origObj free];
    }
    [inList addObject:subObj];
    return TRUE;
}

- (short)writeWaisStruct:(const char *)structName
    forElement:(const char *)elementName
    toFile:(FILE *)file
    withDecoder:(WaisDecoder)theDecoder
{
    id subObj;
    short check_result;
    
    // Check if need next subobject in a list to extract data from.
    // The listCounter keeps track of where we are in list.
    subObj = nil;
    if(0 == strcmp(elementName, ":relevant-documents"))
    	subObj = [relevantList objectAt:listCounter];
    else if(0 == strcmp(elementName, ":result-documents"))
    	subObj = [resultList objectAt:listCounter];
    else if(0 == strcmp(elementName, ":sources"))
    	subObj = [sourceList objectAt:listCounter];
    else
    {
    	listCounter = 0;
    	return [super writeWaisStruct:structName
	    forElement:elementName toFile:file withDecoder:theDecoder];
    }
    
    // Write from subobject, incrementing or clearing listCounter as needed.
    if(!subObj) { listCounter = 0; return END_OF_STRUCT_OR_LIST; }
    else listCounter++;
    check_result = [subObj writeWaisStruct:structName
	forElement:elementName toFile:file withDecoder:theDecoder];
    if(check_result==FALSE || check_result==END_OF_STRUCT_OR_LIST)
	listCounter = 0;
    return check_result;
}

- writeWaisFile
{
    // Fill in missing fields.
    [self insertStringKey:":version" value:WAIS_PROTOCOL_VERSION];
    return [super writeWaisFile];
}


@end



