// -------------------------------------------------------------------------------------
// ReadConfig.m
// Martin D. Flynn, NeXT Computer, Inc.
// Read config file and send methods to target
// Note: See the header file for usage information and examples.
// -------------------------------------------------------------------------------------
// Permission is granted to freely redistribute this source code, and to use fragments
// of this code in your own applications if you find them to be useful.  This class,
// along with the source code, come with no warranty of any kind, and the user assumes
// all responsibility for its use.
// -------------------------------------------------------------------------------------

#import <stdlib.h>
#import <stdio.h>
#import <string.h>
#import <libc.h>
#import <sys/time.h>
#import <objc/objc-runtime.h>
#import <objc/Storage.h>
#import <defaults/defaults.h>
#import <appkit/Panel.h>
#import <appkit/Application.h>
#import "ReadConfig.h"

// -------------------------------------------------------------------------------------
extern char *getwd(char *path);

// -------------------------------------------------------------------------------------
// misc defines
#define maxLEN              512         // max allowable record length
#define prefixMethodNAME    "*cfg"      // internal method prefix "cfg_<methName>"
#define isCOMMENT(T)        ((*T == ' ') || (*T == '\t') || (*T == '\n') || (*T == '/'))
#define parseFORMAT         "%s%[^\n]"

// -------------------------------------------------------------------------------------
// internal config private method definition
@interface ReadConfig(_Private)
- profileElapsedTime:(float)elapsedTime;
- configErrorTrap:(int)errCode inFile:(char*)fileName atLine:(int)lineNbr;
- configWillPerform:(SEL)selector with:anArg;
- setTarget:targetObject;
- (int)readConfigFile:(char*)file;
@end

// -------------------------------------------------------------------------------------
// internal config private method definition
@interface ReadConfig(_InternalMethod)
- (int)cfg_globalMethodTrace:(BOOL)yesNo;
- (int)cfg_globalPrintError:(BOOL)yesNo;
- (int)cfg_globalCheckReturn:(BOOL)yesNo;
- (int)cfg_methodTrace:(BOOL)yesNo;
- (int)cfg_printError:(BOOL)yesNo;
- (int)cfg_checkReturn:(BOOL)yesNo;
- (int)cfg_terminateOnError:(BOOL)yesNo;
- (int)cfg_print:(char*)text;
- (int)cfg_goto:(char*)label;
- (int)cfg_ifTrue:(char*)label;
- (int)cfg_ifFalse:(char*)label;
- (int)cfg_terminate;
- (int)cfg_alertPanel:(char*)message;
- (int)cfg_abortPanel:(char*)message;
- (int)cfg_optionPanel:(char*)message;
- (int)cfg_ifOptionA:(char*)label;
- (int)cfg_ifOptionB:(char*)label;
- (int)cfg_endModalPanel;
- (int)cfg_infoPanel:(char*)message;
- (int)cfg_endInfoPanel;
- (int)cfg_stopPanel:(char*)message;
- (int)cfg_endStopPanel;
- (int)cfg_ifStop:(char*)label;
@end

// -------------------------------------------------------------------------------------
@implementation ReadConfig

// -------------------------------------------------------------------------------------
// global defaults
static BOOL             dftExitOnError      = YES;
static BOOL             dftPrintError       = YES;
static BOOL             dftMethodTrace      = NO;
static BOOL             dftCheckAll         = YES;
static BOOL             dftBackupOnWrite    = NO;

// -------------------------------------------------------------------------------------
// line/level data on last call to ReadConfig
static int              globalLevel = 0;
static int              globalLine  = 0;

// -------------------------------------------------------------------------------------
// global error trap
static id               errorTrap = (id)nil;    // error trap target

// -------------------------------------------------------------------------------------
// time lapse profile variables
static double           profileStart = -1.0;    // profile start time
static int              profileCount = 0;       // profile 'ping' count
static float            profileLapse = 0.0;     // latched profile lapse time
static BOOL             profileActive = NO;
static id               profileTarget = (id)nil;
#define profilePING     @selector(profileElapsedTime:)

// -------------------------------------------------------------------------------------
// constant data
static char             *errorDesc[] = {         // error descriptions
                          "Successful",
                          "Unable to open config file",
                          "Invalid number of configuration parameters",
                          "Selector not found for method",
                          "Method returned TRUE error condition",
                          "Goto label not found",
                          "Invalid error code specified",
                          "Target does not respond to method",
                          0
                        };

