/*
 * This is part of an example of a client application for EasyBTX.
 *
 * Written by: Max Boehm, 01/29/95
 *
 * You may freely copy, distribute and reuse the code in this example.  
 * Don't even talk to me about warranties.
 */


/*
 * The videotex dialog is controled by a DFA (deterministic finite automaton).
 *
 * A number of rules stored in a file are read by the openRules: method.
 * Empty lines and lines beginning with '#' are treated as comments.
 * All other lines are interpreted as rule lines.
 *
 * Each time when the videotex system expects a user input, the
 * method userInputAtRow:col: is called. Then a matching
 * rule for the current state, cursor position, and the expected strings
 * is searched. The first rule with a matching left hand side is executed,
 * i.e. the commands on the right hand side are execuded and the new state
 * is set.
 *
 * The syntax of a rule line is:
 *
 * <rule>:
 *	<state> [<row>,<col>] [<expect> ...] : <new_state> [<command> ...]
 * 
 * <expect>:
 *	expect(<row>,<col>,"<expect-string>")
 *	in(<width0>,<width1>,...)
 *
 * <command>:
 *	"<send-string>"
 *	out(<row>,<col>,<width>)
 *	out(<row>,<col>,<width>,<lines>)
 *	out("<message-string>")
 *	stop
 *
 * <state> is the current state-string (letters, digits, '_' only)
 * <row>, <col> is an expected cursor position of userInputAtRow:col:
 * (optional). <new_state> is the new state which is set after execution
 * of a matching rule.
 *
 *	expect(<row>,<col>,"<expect-string>")
 *
 * An expected string and its position. Such a command is supplied on the
 * pasteboard, when a selection is copied from the ASCII window. The string
 * can contain escape characters as described below.
 *
 *	in(<width0>,<width1>,...)
 *
 * For every given width value a line of the stream inputStream
 * is read. The lines are successively filled in the input variables
 * \0, \1, ... . The string widths are set according to the parameters.
 * Longer input lines are truncated, shorter input lines are padded with
 * spaces. The rule does not match if not all expected input variables
 * could be read.
 *
 *	"<send-string>"
 *
 * The string <send-string> is sent to the videotex system. The string can
 * contain escape characters \* (INI), \# (TER), \! (DCT), \h (home),
 * \b (back), \f (forward), \u (up), \d (down), \r (return), \\ (\),
 * \" ("), and \0..\9 for an input variable.
 *
 *	out(<row>,<col>,<width>)
 *	out(<row>,<col>,<width>,<lines>)
 *	out("<message-string>")
 *
 * The string at position <row>, <col> of length <width> of the videotex ASCII
 * output followed by a newline character is written to the stream
 * outputStream . If the lines parameter is given, that number of 
 * lines will be written to outputStream. In the third form
 * <message-string> is written to outputStream.
 *
 *	stop
 *
 * The message stopInState: with parameter <new_state> is sent to the
 * registered object (sender of openDialog: message). Normally the
 * receiver of the stopInState: method closes the videotex session
 * by sending the closeDialog message.
 * If the stopInState: method returns nil, the rest of the rule is ignored,
 * and state is not set to <new_state>. This should be done, when the
 * stopInState: method has changed the state or a has read a new set of rules.
 */

#import <ctype.h>
#import <remote/NXProxy.h>

#import "StateMachine.h"
#import "StringList.h"
#import "parse_rule.h"


#define DEBUG		/* force generation of debugging messages */


#define TIMEOUT 120	/* timeout in seconds for userInputAtRow:col: */



@implementation StateMachine


- init
{
    static char mem[10][41];
    int i;

    for (i=0; i<10; i++) {	/* initialize input variables \0..\9 */
	inputVariable[i]=mem[i];
	inputVariable[i][0]=0;
    }
    
    return self;
}


- (const char*) data
/* Returns a pointer to the ASCII data of the current page.
 * The character at row,col is data[24*(row-1)+(col-1)].
 * (24 rows x 40 columns)
 */
{
    return data;
}


- (const char*) stateString
{
    return state;
}


- setStateString:(const char*)theState
/* Sets the current state. */
{
    strncpy(state,theState,MAX_STATE_LEN-1);
    state[MAX_STATE_LEN-1]=0;

    return self;
}


- (NXStream*) inputStream
{
    return inputStream;
}


- setInputStream:(NXStream*)stream
/* Set inputStream used by the in() command. The input variables \0..\9
 * are cleared.
 */
{
    int i;

    inputStream=stream;		/* setup inputStream  */

    for (i=0; i<10; i++)		/* clear input variables */
	inputVariable[i][0]=0;

    return self;
}