// -------------------------------------------------------------------------------------
// class initialization
+ initialize
{
  errorTrap = self;
  return self;
}

// -------------------------------------------------------------------------------------
// Return the full path to the current application
// Note: the application name is removed from the path
+ (char*)appPath
{
  int           i;
  static char   *appPathName = (char*)nil;
  if (!appPathName) {
    for (i = strlen(NXArgv[0]) - 1; (i >= 0) && (NXArgv[0][i] != '/'); i--);
    appPathName = (char*)malloc(i + 2);
    strncpy(appPathName, NXArgv[0], i + 1);
    appPathName[i + 1] = 0;
    if (NXArgv[0][0] != '/') {
      char path[1024], *aPtr;
      aPtr = (char*)malloc(strlen(getwd(path)) + strlen(appPathName) + 2);
      sprintf(aPtr, "%s/%s", path, appPathName);
      free(appPathName);
      appPathName = aPtr;
    }
  }
  return appPathName;
}

// -------------------------------------------------------------------------------------
// Build a fully qualified path to the specified file name

/* build full path to specified file name and extension */
+ (char*)configPath:(const char*)fileName
{
  char  *fullPath = (char*)malloc(strlen([self appPath]) + strlen(fileName) + 1);
  sprintf(fullPath, "%s%s", [self appPath], fileName);
  return fullPath;
}

// -------------------------------------------------------------------------------------
// Return status info
// Note:
//   - These functions may be used to get information about configuration file reads
//     when an error condition has occurred.

/* return current line number (usually used in error conditions) */
+ (int)lineNumber
{
  return globalLine;
}

/* return current level number (usually used in error conditions) */
+ (int)levelNumber
{
  return globalLevel;
}

/* return error description text for specified error code */
+ (char*)errorDescription:(int)errorCode
{
  if ((errorCode < 0) || (errorCode > cfgLAST_ERROR)) errorCode = cfgINVALID_CODE;
  return errorDesc[errorCode];
}

// -------------------------------------------------------------------------------------
// Set global error trap target object
// Note:
//   - The 'setErrorTrap:' may be used to set the target object that will be notified
//     when error conditions occur during a readConfigFile: call.
//   - The method that will be called in the target object will be the following:
//          configErrorTrap:(int) inFile:(char*) atLine:(int)
//     See definition below for more information.

/* set trap to specified target */
+ setErrorTrap:trapId
{
  if (![trapId respondsTo:@selector(configErrorTrap:inFile:atLine:)])
    return (id)nil;
  errorTrap = trapId;
  return self;
}

/* example error trap method */
//  errCode  - the error condition that occurred
//  fileName - the current configuration file name that had the error
//  lineNbr  - the line number that caused the error
+ configErrorTrap:(int)errCode inFile:(char*)fileName atLine:(int)lineNbr
{
  
  char  *temp = fileName? rindex(fileName, '/') : (char*)nil, *fName = temp? temp : fileName;
  char  errMsg[256], *title = "ReadConfig Error", *fmt = "File %s (line %d)\n%s";
  
  /* build error message */
  sprintf(errMsg, fmt, fName, lineNbr, [ReadConfig errorDescription:errCode]);

  /* defautl alert panel */
  NXRunAlertPanel(title, errMsg, "Quit", (char*)nil, (char*)nil);
  exit(-1);

  return (id)nil;
}

/* default error trap method */
- configErrorTrap:(int)errCode inFile:(char*)fileName atLine:(int)lineNbr
{
  return [ReadConfig configErrorTrap:errCode inFile:fileName atLine:lineNbr];
}

// -------------------------------------------------------------------------------------
// Set global defaults
// Notes:
//   - These methods may be used to set global defaults that will be in effect for all
//     configuration files read.

/* set global default Terminate-On-Error flag */
+ setTerminateOnError:(BOOL)yesNo
{
  dftExitOnError = yesNo;
  return NO;
}

/* set global default error print mode */
+ setPrintError:(BOOL)yesNo
{
  dftPrintError = yesNo;
  return NO;
}

/* set global default return status checking mode */
+ setCheckReturn:(BOOL)yesNo
{
  dftCheckAll = yesNo;
  return NO;
}

/* set global default method trace mode */
+ (int)setMethodTrace:(BOOL)yesNo
{
  dftMethodTrace = yesNo;
  return NO;
}

/* set global default backup-on-write flag */
+ setBackupOnWrite:(BOOL)yesNo
{
  dftBackupOnWrite = yesNo;
  return NO;
}

// -------------------------------------------------------------------------------------
// read specified config file
// -------------------------------------------------------------------------------------
#define         breakERROR(err)         { error = err; break; }

/* open/read config file */
+ (int)readConfigFile:(char*)file target:cfgTarget
{
  int           rtnError;
  id            mySelf = [[self alloc] init];
  [mySelf setTarget:cfgTarget];
  rtnError = [mySelf readConfigFile:file];
  [mySelf free];
  return rtnError;
}

/* open/read config file relative to application directory */
+ (int)readAppConfig:(char*)file target:cfgTarget
{
  char      *fileName = [self configPath:file];
  int       error = [self readConfigFile:fileName target:cfgTarget];
  free(fileName);
  return error;
}

// -------------------------------------------------------------------------------------
// Update config file methods with new parameters
// Notes:
//   - These methods allow for updating existing config-files method with new parameters.
//   - If duplicate method name occur in the config file, they may be made unique for
//     purposes of updating by placing a '$' followed by a character after the method name.
//          ie. setParmValue:$0     parms
//              setParmValue:$1     parms
//     The '$' and character will be stripped prior to sending the method to the target.
//   - Search for the desired method by including the '$' and character if necessary.
// -------------------------------------------------------------------------------------

/* return Storage object containing entire specified config-file */
+ newConfigStorage:(char*)fileName
{
  FILE          *fNum;
  char          txt[maxLEN + 1];
  char          *cPtr;
  id            storage;
  
  /* open file */
  fNum = fopen(fileName, "r");
  if (!fNum) return (id)nil;
  
  /* create storage */
  storage = [[Storage alloc] initCount:10 elementSize:sizeof(char*) description:(char*)nil];
  [storage empty];
  
  /* fill storage */
  for (*txt = 0; fgets(txt, maxLEN, fNum);) {
    cPtr = NXCopyStringBuffer(txt);
    [storage addElement:&cPtr];
  }
    
  /* close file */
  fclose(fNum);
  return storage;
  
}

/* read config file (relative to app) into storage class */
+ newAppConfigStorage:(char*)file
{
  char          *fileName = [self configPath:file];
  id            storage = [self newConfigStorage:fileName];
  free(fileName);
  return storage;
}

/* return index for matching string in Config-Storage object (return -1 if not found) */
+ (int)findIndexForMethodName:(char*)methName inCfgStorage:storage
{
  char          *cPtr;
  int           i, methLen = strlen(methName);
  for (i = 0; i < [storage count]; i++) {
    cPtr = *((char**)[storage elementAt:i]);
    if (!strncmp(cPtr, methName, methLen)) return i;
  }
  return -1;
}
  
/* replace specified record with corresponding record in Config-Storage object */
+ replaceRecord:(char*)rcd inCfgStorage:storage
{
  int           ndx, len;
  char          *cPtr, *nPtr, *fmt, name[maxLEN];
  
  /* find occurrance of record */
  sscanf(rcd, "%s", name);
  ndx = [self findIndexForMethodName:name inCfgStorage:storage];
  if (ndx < 0) return (id)nil;

  /* make a static copy of the record */
  len = strlen(rcd);
  if (rcd[len - 1] != '\n') { len++; fmt = "%s\n"; } else fmt = "%s";
  nPtr = (char*)malloc(len + 1);
  sprintf(nPtr, fmt, rcd);
  
  /* index found, replace record */
  cPtr = *((char**)[storage elementAt:ndx]);
  [storage replaceElementAt:ndx with:&nPtr];
  free(cPtr);
  
  return self;
}

/* write Config-Storage object to specified file (return nil if error) */
+ writeConfigStorage:storage toFile:(char*)fileName
{
  int           i;
  char          *cPtr;
  FILE          *fNum;

  /* check for backup (bkuFile will first be removed if it exists) */
  if (dftBackupOnWrite) {
    char bkuFile[MAXPATHLEN + 1];
    sprintf(bkuFile, "%s~", fileName);
    if (rename(fileName, bkuFile))
      fprintf(stderr, "ReadConfig: Unable to rename to %s\n", bkuFile);
  }

  /* open output file */
  fNum = fopen(fileName, "w");
  if (!fNum) return (id)nil;
  
  /* write data to file */
  for (i = 0; i < [storage count]; i++) {
    cPtr = *((char**)[storage elementAt:i]);
    fprintf(fNum, "%s", cPtr);
  }
    
  /* close file and return */
  fclose(fNum);
  return self;
  
}