- (NXStream*) outputStream
{
    return outputStream;
}


- setOutputStream:(NXStream*)stream
/* Set outputStream used by the out() command. */
{
    outputStream=stream;		/* setup outputStream  */
    return self;
}


- (const char*) inputVariableAt:(int)i
/* Returns input variable \i (i=0..9) */
{
    return inputVariable[i];
}


- setInputVariable:(const char*)str width:(int)width at:(int)i
/* The input variable \i is set to str formatted to width (width<=40)
 * characters (str is eventually truncated or padded with spaces).
 * Additionally a '\0' terminator is appended.
 */
{
    char *var=inputVariable[i];
    
    if (width>40)
	width=40;
    
    while (*str && width) {
	*var++=*str++;
	width--;
    }
    while (width--)
	*var++=' ';
    *var=0;
    return self;
}


/********************** setup rules in hash table **********************/

/*
 * Rules are stored in a hashtable with state strings beeing used for keys.
 * A state string is hashed on a string list containing all rules for that
 * state.
 */

- freeHashTable
/* frees the hash table and all strings and objects stored in it. */
{
    unsigned int count = 0;
    const   void  *key;
	    void  *value;
    NXHashState  hashState = [hash initState];
    while ([hash nextState: &hashState key: &key value: &value]) {
	free((char*)key);		/* free state string */
	[(id)value freeStrings];	/* free stringList */
	[(id)value free];
	count++;
    }
    [hash free];			/* free hash table itself */
    hash=nil;
    return self;
}


- insertState:(const char*)state_str rule:(const char*)rule_str
/* The string rule_str is appeded to the string list of state_str. */
{
    id stringList;

    stringList=[hash valueForKey:state_str];
    if (stringList==nil) {
	stringList=[[StringList alloc] init];
	[hash insertKey:NXCopyStringBuffer(state_str) value:stringList];
    }
    
    [stringList addString:rule_str];		/* makes a copy */
    return self;
}


- openRules:(const char*)filename
/* Reads the rules of the file filename. Any previous rules are freed.
 * Returns self on success, otherwise nil (filename cannot be mapped)
 */
{
    NXStream *s;
    
    s=NXMapFile(filename,NX_READONLY);
    if (s==NULL)
	return nil;

    if (hash!=nil)			/* free previous rules */
	[self freeHashTable];
    hash=[[HashTable alloc] initKeyDesc:"*"];

    while (!NXAtEOS(s)) {		/* read rule file */
	char line[1000], *l;
	char state_str[MAX_STATE_LEN];
	int  i;
	
	l=line;
	while (!NXAtEOS(s) && (i=NXGetc(s))!='\n' && i!=EOF) {
	    if (l<line+999)
		*l++=(char)i;
	}
	*l=0;
	
	l=line;
	while (isspace(*l))
	    l++;
	if (*l=='#' || *l==0)		/* comment or empty? */
	    continue;

	set_rule(line);			/* for get_state() */
	if (!get_state(state_str,MAX_STATE_LEN)) {
	    fprintf(stderr,"*** no state in rule: %s\n",line);
	    continue;
	}

	[self insertState:state_str rule:line];
    }
    NXCloseMemory(s,NX_FREEBUFFER);
    
    return self;
}


/*************************** State Machine ***************************/


- (BOOL) readInputVariableAt:(int)i width:(int)width
/* Reads one line of input from inputStream. At most with characters of the
 * input line (not including '\n') are written to the the input variable i. 
 * Then the variable is padded by spaces until width characters are written
 * and a '\0' terminator is appended.
 * Returns YES on succes, otherwise NO (end of file).
 */
{
    char str[41];
    int  c=EOF, j;
    
    j=0;
    while (!NXAtEOS(inputStream) && (c=NXGetc(inputStream))!='\n' && c!=EOF)
	if (j<40)
	    str[j++]=(char)c;
    str[j]=0;
    
    [self setInputVariable:str width:width at:i];

    return c!=EOF;
}


- outputString:(const char *)str length:(int)length
/* writes length bytes of str followed by '\n' to outputStream. */
{
    NXWrite(outputStream,str,length);
    NXPutc(outputStream,'\n');
    NXFlush(outputStream);
    return self;
}


- outputRow:(int)row col:(int)col width:(int)width lines:(int)lines
/* writes ascii data at row,col,width,lines to outputStream. */
{
    int r;
    
    for (r=row; r<row+lines; r++)
	[self outputString:data+(r-1)*40+(col-1) length:width];
    return self;
}