/* write config file to file (relative to app) */
+ writeAppConfigStorage:storage toFile:(char*)file
{
  char          *fileName = [self configPath:file];
  id            obj = [self writeConfigStorage:storage toFile:fileName];
  free(fileName);
  return obj;
}

/* free Config-Storage object */
+ freeConfigStorage:storage
{
  int           i;
  for (i = 0; i < [storage count]; i++) free(*((char**)[storage elementAt:i]));
  [storage free];
  return self;
}

/* print contents of Config-Storage object */
+ printConfigStorage:storage
{
  int           i;
  for (i = 0; i < [storage count]; i++)
    printf("%3d: %s", i, *((char**)[storage elementAt:i]));
  return self;
}

// -------------------------------------------------------------------------------------
// parse and execute single config record
// -------------------------------------------------------------------------------------

+ (int)sendConfigMethod:(char*)txt toTarget:theTarget
{
  int           rtn;                        // method call record value
  int           parms;                      // number of parsed parms
  char          methName[maxLEN + 1];       // max method name length
  char          parmString[maxLEN + 1];     // max parm length
  char          *pPtr, *dPtr;               // char pointers
  SEL           selector;                   // method selector

  /* skip record if comment */
  if (isCOMMENT(txt) || (*txt==':') || (*txt=='*') || (*txt=='!')) return cfgSINGLE_MODE;

  /* parse record */
  parms = sscanf(txt, parseFORMAT, methName, parmString);
  if (parms < 1) return cfgINVALID_PARMS;

  /* convert name to selector */
  if (dPtr = index(methName, '$')) *dPtr = 0;
  selector = sel_getUid(methName);
  if (!sel_isMapped(selector)) return cfgNO_METHOD;
      
  /* parse additional parameter */
  if (parms < 2) dPtr = pPtr = (char*)nil;
  else {
    for (pPtr = parmString; *pPtr && ((*pPtr == ' ') || (*pPtr == '\t')); pPtr++);
    dPtr = pPtr;
    if (*pPtr == '*') {
      if (!strcmp(pPtr, "*NIL") || !strcmp(pPtr, "*nil")) pPtr = (char*)nil;        else
      if (!strcmp(pPtr, "*NO" ) || !strcmp(pPtr, "*no" )) pPtr = (char*)((int)NO ); else
      if (!strcmp(pPtr, "*YES") || !strcmp(pPtr, "*yes")) pPtr = (char*)((int)YES); else
      if (!strcmp(pPtr, "*ID" ) || !strcmp(pPtr, "*id") ) pPtr = (char*)nil;
    }
  }

  /* perform method */
  if (parms == 2) rtn = (int)[theTarget perform:selector with:(id)pPtr];
  else            rtn = (int)[theTarget perform:selector];
      
  /* check return status */
  return (rtn)? cfgMETHOD_ERROR : cfgOK;
  
}

// -------------------------------------------------------------------------------------
// profile / time-lapse methods
// -------------------------------------------------------------------------------------

static double currentTimeStamp()
{
  struct timeval    tp;
  struct timezone   tzp;
  gettimeofday(&tp, &tzp);
  return (double)tp.tv_sec + (double)tp.tv_usec / 1000000.00;
}

/* return time lapse from last startProfile */
+ (float)profileElapsedTime
{
  return (profileActive)? (float)(currentTimeStamp() - profileStart) : profileLapse;
}

/* return number of executed ReadConfig methods */
+ (int)profileCount
{
  return profileCount;
}

/* set profiler target */
+ setProfileTarget:profile
{
  if ([profile respondsTo:profilePING]) profileTarget = profile;
  return self;
}

/* start profiler */
+ startProfile
{
  if (!profileTarget) [self setProfileTarget:NXApp];    // default profile target
  profileStart = currentTimeStamp();
  profileCount = 0;
  profileActive = YES;
  return self;
}

/* stop profile */
+ stopProfile
{
  profileLapse = (float)(currentTimeStamp() - profileStart);
  profileActive = NO;
  return self;
}

// -------------------------------------------------------------------------------------
// read specified config file
// Notes:
//   - These methods are for INTERNAL USE ONLY and should not be called directly.
// -------------------------------------------------------------------------------------

/* initialize the internal ReadConfig instance */
- init
{
  [super init];
  level         = 0;
  modalPanel    = (id)nil;
  methodRtn     = 0;
  *findLabel    = 0;
  [self cfg_methodTrace:dftMethodTrace];
  [self cfg_printError:dftPrintError];
  [self cfg_checkReturn:dftCheckAll];
  [self cfg_terminateOnError:dftExitOnError];
  return self;
}

/* free internal ReadConfig instance */
- free
{
  return [super free];
}

/* set method target */
- setTarget:obj
{
  mainTarget = obj;
  return self;
}
 
/* open/read config file */
- (int)readConfigFile:(char*)file
{
  int           error = cfgOK;                  // returned error code
  char          txt[2048];                      // max record length
  char          method[maxLEN + 1], *methName;  // max method name length
  char          parmString[2048];               // max parm length
  char          *pPtr, *dPtr, *cPtr;            // char pointers
  SEL           selector;                       // method selector
  BOOL          checkRtn;                       // check return for method
  int           parms;                          // number of parsed parms
  id            msgTarget = (id)nil;            // method target (default target)
  int           prefixLen;                      // length of internal method prefix
    
  /* open file for current recursion level */
  exc[level].lineNumber = 0;
  exc[level].fNum = fopen(file, "r");
  if (!exc[level].fNum) {
    error = cfgOPEN_ERROR;
    printf("ReadConfig ERROR: Unable to open config file %s\n", file);
    if (errorTrap) [errorTrap configErrorTrap:error inFile:file atLine:0];
    return error;
  }
    
  /* set up internal method name prefix */
  strcpy(method, prefixMethodNAME);
  prefixLen = strlen(method);

  /* read file records */
  for (*txt = 0; fgets(txt, maxLEN, exc[level].fNum);) {
      
    /* count line number */
    exc[level].lineNumber++;

    /* skip record if comment */
    if (isCOMMENT(txt)) continue;

    /* parse record */
    methName = &method[prefixLen];
    parmString[0] = 0;
    parms = sscanf(txt, parseFORMAT, methName, parmString);
    if (parms < 1) breakERROR(cfgINVALID_PARMS);
    
    /* internal method check */
    if (*methName == '*') { *methName = '_'; methName = method; }
    
    /* check for label match */
    if (*findLabel || (*methName == ':')) {
      if ((*methName == ':') && !strcmp(findLabel, &methName[1])) *findLabel = 0;
      continue;
    }
      
    /* parse additional parameter */
    if (parms < 2) dPtr = pPtr = (char*)nil;
    else {
      for (pPtr = parmString; *pPtr && ((*pPtr == ' ') || (*pPtr == '\t')); pPtr++);
      dPtr = pPtr;
      if (*pPtr == '*') {
        if (!strcmp(pPtr, "*NIL") || !strcmp(pPtr, "*nil")) pPtr = (char*)nil;        else
        if (!strcmp(pPtr, "*NO" ) || !strcmp(pPtr, "*no" )) pPtr = (char*)((int)NO ); else
        if (!strcmp(pPtr, "*YES") || !strcmp(pPtr, "*yes")) pPtr = (char*)((int)YES);
      }
    }

    /* return status checking */
    checkRtn = (*methName == '!' || *methName == '*')? NO: YES;
    
    /* check for update keys in method name */
    if (cPtr = index(methName, '$')) *cPtr = 0;

    /* convert name to selector */
    selector = sel_getUid(&methName[(checkRtn)? 0: 1]);
    if (!sel_isMapped(selector)) breakERROR(cfgNO_METHOD);
    
    /* determine real target */
    if (*methName == '*') msgTarget = self;
    else msgTarget = mainTarget;

    /* debug: show method performed */
    if (methodTrace) {
      printf("[%s perform:\"%s\"", [msgTarget name], sel_getName(selector));
      if (parms == 2) printf(((pPtr==dPtr)? " with:\"%s\"": " with:(void*)%d"), (int)pPtr);
      printf("];\n");
    }

    /* precheck method performance */
    if ( [msgTarget respondsTo:@selector(configWillPerform:with:)] &&
        ![msgTarget configWillPerform:selector with:(id)pPtr]) continue;

    /* make sure target can respond to the selector */
    if (![msgTarget respondsTo:selector]) breakERROR(cfgBADTARGET);
    
    /* perform method */
    if (parms == 2) methodRtn = (int)[msgTarget perform:selector with:(id)pPtr];
    else            methodRtn = (int)[msgTarget perform:selector];
      
    /* check return status */
    if (checkAll && checkRtn && methodRtn) breakERROR(cfgMETHOD_ERROR);
    
    /* check for active elapsed time profiling */
    if (profileActive && (msgTarget != self)) {
      profileCount++;
      if (profileTarget) [profileTarget profileElapsedTime:[[self class] profileElapsedTime]];
    }

  }
    
  /* terminate any status modal loop */
  if (modalPanel) [self cfg_endStopPanel];
    
  /* check for unmatched goto label */
  if (!error && *findLabel) error = cfgGOTO_LABEL;

  /* close file */
  fclose(exc[level].fNum);

  /* set global vars */
  globalLevel = level;
  globalLine  = exc[level].lineNumber;
  
  /* print error */
  if (error) {
    int lineNbr = exc[level].lineNumber;
    if (printError) {
      char *fName = rindex(file, '/');
      printf("ReadConfig [%s] (%s): %s\n", [msgTarget name], ((fName)? (fName + 1): file), 
        errorDesc[error]);
      printf(" line %d: %s\n", lineNbr, txt);
    }
    if (errorTrap) [errorTrap configErrorTrap:error inFile:file atLine:lineNbr];
    if (exitOnError) [self cfg_terminate];
  }

  /* return error */
  return error;
  
}
  