- (int) ruleMatchForRow:(int)row col:(int)col
/* The left hand side of the rule is parsed. If row, col, and all <expect>
 * commands match, this method returns YES. The method returns NO if the
 * rule does not match and SYNTAX_ERROR if the syntax of the rule is invalid.
 */
{
    char state_str[MAX_STATE_LEN];
    int  r, c;
    char expect[41];
    
    if (!get_state(state_str,MAX_STATE_LEN))	/* state */
	return SYNTAX_ERROR;

    if (get_row_col(&r,&c))			/* [row,col] */
	if (r!=row || c!=col)
	    return NO;

    while (!expect_char(':')) {			/* : */

	if (expect_string("expect(")) {		/* expect(row,col,"string") */
	    if (!get_row_col(&r,&c))
		return SYNTAX_ERROR;
	    if (!expect_char(','))
		return SYNTAX_ERROR;
	    if (!get_string(expect,41,inputVariable))
		return SYNTAX_ERROR;
	    if (!expect_char(')'))
		return SYNTAX_ERROR;
	    
	    if (strncmp(data+(r-1)*40+(c-1),expect,strlen(expect)))
		return NO;
	}
	
	else if (expect_string("in(")) { 	/* in(width0,width1,...) */
	    int i, width;
	    for (i=0; i<=9; i++) {
		if (!get_int(&width,1,40))
		    return SYNTAX_ERROR;
		if (![self readInputVariableAt:i width:width]) {
#ifdef DEBUG
		    fprintf(stderr,"*** end of input stream!\n");
#endif
		    return NO;			/* end of file */
		}
		if (!expect_char(','))
		    break;
	    }
	    if (!expect_char(')'))
		return SYNTAX_ERROR;
	}
	
	else
	    return SYNTAX_ERROR;
    }

    return YES;
}


- (int) executeRule
/* Executes the current rule and sets the new state.
 * Returns YES if successful, otherwise SYNTAX_ERROR.
 */
{
    char next_state[MAX_STATE_LEN];
    char str[80];

    if (!get_state(next_state,MAX_STATE_LEN))	/* get next state */
	return SYNTAX_ERROR;

    while (!expect_char('\0') && !expect_char('#')) {
	/* while not end of rule */
    
	if (get_string(str,80,inputVariable)) {	/* "send_string" */
#ifdef DEBUG
	    fprintf(stderr,"sendString:%s\n",str);
#endif
	    [server sendString:str];
	}
	
	else if (expect_string("out(")) {
	    int row, col, width, lines;
	    if (get_string(str,80,inputVariable)) {	/* out("string") */
		if (!expect_char(')'))
		    return SYNTAX_ERROR;
		[self outputString:str length:strlen(str)];
	    }
	    else if (get_row_col(&row,&col)) {	/* out(row,col,width) */
		if (!expect_char(','))
		    return SYNTAX_ERROR;
		if (!get_int(&width,1,41-col))
		    return SYNTAX_ERROR;
		if (expect_char(',')) {		/* out(row,col,width,lines) */
		    if (!get_int(&lines,1,25-row))
			return SYNTAX_ERROR;
		}
		else
		    lines=1;
		if (!expect_char(')'))
		    return SYNTAX_ERROR;
		[self outputRow:row col:col width:width lines:lines];
	    }
	}
	
	else if (expect_string("stop")) {	/* stop */
	    if ([stopReceiver stopInState:next_state]==nil)
		return YES;			/* abort rule */
	}
    
	else
	    return SYNTAX_ERROR;		/* unknown command */
    }
    
    [self setStateString:next_state];		/* set next state */
    
    return YES;
}


- timeout:(id)obj
/* userInputAtRow:col: message not received within TIMEOUT seconds.
 * The method stopInState: with parameter "timeout" is sent to the stopReceiver
 * object.
 */
{
#ifdef DEBUG
    fprintf(stderr,"*** timeout.\n");
#endif
    [stopReceiver stopInState:"timeout"];
    
    return self;
}