#undef          breakERROR
  
// -------------------------------------------------------------------------------------
// Internal utility method routines
// Notes:
//   - These methods are for use from within the config file.  They are accessed by
//     placing an '*' in front of the desired method call.  (ie. '*methodTrace: *YES')
//   - See the header file for more information on these methods.
// -------------------------------------------------------------------------------------

/* set method trace mode */
- (int)cfg_methodTrace:(BOOL)yesNo
{
  methodTrace = yesNo;
  return NO;
}

/* set global method trace mode */
- (int)cfg_globalMethodTrace:(BOOL)yesNo
{
  dftMethodTrace = yesNo;
  return [self cfg_methodTrace:dftMethodTrace];
}

/* set error print mode */
- (int)cfg_printError:(BOOL)yesNo
{
  printError = yesNo;
  return NO;
}

/* set global error print mode */
- (int)cfg_globalPrintError:(BOOL)yesNo
{
  dftPrintError = yesNo;
  return [self cfg_printError:dftPrintError];
}

/* set return status checking mode */
- (int)cfg_checkReturn:(BOOL)yesNo
{
  checkAll = yesNo;
  return NO;
}

/* set global return status checking mode */
- (int)cfg_globalCheckReturn:(BOOL)yesNo
{
  dftCheckAll = yesNo;
  return [self cfg_checkReturn:dftCheckAll];
}

/* set global return status checking mode */
- (int)cfg_globalBackupOnWrite:(BOOL)yesNo
{
  dftBackupOnWrite = yesNo;
  return NO;
}

/* set terminate application on error flag */
- (int)cfg_terminateOnError:(BOOL)yesNo
{
  exitOnError = yesNo;
  return NO;
}

/* set terminate application on error flag */
- (int)cfg_globalTerminateOnError:(BOOL)yesNo
{
  dftExitOnError = yesNo;
  return [self cfg_terminateOnError:dftExitOnError];
}
  
// -------------------------------------------------------------------------------------
// branching
  
/* goto label */
- (int)cfg_goto:(char*)label
{
  if (!label) return YES;
  strncpy(findLabel, label, sizeof(findLabel));
  rewind(exc[level].fNum);      // rewind file
  return NO;
}

/* if True goto label */
- (int)cfg_ifTrue:(char*)label
{
  if (methodRtn) return [self cfg_goto:label];
  return NO;
}

/* if False goto label */
- (int)cfg_ifFalse:(char*)label
{
  if (!methodRtn) return [self cfg_goto:label];
  return NO;
}
  
// -------------------------------------------------------------------------------------
// user panels

/* alert panel */
- (int)cfg_alertPanel:(char*)message
{
  char  *title = "Configuration File Alert";
  NXRunAlertPanel(title, message, "OK", (char*)nil, (char*)nil);
  return NO;
}

/* abort panel */
- (int)cfg_abortPanel:(char*)message
{
  int   rtn;
  char  *title = "Configuration File Abort";
  rtn = NXRunAlertPanel(title, message, "Continue", "Terminate", (char*)nil);
  if (!rtn) [self cfg_terminate];
  return NO;
}

/* option panel */
- (int)cfg_optionPanel:(char*)message
{
  int   i;
  int   alertRtn;
  char  btnA[100], btnB[100];
  char  *title = "Configuration File Option";
  if ((message[0]=='<') || (message[0]=='(') || (message[0]=='{')) {
    sscanf(message, "%*c%[^,/| ]%*c%[^>)}]%*c%n", btnA, btnB, &i);
    while (message[i] == ' ') i++;
    alertRtn = NXRunAlertPanel(title, &message[i], btnB, btnA, (char*)nil);
  } else
    alertRtn = NXRunAlertPanel(title, message, "B", "A", (char*)nil);
  return alertRtn;
}

/* if option-A (False) goto label */
- (int)cfg_ifOptionA:(char*)label
{
  return [self cfg_ifFalse:label];
}

/* if option-B (True) goto label */
- (int)cfg_ifOptionB:(char*)label
{
  return [self cfg_ifTrue:label];
}

/* end modal panel */
- (int)cfg_endModalPanel
{
  if (!modalPanel) return YES;
  [NXApp runModalSession:&modalSession];        // eat up any queued events
  [NXApp endModalSession:&modalSession];
  [modalPanel orderOut:nil];
  NXFreeAlertPanel(modalPanel);
  modalPanel = (id)nil;
  return NO;
}

/* begin info modal panel */
- (int)cfg_infoPanel:(char*)message
{
  char          *title = "Configuration Information";
  if (modalPanel) [self cfg_endModalPanel];
  modalPanel = NXGetAlertPanel(title, message, "", 0, 0);
  [NXApp beginModalSession:&modalSession for:modalPanel];
  [NXApp runModalSession:&modalSession];        // show panel
  return NO;
}

/* end modal panel */
- (int)cfg_endInfoPanel
{
  return [self cfg_endModalPanel];
}

/* begin stop modal panel */
- (int)cfg_stopPanel:(char*)message
{
  char          *title = "Configuration File Status";
  if (modalPanel) [self cfg_endModalPanel];
  modalPanel = NXGetAlertPanel(title, message, "Stop", 0, 0);
  [NXApp beginModalSession:&modalSession for:modalPanel];
  [NXApp runModalSession:&modalSession];        // show panel
  return NO;
}

/* end stop panel */
- (int)cfg_endStopPanel
{
  return [self cfg_endModalPanel];
}

/* check for modal stop button */
- (int)ifStop:(char*)label
{
  int           modalRtn;
  if (!modalPanel) return YES;
  modalRtn = [NXApp runModalSession:&modalSession];
  if (modalRtn == NX_ALERTDEFAULT) {
    [self cfg_endStopPanel];
    return [self cfg_goto:label];
  }
  return NO;
}
  
// -------------------------------------------------------------------------------------
// misc

/* print string */
- (int)cfg_print:(char*)text
{
  printf("%s\n", text);
  return NO;
}

/* terminate application */
- (int)cfg_terminate
{
  [NXApp terminate:self];
  return YES;
}

/* include another config file */
- (int)cfg_include:(char*)cfgName
{
  int   error;
  char  *fileName;
  level++;
  fileName = [[self class] configPath:cfgName];
  error = [self readConfigFile:fileName];
  free(fileName);
  level--;
  return error;
}
  
// -------------------------------------------------------------------------------------
// time lapse profiling

/* start time profile */
- (int)cfg_startProfile
{
  [[self class] startProfile];
  return NO;
}

/* stop time profile */
- (int)cfg_stopProfile
{
  [[self class] stopProfile];
  return NO;
}

/* print current elapsed time */
- (int)cfg_printElapsedTime
{
  printf("ReadConfig: Profile elapsed time = %.2f seconds, executed methods = %d\n",
    [[self class] profileElapsedTime], [[self class] profileCount]);
  return NO;
}

@end    