- (oneway void) userInputAtRow:(in int)row col:(in int)col
/* This method is called every time when the videotex system expects user
 * input. It is the heart of the state machine. row and col is the current
 * cursor position. The method searches for a matching rule in the current
 * state and cursor position and executes it. If this method is not sent
 * for TIMEOUT seconds, the timeout: method is invoked.
 */
{
    id	stringList;
    
    if (!server)
	return;

    [self perform:@selector(timeout:) with:nil afterDelay:TIMEOUT*1000
	cancelPrevious:YES];

    /* read 24 rows and 40 columns ASCII data from videotex screen */
    [server readString:&data row:1 col:1 length:24*40];
    
    /* search and execute matching rule */
    stringList=[hash valueForKey:state];
    if (stringList!=nil) {
	int i, count=[stringList count];
	
	for (i=0; i<count; i++) {
	    int ret;
	    const char *rule=[stringList stringAt:i];
	    
	    set_rule(rule);			/* set rule pointer */
    
	    ret=[self ruleMatchForRow:row col:col];
	    
	    if (ret==YES) {			/* rule matches */
#ifdef DEBUG
		fprintf(stderr,"execute rule: %s\n",rule);
#endif
		ret=[self executeRule];
		if (ret==YES)			/* successful */
		    break;
	    }
	    
	    if (ret==SYNTAX_ERROR)
		fprintf(stderr,"*** syntax error in rule: %s\n",rule);
	}
	
	if (i==count)
	    fprintf(stderr,"*** no matching rule for state %s\n",state);
    }
    else
	fprintf(stderr,"*** no rules for state %s.\n",state);
    
    free((char*)data);	/* free memory allocated by Distributed Objects */
}


/********************** Interface to EasyBTXServer **********************/


- server
{
    return server;
}


- stopReceiver
{
    return stopReceiver;
}


- setStopReceiver:(id <stopProtocol>)anObject
{
    stopReceiver=anObject;
    return self;
}


- connect
/* Connects to EasyBTXServer. Returns server on succes, otherwise nil */
{
    if (server==nil) {

	server=[NXConnection connectToName:"EasyBTXServer"];
	if (!server) {
	    NXPortFromName("EasyBTX",NULL);	/* launch EasyBTX.app */
	    usleep(1000000);
	    server=[NXConnection connectToName:"EasyBTXServer"];
	}
	
	if (server) {
	    NXConnection *conn;
	    [server setProtocolForProxy:@protocol(btxServerMethods)];

	    conn = [server connectionForProxy];
	    [conn registerForInvalidationNotification:self];
	    [conn runFromAppKit];
	}
#ifdef DEBUG
	else
	    fprintf(stderr,"*** can't connect to EasyBTXServer\n");
#endif
    }

    return server;
}


- senderIsInvalid:sender
/* This message is sent if connection is invalid. The method stopInState: with
 * parameter "invalid" is sent to the object stopReceiver.
 */
{
#ifdef DEBUG
    fprintf(stderr,"*** connection broken.\n");
#endif
    
    [sender free];
    server=nil;
    
    [stopReceiver stopInState:"invalid"];
    
    return self;
}


- (int)openDialog:sender
/* Connects to the EasyBTX server and dials out to the videotex system.
 * The rules for the dialog have to be read with openRules: before this
 * method is called.
 * When the server successfully connects to the videotex system, the dialog
 * described by the rules is performed. The initial state, inputStream, and
 * outputStream have to be set before this method is called.
 * The stopInState: message is sent to sender when the system reaches a stop
 * command, a timeout has happened while waiting for the userInputAtRow:col:
 * message, or the connection to the server is invalidated. The parameter of
 * the stopInState: command is new_state, "timeout", or "invalid".
 * The stopInState: should close the videotex dialog and hang up the modem by
 * sending the closeDialog message.
 * Return:  0 = Ok, Connection to videotex system successfully established
 *         -1 = IO-error (can't read/write on serial device)
 *         -2 = no or wrong echo when sending an AT command
 *         -3 = Unexpected answer from modem (not "OK" or "CONNECT")
 *         -4 = timeout while waiting for "CONNECT"
 *         -5 = lockfile in /usr/spool/uucp/LCK already exists
 *         -6 = can't open serial device
 *         -7 = connection already established (should be closed)
 *         -8 = can't connect to EasyBTXServer
 */
{
    int ret;

    if (server==nil)
	if ([self connect]==nil)
	    return -8;			/* can't connect to EasyBTXServer */
    
    [[server connectionForProxy] setInTimeout:80000];
    
    ret=[server openBTX];		/* dial out to videotex system */
    
    if (ret==0 || ret==-7) {
	[server setNotify:self];	/* send userInputAtRow:col: to self */
	[self perform:@selector(timeout:) with:nil afterDelay:TIMEOUT*1000
	    cancelPrevious:YES];

	[self setStopReceiver:sender];	/* receiver of stopInState: method */
    }
    
    return ret;
}


- closeDialog
/* should be sent from the stopInState: method to close the videotex dialog
 * and to hang up the modem.
 */
{
    /* cancel registration of timeout: message */
    [self perform:@selector(timeout:) with:nil afterDelay:-1
	cancelPrevious:YES];

    [self setStopReceiver:nil];
    [server setNotify:nil];
    [server closeBTX];			/* hang up */
    
    return self;
}


@end
