/**************************************************************************
   zfuncs.cpp           v.2.21

   Free software licensed under GNU General Public License v.2
   source URL:  kornelix.squarespace.com
***************************************************************************/

#include "zfuncs.h"

/**************************************************************************
      Linux system utility functions
***************************************************************************/


//  Fatal exit with error message to stdout.  
//  Works like printf.

void appcrash(cchar *pMess, ... )
{
   va_list  arglist;
   char     message[200];
   void     *stacklist[50];
   int      ii, nstack = 50;
   char     **stackents;

   va_start(arglist,pMess);
   vsnprintf(message,200,pMess,arglist);
   va_end(arglist);
   
   printf("appcrash: %s \n",message);
   
   nstack = backtrace(stacklist,nstack);                                   //  good for g++ -rdynamic
   stackents = backtrace_symbols(stacklist,nstack);
   for (ii = 0; ii < nstack; ii++) 
      printf(" %s \n",stackents[ii]);

   abort();                                                                //  good for gdb backtrace
}


//  crash with error message in popup window and in stdout file

void zappcrash(cchar *pMess, ... )
{
   va_list     arglist;
   char        message[300], command[400];
   int         err;

   va_start(arglist,pMess);                                                //  v.2.18
   vsnprintf(message,299,pMess,arglist);                                   //  use xterm instead of GTK
   va_end(arglist);
   
   snprintf(command,399,"xterm -hold -T fatal_error -e echo \"%s\" ",message);
   err = system(command);
   appcrash(message);
}


//  Output a message to stdout and wait for user ACK.
//  Works like printf.

void apppause(cchar *pMess, ... )
{
   va_list  arglist;
   char     message[200];

   va_start(arglist,pMess);
   vsnprintf(message,200,pMess,arglist);
   va_end(arglist);

   printf("pause: %s \n",message);
   printf("*** press return to continue: ");
   getchar();
   return;
}

void apppause()
{
   printf("*** pause, press return to continue: ");
   getchar();
   return;
}


//  get time in real seconds (since 2000.01.01 00:00:00)       v.2.12

double get_seconds()
{
   timeval  time1;

   gettimeofday(&time1,0);
   return  time1.tv_sec + 0.000001 * time1.tv_usec - 946684800.0;
}   


//  start a timer or get elapsed time with millisecond resolution.

void start_timer(double &time0)                                            //  double  v.2.20
{
   timeval  timev;

   gettimeofday(&timev,0);
   time0 = timev.tv_sec + 0.000001 * timev.tv_usec;
   return;
}

double get_timer(double &time0)
{
   timeval  timev;
   double   time;

   gettimeofday(&timev,0);
   time = timev.tv_sec + 0.000001 * timev.tv_usec;
   return time - time0;
}


//  start a process CPU timer or get elapsed process CPU time
//  returns seconds with millisecond resolution

void start_CPUtimer(double &time0)                                         //  double  v.2.20
{
   time0 = CPUtime();
   return;
}

double get_CPUtimer(double &time0)
{
   return CPUtime() - time0;
}


//  get elapsed CPU time used by current process
//  returns seconds with millisecond resolution

double CPUtime()
{
   clock_t ctime = clock();
   double dtime = ctime / 1000000.0;
   return dtime;
}


//  start a detached thread using a simplified protocol                    //  v.2.11

void start_detached_thread(void * threadfunc(void *), void * arg)
{
   pthread_t         ptid;
   pthread_attr_t    ptattr;
   int               pterr;

   pthread_attr_init(&ptattr);
   pthread_attr_setdetachstate(&ptattr,PTHREAD_CREATE_DETACHED);
   pterr = pthread_create(&ptid,&ptattr,threadfunc,arg);
   if (pterr) appcrash("start_detached_thread() failure");
   return;
}


//  Synchronize execution of multiple threads.
//  Simultaneously resume NT calling threads.
//  from main():        synch_threads(NT)          //  setup to synch NT threads
//  from each thread:   synch_threads()            //  suspend, resume simultaneously

void synch_threads(int NT)
{
   static pthread_barrier_t  barrier;
   static int  bflag = 0;

   if (NT) {                                                               //  main(), initialize
      if (bflag) pthread_barrier_destroy(&barrier);
      pthread_barrier_init(&barrier,null,NT);
      bflag = 1;
      return;
   }

   pthread_barrier_wait(&barrier);                                         //  thread(), block
   return;                                                                 //  unblock
}


//  sleep for specified time in seconds (double)

void zsleep(double dsecs)
{
   unsigned    isecs, nsecs;
   timespec    tsecs;

   if (dsecs == 0.0) return;   
   isecs = unsigned(dsecs);
   nsecs = unsigned(1000000000.0 * (dsecs - isecs));
   tsecs.tv_sec = isecs;
   tsecs.tv_nsec = nsecs;
   nanosleep(&tsecs,null);
   return;
}


//  malloc() and free() wrappers with auto crash on failure and log option.
//  overflow sentinel is placed after end of allocated memory
//  zmalloc() and zfree() calls are logged if log flag is on
//  benchmark zmalloc() + zfree(): usually < 1 microsec. on 2 GHz CPU

unsigned zmalloc_tot = 0;
#define zmalloc_add 12

char * zmalloc(size_t bytes, int log)
{
   void        *maddr = malloc(bytes + zmalloc_add);
   if (! maddr) appcrash("zmalloc() failure, request: %zu",bytes);

   unsigned    *pbytes = (unsigned *) maddr;                   //  0..3          caller bytes
   char        *pvalid = (char *) maddr + 4;                   //  4..7          validity key "zmal"
   char        *puser =  (char *) maddr + 8;                   //  8..B+7        user data, B chars.
   char        *psent =  (char *) puser + bytes;               //  B+8..B+11     overflow sentinel "sent"

   *pbytes = bytes;
   strncpy(pvalid,"zmal",4);
   memset(puser,0,bytes);
   strncpy(psent,"sent",4);
   zmalloc_tot += bytes;
   if (log) printf("zmalloc loc: %p  bytes: %zu  total: %u \n",puser,bytes,zmalloc_tot);
   return puser;
}


//  free memory allocated by zmalloc(). checks for overflow.

void zfree(void *puser, int log)
{
   void        *maddr = (char *) puser - 8;
   unsigned    *pbytes = (unsigned *) maddr;
   char        *pvalid = (char *) maddr + 4;
   size_t      bytes = *pbytes;
   char        *psent = (char *) puser + bytes;

   if (! puser) appcrash("zfree: null address \n");
   if (strncmp("zmal",pvalid,4)) appcrash("zfree: invalid address %p \n",puser);
   if (strncmp("sent",psent,4)) appcrash("zfree: buffer overflow \n");
   *pvalid = *psent = 0;
   zmalloc_tot -= bytes;
   free(maddr);
   if (log) printf("zfree   loc: %p  bytes: %zu  total: %u \n",puser,bytes,zmalloc_tot);
   return;
}


//  Run a shell command and get its outputs one record at a time.
//  Start a new command with contx = 0. Do not change contx.
//  NULL return means no more output.
//  Get command exit status: err = command_status(contx)
//  Caller owns returned strings which are candidates for zfree()
//
//  strcat(buff," 2>&1");   //  combine stdout and stderr  v.2.14 removed


FILE *   CO_contx[10] = { 0,0,0,0,0,0,0,0,0,0 };
int      CO_status[10];

char * command_output(int &contx, const char *command, ...)                //  simplify, allow parallel usage
{                                                                          //       v.2.3
   FILE           *fid;
   va_list        arglist;
   char           buff[1000], *prec;
   
   if (contx == 0)                                                         //  start new command
   {
      for (contx = 1; contx < 10; contx++) 
         if (CO_contx[contx] == 0) break;
      if (contx == 10) zappcrash("command_output(), parallel usage > 9");
      
      va_start(arglist,command);                                           //  format command
      vsnprintf(buff,990,command,arglist);
      va_end(arglist);
      
      fid = popen(buff,"r");                                               //  execute command, output to FID
      if (fid == 0) {
         CO_status[contx] = errno;                                         //  failed to start
         return 0;
      }
      CO_contx[contx] = fid + 1000;
      CO_status[contx] = -1;                                               //  mark context busy
   }

   fid = CO_contx[contx] - 1000;
   prec = fgets_trim(buff,999,fid,1);                                      //  next output, less trailing \n
   if (prec) return strdupz(prec);                                         //  return output to caller

   CO_status[contx] = pclose(fid);                                         //  EOF, set status
   CO_contx[contx] = 0;                                                    //  mark context free
   return 0;
}

int command_status(int contx)                                              //  get command exit status
{
   int err = CO_status[contx];
   return WEXITSTATUS(err);                                                //  special BS for subprocess   v.2.3
}


//  signal a subprocess to pause, resume, or terminate
//  return:  0: OK  +-N: error

int signalProc(cchar *pname, cchar *signal)
{
   pid_t       pid;
   FILE        *fid;
   char        buff[100], *pp;
   int         err, ignore, nsignal = 0;

   sprintf(buff,"ps -C %s h o pid > /tmp/signalProc-temp",pname);
   err = system(buff);
   if (err) { err = 2; goto cleanup; }
   
   fid = fopen("/tmp/signalProc-temp","r");
   if (! fid) { err = 3; goto cleanup; }

   pp = fgets(buff,100,fid);
   fclose(fid);
   if (! pp) { err = 4; goto cleanup; }

   pid = atoi(buff);
   if (! pid) { err = 5; goto cleanup; }

   if (strEqu(signal,"pause")) nsignal = SIGSTOP; 
   if (strEqu(signal,"resume")) nsignal = SIGCONT; 
   if (strEqu(signal,"kill")) nsignal = SIGKILL; 
   err = kill(pid,nsignal);   

cleanup:
   ignore = system("rm -f /tmp/signalProc-temp");
   return err;
}


//  run a command or program as root user
//  sucomm:  root user access command, "su" or "sudo"
//  command:  shell command or filespec of the program to start
//  returns 0 if successfully started, else returns an error code

int runroot(const char *sucomm, const char *command)                       //  v.2.10
{
   char     xtcommand[500];
   int      err;
   
   if (strcmp(sucomm,"sudo") == 0)
   {
      snprintf(xtcommand,499,"xterm -geometry 40x3 -e sudo -S %s",command);
      err = system(xtcommand);
      return err;
   }
   
   if (strcmp(sucomm,"su") == 0)
   {
      snprintf(xtcommand,499,"xterm -geometry 40x3 -e su -c %s",command);
      err = system(xtcommand);
      return err;
   }

   return -1;
}


//  Check if a list of programs are all installed
//  If any are missing, pop-up a window of missing programs
//  Returns the number of missing programs (zero if none).

int checkinstall(const char *prog1, ...)  //  null terminated list         //  v.2.10
{
   va_list        arglist;
   char           *buff, errmessage[200] = "missing programs:\n";
   const char     *prog, *pp, *missprogs[20];
   int            contx, found, Nmiss = 0;

   va_start(arglist,prog1);
   prog = prog1;

   while (prog)
   {
      contx = 0;
      found = 0;
      
      while (true)
      {
         buff = command_output(contx,"whereis %s",prog);
         if (! buff) break;
         pp = strchr(buff,':');
         if (pp) pp = strchr(pp,'/');
         if (pp) found = 1;
         zfree(buff);
         continue;
      }

      if (! found) {
         if (Nmiss == 20) break;
         missprogs[Nmiss] = prog;
         Nmiss++;
      }

      prog = va_arg(arglist,const char *);
   }
   
   va_end(arglist);
   
   if (Nmiss) {
      for (int ii = 0; ii < Nmiss; ii++)
         strncatv(errmessage,199,missprogs[ii]," ",0);
      zmessageACK(errmessage);
   }

   return Nmiss;
}


//  fgets with additional feature: trailing \n \r are removed
//  optional bf flag: true if trailing blanks are to be removed

char * fgets_trim(char *buff, int maxcc, FILE *fid, int bf)
{
   int      cc;
   char     *pp;
   
   pp = fgets(buff,maxcc,fid);
   if (! pp) return pp;
   cc = strlen(buff);
   if (bf) while (cc && buff[cc-1] > 0 && buff[cc-1] <= ' ') --cc;         //  utf8  v.2.4
   else    while (cc && buff[cc-1] > 0 && buff[cc-1] < ' ') --cc;
   buff[cc] = 0;
   return pp;
}


//  return true if both files are in the same directory
//  both files may be files or directories

int samedirk(const char *file1, const char *file2)
{
   int         cc1, cc2;
   const char  *pp1, *pp2;                                                 //  v.2.15
   
   if (! file1 || ! file2) return 0;
   pp1 = strrchr(file1,'/');
   pp2 = strrchr(file2,'/');
   if (! pp1 && ! pp2) return 1;
   if (pp1 && ! pp2) return 0;
   if (! pp1 && pp2) return 0;
   cc1 = pp1 - file1;
   cc2 = pp2 - file2;
   if (cc1 != cc2) return 0;
   if (strncmp(file1,file2,cc1) == 0) return 1;
   return 0;
}


/**************************************************************************

   Parse a pathname (filespec) and return its components.
   Returned strings are allocated in static memory (no zfree needed).
   Missing components are returned as null pointers.
   
   input ppath         outputs

   /name1/name2/       directory /name1/name2/ with no file
   /name1/name2        directory /name1/name2/ if name2 a directory,
                       otherwise directory /name1/ and file name2
   /name1/name2.xxx    if .xxx < 8 chars, returns file name2 and ext .xxx,
                       otherwise returns file name2.xxx and no ext

   returns 0 if no error, else 1
   
***/

int parsefile(cchar *ppath, char **pdirk, char **pfile, char **pext)       //  v.2.15
{
   struct stat    statb;
   static char    dirk[1000], file[200], ext[8];
   char           *pp;
   int            err, cc1, cc2;

   *pdirk = *pfile = *pext = null;
   
   cc1 = strlen(ppath);
   if (cc1 > 999) return 1;                                                //  ppath too long

   strcpy(dirk,ppath);
   *pdirk = dirk;
   
   err = stat(dirk,&statb);                                                //  have directory only
   if (! err && S_ISDIR(statb.st_mode)) return 0;
   
   pp = (char *) strrchr(dirk,'/');
   if (! pp) return 1;                                                     //  illegal

   pp++;
   cc2 = pp - dirk;
   if (cc2 < 2 || cc2 == cc1) return 0;                                    //  have /xxxx  or  /xxxx/
   
   if (strlen(pp) > 199) return 1;                                         //  filename too long

   strcpy(file,pp);                                                        //  file part
   *pfile = file;
   *pp = 0;                                                                //  remove from dirk part

   pp = (char *) strrchr(file,'.');
   if (! pp || strlen(pp) > 7) return 0;                                   //  file part, no .ext
   
   strcpy(ext,pp);                                                         //  .ext part
   *pext = ext;
   *pp = 0;                                                                //  remove from file part
   return 0;
}


/**************************************************************************

   monitor a directory for file additions and deletions           v.2.16

   action:  open     Start monitoring directory
            event    Return next event or zero if none pending
            close    Stop monitoring

   dirk:  Input directory to monitor (action = open)

   file:  Output filename (action = event) relative to directory
          zfree() is called unless null
          zmalloc() is called to allocate new space

   returned status:
       -1   error
        0   OK (open, close), no event pending (event)
        1   file was possibly added
        2   file was deleted
        3   monitored directory is gone
        9   other
  
   NOTE  The Linux inotify() function is used and is not reliable
         for files renamed. The remove is reported but not the add.
         The 'other' event (with no file name) is reported.

***/

int zmondirk(const char *action, const char *dirk, char **file)
{
   struct inotify_event {
      int         wd;               //  Watch descriptor
      int         evt;              //  Event type
      int         cookie;           //  Associates related events (rename)
      int         len;              //  Size of file name field following
      char        fname[200];       //  null-terminated file name within directory
   };
   
   inotify_event     evbuff;
   struct timeval    waitime;
   fd_set            fids;
   int               wd, retval, cc, fcc;
   int               evbcc = sizeof(evbuff);
   static int        fid = -1;
   
   if (strEqu(action,"open"))                                              //  setup a monitored directory
   {
      fid = inotify_init();
      if (fid < 0) {
         return -1;
      }
      wd = inotify_add_watch(fid,dirk,IN_ALL_EVENTS);
      if (wd < 0) {
         close(fid);
         fid = -1;
         return -1;
      }
      return 0;
   }
   
   if (strEqu(action,"event"))                                             //  return pending event or zero
   {
      if (fid < 0) return -1;

      if (file) {
         if (*file) zfree(*file);                                          //  free prior memory
         *file = 0;
      }

      while (true)
      {
         FD_ZERO(&fids);
         FD_SET(fid, &fids);
         waitime.tv_sec = 0;
         waitime.tv_usec = 1;

         retval = select(fid+1, &fids, null, null, &waitime);
         if (retval == 0) return 0;                                        //  nothing pending

         if (retval == -1) {                                               //  error
            close(fid);
            fid = -1;
            return -1;
         }

         cc = read(fid,&evbuff,evbcc);                                     //  get pending event
         if (cc == -1) {
            close(fid);
            fid = -1;
            return -1;
         }

         fcc = evbuff.len;
         if (fcc > 199) return -1;
         if (fcc > 0 && file) *file = strdupz(evbuff.fname);               //  return filename

         if (evbuff.evt & (IN_CREATE + IN_MOVED_TO))                       //  file was added
            return 1;
         if (evbuff.evt & (IN_CLOSE_WRITE + IN_CLOSE_NOWRITE + IN_MODIFY)) //  file was possibly added
            return 1;
         if (evbuff.evt & (IN_DELETE + IN_MOVED_FROM))                     //  file was deleted
            return 2;
         if (evbuff.evt & (IN_DELETE_SELF + IN_MOVE_SELF))                 //  monitored directory gone
            return 3;
         return 9;                                                         //  other
      }
   }
   
   if (strEqu(action,"close"))                                             //  stop monitoring
   {
      if (fid > -1) retval = close(fid);
      else retval = -1;
      fid = -1;
      return retval;
   }
   
   zappcrash("zmondirk() call error");
   return -1;
}


/**************************************************************************

   utility to measure CPU time spent in various functions or code blocks        v.2.7
   
   cpu_profile_init()            initialize at start of test
   cpu_profile_report()          report CPU time per function
   cpu_profile_enter(fnum)       at entry to a function         //  inline, defined in zfuncs.h
   cpu_profile_exit(fnum)        at exit from a function        //  inline, defined in zfuncs.h

*/

volatile double   cpu_profile_table[100];
volatile double   cpu_profile_timer;
volatile double   cpu_profile_elapsed;
volatile int      cpu_profile_kill = 0;

void cpu_profile_init()
{
   void *   cpu_profile_timekeeper(void *);

   for (int ii = 0; ii < 99; ii++) 
      cpu_profile_table[ii] = 0;
   cpu_profile_elapsed = 0;
   start_detached_thread(cpu_profile_timekeeper,null);
}

void cpu_profile_report()
{
   cpu_profile_kill++;

   printf("elapsed: %.2f \n",cpu_profile_elapsed);

   for (int ii = 0; ii < 100; ii++)
   {
      double dtime = cpu_profile_table[ii];
      if (dtime) printf("cpu profile func: %d  time: %.2f \n",ii,dtime);
   }
}

void * cpu_profile_timekeeper(void *)
{
   timeval  time0, time1;

   gettimeofday(&time0,0);

   while (true)
   {
      gettimeofday(&time1,0);
      cpu_profile_elapsed = time1.tv_sec - time0.tv_sec
              + 0.000001 * (time1.tv_usec - time0.tv_usec);
      zsleep(0.001);
      if (cpu_profile_kill) break;
   }
   
   cpu_profile_kill = 0;
   return 0;
}


/**************************************************************************
    string utility functions
***************************************************************************

    strField()

    char * strField(const char *string, const char *delim, int Nth)

    Get the Nth field in input string, which contains at least N fields 
    delimited by the character(s) in delim (e.g. blank, comma).
    
    Returns a pointer to the found field (actually a pointer to a
    copy of the found field, with a null terminator appended).
    
    If a delimiter is immediately followed by another delimiter, it is 
    considered a field with zero length, and the string "" is returned.

    Leading blanks in a field are omitted from the returned field.
    A field with only blanks is returned as a single blank.

    The last field may be terminated by null or a delimiter.
    
    Characters within quotes (" or ') are treated as data within a
    field, i.e. blanks and delimiters are not processed as such.
    The quotes are removed from the returned field.

    If there are less than N fields, a null pointer is returned.
    
    The last 100 fields are saved and recycled in a circular manner.
    The caller does not have to free memory. If more memory depth is 
    needed, caller must copy the returned data elsewhere.
    
    The input string must be < 1000 characters.
    The output string may be modified if the length is not increased.
    
    Example: input string: ,a,bb,  cc,   ,dd"ee,ff"ggg,
             (first and last characters are comma)
             delimiter: comma
             Nth   returned string
              1:   (null string)
              2:   a
              3:   bb
              4:   cc
              5:   (one blank)
              6:   ddee,ffggg
              7:   (null pointer >> no more fields)

***************************************************************************/

const char * strField(const char *string, const char *delim, int Nth)
{
   static int     ftf = 1, nret = 0;
   static char    *retf[100]; 
   char           *pf1, pf2[1000];
   const char     quote1 = '\'', quote2 = '"';
   int            ii, nf, fcc = 0;
   static char    blankstring[2], nullstring[1];
   
   if (ftf)                                                                //  overall first call
   {
      ftf = 0;
      for (ii = 0; ii < 100; ii++) retf[ii] = 0;
      strcpy(blankstring," ");
      *nullstring = 0;
   }

   if (strlen(string) > 999) return 0;
   if (Nth < 1) return 0;

   pf1 = (char *) string - 1;                                              //  start parse
   nf = 0;
   
   while (nf < Nth)
   {
      pf1++;                                                               //  start field
      nf++;
      fcc = 0;

      while (*pf1 == ' ') pf1++;                                           //  skip leading blanks

      while (true)
      {
         if (*pf1 == quote1) {                                             //  pass chars between single quotes
            pf1++;                                                         //  (but without the quotes)
            while (*pf1 && *pf1 != quote1) pf2[fcc++] = *pf1++;
            if (*pf1 == quote1) pf1++;
         }

         else if (*pf1 == quote2) {                                        //  same for double quotes
            pf1++;
            while (*pf1 && *pf1 != quote2) pf2[fcc++] = *pf1++;
            if (*pf1 == quote2) pf1++;
         }
         
         else if (strchr(delim,*pf1) || *pf1 == 0) break;                  //  found delimiter or null
         
         else pf2[fcc++] = *pf1++;                                         //  pass normal character
      }

      if (*pf1 == 0) break;
   }
      
   if (nf < Nth) return 0;                                                 //  no Nth field
   
   if (fcc == 0) {                                                         //  empty field
      if (*string && pf1[-1] == ' ' && !strchr(delim,' '))                 //  all blanks and blank not delim.
         return blankstring;                                               //     return one blank
      if (*pf1 == 0) return 0;                                             //  no field
      return nullstring;                                                   //  return null string
   }

   if (++nret == 100) nret = 0;                                            //  use next return slot
   if (retf[nret]) zfree(retf[nret]);
   retf[nret] = zmalloc(fcc+2);
   strncpy0(retf[nret],pf2,fcc+1);
   return retf[nret];
}

const char * strField(const char *string, const char delim, int Nth)       //  alternative with one delimiter
{
   char     delims[2] = "x";
   
   *delims = delim;
   return strField(string,delims,Nth);
}


/**************************************************************************

   stat = strParms(begin, input, pname, maxcc, pval)

   Parse an input string with parameter names and values:
     "pname1=pval1 | pname2 | pname3=pval3 | pname4 ..."
   
   begin    int &          must be 1 to start new string, is modified
   input    const char *   input string
   pname    char *         output parameter name
   maxcc    int            max. length for pname, including null
   pval     double &       output parameter value
   stat     int            status: 0=OK, -1=EOL, 1=parse error
   
   Each call returns the next pname and pval.
   A pname with no pval is assigned a value of 1 (present).
   Input format:  pname1 | pname2=pval2 | pname3 ... null
   Leading blanks are ignored, and pnames may have imbedded blanks.
   pvals must convert to double using convSD (accepts decimal point or comma)

***************************************************************************/

int strParms(int &begin, const char *input, char *pname, int maxcc, double &pval)
{
   static int     ii, beginx = 3579246;
   const char     *pnamex, *delim;
   int            cc, err;

   if (begin == 1) {                                                       //  start new string
      begin = ++beginx;
      ii = 0;
   }

   if (begin != beginx) zappcrash("strParms call error");                  //  thread safe, not reentrant
   
   *pname = 0;                                                             //  initz. outputs to nothing
   pval = 0;
   
   while (input[ii] == ' ') ii++;                                          //  skip leading blanks
   if (input[ii] == 0) return -1;                                          //  no more data

   pnamex = input + ii;                                                    //  next pname
   
   for (cc = 0; ; cc++)
   {                                                                       //  look for delimiter
      if (pnamex[cc] == '=') break;
      if (pnamex[cc] == '|') break;
      if (pnamex[cc] == 0) break;
   }
   
   if (cc == 0) return 1;                                                  //  err: 2 delimiters
   if (cc >= maxcc) return 1;                                              //  err: pname too big

   strncpy0(pname,pnamex,cc+1);                                            //  pname >> caller
   strTrim(pname);                                                         //  remove trailing blanks

   if (pnamex[cc] == 0) {                                                  //  pname + null
      ii += cc;                                                            //  position for next call
      pval = 1.0;                                                          //  pval = 1 >> caller
      return 0;
   }

   if (pnamex[cc] == '|') {                                                //  pname + |
      ii += cc + 1;                                                        //  position for next call
      pval = 1.0;                                                          //  pval = 1 >> caller
      return 0;
   }

   ii += cc + 1;                                                           //  pname = pval
   err = convSD(input + ii, pval, &delim);                                 //  parse pval   (was strtod()
   if (err > 1) return 1;
   while (*delim == ' ') delim++;                                          //  skip poss. trailing blanks
   if (*delim && *delim != '|') return 1;                                  //  err: delimiter not | or null
   ii = delim - input;
   if (*delim) ii++;                                                       //  position for next call
   return 0;
}


//  Produce random value from hashed input string.
//  Output range is 0 to max-1.

int strHash(const char *string, int max)
{
   int      hash, ii, cc;
   long     *lstring = (long *) string;

   cc = strlen(string);
   if (cc > 99) appcrash("strHash, too long",null);

   cc = (cc + 3) / 4;
   hash = 0x12345678;

   for (ii = 0; ii < cc; ii++) {
      hash = hash ^ lstring[ii];
      hash = hash ^ (hash << 5);
      hash = hash ^ (hash >> 7);
      hash = hash ^ (hash << 9);
      hash = hash ^ (hash >> 11);
   }

   hash = hash & 0x3FFFFFFF;
   return (hash % max);
}


//  Hash an input string into a random printable (a-z) output string.
//  Returns outcc character random printable string in static memory.
//  Every output character is randomized from the entire input string.

const char * strHash2(const char *instring, int outcc)
{
   int            incc, ii, jj, rani = 0;
   int64          seed = 13579;
   static char    outstring[40];

   incc = strlen(instring);
   if (outcc > 39) appcrash("strHash2() outcc > 39");
   
   for (ii = 0; ii < outcc; ii++)
   {
      for (jj = 0; jj < incc; jj++)
      {
         seed = seed + instring[jj];
         rani = lrandz(&seed);
      }
      outstring[ii] = 'a' + rani % 26;
   }

   outstring[ii] = 0;
   return outstring;
}


//  Copy string with specified max. length (including null terminator).
//  truncate if needed. null terminator is always supplied.

int strncpy0(char *dest, const char *source, uint cc)
{
   strncpy(dest,source,cc);
   dest[cc-1] = 0;
   if (strlen(source) >= cc) return 1;                                     //  truncated  v.2.4
   else return 0;
}


//  Copy string with blank pad to specified length.  No null.

void strnPad(char *dest, const char *source, int cc)
{
   strncpy(dest,source,cc);
   int ii = strlen(source);
   for (int jj = ii; jj < cc; jj++) dest[jj] = ' ';
}


//  Remove trailing blanks from a string. Returns remaining length.

int strTrim(char *dest, const char *source)
{
   if (dest != source) strcpy(dest,source);
   return strTrim(dest);
}

int strTrim(char *dest)
{
   int  ii = strlen(dest);
   while (ii && (dest[ii-1] == ' ')) dest[--ii] = 0;
   return ii;
}


//  Remove leading and trailing blanks from a string. 
//  Returns remaining length, possibly zero.

int strTrim2(char *dest, const char *source)                               //  v.2.4
{
   const char  *pp1, *pp2;
   int         cc;

   pp1 = source;
   pp2 = source + strlen(source) - 1;
   while (*pp1 == ' ') pp1++;
   while (*pp2 == ' ' && pp2 > pp1) pp2--;
   cc = pp2 - pp1 + 1;
   strncpy(dest,pp1,cc);
   dest[cc] = 0;
   return cc;
}


//  Remove all blanks from a string. Returns remaining length.

int strCompress(char *dest, const char *source)
{
   if (dest != source) strcpy(dest,source);
   return strCompress(dest);
}

int strCompress(char *string)
{
   int   ii, jj;

   for (ii = jj = 0; string[ii]; ii++)
   {
      if (string[ii] != ' ')
      {
         string[jj] = string[ii];
         jj++;
      }
   }
   string[jj] = 0;
   return jj;
}


//  Concatenate multiple strings, staying within a specified overall length.
//  The destination string is also the first source string. 
//  Null marks the end of the source strings (omission --> crash).
//  Output is truncated to fit within the specified length.
//  A final null is assured and is included in the length.
//  Returns 0 if OK, 1 if truncation was needed.

int strncatv(char *dest, int maxcc, const char *source, ...)
{
   const char  *ps;
   va_list     arglist;

   maxcc = maxcc - strlen(dest) - 1;
   if (maxcc < 0) return 1;
   va_start(arglist,source);
   ps = source;

   while (ps)
   {
      strncat(dest,ps,maxcc);
      maxcc = maxcc - strlen(ps);
      if (maxcc < 0) break;
      ps = va_arg(arglist,const char *);
   }
   
   va_end(arglist);
   if (maxcc < 0) return 1;
   return 0;
}


//  Match 1st string to N additional strings.
//  Return matching string number 1 to N or 0 if no match.
//  Supply a null argument for end of list.

int strcmpv(const char *string, ...)
{
   int      match = 0;
   char     *stringN;
   va_list  arglist;

   va_start(arglist,string);

   while (1)
   {
      stringN = va_arg(arglist, char *);
      if (stringN == null)
      {
         va_end(arglist);
         return 0;
      }

      match++;
      if (strcmp(string,stringN) == 0)
      {
         va_end(arglist);
         return match;
      }
   }
}


//  convert string to upper case

void strToUpper(char *string)
{
   int         ii;
   char        jj;
   const int   delta = 'A' - 'a';

   for (ii = 0; (jj = string[ii]); ii++)
        if ((jj >= 'a') && (jj <= 'z')) string[ii] += delta;
}

void strToUpper(char *dest, const char *source)
{
   strcpy(dest,source);
   strToUpper(dest);
}


//  convert string to lower case

void strToLower(char *string)
{
   int         ii;
   char        jj;
   const int   delta = 'a' - 'A';

   for (ii = 0; (jj = string[ii]); ii++)
        if ((jj >= 'A') && (jj <= 'Z')) string[ii] += delta;
}

void strToLower(char *dest, const char *source)
{
   strcpy(dest,source);
   strToLower(dest);
}


//  copy string strin to strout, replacing every occurrence
//    of the substring ssin with the substring ssout

int repl_1str(cchar *strin, char *strout, cchar *ssin, cchar *ssout)
{
   int         ccc, cc1, cc2, nfound;
   const char  *ppp;                                                       //  v.2.15
   
   cc1 = strlen(ssin);
   cc2 = strlen(ssout);
   nfound = 0;
   
   while ((ppp = strstr(strin,ssin)))
   {
      nfound++;
      ccc = ppp - strin;
      strncpy(strout,strin,ccc);
      strout += ccc;
      strin += ccc;
      strncpy(strout,ssout,cc2);
      strin += cc1;
      strout += cc2;
   }

   strcpy(strout,strin);
   return nfound;
}


//  like repl_1str, but multiple pairs of substrings are processed
//   (... ssin1, ssout1, ssin2, ssout2, ... null) 

int repl_Nstrs(cchar *strin, char *strout, ...)
{
   va_list     arglist;
   cchar       *ssin, *ssout;
   char        ftemp[maxfcc];
   int         ftf, nfound;
   
   ftf = 1;
   nfound = 0;
   va_start(arglist,strout);
   
   while (true)
   {
      ssin = va_arg(arglist, char *);
      if (! ssin) break;
      ssout = va_arg(arglist, char *);

      if (ftf) {
         ftf = 0;
         nfound += repl_1str(strin,strout,ssin,ssout);
      }

      else {
         strcpy(ftemp,strout);
         nfound += repl_1str(ftemp,strout,ssin,ssout);
      }
   }

   va_end(arglist);
   return nfound;
}


//  Copy and convert string to hex string.
//  Each input character 'A' >> 3 output characters "41 "

void strncpyx(char *out, const char *in, int ccin)
{
   int      ii, jj, c1, c2;
   char     cx[] = "0123456789ABCDEF";

   if (! ccin) ccin = strlen(in);

   for (ii = 0, jj = 0; ii < ccin; ii++, jj += 3)
   {
      c1 = (uchar) in[ii] >> 4;
      c2 = in[ii] & 15;
      out[jj] = cx[c1];
      out[jj+1] = cx[c2];
      out[jj+2] = ' ';
   }
   out[jj] = 0;
   return;
}


//  Strip trailing zeros from ascii floating numbers
//    (e.g. 1.230000e+02  -->  1.23e+02)

void StripZeros(char *pNum)
{
   int     ii, ll;
   int     pp, k1, k2;
   char    work[20];

   ll = strlen(pNum);
   if (ll >= 20) return;

   for (ii = 0; ii < ll; ii++)
   {
      if (pNum[ii] == '.')
      {
         pp = ii;
         k1 = k2 = 0;
         for (++ii; ii < ll; ii++)
         {
            if (pNum[ii] == '0')
            {
               if (! k1) k1 = k2 = ii;
               else k2 = ii;
               continue;
            }

            if ((pNum[ii] >= '1') && (pNum[ii] <= '9'))
            {
               k1 = 0;
               continue;
            }

            break;
         }

         if (! k1) return;

         if (k1 == pp + 1) k1++;
         if (k2 < k1) return;
         strcpy(work,pNum);
         strcpy(work+k1,pNum+k2+1);
         strcpy(pNum,work);
         return;
      }
   }
}


//  test for blank/null string

int blank_null(const char *string)
{
   if (! string) return 1;                                                 //  null string
   if (! *string) return 2;                                                //  zero length string
   int cc = strlen(string);
   for (int ii = 0; ii < cc; ii++)
      if (string[ii] != ' ') return 0;                                     //  non-blank string
   return 3;                                                               //  blank string
}


//  make a copy of a string in non-volatile (heap) memory
//  returned string is subject for zfree();

char * strdupz(const char *string, int more)
{
   char  *pp = zmalloc(strlen(string)+1+more);
   strcpy(pp,string);
   return pp;
}


//  copy into existing 'zmalloc' string if present and long enough         //  v.2.8
//  else free memory and allocate a longer one
//  destination string is subject for zfree()

int strdupz(const char *source, char *&zdest, int more)
{
   int      ccs, ccd;

   ccs = strlen(source);
   if (! zdest) zdest = zmalloc(ccs+1+more);
   ccd = (int) *(zdest-8);
   if (ccd < ccs+1) {
      zfree(zdest);
      zdest = zmalloc(ccs+1+more);
   }
   strcpy(zdest,source);
   return ccs;
} 


//  clean \x escape sequences and replace them with the escaped character
//    \n >> newline  \" >> doublequote  \\ >> backslash   etc.
//  see  $ man ascii  for the complete list

int clean_escapes(char *string)
{
   char     *pp1 = string, *pp2 = string, *pp;
   char     char1;
   char     escapes[] = "abtnvfr";
   int      count = 0;
   
   while (true)
   {
      char1 = *pp1++;

      if (char1 == 0) {
         *pp2 = 0;
         return count;
      }

      else if (char1 == '\\')  {
         char1 = *pp1++;
         pp = strchr(escapes,char1);
         if (pp) char1 = pp - escapes + 7;
         count++;
      }

      *pp2++ = char1;
   }
}


//  Compute the graphic character count for a UTF8 character string.
//  Depends on UTF8 rules:
//    - ascii characters are positive (actually 0x00 to 0x7F)
//    - 1st byte of multibyte sequence is negative (actually 0xC0 to 0xFD)
//    - subsequent bytes are negative and < 0xC0 (actually 0x80 to 0xBF)

int utf8len(const char *utf8string)                                        //  v.2.3
{
   int      ii, cc;
   char     xlimit = 0xC0;

   for (ii = cc = 0; utf8string[ii]; ii++)
   {
      if (utf8string[ii] < 0)                                              //  multibyte character
         while (utf8string[ii+1] < xlimit) ii++;                           //  skip extra bytes
      cc++;
   }

   return cc;
}


//  Extract a UTF8 substring with a specified count of graphic characters.
//    utf8in     input UTF8 string
//    utf8out    output UTF8 string, which must be long enough 
//    pos        initial graphic character position to get (0 = first)
//    cc         max. count of graphic characters to get
//    returns    number of graphic characters extracted, <= cc
//  Output string is null terminated after last extracted character.

int utf8substring(char *utf8out, const char *utf8in, int pos, int cc)      //  v.2.3
{
   int      ii, jj, kk, posx, ccx;
   char     xlimit = 0xC0;

   for (ii = posx = 0; posx < pos && utf8in[ii]; ii++)
   {
      if (utf8in[ii] < 0)
         while (utf8in[ii+1] < xlimit) ii++;
      posx++;
   }

   jj = ii;   

   for (ccx = 0; ccx < cc && utf8in[jj]; jj++)
   {
      if (utf8in[jj] < 0)
         while (utf8in[jj+1] < xlimit) jj++;
      ccx++;
   }
   
   kk = jj - ii;

   strncpy(utf8out,utf8in+ii,kk);
   utf8out[kk] = 0;
   
   return   ccx;
}


//  check a string for valid utf8 encoding
//  returns:  0 = OK,  1 = bad string

int utf8_check(const char *string)                                         //  v.2.4
{
   const char        *pp;
   unsigned char     ch1, ch2, nch;
   
   for (pp = string; *pp; pp++)
   {
      ch1 = *pp;
      if (ch1 < 0x7F) continue;
      if (ch1 > 0xBF && ch1 < 0xE0) nch = 1;
      else if (ch1 < 0xF0) nch = 2;
      else if (ch1 < 0xF8) nch = 3;
      else if (ch1 < 0xFC) nch = 4;
      else if (ch1 < 0xFE) nch = 5;
      else return 1;
      while (nch) {
         pp++;
         ch2 = *pp;
         if (ch2 < 0x80 || ch2 > 0xBF) return 1;
         nch--;
      }
   }

   return 0;
}


//  Find the Nth graphic character position within a UTF8 string
//    utf8in      input UTF8 string
//    Nth         graphic character position, zero based
//  returns starting character (byte) position of Nth graphic character

int utf8_position(const char *utf8in, int Nth)                             //  v.2.4
{
   int      ii, posx;
   char     xlimit = 0xC0;

   for (ii = posx = 0; posx < Nth && utf8in[ii]; ii++)
   {
      if (utf8in[ii] < 0)
         while (utf8in[ii+1] < xlimit) ii++;
      posx++;
   }

   return  ii;
}


/**************************************************************************
   bitmap functions
***************************************************************************/

//  create a new bitmap with specified bit length. 
//  initially all bits are false.

bitmap * bitmap_new(int nbits)
{
   int      cc, ii;
   bitmap   *bm;
   
   bm = (bitmap *) zmalloc(sizeof(bitmap));
   bm->nbits = nbits;
   cc = (nbits + 7) / 8;
   bm->bits = (uchar *) zmalloc(cc);
   for (ii = 0; ii < cc; ii++) bm->bits[ii] = 0;
   return bm;
}


//  set bit in bitmap to true or false

void bitmap_set(bitmap *bm, int bit, bool value)
{
   int      ii, jj;
   uchar    bit1;

   if (bit >= bm->nbits) zappcrash("bitmap, bit %d too big",bit);
   ii = bit / 8;
   jj = bit % 8;
   bit1 = 0x80 >> jj;

   if (value) bm->bits[ii] = bm->bits[ii] | bit1;
   else {
      bit1 = bit1 ^ 0xff;
      bm->bits[ii] = bm->bits[ii] & bit1;
   }

   return;
}


//  fetch bitmap bit, return true or false

bool bitmap_get(bitmap *bm, int bit)
{
   int      ii, jj;
   uchar    bit1;

   ii = bit / 8;
   jj = bit % 8;
   bit1 = bm->bits[ii] << jj;
   if (bit1 < 127) return false;
   else return true;
}


//  delete bitmap

void bitmap_delete(bitmap *bm)
{
   zfree(bm->bits);
   zfree(bm);
   return;
}


/**************************************************************************
   variable list functions - array / list of strings
***************************************************************************/

//  create new variable list with specified capacity

pvlist * pvlist_create(int max)
{
   pvlist      *pv;

   pv = (pvlist *) zmalloc(sizeof(pvlist));
   pv->max = max;
   pv->act = 0;
   pv->list = (char **) zmalloc(max * sizeof(char *));
   return pv;
}

//  free memory for variable list
   
void pvlist_free(pvlist *pv)
{
   int      ii;
   
   for (ii = 0; ii < pv->act; ii++)
      zfree(pv->list[ii]);
   zfree(pv->list);
   zfree(pv);
}

//  append new entry to end of list (optional if unique)
//  if list if full, first entry is removed and rest are packed down
//  return: N >= 0: new entry added at position N
//          N = -1: not unique, not added
   
int pvlist_append(pvlist *pv, const char *entry, int unique)
{
   int      ii;

   if (unique && pvlist_find(pv,entry) >= 0) return -1;                    //  not unique

   if (pv->act == pv->max) pvlist_remove(pv,0);                            //  if list full, remove 1st entry

   ii = pv->act;
   pv->list[ii] = strdupz(entry);                                          //  add to end of list
   pv->act++;
   return ii;
}

//  prepend new entry to list (optional if unique)
//  prior list entries are pushed down to make room
//  if list is full, last entry is removed first
//  return: N = 0: new entry added at position 0
//          N = -1: not unique, not added
   
int pvlist_prepend(pvlist *pv, const char *entry, int unique)
{
   int      ii;
   
   if (unique && pvlist_find(pv,entry) >= 0) return -1;                    //  not unique

   if (pv->act == pv->max) pvlist_remove(pv,pv->act-1);                    //  if list full, remove last entry

   for (ii = pv->act; ii > 0; ii--)                                        //  push all list entries down
      pv->list[ii] = pv->list[ii-1];
   pv->list[0] = strdupz(entry);                                           //  add to start of list
   pv->act++;
   return 0;
}

//  find list entry by name, return -1 if not found
   
int pvlist_find(pvlist *pv, const char *entry)
{
   int      ii;

   for (ii = 0; ii < pv->act; ii++)
      if (strEqu(entry,pv->list[ii])) break;
   if (ii < pv->act) return ii;
   return -1;
}

//  remove an entry by name and repack list
   
int pvlist_remove(pvlist *pv, const char *entry)
{
   int      ii;
   
   ii = pvlist_find(pv,entry);
   if (ii < 0) return -1;
   pvlist_remove(pv,ii);
   return ii;
}

//  remove an entry by number and repack list
   
int pvlist_remove(pvlist *pv, int ii)
{
   if (ii < 0 || ii >= pv->act) return -1;
   zfree(pv->list[ii]);
   for (++ii; ii < pv->act; ii++)
      pv->list[ii-1] = pv->list[ii];
   pv->act--;
   return 0;
}


//  return entry count

int pvlist_count(pvlist *pv)
{
   return pv->act;
}


//  replace Nth entry with new one

int pvlist_replace(pvlist * pv, int ii, char *entry)
{
   if (ii < 0 || ii >= pv->act) return -1;
   zfree(pv->list[ii]);
   pv->list[ii] = strdupz(entry);
   return 0;
}


//  return Nth entry or null

char * pvlist_get(pvlist *pv, int Nth)
{
   if (Nth >= pv->act) return 0;
   return pv->list[Nth];
}


//  sort list in ascending order

int pvlist_sort(pvlist *pv)
{
   HeapSort(pv->list,pv->act);
   return 0;
}


/**************************************************************************

   Conversion Utilities

   convSI(string, inum, delim)                     string to int
   convSI(string, inum, low, high, delim)          string to int with range check

   convSD(string, dnum, delim)                     string to double
   convSD(string, dnum, low, high, delim)          string to double with range check

   convIS(inum, string, cc)                        int to string with returned cc

   convDS(fnum, digits, string, cc)                double to string with specified 
                                                     digits of precision and returned cc
   
   string      input (const char *) or output (char *)
   inum        input (int) or output (int &)
   dnum        input (double) or output (double &)
   delim       optional output pointer to delimiter (null or const char **)
   low, high   input range check (int or double)
   cc          output string length (int &)
   digits      input digits of precision (int) to be used for output string

   function status returned:
       0    normal conversion, no invalid digits, blank/null termination
       1    successful converstion, but trailing non-numeric found
       2    conversion OK, but outside specified limits
       3    null or blank string, converted to zero
       4    conversion error, invalid data in string
   overlapping statuses have following precedence: 4 3 2 1 0

***************************************************************************/

#define  max10  (0x7fffffff / 10)


//  Convert string to integer

int convSI(const char *string, int &inum, const char **delim)
{
   char        ch;
   int         sign = 0, digits = 0, tnb = 0;
   const char  *pch = string;

   inum = 0;

   while ((ch = *pch) == ' ') pch++;                                       //  skip leading blanks

   if (ch == '-') sign = -1;                                               //  process leading +/- sign
   if (ch == '+') sign = 1;                                                //  (at most one sign character)
   if (sign) pch++;

   while ((*pch >= '0') && (*pch <= '9'))                                  //  process digits 0 - 9
   {
      if (inum > max10) goto conv_err;                                     //  value too big
      inum = 10 * inum + *pch - '0';
      digits++;
      pch++;
   }

   if (delim) *delim = pch;                                                //  terminating delimiter
   if (*pch && (*pch != ' ')) tnb++;                                       //  not null or blank

   if (! digits)                                                           //  no digits found
   {
      if (tnb) return 4;                                                   //  non-numeric (invalid) string
      else return 3;                                                       //  null or blank string
   }

   if (sign == -1) inum = -inum;                                           //  negate if - sign

   if (! tnb) return 0;                                                    //  no trailing non-numerics
   else return 1;                                                          //  trailing non-numerics

conv_err:
   inum = 0;
   return 4;
}


int convSI(const char *string, int & inum, int lolim, int hilim, const char **delim)
{
   int   stat = convSI(string,inum,delim);

   if (stat > 2) return stat;                                              //  invalid or null/blank
   if (inum < lolim) return 2;                                             //  return 2 if out of limits
   if (inum > hilim) return 2;                                             //  (has precedence over status 1)
   return stat;                                                            //  limits OK, return 0 or 1
}


//  Convert string to double.

int convSD(const char *string, double &dnum, const char **delim)
{
   char        ch;
   int         ii, sign = 0, digits = 0, ndec = 0;
   int         exp = 0, esign = 0, edigits = 0, tnb = 0;
   const char  *pch = string;

   static int  first = 1;
   static double  decimals[21], exponents[74];

   if (first)                                                              //  first-time called
   {
      first = 0;                                                           //  pre-calculate constants
      for (ii = 1; ii <= 20; ii++) decimals[ii] = pow(10.0,-ii);
      for (ii = -36; ii <= 36; ii++) exponents[ii+37] = pow(10.0,ii);
   }

   dnum = 0.0;

   while ((ch = *pch) == ' ') pch++;                                       //  skip leading blanks

   if (ch == '-') sign = -1;                                               //  process leading +/- sign
   if (ch == '+') sign = 1;                                                //  (at most one sign character)
   if (sign) pch++;

get_digits:

   while ((*pch >= '0') && (*pch <= '9'))                                  //  process digits 0 - 9
   {
      dnum = 10.0 * dnum + (*pch - '0');
      pch++;
      digits++;
      if (ndec) ndec++;
   }

   if ((*pch == '.') || (*pch == ','))                                     //  process decimal point
   {                                                                       //  (allow comma or period)
      if (ndec) goto conv_err;
      ndec++;
      pch++;
      goto get_digits;
   }

   if ((*pch == 'e') || (*pch == 'E'))                                     //  process optional exponent
   {
      pch++;
      if (*pch == '+') esign = 1;                                          //  optional +/- sign
      if (*pch == '-') esign = -1;
      if (esign) pch++;

      if ((*pch < '0') || (*pch > '9')) goto conv_err;                     //  1st digit
      exp = *pch - '0';
      edigits++;
      pch++;

      if ((*pch >= '0') && (*pch <= '9'))                                  //  optional 2nd digit
      {
         exp = 10 * exp + (*pch - '0');
         edigits++;
         pch++;
      }

      if ((exp < -36) || (exp > 36)) goto conv_err;                        //  exponent too big 
   }

   if (delim) *delim = pch;                                                //  terminating delimiter 
   if (*pch && (*pch != ' ')) tnb++;                                       //  not null or blank

   if (!(digits + edigits))                                                //  no digits found
   {
      if (tnb) return 4;                                                   //  non-numeric (invalid) string
      else return 3;                                                       //  null or blank string
   }

   if (ndec > 1) dnum = dnum * decimals[ndec-1];                           //  compensate for decimal places

   if (sign == -1) dnum = - dnum;                                          //  negate if negative

   if (exp)                                                
   {
      if (esign == -1) exp = -exp;                                         //  process exponent
      dnum = dnum * exponents[exp+37];
   }

   if (! tnb) return 0;                                                    //  no trailing non-numerics
   else return 1;                                                          //  trailing non-numerics

conv_err:
   dnum = 0.0;
   return 4;
}


int convSD(const char *string, double &dnum, double lolim, double hilim, const char **delim)
{
   int stat = convSD(string,dnum,delim);

   if (stat > 2) return stat;                                              //  invalid or null/blank
   if (dnum < lolim) return 2;                                             //  return 2 if out of limits
   if (dnum > hilim) return 2;                                             //  (has precedence over status 1)
   return stat;                                                            //  limits OK, return 0 or 1
}


//  Convert int to string with returned length.

int convIS(int inum, char *string, int *cc)
{
   int   ccc;

   ccc = sprintf(string,"%d",inum);
   if (cc) *cc = ccc;
   return 0;
}


//  Convert double to string with specified digits of precision.
//  Shortest length format (f/e) will be used.  
//  Output length is returned in optional argument cc.

int convDS(double dnum, int digits, char *string, int *cc)
{
   char     *pstr;
   int      ccc;
   
   sprintf(string,"%.*g",digits,dnum);

   pstr = strstr(string,"e+");
   if (pstr) strcpy(pstr+1,pstr+2);

   pstr = strstr(string,"e0");            
   if (pstr) strcpy(pstr+1,pstr+2);

   pstr = strstr(string,"e0");            
   if (pstr) strcpy(pstr+1,pstr+2);

   pstr = strstr(string,"e-0");
   if (pstr) strcpy(pstr+2,pstr+3);

   pstr = strstr(string,"e-0");
   if (pstr) strcpy(pstr+2,pstr+3);

   ccc = strlen(string);
   if (cc) *cc = ccc;

   return 0;
}


/**************************************************************************

    Wildcard string match

    Match candidate string to wildcard string containing any number of 
    '*' or '?' wildcard characters. '*' matches any number of characters, 
    including zero characters. '?' matches any one character.

    Returns 0 if match, 1 if no match.

***************************************************************************/

int MatchWild(const char *pWild, const char *pString)
{
   int   ii, star;

new_segment:

   star = 0;
   while (pWild[0] == '*')
   {
      star = 1;
      pWild++;
   }

test_match:

   for (ii = 0; pWild[ii] && (pWild[ii] != '*'); ii++)
   {
      if (pWild[ii] != pString[ii])
      {
         if (! pString[ii]) return 1;
         if (pWild[ii] == '?') continue;
         if (! star) return 1;
         pString++;
         goto test_match;
      }
   }

   if (pWild[ii] == '*')
   {
      pString += ii;
      pWild += ii;
      goto new_segment;
   }

   if (! pString[ii]) return 0;
   if (ii && pWild[ii-1] == '*') return 0;
   if (! star) return 1;
   pString++;
   goto test_match;
}


/**************************************************************************

   SearchWild  - wildcard file search

   Find all files with total /pathname/filename matching a pattern,
   which may have any number of the wildcard characters '*' and '?'
   in either or both the pathname and filename.

   const char * SearchWild(const char *wfilespec, int &flag)
   
   inputs:  flag = 1 to start a new search
            flag = 2 abort a running search
            *** do not modify flag within a search ***

            wfilespec = filespec to search with optional wildcards
               e.g. "/name1/na*me2/nam??e3/name4*.ext?"
               
   return:  a pointer to one matching file is returned per call,
            or null when there are no more matching files.
             
   The search may be aborted before completion, but make a final 
   call with flag = 2 to clean up temp file. A new search with 
   flag = 1 will also finish the cleanup.
   
   NOT THREAD SAFE - do not use in parallel threads
   
   shell find command is used for the initial search because this
   is much faster than recursive use of readdir() (why?). 

   (#) is used in place of (*) in comments below to prevent 
   compiler from interpreting (#/) as end of comments

   GNU find peculiarities: 
     find /path/#      omits "." files
     find /path/       includes "." files
     find /path/#      recurses directories under /path/
     find /path/#.txt  does not recurse directories
     find /path/#/     finds all files under /path/
     find /path/#/#    finds files >= 1 directory level under /path/
     find /path/xxx#   never finds anything

   SearchWild uses simpler and more intuitive matching: 
     '/' and '.' are matched by '#'
     /path/#.txt finds all .txt files under /path/ at any directory level
   
***************************************************************************/

const char * SearchWild(const char *wpath, int &uflag)
{
   static FILE    *fid = 0;
   static char    tempfile[60];
   static char    matchfile[maxfcc];
   char           searchpath[maxfcc];
   char           command[maxfcc];
   int            cc, err, ignore;
   char           *pp;
   pid_t          pid;
   
   if ((uflag == 1) || (uflag == 2)) {                                     //  first call or stop flag
      if (fid) {
         fclose(fid);                                                      //  if file open, close it
         fid = 0;
         sprintf(command,"rm -f %s",tempfile);                             //  and delete it
         ignore = system(command);
      }
   }
   
   if (uflag == 2) return 0;                                               //  kill flag, done
      
   if (uflag == 1)                                                         //  first call flag
   {
      cc = strlen(wpath);
      if (cc == 0) return 0;
      if (cc > maxfcc-20) appcrash("SearchWild: wpath > max");
      
      pp = (char *) wpath;
      repl_Nstrs(pp,searchpath,"$","\\$","\"","\\\"",null);                //  init. search path, escape $ and "

      pp = strchr(searchpath,'*');
      if (pp) {                                                            //  not efficient but foolproof 
         while ((*pp != '/') && (pp > searchpath)) pp--;                   //  /aaa/bbb/cc*cc... >>> /aaa/bbb/
         if (pp > searchpath) *(pp+1) = 0;
      }

      pid = getpid();
      sprintf(tempfile,"/tmp/searchwild-%u",pid);                          //  unique temp file name
      sprintf(command,"find \"%s\" -type f -or -type l > %s",              //  find files (ordinary, symlink)
                                 searchpath,tempfile);                     //  output to temp file
      err = system(command);                                               //  ignore error
      fid = fopen(tempfile,"r");                                           //  open temp file
      uflag = 763568954;                                                   //  begin search
   }

   if (uflag != 763568954) appcrash("SearchWild, uflag invalid");
   
   while (true)
   {
      pp = fgets(matchfile,maxfcc-2,fid);                                  //  next matching file
      if (! pp) {
         fclose(fid);                                                      //  no more
         fid = 0;
         sprintf(command,"rm -f %s \n",tempfile);
         ignore = system(command);
         return 0;
      }

      cc = strlen(matchfile);                                              //  get rid of trailing \n
      matchfile[cc-1] = 0;

      err = MatchWild(wpath,matchfile);                                    //  wildcard match?
      if (err) continue;                                                   //  no

      return matchfile;                                                    //  return file
   }
}


/**************************************************************************/

//  perform a binary search on sorted list of integers
//  return matching element or -1 if not found

int bsearch(int element, int nn, int list[])
{
   int      ii, jj, kk, rkk;

   ii = nn / 2;                                                            //  next element to search
   jj = (ii + 1) / 2;                                                      //  next increment
   nn--;                                                                   //  last element
   rkk = 0;

   while (true)
   {
      kk = list[ii] - element;                                             //  check element

      if (kk > 0)
      {
         ii -= jj;                                                         //  too high, go down
         if (ii < 0) return -1;
      }

      else if (kk < 0)
      {
         ii += jj;                                                         //  too low, go up
         if (ii > nn) return -1;
      }

      else if (kk == 0) return ii;                                         //  matched

      jj = jj / 2;                                                         //  reduce increment

      if (jj == 0)
      {
         jj = 1;                                                           //  step by 1 element
         if (! rkk) rkk = kk;                                              //  save direction
         else
         {
            if (rkk > 0) { if (kk < 0) return -1; }                        //  if change direction, fail
            else if (kk > 0) return -1;
         }
      }
   }
}


/**************************************************************************
   heap sort functions
***************************************************************************/

#define SWAP(x,y) (temp = (x), (x) = (y), (y) = temp)


//  heapsort for array of integers 

static void adjust(int vv[], int n1, int n2)
{
   int   *bb, jj, kk, temp;

   bb = vv - 1;
   jj = n1;
   kk = n1 * 2;

   while (kk <= n2)
   {
      if (kk < n2 && bb[kk] < bb[kk+1]) kk++;
      if (bb[jj] < bb[kk]) SWAP(bb[jj],bb[kk]);
      jj = kk;
      kk *= 2;
   }
}

void HeapSort(int vv[], int nn)
{
   int   *bb, jj, temp;

   for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn);

   bb = vv - 1;

   for (jj = nn-1; jj > 0; jj--)
   {
      SWAP(bb[1], bb[jj+1]);
      adjust(vv,1,jj);
   }
}


//  heapsort for array of floats 

static void adjust(float vv[], int n1, int n2)
{
   float    *bb, temp;
   int      jj, kk;

   bb = vv - 1;
   jj = n1;
   kk = n1 * 2;

   while (kk <= n2)
   {
      if (kk < n2 && bb[kk] < bb[kk+1]) kk++;
      if (bb[jj] < bb[kk]) SWAP(bb[jj],bb[kk]);
      jj = kk;
      kk *= 2;
   }
}

void HeapSort(float vv[], int nn)
{
   float    *bb, temp;
   int      jj;

   for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn);

   bb = vv - 1;

   for (jj = nn-1; jj > 0; jj--)
   {
      SWAP(bb[1], bb[jj+1]);
      adjust(vv,1,jj);
   }
}


//  heapsort for array of doubles 

static void adjust(double vv[], int n1, int n2)
{
   double   *bb, temp;
   int      jj, kk;

   bb = vv - 1;
   jj = n1;
   kk = n1 * 2;

   while (kk <= n2)
   {
      if (kk < n2 && bb[kk] < bb[kk+1]) kk++;
      if (bb[jj] < bb[kk]) SWAP(bb[jj],bb[kk]);
      jj = kk;
      kk *= 2;
   }
}

void HeapSort(double vv[], int nn)
{
   double   *bb, temp;
   int      jj;

   for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn);

   bb = vv - 1;

   for (jj = nn-1; jj > 0; jj--)
   {
      SWAP(bb[1], bb[jj+1]);
      adjust(vv,1,jj);
   }
}


//  heapsort array of pointers to strings in ascending order of strings
//  pointers are sorted, strings are not changed.

static void adjust(char *vv[], int n1, int n2)
{
   char     **bb, *temp;
   int      jj, kk;

   bb = vv - 1;
   jj = n1;
   kk = n1 * 2;

   while (kk <= n2)
   {
      if (kk < n2 && strcmp(bb[kk],bb[kk+1]) < 0) kk++;
      if (strcmp(bb[jj],bb[kk]) < 0) SWAP(bb[jj],bb[kk]);
      jj = kk;
      kk *= 2;
   }
}

void HeapSort(char *vv[], int nn)
{
   char     **bb, *temp;
   int      jj;

   for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn);

   bb = vv;

   for (jj = nn-1; jj > 0; jj--)
   {
      SWAP(bb[0], bb[jj]);
      adjust(vv,1,jj);
   }
}


//  heapsort array of pointers to strings in user-defined order.
//  pointers are sorted, strings are not changed.

static void adjust(char *vv[], int n1, int n2, HeapSortUcomp fcomp)
{
   char     **bb, *temp;
   int      jj, kk;

   bb = vv - 1;
   jj = n1;
   kk = n1 * 2;

   while (kk <= n2)
   {
      if (kk < n2 && fcomp(bb[kk],bb[kk+1]) < 0) kk++;
      if (fcomp(bb[jj],bb[kk]) < 0) SWAP(bb[jj],bb[kk]);
      jj = kk;
      kk *= 2;
   }
}

void HeapSort(char *vv[], int nn, HeapSortUcomp fcomp)
{
   char     **bb, *temp;
   int      jj;

   for (jj = nn/2; jj > 0; jj--) adjust(vv,jj,nn,fcomp);

   bb = vv;

   for (jj = nn-1; jj > 0; jj--)
   {
      SWAP(bb[0], bb[jj]);
      adjust(vv,1,jj,fcomp);
   }
}


//  heapsort for array of strings or records, 
//  using caller-supplied record compare function.
//  HeapSortUcomp returns [ -1 0 +1 ]  for  rec1 [ < = > ] rec2
//  method: build array of pointers and sort these, then
//  use this sorted array to re-order the records at the end.

static int     *vv1, *vv2;

static void adjust(char *recs, int RL, int n1, int n2, HeapSortUcomp fcomp)
{
   int      *bb, jj, kk, temp;
   char     *rec1, *rec2;

   bb = vv1 - 1;
   jj = n1;
   kk = n1 * 2;

   while (kk <= n2)
   {
      rec1 = recs + RL * bb[kk];
      rec2 = recs + RL * bb[kk+1];
      if (kk < n2 && fcomp(rec1,rec2) < 0) kk++;
      rec1 = recs + RL * bb[jj];
      rec2 = recs + RL * bb[kk];
      if (fcomp(rec1,rec2) < 0) SWAP(bb[jj],bb[kk]);
      jj = kk;
      kk *= 2;
   }
}

void HeapSort(char *recs, int RL, int NR, HeapSortUcomp fcomp)
{
   int      *bb, jj, kk, temp, flag;
   char     *vvrec;

   vv1 = new int[NR];
   for (jj = 0; jj < NR; jj++) vv1[jj] = jj;

   for (jj = NR/2; jj > 0; jj--) adjust(recs,RL,jj,NR,fcomp);

   bb = vv1 - 1;

   for (jj = NR-1; jj > 0; jj--)
   {
      SWAP(bb[1], bb[jj+1]);
      adjust(recs,RL,1,jj,fcomp);
   }

   vv2 = new int[NR];
   for (jj = 0; jj < NR; jj++) vv2[vv1[jj]] = jj;

   vvrec = new char[RL];
   flag = 1;
   while (flag)
   {
      flag = 0;
      for (jj = 0; jj < NR; jj++)
      {
         kk = vv2[jj];
         if (kk == jj) continue;
         memmove(vvrec,recs+jj*RL,RL);
         memmove(recs+jj*RL,recs+kk*RL,RL);
         memmove(recs+kk*RL,vvrec,RL);
         SWAP(vv2[jj],vv2[kk]);
         flag = 1;
      }
   }

   delete vv1;
   delete vv2;
   delete vvrec;
}


/**************************************************************************

         int MemSort (char *RECS, int RL, int NR, int KEYS[][3], int NK)

         RECS is an array of records, to be sorted in-place.
         (record length = RL, record count = NR)

         KEYS[NK,3]  is an integer array defined as follows:
              [N,0]    starting position of Nth key field in RECS
              [N,1]    length of Nth key field in RECS
              [N,2]    type of sort for Nth key:
                        1 = char ascending
                        2 = char descending
                        3 = int*4 ascending (int, long)
                        4 = int*4 descending
                        5 = float*4 ascending (float)
                        6 = float*4 descending
                        7 = float*8 ascending (double)
                        8 = float*8 descending

***************************************************************************/

int MemSortComp(const char *rec1, const char *rec2);
int MemSortKeys[10][3], MemSortNK;

int MemSort(char *RECS, int RL, int NR, int KEYS[][3], int NK)
{
   int   ii;

   if (NR < 2) return 1;

   if (NK > 10) appcrash("MemSort, bad NK");
   if (NK < 1) appcrash("MemSort, bad NK");

   MemSortNK = NK;

   for (ii = 0; ii < NK; ii++)
   {
      MemSortKeys[ii][0] = KEYS[ii][0];
      MemSortKeys[ii][1] = KEYS[ii][1];
      MemSortKeys[ii][2] = KEYS[ii][2];
   }

   HeapSort(RECS,RL,NR,MemSortComp);
   return 1;
}

int MemSortComp(const char *rec1, const char *rec2)
{
   int            ii, stat, kpos, ktype, kleng;
   int            inum1, inum2;
   float          rnum1, rnum2;
   double         dnum1, dnum2;
   const char     *p1, *p2;

   for (ii = 0; ii < MemSortNK; ii++)                                      //  loop each key
   {
      kpos = MemSortKeys[ii][0];                                           //  relative position
      kleng = MemSortKeys[ii][1];                                          //  length
      ktype = MemSortKeys[ii][2];                                          //  type

      p1 = rec1 + kpos;                                                    //  absolute position
      p2 = rec2 + kpos;

      switch (ktype)
      {
         case 1:                                                           //  char ascending
            stat = strncmp(p1,p2,kleng);                                   //  compare 2 key values
            if (stat) return stat;                                         //  + if rec1 > rec2, - if <
            break;                                                         //  2 keys are equal, check next key

         case 2:                                                           //  char descending
            stat = strncmp(p1,p2,kleng);
            if (stat) return -stat;
            break;

         case 3:                                                           //  int ascending
            memmove(&inum1,p1,4);
            memmove(&inum2,p2,4);
            if (inum1 > inum2) return 1;
            if (inum1 < inum2) return -1;
            break;

         case 4:                                                           //  int descending
            memmove(&inum1,p1,4);
            memmove(&inum2,p2,4);
            if (inum1 > inum2) return -1;
            if (inum1 < inum2) return 1;
            break;

         case 5:                                                           //  float ascending
            memmove(&rnum1,p1,4);
            memmove(&rnum2,p2,4);
            if (rnum1 > rnum2) return 1;
            if (rnum1 < rnum2) return -1;
            break;

         case 6:                                                           //  float descending
            memmove(&rnum1,p1,4);
            memmove(&rnum2,p2,4);
            if (rnum1 > rnum2) return -1;
            if (rnum1 < rnum2) return 1;
            break;

         case 7:                                                           //  double ascending
            memmove(&dnum1,p1,8);
            memmove(&dnum2,p2,8);
            if (dnum1 > dnum2) return 1;
            if (dnum1 < dnum2) return -1;
            break;

         case 8:                                                           //  double descending
            memmove(&dnum1,p1,8);
            memmove(&dnum2,p2,8);
            if (dnum1 > dnum2) return -1;
            if (dnum1 < dnum2) return 1;
            break;

         default:                                                          //  key type not 1-8
            appcrash("MemSort, bad KEYS sort type");
      }
   }

   return 0;                                                               //  records match on all keys
}


/**************************************************************************/

//  random number generators with explicit context
//  and improved randomness over a small series

int lrandz(int64 *seed)                                                    //  returns 0 to 0x7fffffff
{
   *seed = *seed ^ (*seed << 17);
   *seed = *seed ^ (*seed << 20);
   return nrand48((unsigned int16 *) seed);
}

double drandz(int64 *seed)                                                 //  returns 0.0 to 0.99999...
{
   *seed = *seed ^ (*seed << 17);
   *seed = *seed ^ (*seed << 20);
   return erand48((unsigned int16 *) seed);
}


/**************************************************************************
   Quick Math Functions

   These functions duplicate C library standard functions but provide 
   faster execution at the expense of precision. The precision is 
   generally 5 or 6 digits, but test and use with caution.
   
   They all build a table of values on first call (1-2 millisecs),
   which is used to interpolate values in subsequent calls.

   The argument range is also limited - see comments in each function.
   
   Following benchmarks done with Intel Core 2 Duo at 2.4 GHz

   double qsine(double)       3.2 times as fast as sin()
   double qcosine(double)     3.2 times as fast as cos()
   double qarcsine(double)    5.1 times as fast as asin()
   double qsqrt(double)       2.1 times as fast as sqrt()

***************************************************************************/

double qsine(double x)                                                     //  sine(x) for x = -pi to +pi
{                                                                          //  (seg. fault if out of range)
   int            ii;
   static int     ftf = 1;
   double         v1, s1, s2, ans;
   static double  base = 3.15;                                             //  pi + a bit more
   static double  K1 = base / 500.0;
   static double  K2 = 500.0 / base;
   static double  v[1002], sinv[1002];

   if (ftf) {
      ftf = 0;
      for (ii = 0; ii < 1002; ii++)
      {
         v1 = ii * K1 - base;                                              //  -pi to +pi (a bit more each end)
         v[ii] = v1;
         sinv[ii] = sin(v1);
      }
   }
   
   ii = int(K2 * (x + base));
   v1 = v[ii];
   s1 = sinv[ii];
   s2 = sinv[ii+1];
   ans = s1 + (s2 - s1) * (x - v1) * K2;
   return ans;
}

double qcosine(double x)                                                   //  cosine(x) for x = -pi to +pi
{                                                                          //  (seg. fault if out of range)
   int            ii;
   static int     ftf = 1;
   double         v1, s1, s2, ans;
   static double  base = 3.15;                                             //  pi + a bit more
   static double  K1 = base / 500.0;
   static double  K2 = 500.0 / base;
   static double  v[1002], cosinv[1002];

   if (ftf) {
      ftf = 0;
      for (ii = 0; ii < 1002; ii++)
      {
         v1 = ii * K1 - base;                                              //  -pi to +pi (a bit more each end)
         v[ii] = v1;
         cosinv[ii] = cos(v1);
      }
   }
   
   ii = int(K2 * (x + base));
   v1 = v[ii];
   s1 = cosinv[ii];
   s2 = cosinv[ii+1];
   ans = s1 + (s2 - s1) * (x - v1) * K2;
   return ans;
}

double qarcsine(double x)                                                  //  arcsine(x) for x = -1 to +1
{                                                                          //  (seg. fault if out of range)
   int            ii;
   static int     ftf = 1;
   double         v1, s1, s2, ans;
   static double  base = 1.0;
   static double  K1 = base / 1000.0;
   static double  K2 = 1000.0 / base;
   static double  v[2001], arcsinv[2001];

   if (ftf) {
      ftf = 0;
      for (ii = 0; ii < 2001; ii++)
      {
         v1 = ii * K1 - base;                                              //  -1 to +1, exactly
         v[ii] = v1;
         arcsinv[ii] = asin(v1);
      }
   }

   ii = int(K2 * (x + base));
   if (ii < 50 || ii > 1950) return asin(x);                               //  improve accuracy near 90 deg.
   v1 = v[ii];
   s1 = arcsinv[ii];
   s2 = arcsinv[ii+1];
   ans = s1 + (s2 - s1) * (x - v1) * K2;
   return ans;
}

double qsqrt(double x)                                                     //  sqrt(x) for x = 0 to 1
{                                                                          //  (seg. fault if out of range)
   int               ii;
   static int        ftf = 1;
   static double     *sqrtv;

   if (ftf) {
      ftf = 0;
      sqrtv = (double *) zmalloc(100001 * sizeof(double));
      for (ii = 0; ii < 100001; ii++)
            sqrtv[ii] = sqrt(0.00001 * ii + 0.000005);
   }

   ii = int(100000.0 * x);
   if (ii < 100) return sqrt(x);                                           //  improve accuracy near zero

   return sqrtv[ii];                                                       //  interpolation slower than sqrt()
}                                                                          //  (done in cpu microcode)


/**************************************************************************

   spline1: define a curve using a set of data points (x and y values)        v.2.11
   spline2: for a given x-value, return a y-value fitting the curve

   For spline1, the no. of curve-defining points must be < 100.
   For spline2, the given x-value must be within the range defined in spline1.

***************************************************************************/

namespace splinedata 
{
   int      nn;
   double   px1[100], py1[100], py2[100];
}

using namespace splinedata;


void spline1(int dnn, double *dx1, double *dy1)
{
   double   sig, p, u[100];
   int      ii;
   
   nn = dnn;
   if (nn > 100) zappcrash("spline1(), > 100 data points");

   for (ii = 0; ii < nn; ii++)
   {
      px1[ii] = dx1[ii];
      py1[ii] = dy1[ii];
      if (ii && px1[ii] <= px1[ii-1]) 
         zappcrash("spline1(), x-value not increasing");
   }

   py2[0] = u[0] = 0;
   
   for (ii = 1; ii < nn-1; ii++)
   {
      sig = (px1[ii] - px1[ii-1]) / (px1[ii+1] - px1[ii-1]);
      p = sig * py2[ii-1] + 2;
      py2[ii] = (sig - 1) / p;
      u[ii] = (6 * ((py1[ii+1] - py1[ii]) / (px1[ii+1] - px1[ii]) - (py1[ii] - py1[ii-1])
            / (px1[ii] - px1[ii-1])) / (px1[ii+1] - px1[ii-1]) - sig * u[ii-1]) / p;
   }
   
   py2[nn-1] = 0;
   
   for (ii = nn-2; ii >= 0; ii--)
      py2[ii] = py2[ii] * py2[ii+1] + u[ii];

   return;
}


double spline2(double x)
{
   int      kk, klo = 0, khi = nn-1;
   double   h, a, b, y;
   
   while (khi - klo > 1)
   {
      kk = (khi + klo) / 2;
      if (px1[kk] > x) khi = kk;
      else klo = kk;
   }
   
   h = px1[khi] - px1[klo];
   a = (px1[khi] - x) / h;
   b = (x - px1[klo]) / h;
   y = a * py1[klo] + b * py1[khi] + ((a*a*a - a) * py2[klo] 
                                   + (b*b*b - b) * py2[khi]) * (h*h) / 6;
   
   return y;
}

using namespace std;


/**************************************************************************
   Initialize application files according to following conventions:
     + use application directories defined by Makefile 
     + user data files are in /home/user/.appname/
     + if not already attached to a terminal, redirect stdout/stderr 
       to log file at /home/user/.appname/appname.log
   
   zappname    application name:          dcopp, fotoxx, etc.
   zdatadir    installed data files       translations, parameters, etc.
   zdocdir     user documentation         userguide, README, CHANGES
   zicondir    icons                      icon files .png
   zuserdir    /home/user/.appname/       log file, user parameters
***************************************************************************/

char     zappname[20];
char     zdatadir[200], zdocdir[200], zicondir[200], zuserdir[200];
char     zlanguage[8] = "en";                                              //  "lc" or "lc_RC"   v.2.14

const char * get_zuserdir() { return  zuserdir; }                          //  /home/user/.appname
const char * get_zdatadir() { return  zdatadir; }                          //  parameters, icons

int initz_appfiles(const char *appname, ...)
{
   char           work[200], *pp;
   const char     *appfile;
   int            err;
   FILE           *fid;
   struct stat    statdat;
   va_list        arglist;
   
   strcpy(zappname,appname);

   strcpy(zdatadir,DATADIR);                                               //  macros from build script
   strcpy(zdocdir,DOCDIR);
   strcpy(zicondir,zdatadir);
   strcat(zicondir,"/icons");

   pp = cuserid(0);
   if (strEqu(pp,"root")) snprintf(zuserdir,199,"/root/.%s",zappname);     //  get /root/.appname
   else snprintf(zuserdir,199,"%s/.%s",getenv("HOME"),zappname);           //  or /home/user/.appname

   err = stat(zuserdir,&statdat);                                          //  does it exist already?
   if (err) {
      snprintf(work,199,"mkdir -m 0700 %s",zuserdir);                      //  no, create it
      err = system(work);
      if (err) zappcrash("%s, %s \n",wstrerror(err),work);                 //  cannot

      va_start(arglist,appname);                                           //  copy req. application files
      while (true) {                                                       //   from install directory to 
         appfile = va_arg(arglist, const char *);                          //    to /home/user/.appname/
         if (! appfile) break;
         snprintf(work,199,"cp %s/%s %s",zdatadir,appfile,zuserdir);
         err = system(work);
         if (err) printf("%s, %s \n",wstrerror(err),work);                 //  GTK not started yet
      }
      va_end(arglist);
   }

   if (! isatty(1)) { 
      snprintf(work,199,"%s/%s.log",zuserdir,zappname);                    //  if not attached to a terminal,
      fid = freopen(work,"w",stdout);                                      //   redirect output to log file
      fid = freopen(work,"w",stderr);                                      //    /home/user/.appname/appname.log
      printf("%s message and error log \n",zappname);                      //  append to old file   v.2.5
   }

   return 1;
}


//  display help file in a separate process so application is not blocked

void showz_helpfile()
{
   int      err;
   char     docfile[200], lang[4];

   snprintf(docfile,199,"%s/userguide-%s.html",zdocdir,zlanguage);         //  .../userguide-lc_RC.html  v.2.14
   err = access(docfile,R_OK);
   if (err) {
      strncpy0(lang,zlanguage,3);
      snprintf(docfile,199,"%s/userguide-%s.html",zdocdir,lang);           //  .../userguide-lc.html
      err = access(docfile,R_OK);
   }

   if (err) {
      snprintf(docfile,199,"%s/userguide-en.html",zdocdir);                //  .../userguide-en.html
      err = access(docfile,R_OK);
   }

   if (err) {
      zmessageACK(ZTX("help file not found: %s"),docfile);
      return;
   }

   showz_html(docfile);                                                    //  v.2.18
   return;
}


//  display various admin text files in popup window

void showz_readme()
{
   char     command[200];
   snprintf(command,199,"cat %s/README",zdocdir);
   popup_command(command,500,300);
   return;
}

void showz_changelog()                                                     //  1st 50 lines
{
   char     command[200];
   snprintf(command,199,"head -n 50 %s/CHANGES",zdocdir);
   popup_command(command,500,300);
   return;
}

void showz_translations()
{
   char     command[200];
   snprintf(command,199,"cat %s/TRANSLATIONS",zdocdir);
   popup_command(command,500,300);
   return;
}


//  create a desktop icon / launcher with icon                             //  v.2.18
//  target system needs to be LSB compliant

void zmake_launcher(const char *categories, const char *genericname)
{
   char     dtfile[200], work[200];
   FILE     *fid;
   int      err;
   
   snprintf(dtfile,199,"%s/Desktop/kornelix-%s.desktop",getenv("HOME"),zappname);
   fid = fopen(dtfile,"w");
   if (! fid) {
      zmessageACK(ZTX("error: %s"),strerror(errno));
      return;
   }
   
   fputs("[Desktop Entry]\n",fid);
   snprintf(work,199,"Name=%s\n",zappname);
   fputs(work,fid);
   snprintf(work,199,"Categories=%s\n",categories);
   fputs(work,fid);
   fputs("Type=Application\n",fid);
   fputs("Terminal=false\n",fid);
   snprintf(work,199,"Exec=%s/%s\n",BINDIR,zappname);
   fputs(work,fid);
   snprintf(work,199,"Icon=%s/icons/%s.png\n",DATADIR,zappname);
   fputs(work,fid);
   snprintf(work,199,"GenericName=%s\n",genericname);
   fputs(work,fid);
   fclose(fid);

   snprintf(work,199,"xdg-desktop-menu install %s",dtfile);
   err = system(work);
   if (err) zmessLogACK("error: %s",wstrerror(err));
   return;
}


/**************************************************************************

   Translation Functions               v.2.9 revised to use .po files
   
   Translation files are standard .po files as used in the Gnu gettext
   system. However the .po files are used directly, and there is no need
   to merge and compile them into a binary format (.mo files).

   Initialize: int ZTXinit(const char *lang)
      lang is "lc" or "lc_RC" or current locale if null.
      status: 0 = OK, 1 = cannot process .po translation file(s).

   If files are found in both  .../lc/xxx.po  and  .../lc_RC/xxx.po,
   both sets of files are processed. If a translation is present in
   both sets, the regional dialect (lc_RC) will be used.

   Translate a text string: const char * ZTX(const char *english)
      english: text string to translate, possibly containing 
      printf formats (%d %s ...) and escaped characters (\" \n ...).

   If the user language is English or if no translation is found, 
   the input string is returned, else the translated string.
   
   A text string may have a context part "context::string", where
   "context" is any string < 30 characters and "string" is the 
   English text or the translation text. The context part "context::" 
   is removed in the returned string. This is to handle the case where 
   a single English string may need multiple translations, depending 
   on context. The English string may be present multiple times in a 
   .po file, each one marked with a different context and having a 
   different translation. Context is optional in translation strings.
   
   example: 
      
      program code: 

         printf(ZTX("answer: %d %s \n more on next line"), 123, "qwerty");
      
      A German .po file would have the following entry:

         msgid: "answer: %d %s \n"
                " more on next line"
         msgstr: "Antwort: %d %s \n"
                 " mehr in der nächsten Zeile"

***************************************************************************/

namespace ZTXnames                                                         //  remove GOFUNC usage becasue of
{                                                                          //    GCC optimization errors  v.2.18
   FILE     *fid;
   char     buff[ZTXmaxcc], *porec, *ppq1, *ppq2, lc_RC[8];
   char     *pstring, Estring[ZTXmaxcc], Tstring[ZTXmaxcc];
   int      ent, scc;
   void     ZTXread_pofiles();
   void     ZTXgetstring();
   char     **ZTXenglish = null;                                           //  English strings and 
   char     **ZTXtrans = null;                                             //    corresp. translations
}

using namespace ZTXnames;

int ZTXinit(const char *lang)                                              //  initialize translations
{
   int      ii;
   char     *pp;

   if (ZTXenglish) {
      for (ii = 0; ZTXenglish[ii]; ii++) {                                 //  free prior translation
         zfree(ZTXenglish[ii]);
         if (ZTXtrans[ii]) zfree(ZTXtrans[ii]);
      }
      zfree(ZTXenglish);
      zfree(ZTXtrans);
      ZTXenglish = ZTXtrans = null;                                        //  set no translation
   }

   if (! blank_null(lang)) strncpy0(zlanguage,lang,6);                     //  use language from caller
   else {                                                                  //  help Linux chaos     v.2.9.1
      pp = getenv("LANG");                                                 //  use $LANG if defined
      if (! pp) pp = getenv("LANGUAGE");                                   //  use $LANGUAGE if defined 
      if (! pp) pp = setlocale(LC_MESSAGES,"");                            //  use locale if defined
      if (pp) strncpy0(zlanguage,pp,6);                                    //  "lc_RC" lang/region codes  v.2.14
      else strcpy(zlanguage,"en");                                         //  use English
   }

   if (*zlanguage < 'a') strcpy(zlanguage,"en");                           //  use English if garbage
   printf("language: %s \n",zlanguage);

   if (strEqu(zlanguage,"en")) return 0;                                   //  English, do nothing

   ZTXenglish = (char **) zmalloc(ZTXmaxent * sizeof(char *));             //  allocate slots for english text
   ZTXtrans = (char **) zmalloc(ZTXmaxent * sizeof(char *));               //    strings and translations
   ent = 0;

   strncpy0(lc_RC,zlanguage,6);                                            //  process .../locales/lc_RC/*.po
   if (strlen(lc_RC) > 3 && strNeq(lc_RC,"en_US")) 
      ZTXread_pofiles();

   strncpy0(lc_RC,zlanguage,3);                                            //  process .../locales/lc/*.po
   if (strNeq(lc_RC,"en")) ZTXread_pofiles();

   ZTXenglish[ent] = ZTXtrans[ent] = null;                                 //  mark both EOLs
   return 0;
}

void ZTXnames::ZTXread_pofiles()
{
   int      ii, contx = 0;
   char     *pofile, command[200];

   snprintf(command,199,"find %s/locales/%s/*.po 2>/dev/null",zdatadir,lc_RC);

   while ((pofile = command_output(contx,command)))
   {
      fid = fopen(pofile,"r");                                             //  open .po file
      if (! fid) {
         printf("cannot open translation file: %s \n",pofile);             //  error, ignore file
         zfree(pofile);
         continue;
      }
      
      porec = 0;                                                           //  no .po record yet
      *Estring = *Tstring = 0;                                             //  no strings yet

      while (true)
      {
         if (! porec) porec = fgets_trim(buff,ZTXmaxcc,fid);               //  get next .po record
         if (! porec) break;                                               //  EOF
         
         if (blank_null(porec)) {                                          //  blank record
            porec = 0;
            continue;
         }
         if (*porec == '#') {                                              //  comment
            porec = 0;
            continue;
         }

         if (strnEqu(porec,"msgid",5))                                     //  start new english string
         {
            if (*Estring) {                                                //  two in a row
               printf(" no translation: %s \n",Estring);
               *Estring = 0;
            }

            if (*Tstring) {                                                //  should not happen
               printf(" orphan translation: %s \n",Tstring);
               *Tstring = 0;
            }

            porec += 5;                                                    //  parse string (multiple pieces)
            pstring = Estring;
            scc = 0;
            ZTXgetstring();

            if (*Estring) {
               strcpy(Tstring,Estring);                                    //  borrow Tstring
               clean_escapes(Tstring);
               for (ii = 0; ZTXenglish[ii]; ii++)                          //  find existing entry, if any
                  if (strEqu(Tstring,ZTXenglish[ii])) break;
               if (ii < ent) {
                  {  /*** duplicate english ***/  }
                  *Estring = 0;
               }
               *Tstring = 0;
            }
            else  {  /*** empty english string ***/  }
         }

         else if (strnEqu(porec,"msgstr",6))                               //  start new translation
         {
            porec += 6;                                                    //  parse the string
            pstring = Tstring;
            scc = 0;
            ZTXgetstring();
            
            if (! *Tstring) {
               if (*Estring) printf(" no translation: %s \n",Estring);
               *Estring = *Tstring = 0;
            }
            else if (! *Estring) {
               {  /*** orphan translation ***/  }
               *Estring = *Tstring = 0;
            }
         }
         
         else
         {
            printf(" unrecognized input record: %s \n",porec);
            porec = 0;
            continue;
         }
         
         if (*Estring && *Tstring)
         {
            if (ent == ZTXmaxent)
               zappcrash("more than %d translations",ZTXmaxent);
            clean_escapes(Estring);
            clean_escapes(Tstring);
            ZTXenglish[ent] = strdupz(Estring);                            //  add new English string
            ZTXtrans[ent] = strdupz(Tstring);                              //    and translation
            ent++;
            *Estring = *Tstring = 0;
         }
      }

      fclose(fid);
      zfree(pofile);
   }
   
   return;
}

void ZTXnames::ZTXgetstring()
{
   int      cc;

   while (true)                                                            //  join multiple quoted strings
   {
      while (*porec && *porec != '"') porec++;                             //  find opening string quote
      {
         if (! *porec) {
            porec = fgets_trim(buff,ZTXmaxcc,fid);                         //  get next .po record
            if (! porec) return;
            if (strnEqu(porec,"msgid",5)) return;
            if (strnEqu(porec,"msgstr",6)) return;
         }
      }
      ppq1 = porec;
      ppq2 = ppq1 + 1;
      while ((*ppq2 && *ppq2 != '"') ||                                    //  find closing (non-escaped) quote
             (*ppq2 == '"' && *(ppq2-1) == '\\')) ppq2++;
      if (! *ppq2) return;
      cc = ppq2 - ppq1 - 1;
      if (cc && scc + cc + 1 < ZTXmaxcc) {                                 //  truncate if too long
         strncpy0(pstring+scc,ppq1+1,cc+1);                                //  accum. substrings, minus quotes
         scc += cc;
      }
      porec = ppq2 + 1;
   }
   
   return;
}

using namespace std;


//  Translate the input english string or return the input string.
//  Look for "context::string" and return "string" only if context found.
//
//  This function is not efficient: may need a few microseconds.
//  This can be improved if needed by sorting the english strings and 
//  using a binary search (1000 strings >> 10 compares instead of 1-1000).

const char * ZTX(const char *english)
{
   const char  *pp, *pp2;
   
   if (! english) return 0;

   pp = 0;

   if (ZTXtrans) 
   {
      for (int ii = 0; ZTXenglish[ii]; ii++)                               //  find translation
      {
         if (strEqu(english,ZTXenglish[ii])) {
            pp = ZTXtrans[ii];
            break;
         }
      }
   }
   
   if (! pp) pp = english;
   
   for (pp2 = pp; *pp2 && pp2 < pp+30; pp2++)                              //  remove context if present  v.2.11
      if (*pp2 == ':' && *(pp2+1) == ':') return pp2+2;

   return pp;
}


/**************************************************************************
   GTK windowing system utility functions
***************************************************************************/

//  functions to lock GTK calls from threads only
//    - do nothing if called from main thread (avoid fatal bug)
//    - detect nested calls and make inoccuous (avoid fatal bug)

#define     tmax 20                                                        //  max. simultaneous GTK threads
pthread_t   tid_main = 0;
pthread_t   tids[tmax];
int         tlocks[tmax];
int         zinit = 0;
mutex       zmutex;

void zlockInit()                                                           //  initz. call from main()
{
   tid_main = pthread_self();
   mutex_init(&zmutex,null);
   zinit++;

   for (int ii = 0; ii < tmax; ii++) {
      tids[ii] = 0; 
      tlocks[ii] = 0;
   }
   return;
}

void zlock()                                                               //  lock GTK if in a thread
{
   int         ii;
   pthread_t   tid_me;

   if (! zinit) zappcrash("zlock(): zinit() not done");

   tid_me = pthread_self();
   if (pthread_equal(tid_main,tid_me)) return;                             //  main() thread, do nothing

   mutex_lock(&zmutex);

   for (ii = 0; ii < tmax; ii++)                                           //  find my thread slot
         if (pthread_equal(tids[ii],tid_me)) goto zret;
   for (ii = 0; ii < tmax; ii++)                                           //  find a free slot
         if (tids[ii] == 0) goto zret;
   zappcrash("zlock(): too many threads");

zret:
   tids[ii] = tid_me;
   ++tlocks[ii];
   mutex_unlock(&zmutex);

   if (tlocks[ii] == 1) gdk_threads_enter();                               //  1st lock, lock GTK
   return;
}

void zunlock()                                                             //  unlock GTK if in a thread
{
   int         ii;
   pthread_t   tid_me;

   tid_me = pthread_self();
   if (pthread_equal(tid_main,tid_me)) return;                             //  main() thread, do nothing

   for (ii = 0; ii < tmax; ii++)                                           //  find this thread
      if (pthread_equal(tids[ii],tid_me)) break;
   if (ii == tmax) zappcrash("zunlock(): not locked");

   --tlocks[ii];                                                           //  decrement locks
   if (tlocks[ii] == 0) {
      tids[ii] = 0;                                                        //  last lock removed, free slot
      gdk_flush();
      gdk_threads_leave();                                                 //  unlock GTK
   }
   return;
}


//  iterate main loop every "skip" calls and only if in main() thread

void zmainloop(int skip)                                                   //  v.2.8
{
   static int  xskip = 0;
   pthread_t   tid_me;
   
   if (skip) {
      if (++xskip < skip) return;
      xskip = 0;
   }

   if (! zinit) zappcrash("zmainloop(): zinit() not done");
   tid_me = pthread_self();
   if (! pthread_equal(tid_main,tid_me)) return;

   while (gtk_events_pending()) gtk_main_iteration(); 
   return;
}


//  write message to text view window
//  line:   +N    existing lines from top (replace)
//          -N    existing lines from bottom (replace)
//           0    next line (add new line at bottom)
//  scroll logic assumes only one \n per message

void wprintx(GtkWidget *mLog, int line, const char *message, const char *font)
{
   static GtkTextMark      *endMark = 0;
   GtkTextBuffer           *textBuff;
   GtkTextIter             iter1, iter2;
   static GtkTextTag       *fontag, *fontags[20];
   static char             *fontmem[20];
   static int              nfonts = 0;
   int                     ii, nlines, scroll = 0;

   zlock();
   
   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog));
   
   endMark = gtk_text_buffer_get_mark(textBuff,"wpxend");                  //  new buffer?
   if (! endMark) {
      gtk_text_buffer_get_end_iter(textBuff,&iter1);                       //  yes, set my end mark
      endMark = gtk_text_buffer_create_mark(textBuff,"wpxend",&iter1,0);
      for (ii = 0; ii < nfonts; ii++) zfree(fontmem[ii]);                  //  free font tags memory
      nfonts = 0;
   }

   if (font) {
      for (ii = 0; ii < nfonts; ii++) 
            if (strEqu(font,fontmem[ii])) break;                           //  use existing font tag if poss.
      if (ii == nfonts) {
         if (nfonts == 20) zappcrash("wprintx: exceed 20 font tags");
         fontmem[ii] = strdupz(font);                                      //  create new font tag
         fontags[ii] = gtk_text_buffer_create_tag(textBuff,0,"font",font,0);
         nfonts++;
      }
      fontag = fontags[ii];
   }
   
   nlines = gtk_text_buffer_get_line_count(textBuff);                      //  lines now in buffer

   if (line == 0) scroll++;                                                //  auto scroll is on

   if (line < 0) {
      line = nlines + line + 1;                                            //  last lines: -1, -2 ...
      if (line < 1) line = 1;                                              //  above top, use line 1
   }
   
   if (line > nlines) line = 0;                                            //  below bottom, treat as append

   if (line == 0) gtk_text_buffer_get_end_iter(textBuff,&iter1);           //  append new line
   
   if (line > 0) {
      gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line-1);            //  old line start
      if (line < nlines)
          gtk_text_buffer_get_iter_at_line(textBuff,&iter2,line);          //  old line end
      if (line == nlines)                                                  //    or buffer end
          gtk_text_buffer_get_end_iter(textBuff,&iter2);                   
      gtk_text_buffer_delete(textBuff,&iter1,&iter2);                      //  delete old line
   }
                                                                           //  insert new line
   if (font)                                                               //    with optional font
         gtk_text_buffer_insert_with_tags(textBuff,&iter1,message,-1,fontag,null);
   else  gtk_text_buffer_insert(textBuff,&iter1,message,-1);

   if (scroll)                                                             //  scroll line into view
      gtk_text_view_scroll_to_mark(GTK_TEXT_VIEW(mLog),endMark,0,0,1,1);

   zmainloop();
   zunlock();
   return;
}

void wprintf(GtkWidget *mLog, int line, cchar *mess, ... )                 //  "printf" version
{
   va_list  arglist;
   char     message[1000];
   int      cc;

   va_start(arglist,mess);
   cc = vsnprintf(message,999,mess,arglist);
   va_end(arglist);

   wprintx(mLog,line,message);
   return;
}

void wprintf(GtkWidget *mLog, cchar *mess, ... )                           //  "printf", scrolling output
{
   va_list  arglist;
   char     message[1000];
   int      cc;

   va_start(arglist,mess);
   cc = vsnprintf(message,999,mess,arglist);                               //  stop overflow, remove warning
   va_end(arglist);

   wprintx(mLog,0,message);
   return;
}


//  scroll a text view window to put a given line on screen
//  1st line = 1.  for last line use line = 0.

void wscroll(GtkWidget *mLog, int line)                                    //  v.2.3
{
   GtkTextBuffer  *textbuff;
   GtkTextIter    iter;

   zlock();
   textbuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog));
   if (line <= 0) line = gtk_text_buffer_get_line_count(textbuff);
   line = line - 1;
   gtk_text_buffer_get_iter_at_line(textbuff,&iter,line);
   gtk_text_view_scroll_to_iter(GTK_TEXT_VIEW(mLog),&iter,0,0,0,0);
   zunlock();
   return;
}


//  clear a text view window and get a new buffer (a kind of defrag)

void wclear(GtkWidget *mLog)
{
   GtkTextBuffer  *buff;

   zlock();
   buff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog));
   gtk_text_buffer_set_text(buff,"",-1);
   zunlock();
   return;
}


//  clear a text view window from designated line to end of buffer

void wclear(GtkWidget *mLog, int line)
{
   GtkTextBuffer           *textBuff;
   GtkTextIter             iter1, iter2;

   zlock();

   textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog));
   gtk_text_buffer_get_iter_at_line(textBuff,&iter1,line-1);               //  iter at line start
   gtk_text_buffer_get_end_iter(textBuff,&iter2);
   gtk_text_buffer_delete(textBuff,&iter1,&iter2);                         //  delete existing line

   zmainloop();
   zunlock();
   return;
}


//  get text records from a text view window, one per call
//  removes trailing new line characters ( \n )

char * wscanf(GtkWidget *mLog, int & ftf)
{
   GtkTextBuffer  *textBuff;
   GtkTextIter    iter1, iter2;
   static char    *precs = 0, *prec1, *pret;
   static int     cc;
   
   if (ftf)
   {                                                                       //  get all window text
      ftf = 0;
      if (precs) g_free(precs);                                            //  free prior memory if there
      zlock();
      textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(mLog));            //  get all text
      gtk_text_buffer_get_bounds(textBuff,&iter1,&iter2);
      precs = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0);
      prec1 = precs;                                                       //  1st record
      zunlock();
   }
   
   if (! precs || (*prec1 == 0))                                           //  no more records
   {
      if (precs) g_free(precs);
      precs = 0;
      return 0;
   }

   cc = 0;
   while ((prec1[cc] != 0) && (prec1[cc] != '\n')) cc++;                   //  scan for terminator
   pret = prec1;
   prec1 = prec1 + cc;                                                     //  next record
   if (*prec1 == '\n') prec1++;
   pret[cc] = 0;                                                           //  replace \n with 0
   return pret;
}


//  dump text window into file
//  return:  0: OK  +N: error

int   wfiledump_maxcc = 0;

int wfiledump(GtkWidget *mLog, char *filespec)
{
   FILE        *fid;
   char        *prec;
   int         ftf, err, cc;

   fid = fopen(filespec,"w");                                              //  open file
   if (! fid) { 
      zmessageACK(ZTX("cannot open file %s"),filespec);
      return 1; 
   }
   
   wfiledump_maxcc = 0;
   
   ftf = 1;
   while (true)
   {
      prec = wscanf(mLog,ftf);                                             //  get text line
      if (! prec) break;
      fprintf(fid,"%s\n",prec);                                            //  output with \n
      cc = strlen(prec);
      if (cc > wfiledump_maxcc) wfiledump_maxcc = cc;
   }
   
   err = fclose(fid);                                                      //  close file
   if (err) { zmessageACK("file close error"); return 2; }
   else return 0;
}


//  save text window to file, via file chooser dialog

void wfilesave(GtkWidget *mLog)
{
   int      err;
   char     *file;

   file = zgetfile(ZTX("save screen to file"),"screen-save.txt","save");
   if (! file) return;
   err = wfiledump(mLog,file);
   if (err) zmessageACK("save screen failed (%d)",err);
   zfree(file);
   return;
}


//  print text window to default printer
//  use landscape mode if max. print line > A4 width

void wprintp(GtkWidget *mLog)
{
   int      pid, err;
   char     tempfile[50], command[200];

   pid = getpid();
   snprintf(tempfile,49,"/tmp/wprintp-%d",pid);
   err = wfiledump(mLog,tempfile);
   if (err) return;

   if (wfiledump_maxcc < 97)
      snprintf(command,199,"lp -o %s -o %s -o %s -o %s -o %s -o %s %s",
                     "cpi=14","lpi=8","page-left=50","page-top=50",
                     "page-right=40","page-bottom=40",tempfile);

   else
      snprintf(command,199,"lp -o %s -o %s -o %s -o %s -o %s -o %s -o %s %s",
                     "landscape","cpi=14","lpi=8","page-left=50","page-top=50",
                     "page-right=40","page-bottom=40",tempfile);

   err = system(command);
   if (err) zmessLogACK("print error %s",wstrerror(err));
   return;
}


/**************************************************************************
   simplified GTK menu bar, tool bar, status bar functions
***************************************************************************/

int            mbIconSize = 16;                                            //  valid during menu bar construction
int            tbIconSize = 24;                                            //  valid during toolbar construction
GtkTooltips    *tbTooltips = 0;                                            //  one instance for all toolbars


//  create menu bar and add to vertical packing box

GtkWidget * create_menubar(GtkWidget *vbox, int iconsize)
{
   GtkWidget   *wmbar;

   wmbar = gtk_menu_bar_new(); 
   gtk_box_pack_start(GTK_BOX(vbox),wmbar,0,0,0);
   mbIconSize = iconsize;
   return wmbar;
}


//  add menu item to menu bar

GtkWidget * add_menubar_item(GtkWidget *wmbar, const char *mname, mtFunc func)
{
   GtkWidget   *wmitem;

   wmitem = gtk_menu_item_new_with_label(mname);
   gtk_menu_shell_append(GTK_MENU_SHELL(wmbar),wmitem);
   if (func) G_SIGNAL(wmitem,"activate",func,mname);
   return  wmitem;
}


//  add submenu item (with optional icon) to menu item 

GtkWidget * add_submenu_item(GtkWidget *wmitem, cchar *mlab, cchar *icon, mtFunc func)
{
   GtkWidget      *wmsub, *wmsubitem;
   GError         **gerror = 0;
   GdkPixbuf      *pixbuf;
   GtkWidget      *wicon = 0;
   char           iconpath[1000];

   wmsub = gtk_menu_item_get_submenu(GTK_MENU_ITEM(wmitem));
   if (wmsub == null) {
      wmsub = gtk_menu_new();
      gtk_menu_item_set_submenu(GTK_MENU_ITEM(wmitem),wmsub);
   }

   if (icon) {
      *iconpath = 0;
      strncatv(iconpath,999,zicondir,"/",icon,null);
      pixbuf = gdk_pixbuf_new_from_file_at_scale(iconpath,mbIconSize,mbIconSize,1,gerror);
      if (pixbuf) wicon = gtk_image_new_from_pixbuf(pixbuf);
   }

   if (strEqu(mlab,"separator"))                                           //  v.2.16
      wmsubitem = gtk_separator_menu_item_new();
   else {
      if (wicon) {
         wmsubitem = gtk_image_menu_item_new_with_label(mlab);
         gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(wmsubitem),wicon);
      }
      else  wmsubitem = gtk_menu_item_new_with_label(mlab);
   }

   gtk_menu_shell_append(GTK_MENU_SHELL(wmsub),wmsubitem);

   if (func) G_SIGNAL(wmsubitem,"activate",func,mlab);
   return  wmsubitem;
}


//  create toolbar and add to vertical packing box

GtkWidget * create_toolbar(GtkWidget *vbox, int iconsize, int vert)
{
   GtkWidget   *wtbar;

   wtbar = gtk_toolbar_new();
   if (vert) gtk_toolbar_set_orientation(GTK_TOOLBAR(wtbar),GTK_ORIENTATION_VERTICAL);
   gtk_box_pack_start(GTK_BOX(vbox),wtbar,0,0,0);
   tbIconSize = iconsize;
   
   if (! tbTooltips) tbTooltips = gtk_tooltips_new();
   return  wtbar;
}


//  add toolbar button with stock icon ("gtk-quit") or custom icon ("iconfile.png")

GtkWidget * add_toolbar_button(GtkWidget *wtbar, cchar *blab, cchar *btip, cchar *icon, mtFunc func)
{
   GtkToolItem    *tbutton;
   GError         **gerror = 0;
   GdkPixbuf      *pixbuf;
   GtkWidget      *wicon = 0;
   char           iconpath[1000];

   if (icon == null || *icon == 0) 
      tbutton = gtk_tool_button_new(0,0);

   else if (strnEqu(icon,"gtk-",4)) 
      tbutton = gtk_tool_button_new_from_stock(icon);

   else {
      *iconpath = 0;
      strncatv(iconpath,999,zicondir,"/",icon,null);
      pixbuf = gdk_pixbuf_new_from_file_at_scale(iconpath,tbIconSize,tbIconSize,1,gerror);
      if (pixbuf) wicon = gtk_image_new_from_pixbuf(pixbuf);
      if (wicon) tbutton = gtk_tool_button_new(wicon,0);
      else  tbutton = gtk_tool_button_new_from_stock("gtk-missing-image");
   }

   gtk_tool_button_set_label(GTK_TOOL_BUTTON(tbutton),blab);
   if (btip) gtk_tool_item_set_tooltip(tbutton,tbTooltips,btip,"");
   gtk_toolbar_insert(GTK_TOOLBAR(wtbar),GTK_TOOL_ITEM(tbutton),-1);
   G_SIGNAL(tbutton,"clicked",func,blab);
   return  (GtkWidget *) tbutton;
}


//  create a status bar and add to the start of a packing box

GtkWidget * create_stbar(GtkWidget *pbox)
{
   GtkWidget      *stbar;
   static PangoFontDescription    *fontdesc;

   stbar = gtk_statusbar_new(); 
   fontdesc = pango_font_description_from_string("Monospace 9");
   gtk_widget_modify_font(stbar,fontdesc);                                 //  *** GTK does not work ***
   gtk_box_pack_start(GTK_BOX(pbox),stbar,0,0,0);
   gtk_widget_show(stbar);
   return  stbar;
}


//  display message in status bar - callable from threads

int stbar_message(GtkWidget *wstbar, const char *message)
{
   static int     ctx = -1;

   zlock();
   if (ctx == -1) ctx = gtk_statusbar_get_context_id(GTK_STATUSBAR(wstbar),"all");
   gtk_statusbar_pop(GTK_STATUSBAR(wstbar),ctx);
   gtk_statusbar_push(GTK_STATUSBAR(wstbar),ctx,message);
   zmainloop();
   zunlock();
   return 0;
}


/**************************************************************************
   simplified GTK dialog functions
***************************************************************************/

//  private functions for interception of widget events and dialog completion

void  zdialog_widget_event(GtkWidget *, zdialog *zd);
void  zdialog_response_event(GtkWidget *, int stat, zdialog *zd);


//  create a new zdialog dialog
//  optional arguments: list of 0-5 button names, null
//  returned dialog status:  N = button N (1 to 5)
//                          -4 = "X" destroy button

zdialog * zdialog_new(const char *title, GtkWidget *parent, ...)           //  parent added     v.2.5
{
   zdialog         *zd;
   GtkWidget       *dialog;
   char            *butt[5];
   int             ii;
   va_list         arglist;
   GtkDialogFlags  flags = (GtkDialogFlags) 0;
   GtkWindow       *wparent = (GtkWindow *) parent;

   zd = (zdialog *) zmalloc(sizeof(zdialog));
   
   for (ii = 0; ii < 5; ii++) butt[ii] = 0;

   va_start(arglist,parent);

   for (ii = 0; ii < 5; ii++)
   {
      butt[ii] = va_arg(arglist, char *);                                  //  get up to 5 buttons
      if (! butt[ii]) break;
   }

   va_end(arglist);

   zlock();                                                                //  create dialog box
   dialog = gtk_dialog_new_with_buttons(title,wparent,flags,
              butt[0],1, butt[1],2, butt[2],3, butt[3],4, butt[4],5, null);
   gtk_box_set_spacing(GTK_BOX(GTK_DIALOG(dialog)->vbox),5);
   gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_MOUSE);          //  v.2.7.1

   zunlock();

   zd->sentinel = zdsentinel;                                              //  validity sentinel         v.2.2
   zd->eventCB = 0;                                                        //  no user event callback
   zd->complCB = 0;                                                        //  no user dialog completion callback
   zd->zstat = 0;                                                          //  no zdialog status
   zd->disabled = 1;                                                       //  widget signals disabled

   zd->widget[0].type = "dialog";                                          //  set up 1st widget = dialog
   zd->widget[0].name = "dialog";
   zd->widget[0].pname = 0;
   zd->widget[0].data = strdupz(title);
   zd->widget[0].cblist = 0;
   zd->widget[0].widget = dialog;

   zd->widget[1].type = 0;                                                 //  eof - no contained widgets yet
   return zd;
}


//  add widget to existing zdialog

int zdialog_add_widget (
     zdialog *zd, const char *type, const char *name, const char *pname,   //  mandatory args
     const char *data, int scc, int homog, int expand, int space)          //  optional args (default = 0)
{
   GtkWidget      *widget = 0, *pwidget = 0;
   GtkTextBuffer  *editBuff = 0;
   GdkColor       gdkcolor;
   const char     *pp, *ptype = 0;
   char           vdata[30];
   double         min, max, step, val;
   int            iiw, iip, kk, err;

   static PangoFontDescription    *monofont = 0;
   
   if (! zd) zappcrash("zdialog null pointer");                            //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) zappcrash("zdialog not valid");

   for (iiw = 1; zd->widget[iiw].type; iiw++);                             //  find next avail. slot
   if (iiw > zdmaxwidgets-2) zappcrash("too many widgets: %d",iiw);

   zd->widget[iiw].type = strdupz(type);                                   //  init. widget struct
   zd->widget[iiw].name = strdupz(name);                                   //  all strings in nonvolatile mem
   zd->widget[iiw].pname = strdupz(pname);
   zd->widget[iiw].data = 0;
   zd->widget[iiw].cblist = 0;
   zd->widget[iiw].scc = scc;
   zd->widget[iiw].homog = homog;
   zd->widget[iiw].expand = expand;
   zd->widget[iiw].space = space;
   zd->widget[iiw].widget = 0;

   zd->widget[iiw+1].type = 0;                                             //  new EOF marker

   if (strcmpv(type,"dialog","hbox","vbox","hsep","vsep","frame","scrwin",
                    "label","entry","edit","button","togbutt","check","combo",
                    "comboE","radio","spin","hscale","vscale","colorbutt", null) == 0)
      zappcrash("zdialog, bad widget type: %s",type);

   for (iip = iiw-1; iip >= 0; iip--)                                      //  find parent (container) widget
      if (strEqu(pname,zd->widget[iip].name)) break;
   if (iip < 0) zappcrash("zdialog, no parent for widget: %s",name);

   pwidget = zd->widget[iip].widget;                                       //  parent widget, type
   ptype = zd->widget[iip].type;
   
   if (strcmpv(ptype,"dialog","hbox","vbox","frame","scrwin",null) == 0)
      zappcrash("zdialog, bad widget parent type: %s",ptype);

   zlock();
      
   if (! monofont) monofont = pango_font_description_from_string("Monospace");

   if (strEqu(type,"hbox")) widget = gtk_hbox_new(homog,space);            //  expandable container boxes
   if (strEqu(type,"vbox")) widget = gtk_vbox_new(homog,space);

   if (strEqu(type,"hsep")) widget = gtk_hseparator_new();                 //  horiz. & vert. separators
   if (strEqu(type,"vsep")) widget = gtk_vseparator_new();
         
   if (strEqu(type,"frame")) {                                             //  frame around contained widgets
      widget = gtk_frame_new(data);
      gtk_frame_set_shadow_type(GTK_FRAME(widget),GTK_SHADOW_IN);
   }

   if (strEqu(type,"scrwin")) {                                            //  scrolled window container
      widget = gtk_scrolled_window_new(0,0);
      gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(widget),          //  v.2.10
                        GTK_POLICY_AUTOMATIC,GTK_POLICY_AUTOMATIC);
   }

   if (strEqu(type,"label")) widget = gtk_label_new(data);                 //  label (static text)

   if (strEqu(type,"entry")) {                                             //  1-line text entry
      widget = gtk_entry_new();
      if (data) gtk_entry_set_text(GTK_ENTRY(widget),data);
      if (scc) gtk_entry_set_width_chars(GTK_ENTRY(widget),scc);
      gtk_widget_modify_font(widget,monofont);
      G_SIGNAL(widget,"changed",zdialog_widget_event,zd)
   }
      
   if (strEqu(type,"edit")) {                                              //  multiline edit box
      widget = gtk_text_view_new();
      editBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
      if (data) gtk_text_buffer_set_text(editBuff,data,-1);
      gtk_text_view_set_editable(GTK_TEXT_VIEW(widget),1);
      gtk_widget_modify_font(widget,monofont);
      G_SIGNAL(editBuff,"changed",zdialog_widget_event,zd)                 //  buffer signals, not widget
   }
      
   if (strEqu(type,"button")) {                                            //  button
      widget = gtk_button_new_with_label(data);
      G_SIGNAL(widget,"clicked",zdialog_widget_event,zd)
   }

   if (strEqu(type,"togbutt")) {                                           //  toggle button
      widget = gtk_toggle_button_new_with_label(data);
      G_SIGNAL(widget,"toggled",zdialog_widget_event,zd)
   }

   if (strEqu(type,"check")) {                                             //  checkbox
      if (data) widget = gtk_check_button_new_with_label(data);
      else  widget = gtk_check_button_new();
      G_SIGNAL(widget,"toggled",zdialog_widget_event,zd)
   }
      
   if (strEqu(type,"combo")) {                                             //  combo box
      widget = gtk_combo_box_new_text();
      zd->widget[iiw].cblist = pvlist_create(zdcbmax);                     //  for drop-down list
      if (! blank_null(data)) {
         pvlist_append(zd->widget[iiw].cblist,data);                       //  add data to drop-down list
         gtk_combo_box_append_text(GTK_COMBO_BOX(widget),data);
         gtk_combo_box_set_active(GTK_COMBO_BOX(widget),0);
      }
      gtk_widget_modify_font(widget,monofont);
      G_SIGNAL(widget,"changed",zdialog_widget_event,zd)
   }

   if (strEqu(type,"comboE")) {                                            //  combo box with entry box
      widget = gtk_combo_box_entry_new_text();
      zd->widget[iiw].cblist = pvlist_create(zdcbmax);                     //  for drop-down list
      if (! blank_null(data)) {
         gtk_entry_set_text(GTK_ENTRY(GTK_BIN(widget)->child),data);       //  entry = initial data
         pvlist_append(zd->widget[iiw].cblist,data);                       //  add data to drop-down list
         gtk_combo_box_append_text(GTK_COMBO_BOX(widget),data);
      }
      gtk_widget_modify_font(widget,monofont);
      G_SIGNAL(widget,"changed",zdialog_widget_event,zd)
   }
      
   if (strEqu(type,"radio")) {                                             //  radio button
      for (kk = iip+1; kk <= iiw; kk++) 
         if (strEqu(zd->widget[kk].pname,pname) &&                         //  find first radio button
             strEqu(zd->widget[kk].type,"radio")) break;                   //    with same container
      if (kk == iiw) 
         widget = gtk_radio_button_new_with_label(null,data);              //  this one is first
      else 
         widget = gtk_radio_button_new_with_label_from_widget              //  not first, add to group
              (GTK_RADIO_BUTTON(zd->widget[kk].widget),data);
      G_SIGNAL(widget,"toggled",zdialog_widget_event,zd)
   }

   if (strcmpv(type,"spin","hscale","vscale",null)) {                      //  spin button or sliding scale
      pp = strField(data,'|',1); err = convSD(pp,min);                     //  locale fix
      pp = strField(data,'|',2); err += convSD(pp,max);
      pp = strField(data,'|',3); err += convSD(pp,step);
      pp = strField(data,'|',4); err += convSD(pp,val);
      if (err) { min = 0; max = 100; step = 1; val = 50; }

      if (*type == 's') {
         widget = gtk_spin_button_new_with_range(min,max,step);
         gtk_spin_button_set_value(GTK_SPIN_BUTTON(widget),val);
      }
      if (*type == 'h') {
         widget = gtk_hscale_new_with_range(min,max,step);
         gtk_range_set_value(GTK_RANGE(widget),val);
         gtk_scale_set_draw_value(GTK_SCALE(widget),0);
      }
      if (*type == 'v') {
         widget = gtk_vscale_new_with_range(min,max,step);
         gtk_range_set_value(GTK_RANGE(widget),val);
         gtk_scale_set_draw_value(GTK_SCALE(widget),0);
      }
      G_SIGNAL(widget,"value-changed",zdialog_widget_event,zd)
      sprintf(vdata,"%g",val);
      data = vdata;
   }
   
   if (strEqu(type,"colorbutt")) {                                         //  color edit button        v.2.17
      if (! data) data = "0|0|0";                                          //  data format: "nnn|nnn|nnn" = RGB
      pp = strField(data,'|',1); gdkcolor.red = 256 * atoi(pp);
      pp = strField(data,'|',2); gdkcolor.green = 256 * atoi(pp);
      pp = strField(data,'|',3); gdkcolor.blue = 256 * atoi(pp);
      widget = gtk_color_button_new_with_color(&gdkcolor);
      G_SIGNAL(widget,"color-set",zdialog_widget_event,zd)
   }
      
   //  all widget types come here

   zd->widget[iiw].widget = widget;                                        //  set widget in zdialog

   if (strEqu(ptype,"hbox") || strEqu(ptype,"vbox"))                       //  add to hbox/vbox
      gtk_box_pack_start(GTK_BOX(pwidget),widget,expand,expand,space);
   if (strEqu(ptype,"frame"))                                              //  add to frame
      gtk_container_add(GTK_CONTAINER(pwidget),widget);
   if (strEqu(ptype,"scrwin"))                                             //  add to scroll window
      gtk_container_add(GTK_CONTAINER(pwidget),widget);
   if (strEqu(ptype,"dialog"))                                             //  add to dialog box
      gtk_box_pack_start(GTK_BOX(GTK_DIALOG(pwidget)->vbox),
                                   widget,expand,expand,space);
   if (data) zd->widget[iiw].data = strdupz(data);                         //  data is nonvolatile memory

   zunlock();
   return 0;
}


//  add widget to existing zdialog - alternative form (clearer and easier code)
//  options: "scc=nn | homog | expand | space=nn"  (all optional, any order)

int zdialog_add_widget(zdialog *zd, const char *type, const char *name, 
                       const char *parent, const char *data, const char *options)                                
{
   int      stat, scc = 0, homog = 0, expand = 0, space = 0, begin = 1;
   char     pname[8];
   double   pval;
   
   while (true)
   {
      stat = strParms(begin,options,pname,8,pval);
      if (stat == -1) break;
      if (stat == 1) zappcrash("bad zdialog options: %s",options);
      if (strEqu(pname,"scc")) scc = (int(pval));
      else if (strEqu(pname,"homog")) homog = 1;
      else if (strEqu(pname,"expand")) expand = 1;
      else if (strEqu(pname,"space")) space = (int(pval));
      else zappcrash("bad zdialog options: %s",options);
   }
   
   stat = zdialog_add_widget(zd,type,name,parent,data,scc,homog,expand,space);
   return stat;
}


//  resize dialog to a size greater than initial size
//  (as determined by the included widgets)

int zdialog_resize(zdialog *zd, int width, int height)
{
   if (! zd) zappcrash("zdialog null pointer");                            //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) zappcrash("zdialog not valid");

   zlock();
   GtkWidget *window = zd->widget[0].widget;
   gtk_window_set_default_size(GTK_WINDOW(window),width,height);
   zunlock();
   return 1;
}


//  put data into a zdialog widget

int zdialog_put_data(zdialog *zd, const char *name, const char *data)
{
   GtkWidget      *widget;
   GtkTextBuffer  *textBuff;
   GdkColor       gdkcolor;
   int            iiw, iip, nn, kk;
   const char     *type, *pname, *pp;
   char           *wdata;
   double         val;
   
   if (! zd || zd->sentinel != zdsentinel) {                               //  detect destroyed dialog  v.2.2
      printf("zdialog_put_data(%s,%s), zdialog invalid \n",name,data);
      return 0;
   }
   
   for (iiw = 1; zd->widget[iiw].type; iiw++)                              //  find widget
      if (strEqu(zd->widget[iiw].name,name)) break;
   if (! zd->widget[iiw].type) return 0;
   
   type = zd->widget[iiw].type;
   widget = zd->widget[iiw].widget;
   pname = zd->widget[iiw].pname;

   wdata = zd->widget[iiw].data;
   if (wdata) zfree(wdata);                                                //  free prior data memory
   zd->widget[iiw].data = 0;

   if (data) {
      wdata = strdupz(data);                                               //  set new data for widget
      zd->widget[iiw].data = wdata;
      if (utf8_check(wdata))
         printf("zdialog: bad UTF8 encoding %s \n",wdata);                 //  v.2.4
   }
   
   zd->disabled++;                                                         //  disable for widget stuffing  v.2.9

   zlock();   

   if (strEqu(type,"label")) 
      gtk_label_set_text(GTK_LABEL(widget),data);

   if (strEqu(type,"entry")) 
      gtk_entry_set_text(GTK_ENTRY(widget),data);

   if (strEqu(type,"button"))                                              //  change button label  v.2.21
      gtk_button_set_label(GTK_BUTTON(widget),data);

   if (strEqu(type,"edit")) {
      textBuff = gtk_text_view_get_buffer(GTK_TEXT_VIEW(widget));
      gtk_text_buffer_set_text(textBuff,data,-1);
   }
   
   if (strcmpv(type,"togbutt","check","radio",null)) 
   {
      if (! data) kk = nn = 0;
      else kk = convSI(data,nn);
      if (kk != 0) nn = 0;                                                 //  data not integer, force zero
      if (nn <= 0) nn = 0; else nn = 1;
      gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(widget),nn);          //  set gtk widget value

      for (iip = 1; zd->widget[iip].type; iip++)                           //  find widgets of same type 
      {                                                                    //    having same parent       v.2.0
         if (iip == iiw) continue;
         if (strEqu(zd->widget[iip].pname,pname) && strEqu(zd->widget[iip].type,type)) 
         {
            wdata = zd->widget[iip].data;
            if (wdata) zfree(wdata);                                       //  free prior data memory
            if (nn == 0) zd->widget[iip].data = strdupz("1");
            else  zd->widget[iip].data = strdupz("0");                     //  family members = opposite  v.2.0
         }
      }
   }
   
   if (strEqu(type,"spin")) {
      kk = convSD(data,val);
      if (kk != 0) val = 0.0;
      gtk_spin_button_set_value(GTK_SPIN_BUTTON(widget),val);
   }
   
   if (strEqu(type,"colorbutt")) {                                         //  color button        v.2.17
      pp = strField(data,'|',1); gdkcolor.red = 256 * atoi(pp);
      pp = strField(data,'|',2); gdkcolor.green = 256 * atoi(pp);
      pp = strField(data,'|',3); gdkcolor.blue = 256 * atoi(pp);
      gtk_color_button_set_color(GTK_COLOR_BUTTON(widget),&gdkcolor);
   }
   
   if (strcmpv(type,"hscale","vscale",null)) {
      kk = convSD(data,val);
      if (kk != 0) val = 0.0;
      gtk_range_set_value(GTK_RANGE(widget),val);
   }
   
   if (strEqu(type,"combo")) {
      if (! blank_null(data)) {
         kk = pvlist_prepend(zd->widget[iiw].cblist,data,1);               //  add to drop-down list
         if (kk == 0)                                                      //  (only if unique)
            gtk_combo_box_prepend_text(GTK_COMBO_BOX(widget),data);
         kk = pvlist_find(zd->widget[iiw].cblist,data);
         gtk_combo_box_set_active(GTK_COMBO_BOX(widget),kk);               //  make the active entry   v.2.7
      }
      else gtk_combo_box_set_active(GTK_COMBO_BOX(widget),-1);             //  make no active entry
   }

   if (strEqu(type,"comboE")) {
      if (! blank_null(data)) {
         kk = pvlist_prepend(zd->widget[iiw].cblist,data,1);               //  add to drop-down list
         if (kk == 0)                                                      //  (only if unique)
            gtk_combo_box_prepend_text(GTK_COMBO_BOX(widget),data);
         gtk_entry_set_text(GTK_ENTRY(GTK_BIN(widget)->child),data);       //  stuff entry box with new data
      }
      else gtk_entry_set_text(GTK_ENTRY(GTK_BIN(widget)->child),"");       //  stuff entry box with nothing
   }

   zunlock();
   zd->disabled--;                                                         //  re-enable dialog   v.2.9
   return iiw;
}


//  get data from a dialog widget based on its name

const char * zdialog_get_data(zdialog *zd, const char *name)
{
   if (! zd) return 0;                                                     //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) return 0;

   for (int ii = 1; zd->widget[ii].type; ii++)
      if (strEqu(zd->widget[ii].name,name)) 
            return zd->widget[ii].data;
   return 0;
}


//  get GTK widget from zdialog and widget name

GtkWidget * zdialog_widget(zdialog *zd, const char *name)
{
   if (! zd) return 0;                                                     //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) return 0;

   for (int ii = 1; zd->widget[ii].type; ii++)
   if (strEqu(zd->widget[ii].name,name)) return zd->widget[ii].widget;
   return 0;
}


//  run the zdialog
//  if modal, return after complete with status of button (OK, cancel ...)
//  if not modal, return immediately with dialog active

int zdialog_run(zdialog *zd, zdialog_event evfunc, zdialog_compl compfunc)
{
   int         ii, zstat;
   GtkWidget   *widget, *dialog;

   if (! zd) zappcrash("zdialog null pointer");                            //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) zappcrash("zdialog not valid");

   zlock();

   dialog = zd->widget[0].widget;
   gtk_widget_show_all(dialog);                                            //  activate dialog

   for (ii = 1; zd->widget[ii].type; ii++)                                 //  *** stop auto-selection
   {                                                                       //  (GTK "feature")
      if (strEqu(zd->widget[ii].type,"entry")) {
         widget = zd->widget[ii].widget;
         gtk_editable_set_position(GTK_EDITABLE(widget),-1);
         break;
      }

      if (strEqu(zd->widget[ii].type,"comboE")) {                          //  bugfix, remove "combo"  v.2.8
         widget = zd->widget[ii].widget;
         gtk_editable_set_position(GTK_EDITABLE(GTK_BIN(widget)->child),-1);
         break;
      }
   }

   if (evfunc) zd->eventCB = (void *) evfunc;                              //  link to user event callback

   zd->disabled--;                                                         //  enable widget events

   if (compfunc) {
      G_SIGNAL(dialog,"response",zdialog_response_event,zd);               //  internal dialog response function
      zd->complCB = (void *) compfunc;                                     //  link to user completion callback
      zunlock();
      return 0;                                                            //  return now, dialog is non-modal
   }

   else zstat = gtk_dialog_run(GTK_DIALOG(dialog));                        //  modal dialog, return when complete
   zd->zstat = zstat;                                                      //  set zdialog status (from button)
   zunlock();
   return zstat;                                                           //  and return status
}


//  zdialog event handler - private function called when a widget is edited.
//  Updates data in zdialog, calls user callback function (if present).
//  This function always runs in main() thread, so zlock() unnecessary.

void zdialog_widget_event(GtkWidget *widget, zdialog *zd)
{
   GtkTextView       *textView = 0;
   GtkTextBuffer     *textBuff = 0;
   GtkTextIter       iter1, iter2;
   GdkColor          gdkcolor;
   static GtkWidget  *lastwidget = 0;
   int               ii, nn;
   const char        *name, *type, *wdata;
   char              sdata[20];
   double            dval;
   static int        cbadded = 0;

   zdialog_event  *callbackfunc = 0;                                       //  user event callback function

   if (! zd) zappcrash("zdialog null pointer");                            //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) zappcrash("zdialog not valid");

   if (zd->disabled) return;                                               //  stop re-entrance from own updates

   for (ii = 1; zd->widget[ii].type; ii++)                                 //  find widget in zdialog
      if (zd->widget[ii].widget == widget) goto found_widget;

   for (ii = 1; zd->widget[ii].type; ii++) {                               //  failed, test if buffer
      if (strEqu(zd->widget[ii].type,"edit")) {                            //    of text view widget
         textView = GTK_TEXT_VIEW(zd->widget[ii].widget);
         textBuff = gtk_text_view_get_buffer(textView);
         if (widget == (GtkWidget *) textBuff) goto found_widget;
      }
   }

   return;                                                                 //  widget not found, ignore

found_widget:

   zd->disabled++;                                                         //  stop re-entrance from own updates
                                                                           //        v.2.9
   name = zd->widget[ii].name;
   type = zd->widget[ii].type;
   wdata = 0;

   if (strEqu(type,"button")) wdata = "clicked";

   if (strEqu(type,"entry"))
         wdata = gtk_entry_get_text(GTK_ENTRY(widget));
         
   if (strEqu(type,"edit")) {
      gtk_text_buffer_get_bounds(textBuff,&iter1,&iter2);
      wdata = gtk_text_buffer_get_text(textBuff,&iter1,&iter2,0);
   }

   if (strcmpv(type,"radio","check","togbutt",null)) 
   {
      nn = gtk_toggle_button_get_active(GTK_TOGGLE_BUTTON(widget));
      if (nn == 0) wdata = "0";
      else wdata = "1";
   }

   if (strEqu(type,"combo"))
         wdata = gtk_combo_box_get_active_text(GTK_COMBO_BOX(widget));

   if (strEqu(type,"comboE"))
   {
      if (widget == lastwidget && cbadded) {
         pvlist_remove(zd->widget[ii].cblist,0);                           //  detect multiple edits (keystrokes)
         gtk_combo_box_remove_text(GTK_COMBO_BOX(widget),0);               //    and replace prior entry with new
      }
      wdata = gtk_entry_get_text(GTK_ENTRY(GTK_BIN(widget)->child));
      cbadded = 0;
      if (! blank_null(wdata)) {
         nn = pvlist_prepend(zd->widget[ii].cblist,wdata,1);               //  add entry to drop-down list
         if (nn == 0) {                                                    //  (only if unique)
            gtk_combo_box_prepend_text(GTK_COMBO_BOX(widget),wdata);
            cbadded = 1;
         }
      }
   }
   
   if (strEqu(type,"spin"))
   {
      dval = gtk_spin_button_get_value(GTK_SPIN_BUTTON(widget));
      sprintf(sdata,"%g",dval);
      wdata = sdata;
   }
   
   if (strEqu(type,"colorbutt"))                                           //  color button        v.2.17
   {
      gtk_color_button_get_color(GTK_COLOR_BUTTON(widget),&gdkcolor);
      sprintf(sdata,"%d|%d|%d",gdkcolor.red/256,gdkcolor.green/256,gdkcolor.blue/256);
      wdata = sdata;
   }
   
   if (strcmpv(type,"hscale","vscale",null))
   {
      dval = gtk_range_get_value(GTK_RANGE(widget));
      sprintf(sdata,"%g",dval);
      wdata = sdata;
   }
   
   //  all widgets come here

   if (zd->widget[ii].data) zfree(zd->widget[ii].data);                    //  clear prior data
   zd->widget[ii].data = 0;

   if (wdata) zd->widget[ii].data = strdupz(wdata);                        //  set new data
   
   lastwidget = widget;                                                    //  remember last widget updated

   if (zd->eventCB) {
      callbackfunc = (zdialog_event *) zd->eventCB;                        //  do user callback function
      callbackfunc(zd,name);
   }

   zd->disabled--;
   return;
}


//  zdialog response handler - private function 
//  called when dialog is completed
//  status corresponds to completion button from user (OK, cancel ...)

void zdialog_response_event(GtkWidget *, int zstat, zdialog *zd)
{
   zdialog_compl  *callbackfunc = 0;
   
   if (! zd) zappcrash("zdialog null pointer");                            //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) zappcrash("zdialog not valid");

   zd->zstat = zstat;                                                      //  set zdialog status 

   if (zd->complCB) {
      callbackfunc = (zdialog_compl *) zd->complCB;                        //  do user callback function
      callbackfunc(zd,zstat);
   }

   return;
}


//  send an event to an active dialog
//  returns:  0 = no active dialog, 1 = OK

int zdialog_send_event(zdialog *zd, cchar *event)                          //  new     v.2.17
{
   zdialog_event * evfunc = 0;

   if (zd && zd->sentinel == zdsentinel) {                                 //  check dialog is active
      evfunc = (zdialog_event *) zd->eventCB;
      if (evfunc) evfunc(zd,event);                                        //  call dialog event function
      else return 0;
   }
   else return 0;

   return 1;
}


//  Destroy the zdialog - must be done by zdialog_run() caller
//  (else dialog continues active even after completion button).
//  Data in widgets remains valid until zdialog_free() is called.

int zdialog_destroy(zdialog *zd)
{
   if (! zd) return 0;                                                     //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) return 0;

   zlock();                                                                //  bugfix  v.2.13
   if (zd->widget[0].widget)                                               //  multiple destroys OK   v.2.7.1
      gtk_widget_destroy(zd->widget[0].widget);                            //  destroy GTK dialog
   zd->widget[0].widget = 0;
   zunlock();

   zmainloop();                                                            //  avoid GTK bug   v.2.12
   return 1;
}


//  free zdialog memory (destroy first, if not already)

int zdialog_free(zdialog *zd)
{
   if (! zd) return 0;                                                     //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) return 0;
   
   zdialog_destroy(zd);                                                    //  destroy GTK dialog if there

   zd->sentinel = 0;                                                       //  mark invalid           v.2.2

   for (int ii = 1; zd->widget[ii].type; ii++)                             //  loop through widgets
   {
      zfree((char *) zd->widget[ii].type);                                 //  free strings
      zfree((char *) zd->widget[ii].name);
      zfree((char *) zd->widget[ii].pname);
      if (zd->widget[ii].data) zfree(zd->widget[ii].data);                 //  free data 
      if (strcmpv(zd->widget[ii].type,"combo","comboE",0))                 //  if combo box, free drop-down list
         pvlist_free(zd->widget[ii].cblist);
   }
   
   zfree(zd);                                                              //  free zdialog memory
   return 1;
}


//  convenience functions for stuffing and retrieving widget data

int zdialog_stuff(zdialog *zd, const char *name, const char *data)         //  stuff a string
{
   zdialog_put_data(zd, name, data);
   return 1;
}

int zdialog_stuff(zdialog *zd, const char *name, int idata)                //  stuff an integer
{
   char  string[16];

   sprintf(string,"%d",idata);
   zdialog_put_data(zd,name,string);
   return 1;
}

int zdialog_stuff(zdialog *zd, const char *name, double ddata)             //  stuff a double
{
   char  string[32];
   
   snprintf(string,31,"%g",ddata);                                         //  outputs decimal point or comma
   zdialog_put_data(zd,name,string);                                       //  (per locale)
   return 1;
}

int zdialog_fetch(zdialog *zd, const char *name, char *data, int maxcc)    //  fetch string data
{
   const char  *zdata;

   zdata = zdialog_get_data(zd,name);
   if (! zdata) {
      *data = 0;
      return 0;
   }
   
   return strncpy0(data,zdata,maxcc);                                      //  0 = OK, 1 = truncation  v.2.4
}

int zdialog_fetch(zdialog *zd, const char *name, int &idata)               //  fetch an integer
{
   const char  *zdata;

   zdata = zdialog_get_data(zd,name);
   if (! zdata) {
      idata = 0;
      return 0;
   }
   
   idata = atoi(zdata);
   return 1;
}

int zdialog_fetch(zdialog *zd, const char *name, double &ddata)            //  fetch a double
{
   int         stat;
   const char  *zdata;

   zdata = zdialog_get_data(zd,name);
   if (! zdata) {
      ddata = 0;
      return 0;
   }
   
   stat = convSD(zdata,ddata);                                             //  period or comma decimal point OK
   if (stat < 4) return 1;
   return 0;
}


//  append new item to combo box list without changing entry box

int zdialog_cb_app(zdialog *zd, const char *name, const char *data)
{
   int         ii, nn;

   if (! zd) zappcrash("zdialog null pointer");                            //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) zappcrash("zdialog not valid");

   if (blank_null(data)) return 0;                                         //  find widget
   for (ii = 1; zd->widget[ii].type; ii++) 
      if (strEqu(zd->widget[ii].name,name)) break;
   if (! zd->widget[ii].type) return 0;                                    //  not found
   if (! strcmpv(zd->widget[ii].type,"combo","comboE",0)) return 0;        //  not combo box

   nn = pvlist_append(zd->widget[ii].cblist,data,1);                       //  append unique
   if (nn >= 0) 
      gtk_combo_box_append_text(GTK_COMBO_BOX(zd->widget[ii].widget),data);
   return 1;
}


//  prepend new item to combo box list without changing entry box

int zdialog_cb_prep(zdialog *zd, const char *name, const char *data)
{
   int         ii, nn;

   if (! zd) zappcrash("zdialog null pointer");                            //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) zappcrash("zdialog not valid");

   if (blank_null(data)) return 0;                                         //  find widget
   for (ii = 1; zd->widget[ii].type; ii++) 
      if (strEqu(zd->widget[ii].name,name)) break;
   if (! zd->widget[ii].type) return 0;                                    //  not found
   if (! strcmpv(zd->widget[ii].type,"combo","comboE",0)) return 0;        //  not combo box

   nn = pvlist_prepend(zd->widget[ii].cblist,data,1);                      //  append unique
   if (nn == 0) 
      gtk_combo_box_prepend_text(GTK_COMBO_BOX(zd->widget[ii].widget),data);
   return 1;
}


//  get combo box drop-down list entry

char * zdialog_cb_get(zdialog *zd, const char *name, int Nth)
{
   int      ii;

   if (! zd) return 0;                                                     //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) return 0;

   for (ii = 1; zd->widget[ii].type; ii++)                                 //  find widget
      if (strEqu(zd->widget[ii].name,name)) break;
   if (! zd->widget[ii].type) return 0;                                    //  not found
   if (! strcmpv(zd->widget[ii].type,"combo","comboE",0)) return 0;        //  not combo box
   return pvlist_get(zd->widget[ii].cblist,Nth);
}


//  delete entry by name from combo drop down list                         //  v.2.4

int zdialog_cb_delete(zdialog *zd, const char *name, const char *data)
{
   int      ii, nn;

   if (! zd) return 0;                                                     //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) return 0;

   for (ii = 1; zd->widget[ii].type; ii++)                                 //  find widget
      if (strEqu(zd->widget[ii].name,name)) break;
   if (! zd->widget[ii].type) return 0;                                    //  not found
   if (! strcmpv(zd->widget[ii].type,"combo","comboE",0)) return 0;        //  not combo box

   nn = pvlist_find(zd->widget[ii].cblist,data);                           //  find entry by name
   if (nn < 0) return -1;
   pvlist_remove(zd->widget[ii].cblist,nn);                                //  remove from memory list
   gtk_combo_box_remove_text(GTK_COMBO_BOX(zd->widget[ii].widget),nn);     //    and from widget
   gtk_combo_box_set_active(GTK_COMBO_BOX(zd->widget[ii].widget),-1);      //  set no active entry
   return 0;
}


//  delete all entries from combo drop down list

int zdialog_cb_clear(zdialog *zd, const char *name)
{
   int      ii, jj, nn;

   if (! zd) return 0;                                                     //  detect destroyed dialog  v.2.2
   if (zd->sentinel != zdsentinel) return 0;

   for (ii = 1; zd->widget[ii].type; ii++)                                 //  find widget
      if (strEqu(zd->widget[ii].name,name)) break;
   if (! zd->widget[ii].type) return 0;                                    //  not found
   if (! strcmpv(zd->widget[ii].type,"combo","comboE",0)) return 0;        //  not combo box

   nn = pvlist_count(zd->widget[ii].cblist);                               //  entry count
   for (jj = nn-1; jj >= 0; jj--) {
      pvlist_remove(zd->widget[ii].cblist,jj);                             //  remove from memory list
      gtk_combo_box_remove_text(GTK_COMBO_BOX(zd->widget[ii].widget),jj);  //  remove from widget
   }

   gtk_combo_box_set_active(GTK_COMBO_BOX(zd->widget[ii].widget),-1);      //  set no active entry
   if (strEqu(zd->widget[ii].type,"comboE"))                               //  stuff entry box with nothing
      gtk_entry_set_text(GTK_ENTRY(GTK_BIN(zd->widget[ii].widget)->child),"");
   return 0;
}


/**************************************************************************
    GTK misc. utility functions
***************************************************************************/

//  output text to a popup window
//  action: open, write, close                                             //  v.2.4

zdialog     *wtpopup_zd = 0;                                               //  v.2.18   bugfix
GtkWidget   *wtpopup_wedit = 0;

int write_popup_text(const char *action, const char *text, int ww, int hh)
{
   int wtpopup_zdcompl(zdialog *zd, int zstat);

   if (strEqu(action,"open"))
   {
      if (wtpopup_zd) zappcrash("write_popup_text re-entered");

      wtpopup_zd = zdialog_new(text,null,"OK",null);
      zdialog_add_widget(wtpopup_zd,"scrwin","scrwin","dialog",0,"expand");
      zdialog_add_widget(wtpopup_zd,"edit","edit","scrwin");
      wtpopup_wedit = zdialog_widget(wtpopup_zd,"edit");
      zdialog_resize(wtpopup_zd,ww,hh);
      zdialog_run(wtpopup_zd,null,wtpopup_zdcompl);
   }
   
   if (strEqu(action,"write"))
      if (wtpopup_wedit) wprintf(wtpopup_wedit," %s\n",text);
   
   if (strEqu(action,"close"))
   {
      if (wtpopup_zd) zdialog_free(wtpopup_zd);
      wtpopup_zd = 0; 
      wtpopup_wedit = 0;
   }

   zmainloop();                                                            //  v.2.18
   return 0;
}

int wtpopup_zdcompl(zdialog *zd, int zstat)
{
   zdialog_free(wtpopup_zd);
   wtpopup_zd = 0; 
   wtpopup_wedit = 0;
   return 0;
}


//  execute a command and show the output in a scrolling popup window

int popup_command(const char *command, int ww, int hh)
{
   int popup_zdcompl(zdialog*zd, int zstat);

   char        *buff;
   zdialog     *zd;
   GtkWidget   *wedit;
   int         err, contx = 0;

   zd = zdialog_new(command,null,"OK",null);                               //  create dialog to show outputs
   zdialog_add_widget(zd,"scrwin","scrwin","dialog",0,"expand");
   zdialog_add_widget(zd,"edit","edit","scrwin");
   wedit = zdialog_widget(zd,"edit");
   zdialog_resize(zd,ww,hh);
   zdialog_run(zd,null,popup_zdcompl);                                     //  show dialog with command outputs

   while ((buff = command_output(contx,command)))                          //  v.2.3
   {
      wprintf(wedit," %s \n",buff);                                        //  *** GTK memory leak is here ***
      zfree(buff);
   }
   
   wscroll(wedit,1);                                                       //  scroll back to first line    v.2.3
   
   err = command_status(contx);
   return err;                                                             //  v.2.12
}

int popup_zdcompl(zdialog *zd, int zstat)
{
   zdialog_free(zd);
   return 0;
}


//  display message box and wait for user OK
//  *** be careful about calling this before gtk_main() is started *** 

void zmessageACK(cchar *pMess, ... )
{
   GtkWidget      *dialog;
   va_list        arglist;
   char           message[400];
   const char     *blanks = "                           ";

   va_start(arglist,pMess);
   vsnprintf(message,400,pMess,arglist);
   va_end(arglist);
   
   if (strlen(message) < 20) strcat(message,blanks);                       //  make box longer

   zlock();
   dialog = gtk_message_dialog_new(null,GTK_DIALOG_MODAL,
                    GTK_MESSAGE_INFO,GTK_BUTTONS_OK, message, null);
   gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_MOUSE);          //  v.2.7.1
   gtk_dialog_run(GTK_DIALOG(dialog));
   gtk_widget_destroy(dialog);
   zunlock();

   return;
}


//  log error message to STDOUT as well as message box and user OK

void zmessLogACK(cchar *pMess, ...)                                        //  v.2.9
{
   va_list        arglist;
   char           message[200];

   va_start(arglist,pMess);
   vsnprintf(message,200,pMess,arglist);
   va_end(arglist);
   
   printf("%s \n",message);
   zmessageACK(message);
   return;
} 


//  display message box and wait for user Yes or No response
//  *** be careful about calling this before gtk_main() is started *** 
//  *** uses text buttons "yes" and "no" regardless of locale ***

int zmessageYN(cchar *pMess, ... )
{
   GtkWidget  *dialog;
   va_list     arglist;
   char        message[400];
   int         stat;

   va_start(arglist,pMess);
   vsnprintf(message,400,pMess,arglist);
   va_end(arglist);

   zlock();
   dialog = gtk_message_dialog_new(null,GTK_DIALOG_MODAL,
                 GTK_MESSAGE_QUESTION,GTK_BUTTONS_YES_NO, message, null);
   gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_MOUSE);          //  v.2.7.1
   stat = gtk_dialog_run(GTK_DIALOG(dialog));
   gtk_widget_destroy(dialog);
   zunlock();

   if (stat == GTK_RESPONSE_YES) return 1;
   else return 0;
}


//  get text input from a popup dialog

const char * dialogText(const char *title, const char *inittext)
{
   zdialog  *zd;
   
   zd = zdialog_new(title,null,"OK",ZTX("cancel"),null);
   zdialog_add_widget(zd,"frame","fred","dialog");
   zdialog_add_widget(zd,"edit","edit","fred");
   zdialog_stuff(zd,"edit",inittext);

   int zstat = zdialog_run(zd);
   zdialog_destroy(zd);
   if (zstat != 1) return 0;
   return zdialog_get_data(zd,"edit");
}


//  File chooser dialog. 
//
//  Action:  "open"           select existing file
//           "save"           select existing or new file
//           "folder"         select existing folder
//           "create folder"  select existing or new folder
//
//  buttx:   "hidden"         button toggles display of hidden files
//           "quality"        button to set JPG file save quality
//
//  Memory for returned filespec should be freed via zfree()

char     JPGquality[4] = "85";                                             //  JPG file save quality (extern)

char * zgetfile(cchar *title, cchar *initfile, cchar *action, cchar *buttx)
{
   void zgetfile_preview(GtkWidget *dialog, GtkWidget *pvwidget);          //  private function    v.2.0

   GtkFileChooserAction fcact = GTK_FILE_CHOOSER_ACTION_OPEN;

   GtkWidget      *dialog;
   GtkWidget      *pvwidget = gtk_image_new();
   int            fcstat, err, bcode = 0, qnum, hide = 0;
   char           *pp, *gfile, *rfile;
   const char     *qual, *button1 = 0, *fakeit;
   struct stat    fstat;
   
   zlock();

   if (strEqu(action,"open")) {
      fcact = GTK_FILE_CHOOSER_ACTION_OPEN;
      button1 = ZTX("open");
   }

   if (strEqu(action,"save")) {
      fcact = GTK_FILE_CHOOSER_ACTION_SAVE;
      button1 = ZTX("save");
   }

   if (strEqu(action,"folder")) {
      fcact = GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER;
      button1 = ZTX("open folder");
   }

   if (strEqu(action,"create folder")) {
      fcact = GTK_FILE_CHOOSER_ACTION_CREATE_FOLDER;
      button1 = ZTX("create folder");
   }
   
   if (buttx) {
      if (strnEqu(buttx,"hidden",6)) bcode = 103;
      if (strEqu(buttx,"quality")) bcode = 104;
      fakeit = ZTX("hidden");                                              //  generate text for translation
      fakeit = ZTX("quality");
   }
   
   dialog = gtk_file_chooser_dialog_new(title, null, fcact,                //  create file selection dialog
                              button1, GTK_RESPONSE_ACCEPT, 
                              ZTX("cancel"), GTK_RESPONSE_CANCEL, 
                              ZTX(buttx), bcode, null);
   gtk_window_set_position(GTK_WINDOW(dialog),GTK_WIN_POS_MOUSE);

   if (! strstr(action,"folder")) {                                        //  connect preview maker
      gtk_file_chooser_set_preview_widget(GTK_FILE_CHOOSER(dialog),pvwidget);
      G_SIGNAL(dialog,"update-preview",zgetfile_preview,pvwidget);
   }

   if (initfile) {                                                         //  pre-select filespec   v.2.19
      err = stat(initfile,&fstat);
      if (err) {                                                           //  attempt to set folder and file name
         gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog),initfile);
         pp = (char *) strrchr(initfile,'/');
         if (pp) gtk_file_chooser_set_current_name(GTK_FILE_CHOOSER(dialog),pp+1);
      }
      else if (S_ISREG(fstat.st_mode))
         gtk_file_chooser_set_filename(GTK_FILE_CHOOSER(dialog),initfile);
      else if (S_ISDIR(fstat.st_mode))
         gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog),initfile);
   }

   if (strEqu(action,"save"))                                              //  overwrite confirmation
      gtk_file_chooser_set_do_overwrite_confirmation(GTK_FILE_CHOOSER(dialog),1);

   gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(dialog),0);           //  default: no show hidden

   while (true)
   {
      fcstat = gtk_dialog_run(GTK_DIALOG(dialog));                         //  run dialog, get status button

      if (fcstat == 103) {                                                 //  show/hide hidden files
         hide = gtk_file_chooser_get_show_hidden(GTK_FILE_CHOOSER(dialog));
         hide = 1 - hide;
         gtk_file_chooser_set_show_hidden(GTK_FILE_CHOOSER(dialog),hide);
      }

      else if (fcstat == 104) {                                            //  get JPG quality parameter 
         while (true) {
            qual = dialogText(ZTX("JPG quality 0-100"),JPGquality);
            if (! qual) break;                                             //  cancel = no change
            err = convSI(qual,qnum,0,100);
            if (err) continue;                                             //  enforce 0-100
            snprintf(JPGquality,4,"%d",qnum);
            break;
         }
      }

      else break;                                                          //  some other button 
   }

   if (fcstat == GTK_RESPONSE_ACCEPT) 
      gfile = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));     //  get chosen file
   else  gfile = 0;

   gtk_widget_destroy(dialog);

   if (gfile) {                                                            //  copy into own memory
      rfile = strdupz(gfile);
      g_free(gfile);                                                       //  free GTK resource
   }
   else rfile = 0;
   
   zunlock();
   return rfile;
}


//  zgetfile private function - get preview images for image files         //  v.2.0

void zgetfile_preview(GtkWidget *dialog, GtkWidget *pvwidget)
{
   GdkPixbuf   *thumbnail;
   char        *filename;
   struct stat statbuf;
   int         err;

   thumbnail = 0;
   filename = gtk_file_chooser_get_preview_filename(GTK_FILE_CHOOSER(dialog));
   err = stat(filename,&statbuf);
   if (! err && ! S_ISDIR(statbuf.st_mode))                                //  not for directories   v.2.9
      thumbnail = image_thumbnail(filename,128);                           //  use 128x128 pixels    v.2.9
   g_free(filename);

   if (thumbnail) {
      gtk_image_set_from_pixbuf(GTK_IMAGE(pvwidget),thumbnail);
      gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(dialog),1);
      g_object_unref(thumbnail);                                           //  v.2.5  leak
   }
   else
      gtk_file_chooser_set_preview_widget_active(GTK_FILE_CHOOSER(dialog),0);
   return;
}


//  show a local or remote html file using the user's preferred browser    //  v.2.18

void showz_html(const char *url)
{
   char           command[500];
   static char    prog[20];
   static int     ftf = 1, err;

   if (ftf) {
      ftf = 0;
      strcpy(prog,"xdg-open");
      err = system("xdg-open --version");
      if (err) {
         strcpy(prog,"firefox");
         err = system("firefox -v");
         if (err) *prog = 0;
      }
   }
   
   if (! *prog) {
      zmessLogACK("no xdg-open or firefox, cannot show document");
      return;
   }
   
   snprintf(command,499,"%s %s &",prog,url);                               //  add '&'   v.2.18
   err = system(command);
   return;
}


//  display message box indefinitely, until caller kills it                v.2.6

GtkWidget * zmessage_post(cchar *pMess, ... )
{
   GtkWidget      *dialog, *label1, *label2, *label3;
   va_list        arglist;
   char           message1[400];
   char           message2[400];
   const char     *b20 = "                    ";
   int            cc1, cc2;

   va_start(arglist,pMess);
   vsnprintf(message1,400,pMess,arglist);
   va_end(arglist);
   
   cc1 = strlen(message1);
   if (cc1 > 20) strcpy(message2,message1);
   else {
      cc2 = (22 - cc1) / 2;
      strncpy(message2,b20,cc2);
      strcpy(message2+cc2,message1);
      strncpy(message2+cc2+cc1,b20,cc2);
      message2[cc2+cc1+cc2] = 0;
   }
   
   zlock();
   dialog = gtk_dialog_new_with_buttons("",null,(GtkDialogFlags) 0,null);
   label1 = gtk_label_new("  ");
   label2 = gtk_label_new(message2);
   label3 = gtk_label_new("  ");
   gtk_container_add(GTK_CONTAINER(GTK_DIALOG(dialog)->vbox),label1);
   gtk_container_add(GTK_CONTAINER(GTK_DIALOG(dialog)->vbox),label2);
   gtk_container_add(GTK_CONTAINER(GTK_DIALOG(dialog)->vbox),label3);
   gtk_widget_show_all(dialog);
   zunlock();

   return dialog;
}

void zmessage_kill(GtkWidget *dialog)
{
   zlock();
   gtk_widget_destroy(dialog);
   zunlock();
   return;
}


//  connect a user callback function to a window drag-drop event

void drag_drop_connect(GtkWidget *window, drag_drop_func *ufunc)           //  v.2.19
{
   int  drag_drop_connect2(GtkWidget *, void *, int, int, void *, int, int, void *);
   char     string[] = "STRING";
   GtkTargetEntry  file_drop_target = { string, 0, 0 };

   gtk_drag_dest_set(window, GTK_DEST_DEFAULT_ALL, &file_drop_target, 1, GDK_ACTION_COPY);
   G_SIGNAL(window, "drag-data-received", drag_drop_connect2, ufunc)
   return;
}


//  private function 
//  get dropped file, clean escapes, pass to user function
//  passed filespec is subject for zfree()

int drag_drop_connect2(GtkWidget *, void *, int mpx, int mpy, void *sdata, int, int, void *ufunc)
{
   char  * drag_drop_unescape(const char *escaped_string);
   drag_drop_func  *ufunc2;

   char     *text, *text2, *file, *file2;
   int      cc;
   
   text = (char *) ((GtkSelectionData *) sdata)->data;
   ufunc2 = (drag_drop_func *) ufunc;

   if (strstr(text,"file://"))                                             //  text is a filespec
   {
      file = strdupz(text+7);                                              //  get rid of junk added by GTK
      cc = strlen(file);
      while (file[cc-1] < ' ') cc--;
      file[cc] = 0;
      file2 = drag_drop_unescape(file);                                    //  clean %xx escapes from Nautilus
      zfree(file);
      ufunc2(mpx,mpy,file2);                                               //  pass file to user function
   }
   
   else 
   {
      text2 = strdupz(text);
      ufunc2(mpx,mpy,text2);
   }
   
   return 1;
}

//  private function
//  Clean %xx escapes from strange Nautilus drag-drop file names

char * drag_drop_unescape(const char *inp)
{
   int  drag_drop_convhex(char ch);

   char     inch, *out, *outp;
   int      nib1, nib2;
   
   out = zmalloc(strlen(inp) + 1);
   outp = out;
   
   while ((inch = *inp++))
   {
      if (inch == '%')
      {
         nib1 = drag_drop_convhex(*inp++);
         nib2 = drag_drop_convhex(*inp++);
         *outp++ = nib1 << 4 | nib2;
      }
      else *outp++ = inch;
   }
   
   *outp = 0;
   return out;
}

//  private function - convert character 0-F to number 0-15

int drag_drop_convhex(char ch)
{
   if (ch >= '0' && ch <= '9') return  ch - '0';
   if (ch >= 'A' && ch <= 'F') return  ch - 'A' + 10;
   if (ch >= 'a' && ch <= 'f') return  ch - 'a' + 10;
   return ch;
}


/**************************************************************************
   GDK graphics utilities
***************************************************************************/

/**************************************************************************  

     GdkPixbuf * gdk_pixbuf_rotate(GdkPixbuf *pixbuf, double angle, int acolor)

     Rotate a pixbuf through an arbitrary angle (degrees).

     The returned image has the same size as the original, but the
     pixbuf envelope is increased to accomodate the rotated original
     (e.g. a 100x100 pixbuf rotated 45 deg. needs a 142x142 pixbuf).

     Pixels added around the rotated image have all RGB values = acolor.   //  v.2.17
     Angle is in degrees. Positive direction is clockwise.
     Pixbuf must have 8 bits per channel and 3 or 4 channels.
     Loss of resolution is < 1 pixel.
     Speed is about 2.4 million pixels/sec. for a 2 GHz CPU.
     
     NULL is returned if the function fails for one of the following:
         - pixbuf not 8 bits/channel or < 3 channels
         - unable to create output pixbuf (lack of memory?)
    
     Algorithm:
         create output pixbuf big enough for rotated input pixbuf
         loop all output pixels
            get next output pixel (px2,py2)
            compute (R,theta) from center of pixbuf
            rotate theta by -angle
            (R,theta) is now within the closest input pixel
            convert to input pixel (px1,py1)
            if outside of pixmap
               output pixel = black
               continue
            for 4 input pixels based at (px0,py0) = (int(px1),int(py1))
               compute overlap (0 to 1) with (px1,py1)
               sum RGB values * overlap
            output aggregate RGB to pixel (px2,py2)

***************************************************************************/

GdkPixbuf * gdk_pixbuf_rotate(GdkPixbuf *pixbuf1, double angle, int acolor)
{
   typedef unsigned char  *pixel;                                          //  3 RGB values, 0-255 each

   GdkPixbuf      *pixbuf2;
   GdkColorspace  color;

   int      nch, nbits, alpha;
   int      pbW1, pbH1, pbR1, pbW2, pbH2, pbR2;
   int      px2, py2, px0, py0;
   pixel    ppix1, ppix2, pix0, pix1, pix2, pix3;
   double   rx1, ry1, rx2, ry2, R, theta, px1, py1;
   double   f0, f1, f2, f3, red, green, blue;
   double   pi = 3.141592654;

   nch = gdk_pixbuf_get_n_channels(pixbuf1);
   nbits = gdk_pixbuf_get_bits_per_sample(pixbuf1);
   if (nch < 3) return 0;                                                  //  must have 3+ channels (colors)
   if (nbits != 8) return 0;                                               //  must be 8 bits per channel

   color = gdk_pixbuf_get_colorspace(pixbuf1);                             //  get input pixbuf1 attributes
   alpha = gdk_pixbuf_get_has_alpha(pixbuf1);
   pbW1 = gdk_pixbuf_get_width(pixbuf1);
   pbH1 = gdk_pixbuf_get_height(pixbuf1);
   pbR1 = gdk_pixbuf_get_rowstride(pixbuf1);

   while (angle < -180) angle += 360;                                      //  normalize, -180 to +180
   while (angle > 180) angle -= 360;
   angle = angle * pi / 180;                                               //  radians, -pi to +pi
   
   if (fabs(angle) < 0.001) {                                              //  bugfix 0.01 >> 0.001   v.2.1
      pixbuf2 = gdk_pixbuf_copy(pixbuf1);                                  //  angle is zero within my precision
      return pixbuf2;
   }

   pbW2 = int(pbW1*fabs(cos(angle)) + pbH1*fabs(sin(angle)));              //  rectangle containing rotated image
   pbH2 = int(pbW1*fabs(sin(angle)) + pbH1*fabs(cos(angle)));

   pixbuf2 = gdk_pixbuf_new(color,alpha,nbits,pbW2,pbH2);                  //  create output pixbuf2
   if (! pixbuf2) return 0;
   pbR2 = gdk_pixbuf_get_rowstride(pixbuf2);
   
   ppix1 = gdk_pixbuf_get_pixels(pixbuf1);                                 //  input pixel array
   ppix2 = gdk_pixbuf_get_pixels(pixbuf2);                                 //  output pixel array
   
   for (py2 = 0; py2 < pbH2; py2++)                                        //  loop through output pixels
   for (px2 = 0; px2 < pbW2; px2++)                                        //  outer loop y   v.2.11
   {
      rx2 = px2 - 0.5 * pbW2;                                              //  (rx2,ry2) = center of pixel
      ry2 = py2 - 0.5 * pbH2;
      R = sqrt(rx2*rx2 + ry2*ry2);                                         //  convert to (R,theta)
      if (R < 0.1) theta = 0;
      else theta = qarcsine(ry2 / R);                                      //  quick arc sine
      if (rx2 < 0) {
         if (theta < 0) theta = - pi - theta;                              //  adjust for quandrant
         else theta = pi - theta;
      }

      theta = theta - angle;                                               //  rotate theta backwards
      if (theta > pi) theta -= 2 * pi;                                     //  range -pi to +pi
      if (theta < -pi) theta += 2 * pi;

      rx1 = R * qcosine(theta);                                            //  quick cosine, sine
      ry1 = R * qsine(theta);
      px1 = rx1 + 0.5 * pbW1;                                              //  (px1,py1) = corresponding
      py1 = ry1 + 0.5 * pbH1;                                              //    point within input pixels

      px0 = int(px1);                                                      //  pixel containing (px1,py1)
      py0 = int(py1);
      
      if (px1 < 0 || px0 >= pbW1-1 || py1 < 0 || py0 >= pbH1-1) {          //  if outside input pixel array
         pix2 = ppix2 + py2 * pbR2 + px2 * nch;                            //    output is acolor    v.2.17
         pix2[0] = pix2[1] = pix2[2] = acolor;
         continue;
      }

      pix0 = ppix1 + py0 * pbR1 + px0 * nch;                               //  4 input pixels based at (px0,py0)
      pix1 = pix0 + pbR1;
      pix2 = pix0 + nch;
      pix3 = pix0 + pbR1 + nch;

      f0 = (px0+1 - px1) * (py0+1 - py1);                                  //  overlap of (px1,py1)
      f1 = (px0+1 - px1) * (py1 - py0);                                    //    in each of the 4 pixels
      f2 = (px1 - px0) * (py0+1 - py1);
      f3 = (px1 - px0) * (py1 - py0);
   
      red =   f0 * pix0[0] + f1 * pix1[0] + f2 * pix2[0] + f3 * pix3[0];   //  sum the weighted inputs
      green = f0 * pix0[1] + f1 * pix1[1] + f2 * pix2[1] + f3 * pix3[1];
      blue =  f0 * pix0[2] + f1 * pix1[2] + f2 * pix2[2] + f3 * pix3[2];
      
      if (red == acolor && green == acolor && blue == acolor) {            //  avoid acolor in image     v.2.17
         if (blue == 0) blue = 1;
         else blue--;
      }
      
      pix2 = ppix2 + py2 * pbR2 + px2 * nch;                               //  output pixel
      pix2[0] = int(red);
      pix2[1] = int(green);
      pix2[2] = int(blue);
   }
      
   return pixbuf2;
}


/**************************************************************************

   functions for navigtion of image files in a directory                   //  overhauled  v.2.3
      - get first or last image, previous or next image
      - create a window of thumbnail images (index window)
      - use thumbnail window to navigate and select images

**************************************************************************/

namespace image_navi {

   #define flimit 10000                                                    //  max image files in one directory
   #define indexfont "sans 8"                                              //  font for thumbnail labels
   #define imagefiles ".jpeg .jpg .png .tif .tiff .bmp .gif .svg .xpm"     //  supported file types

   #define nodither GDK_RGB_DITHER_NONE,0,0
   #define interp GDK_INTERP_BILINEAR
   #define colorspace GDK_COLORSPACE_RGB

   #define     thumbfilesize 256                                           //  thumbnail file image size
   #define     thumbxx 5                                                   //  thumbx array size
   int         thumbx[5] = { 256, 180, 128, 90, 64 };                      //  thumbnail image step sizes   v.2.9

   char        dirkx[maxfcc];                                              //  image directory
   int         nfiles = 0;                                                 //  image file count
   char        **flist = 0;                                                //  image file list

   image_cbfunc   *userfunc;                                               //  callback function for clicked image

   GtkWidget   *windx = 0, *vboxx, *dwindx;                                //  thumbnail index and drawing window
   GdkGC       *gdkgc = 0;                                                 //  graphics context
   GError      **gerror = 0;

   char        *filex = 0;                                                 //  index window anchor file
   int         xwinW = 1000, xwinH = 700;                                  //  index window size (dwindx)
   int         thumbsize = thumbfilesize;                                  //  thumbnail image <= thumbnail file
   int         thumbW, thumbH;                                             //  index window thumbnail cell size
   int         xrows, xcols;                                               //  index window thumbnail rows, cols
   int         xmargW, xmargH;                                             //  cell margin from left and top edge

   //  private functions
   void  windx_paint();                                                    //  index window paint function
   void  windx_destroy();                                                  //  index window destroy event function
   void  draw_xtext(GtkWidget *win, char *text, int x, int y);             //  draw text in index window
   void  menufuncx(GtkWidget *win, const char *menu);                      //  function for index window buttons
   void  mouse_xevent(GtkWidget *, GdkEventButton *, void *);              //  index window mouse event function
	int   KBxpress(GtkWidget *, GdkEventKey *, void *);                     //  index window key press event func.
	int   KBxrelease(GtkWidget *, GdkEventKey *, void *);                   //  index window key release event
   char * image_navigate(cchar *filez, cchar *action, int Nth = 0);        //  image file list setup and navigate
   int image_fcomp(const char *file1, const char *file2);                  //  file name compare (special sort)
}

using namespace image_navi;


/**************************************************************************

   public function to create/update thumbnail window (index window)                 v.2.3

   Make window of thumbnails starting with filez (anchor)
   Handle window buttons (up row, down page, open file, etc.)
   Call ufunc() when thumbnail image is clicked
   
   filez:   anchor file or directory of image files

   action:  init:    get internal list of image files and subdirectories
            initF:   filez contains a list of image files to use                    v.2.4
            find     return filez if in current list, else null
            prev:    return filez Nth previous file
            next:    return filez Nth next file
            first:   return first file in list (may be a subdirectory)
            last:    return Nth last file in list
            paint1:  create or refresh thumbnail window, anchor = filez
            paint2:  refresh thumbnail window if present, anchor = filez

   Nth:     1 means filez pevious, filez next, or last file in list
            greater values mean correspondingly greater offsets

   void ufunc(char *filez)
      - receives filename (path) of selected thumbnail
      - filez belongs to caller and is a subject for zfree()
   
   Returned values:
      For init, initF, paint1, paint2: null
      For find, prev, next, first, last: filespec or null if not found
      The returned file belongs to caller and is subject for zfree().

***************************************************************************/

char * image_xthumbs(cchar *filez, cchar *action, int Nth, image_cbfunc ufunc)
{
   GtkWidget      *tbarx;

   if (ufunc) userfunc = ufunc;                                            //  save callback function
   
   if (strstr("init initF find prev next first last",action))
      return image_navigate(filez,action,Nth);                             //  create or navigate image file list
      
   if (filez) strdupz(filez,filex);                                        //  set new anchor file  v.2.4

   if (strEqu(action,"paint2") && ! windx) return 0;                       //  refresh, but window not active

   if (windx) {                                                            //  refresh existing index window
      windx_paint();                                                       //  repaint
      gtk_window_present(GTK_WINDOW(windx));                               //  bring index window to top
      return 0;
   }
   
   windx = gtk_window_new(GTK_WINDOW_TOPLEVEL);                            //  create index window
   gtk_window_set_position(GTK_WINDOW(windx),GTK_WIN_POS_CENTER);
   gtk_window_set_default_size(GTK_WINDOW(windx),xwinW,xwinH+56);          //  (+ toolbar size)

   vboxx = gtk_vbox_new(0,0);                                              //  vertical packing box
   gtk_container_add(GTK_CONTAINER(windx),vboxx);                          //  add to main window

   tbarx = create_toolbar(vboxx,24);                                       //  add toolbar and buttons 

   add_toolbar_button(tbarx, ZTX("bigger"), ZTX("increase thumbnail size"), "bigger.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("smaller"), ZTX("reduce thumbnail size"), "smaller.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("prev row"), ZTX("previous row"), "prev-row.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("next row"), ZTX("next row"), "next-row.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("prev page"), ZTX("previous page"), "prev-page.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("next page"), ZTX("next page"), "next-page.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("first page"), ZTX("jump to first file"), "first-page.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("last page"), ZTX("jump to last file"), "last-page.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("file"), ZTX("jump to specific file"), "file.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("folder"), ZTX("jump to new directory"), "folder.png", menufuncx);
   add_toolbar_button(tbarx, ZTX("close"), ZTX("close thumbnail window"), "gtk-close", menufuncx);

   dwindx = gtk_drawing_area_new();                                        //  add drawing window
   gtk_container_add(GTK_CONTAINER(vboxx),dwindx);                         //  add to main window

   gtk_widget_show_all(windx);                                             //  show all widgets (will paint)

   gdkgc = gdk_gc_new(dwindx->window);                                     //  initz. graphics context

   G_SIGNAL(windx,"destroy",windx_destroy,0)                               //  connect window events
   G_SIGNAL(dwindx,"expose-event",windx_paint,0)
   gtk_widget_add_events(dwindx,GDK_BUTTON_PRESS_MASK);                    //  connect mouse events
   G_SIGNAL(dwindx,"button-press-event",mouse_xevent,0)
   G_SIGNAL(windx,"key-press-event",KBxpress,0)                           	//  connect KB events
   G_SIGNAL(windx,"key-release-event",KBxrelease,0)

   return 0;
}


//  public function
//  get current image position and total count

void image_position(const char *filez, int &posn, int &count)
{
   int      ii, nn;

   for (ii = nn = 0; ii < nfiles; ii++)                                    //  search for filez in file list
   {
      if (flist[ii][0] == '!') nn++;                                       //  directories are marked
      if (strcmp(filez,flist[ii]) == 0) break;
   }
   
   posn = ii + 1 - nn;                                                     //  return position, 1 - max
   count = nfiles - nn;                                                    //  return total count
   return;
}


//  public function
//  determine if a file is a directory or a supported image file type            v.2.3
//  return: 0 = error, 1 = directory, 2 = image file, 3 = other

int image_file_type(const char *file)
{
   int            err, cc;
   const char     *pp;
   struct stat    statbuf;

   if (! file) return 0;
   err = stat(file,&statbuf);
   if (err) return 0;

   if (S_ISDIR(statbuf.st_mode)) {                                         //  directory
      cc = strlen(file);
      if (cc > 12) {
         pp = file + cc - 12;
         if (strEqu(pp,"/.thumbnails")) return 3;                          //  .thumbnails
      }
      return 1;
   }

   if (S_ISREG(statbuf.st_mode)) {                                         //  reg. file
      pp = strrchr(file,'.');
      if (! pp) return 3;
      pp = strcasestr(imagefiles,pp);                                      //  supported image type
      if (pp) return 2;
   }
   
   return 3;
}


//  Public function
//  Get thumbnail image for given file, from .thumbnails directory if found.           v.2.3
//  Add missing thumbnail to cache if .thumbnails directory is present.
//  Returned thumbnail belongs to caller: g_object_unref() is necessary.

GdkPixbuf * image_thumbnail(const char *fpath, int size)
{
   char              bpath[maxfcc+20], *bfile;
   const char        *pfile;                                               //  v.2.15
   GdkPixbuf         *thumbpxb, *temppxb;
   int               err, sizew, sizeh;
   struct stat       statf, statb;

   if (! size) size = thumbfilesize;                                       //  default max. size   v.2.9

   err = stat(fpath,&statf);
   if (err) return 0;
   
   if (S_ISDIR(statf.st_mode)) {                                           //  if directory, return folder image
      *bpath = 0;
      strncatv(bpath,maxfcc,zicondir,"/folder256.png",null);
      thumbpxb = gdk_pixbuf_new_from_file_at_size(bpath,size,size,gerror);
      return thumbpxb;
   }
   
   pfile = strrchr(fpath,'/');
   if (! pfile) return 0;
   pfile++;                                                                //  file name part

   strcpy(bpath,fpath);                                                    //  construct thumbnail path:
   bfile = bpath + (pfile - fpath);                                        //  /directory/.thumbnails/filename.png
   strcpy(bfile,".thumbnails/");                                           //  use filename instead of hash
   bfile += 12;
   strcpy(bfile,pfile);
   strcat(bfile,".png");

   sizew = sizeh = thumbfilesize;                                          //  use max. size
   thumbpxb = gdk_pixbuf_new_from_file_at_size(bpath,sizew,sizeh,gerror);  //  get thumbnail 
   if (thumbpxb) {
      err = stat(bpath,&statb);                                            //  found, compare date to image date
      if (err || (statb.st_mtime < statf.st_mtime)) {
         g_object_unref(thumbpxb);                                         //  stale - regenerate
         thumbpxb = 0;
      }
   }

   if (! thumbpxb) {                                                       //  generate thumbnail from image file
      thumbpxb = gdk_pixbuf_new_from_file_at_size(fpath,sizew,sizeh,gerror);
      if (! thumbpxb) return 0;                                            //  hopeless
      gdk_pixbuf_save(thumbpxb,bpath,"png",gerror,null);                   //  save to thumbnail cache (if present)
   }

   if (size == thumbfilesize) return thumbpxb;                             //  return thumbnail if right size

   sizew = gdk_pixbuf_get_width(thumbpxb) * size / thumbfilesize;          //  rescale to right size
   sizeh = gdk_pixbuf_get_height(thumbpxb) * size / thumbfilesize;
   temppxb = gdk_pixbuf_scale_simple(thumbpxb,sizew,sizeh,interp);
   g_object_unref(thumbpxb);
   return temppxb;
}


//  private function
//  paint index window - draw all thumbnail images that can fit

void image_navi::windx_paint()
{
   GdkPixbuf      *pxbT;
   int            x, y, row, col;
   char           *fileC, *fileN, *pp, *fname;
   char           wintitle[200];

   snprintf(wintitle,199,"%s  %d files",dirkx,nfiles);                     //  v.2.9
   gtk_window_set_title(GTK_WINDOW(windx),wintitle);
   gdk_window_clear(dwindx->window);

   xwinW = dwindx->allocation.width;                                       //  curr. index window size
   xwinH = dwindx->allocation.height;

   thumbW = thumbsize + 10;                                                //  thumbnail cell size
   thumbH = thumbsize + 30;

   xmargW = xmargH = 5;                                                    //  edge margins
   
   xrows = int(0.2 + 1.0 * xwinH / thumbH);                                //  get thumbnail rows and cols that
   xcols = int(0.3 + 1.0 * xwinW / thumbW);                                //    (almost) fit in window 
   if (xrows < 1) xrows = 1;
   if (xcols < 1) xcols = 1;
   
   if (! filex) return;
   fileC = strdupz(filex);                                                 //  start with anchor file

   for (row = 0; row < xrows; row++)                                       //  draw thumbnails
   for (col = 0; col < xcols; col++)
   {
      x = col * thumbW + xmargW;
      y = row * thumbH + xmargH;

      pp = (char *) strrchr(fileC,'/');                                    //  draw file name 
      if (pp) fname = pp + 1;
      else fname = fileC;
      draw_xtext(dwindx,fname,x,y);

      pxbT = image_thumbnail(fileC,thumbsize);                             //  get thumbnail, draw it
      if (pxbT) {
         gdk_draw_pixbuf(dwindx->window,0,pxbT,0,0,x,y+20,-1,-1,nodither); 
         g_object_unref(pxbT);
      }

      fileN = image_navigate(fileC,"next",1);                              //  get next image file
      zfree(fileC);
      if (! fileN) return;
      fileC = fileN;
   }

   return;
}


//  private function
//  write text for thumbnail limited by width of thumbnail

void image_navi::draw_xtext(GtkWidget *win, char *text, int x, int y)
{
   static PangoFontDescription   *pfont = 0;
   static PangoLayout            *playout = 0;

   int            ww, hh, cc;
   char           text2[100];

   if (! pfont) {
      pfont = pango_font_description_from_string(indexfont);               //  first call, get font sizing poop
      playout = gtk_widget_create_pango_layout(win,0);
      pango_layout_set_font_description(playout,pfont);
   }

   for (cc = 50; cc > 7; cc--)                                             //  allow up to 50 graphic chars.
   {                                                                       //    and reduce until fits      v.2.3
      cc = utf8substring(text2,text,0,cc);                                 //  get substring up to cc chars.
      pango_layout_set_text(playout,text2,-1);                             //  compute layout
      pango_layout_get_pixel_size(playout,&ww,&hh);                        //  pixel width of layout
      if (ww < thumbsize+5) break;                                         //  stop if it fits thumbnail size
   }

   gdk_draw_layout(win->window,gdkgc,x,y,playout);
   return;
}


//  private function
//  index window destroy event - track if window is active or not

void image_navi::windx_destroy()
{
   windx = 0;                                                              //  no window
   if (filex) zfree(filex);                                                //  no anchor file
   filex = 0;
   return;
}


//  private function - menu function for index window
//    - scroll window as requested
//    - jump to new file or folder as requested

void image_navi::menufuncx(GtkWidget *win, const char *menu)
{
   char        *filez;
   cchar       *action = 0;
   int         ii, count = 0;
   
   if (strEqu(menu,ZTX("bigger")))  {                                      //  next bigger thumbnail size
      for (ii = 0; ii < thumbxx; ii++) 
            if (thumbsize == thumbx[ii]) break;
      if (ii == 0) return;
      thumbsize = thumbx[ii-1];
      windx_paint();
      return;
   }

   if (strEqu(menu,ZTX("smaller")))  {                                     //  next smaller
      for (ii = 0; ii < thumbxx; ii++) 
            if (thumbsize == thumbx[ii]) break;
      if (ii == thumbxx-1) return;
      thumbsize = thumbx[ii+1];
      windx_paint();
      return;
   }

   if (strEqu(menu,ZTX("file"))) {                                         //  go to specific file
      filez = zgetfile(ZTX("select new file"),filex,"open");               //  file chooser dialog
      if (filez) {
         image_navigate(filez,"init");                                     //  get new file list
         windx_paint();
      }
      return;
   }

   if (strEqu(menu,ZTX("folder"))) {                                       //  initz. all files in folder
      filez = zgetfile(ZTX("select new folder"),filex,"folder");           //  file chooser dialog (folder)
      if (filez) {
         image_navigate(filez,"init");                                     //  get new file list
         windx_paint();
      }
      return;
   }
   
   if (strEqu(menu,ZTX("close"))) {
      gtk_widget_destroy(windx);                                           //  close thumbnail window
      return;
   }

   if (! filex) return;                                                    //  no anchor file

   if (strEqu(menu,ZTX("prev row"))) {
      action = "prev";
      count = xcols;
   }

   if (strEqu(menu,ZTX("next row"))) {
      action = "next";
      count = xcols;
   }

   if (strEqu(menu,ZTX("prev page"))) {
      action = "prev";
      count = xcols * xrows;
   }

   if (strEqu(menu,ZTX("next page"))) {
      action = "next";
      count = xcols * xrows;
   }

   if (strEqu(menu,ZTX("first page"))) {
      action = "first";
      count = 0;
   }

   if (strEqu(menu,ZTX("last page"))) {
      action = "last";
      count = xcols * xrows;                                               //  last + prior fitting in window
   }
   
   filez = image_navigate(filex,action,count);                             //  get file starting prev/next row/page
   if (! filez) return;
   zfree(filex);
   filex = filez;                                                          //  new anchor file
   windx_paint();                                                          //  refresh window 

   return;
}


//  private function
//  mouse event function for index window - get selected thumbnail and file

void image_navi::mouse_xevent(GtkWidget *, GdkEventButton *event, void *)
{
   int            mousex, mousey;
   int            row, col, Nth, err;
   char           *filez;
   struct stat    statb;
   
   mousex = int(event->x);
   mousey = int(event->y);

   row = (mousey - xmargH) / thumbH;                                       //  find selected row, col
   col = (mousex - xmargW) / thumbW;

   if (row < 0 || row >= xrows) return;
   if (col < 0 || col >= xcols) return;
   
   Nth = xcols * row + col;

   if (Nth == 0) filez = strdupz(filex);                                   //  anchor file selected
   else  {
      filez = image_navigate(filex,"next",Nth);                            //  else get Nth file after anchor
      if (! filez) return;
   }
   
   err = stat(filez,&statb);                                               //  file is gone?
   if (err) {
      image_navigate(filex,"init");                                        //  initz. window    v.2.11
      windx_paint();
      return;
   }

   if (S_ISDIR(statb.st_mode)) {                                           //  if directory, go there
      image_navigate(filez,"init");
      if (filex) zfree(filex);
      filex = image_navigate(filez,"first");                               //  set anchor file 
      windx_paint();
      return;
   }
   
   if (userfunc) userfunc(filez);                                          //  pass clicked file to user function
   return;
}


//  private function
//  KB event function - respond to keyboard navigation keys

int image_navi::KBxpress(GtkWidget *win, GdkEventKey *event, void *)       //  prevent propagation of key-press
{                                                                          //    events to toolbar buttons  v.2.9
   return 1;
}

int image_navi::KBxrelease(GtkWidget *win, GdkEventKey *event, void *)
{
   int      KBkey;
   
   KBkey = event->keyval;

   if (KBkey == GDK_plus) menufuncx(win,ZTX("bigger"));                    //  +/- = bigger/smaller thumbnails
   if (KBkey == GDK_equal) menufuncx(win,ZTX("bigger"));                   //                            v.2.5
   if (KBkey == GDK_minus) menufuncx(win,ZTX("smaller"));

   if (KBkey == GDK_Left) menufuncx(win,ZTX("prev page"));                 //  left arrow = previous page
   if (KBkey == GDK_Right) menufuncx(win,ZTX("next page"));                //  right arrow = next page
   if (KBkey == GDK_Up) menufuncx(win,ZTX("prev row"));                    //  up arrow = previous row
   if (KBkey == GDK_Down) menufuncx(win,ZTX("next row"));                  //  down arrow = next row

   if (KBkey == GDK_Escape) gtk_widget_destroy(win);                       //  Escape = quit thumbnail window
   
   return 1;
}


/**************************************************************************

   private function - manage list of image files within a directory

   get an image file in the same directory as given file or directory

   action: init      initz. list of image files and subdirectories
           initF     initz. from given file list
           find      return filez if in list, else null
           prev      return filez Nth previous file
           next      return filez Nth next file
           first     return first file in list
           last      return Nth last file in list

   Nth = 1  means filez -1, filez +1, and last file

   For init and initF, null is returned.
   For the others, filespec is returned or null if not found.
   Returned file belongs to caller and is a subject for zfree().

***************************************************************************/

char * image_navi::image_navigate(cchar *filez, cchar *action, int Nth)
{
   char           *buff;
   const char     *findcommand = "find \"%s\" -maxdepth 1";
   char           filezz[maxfcc], *pp, *file2;
   int            err, ii, nn = 0, ftyp, contx = 0;
   FILE           *fid;
   struct stat    statbuf;
   
   if (! strstr("init initF find first last prev next",action))            //  v.2.5
         zappcrash("image_navigate %s",action);

   if (strEqu(action,"init"))                                              //  initialize from given directory
   {
      for (int ii = 0; ii < nfiles; ii++) zfree(flist[ii]);                //  free prior list memory
      if (flist) zfree(flist);
      flist = (char **) zmalloc(flimit * sizeof(char *));                  //  list of file pointers

      nfiles = 0;                                                          //  no files
      if (filex) zfree(filex);                                             //  no anchor file
      filex = 0;

      strncpy0(dirkx,filez,maxfcc-2);                                      //  copy input file
      err = stat(dirkx,&statbuf);
      if (err) {
         pp = (char *) strrchr(dirkx,'/');                                 //  bad file, check directory part  
         if (! pp) return 0;
         pp[1] = 0;
         err = stat(dirkx,&statbuf);
         if (err) return 0;                                                //  give up, empty file list
      }

      if (S_ISREG(statbuf.st_mode)) {                                      //  if a file, get directory part
         pp = (char *) strrchr(dirkx,'/');
         if (! pp) return 0;
         pp[1] = 0;
      }

      while ((buff = command_output(contx,findcommand,dirkx)))             //  find all files
      {
         if (strEqu(buff,dirkx)) {                                         //  skip self directory
            zfree(buff);
            continue;
         }
         
         ftyp = image_file_type(buff);
         if (ftyp != 1 && ftyp != 2) {                                     //  not directory or image file type
            zfree(buff);                                                   //  (.thumbnails not ftyp 1)
            continue;
         }

         flist[nfiles] = buff;                                             //  add to file list
         if (ftyp == 1) flist[nfiles][0] = '!';                            //  if directory, make it sort first
         nfiles++;
         if (nfiles == flimit) zappcrash("more than %d files",flimit);
      }

      if (nfiles == 0) return 0;                                           //  no files

      HeapSort(flist,nfiles,image_fcomp);                                  //  Heap Sort - pointers to strings
   }

   if (strEqu(action,"initF"))                                             //  initialize from given list  v.2.4
   {
      for (int ii = 0; ii < nfiles; ii++) zfree(flist[ii]);                //  free prior list memory
      if (flist) zfree(flist);
      flist = (char **) zmalloc(flimit * sizeof(char *));                  //  list of file pointers

      nfiles = 0;                                                          //  no files
      if (filex) zfree(filex);                                             //  no anchor file
      filex = 0;

      strncpy0(dirkx,filez,maxfcc-1);                                      //  copy input file
      
      fid = fopen(dirkx,"r");                                              //  open file
      if (! fid) return 0;

      buff = zmalloc(maxfcc);
      
      while (true)                                                         //  read list of files
      {
         pp = fgets_trim(buff,maxfcc-1,fid,1);
         if (! pp) break;
         flist[nfiles] = strdupz(buff);                                    //  add files to memory list
         nfiles++;
         if (nfiles == flimit) zappcrash("more than %d files",flimit);
      }
      
      fclose(fid);
      zfree(buff);

      if (nfiles == 0) return 0;                                           //  no files

      HeapSort(flist,nfiles,image_fcomp);                                  //  Heap Sort - pointers to strings
   }

   if (nfiles == 0) return 0;                                              //  file list is empty
   
   if (! filez) *filezz = 0;                                               //  no filez arg               v.2.5
   else strncpy0(filezz,filez,maxfcc);                                     //  make modifiable copy of filez
   err = stat(filezz,&statbuf);
   if (! err && S_ISDIR(statbuf.st_mode)) filezz[0] = '!';                 //  if directory, match with sorted list

   for (ii = 0; ii < nfiles; ii++) {                                       //  search for filez in file list
      nn = image_fcomp(filezz,flist[ii]);                                  //  = 0: filez = flist[ii]
      if (nn <= 0) break;                                                  //  < 0: flist[ii-1] < filez < flist[ii]
   }
   
   if (ii == nfiles) ii--;                                                 //  filez > last file in list

   if (strnEqu(action,"init",4)) {                                         //  init or initF
      filex = strdupz(flist[ii]);                                          //  anchor file = 1st file
      filex[0] = '/';                                                      //  restore initial '/'
      return 0;
   }
   
   if (strEqu(action,"find"))                                              //  is filez in list or not   v.2.4
      if (nn != 0) return 0;

   if (strEqu(action,"first"))                                             //  return first file
      ii = 0;

   if (strEqu(action,"last")) {                                            //  Nth last file (1 = last)
      ii = nfiles - Nth;
      if (ii < 0) ii = 0;                                                  //  if < first file, return first
   }
   
   if (strEqu(action,"prev")) {                                            //  Nth previous file
      if (ii == 0) return 0;                                               //  filez <= first in list
      ii -= Nth;
      if (ii < 0) ii = 0;                                                  //  first file
   }
   
   if (strEqu(action,"next")) {                                            //  Nth next file
      if (nn == 0) ii += Nth;                                              //  get Nth next file       v.2.6
      if (ii > nfiles-1) return 0;                                         //  beyond last file
   }
   
   file2 = strdupz(flist[ii]);                                             //  copy file into new memory
   file2[0] = '/';                                                         //  restore initial '/'
   err = stat(file2,&statbuf);
   if (! err) return file2;                                                //  return file

   if (strcmpv(action,"prev","next",null))                                 //  try to skip over missing file
      return image_navigate(file2,action,1);                               //                     v.2.16
   else return 0;
}


//  private function for special file name compare
//  directories sort first, upper/lower case names sort together

int image_navi::image_fcomp(const char *file1, const char *file2)
{
   int      nn;
   nn = strcasecmp(file1,file2);                                           //  compare ignoring case
   if (nn != 0) return nn;
   nn = strcmp(file1,file2);                                               //  if equal, do normal compare
   return nn;
}

using namespace std;


/**************************************************************************
    parameter management functions
***************************************************************************/

struct t_parmlist {                                                        //  parameter list in memory
   int      max;                                                           //  max parameter count
   int      count;                                                         //  actual parameter count
   char     **name;                                                        //  pointer to names (list of char *)
   double   *value;                                                        //  pointer to values (list of double)
} parmlist;

int      parmlistvalid = 0;                                                //  flag
char     zparmfile[1000];                                                  //  last used parm file


//  initialize parameter list - must be called first

int initParmlist(int max)
{
   if (! parmlistvalid) {                                                  //  start with default parms file
      strcpy(zparmfile,get_zuserdir());
      strcat(zparmfile,"/parameters");                                     //  /home/user/.appname/parameters
   }
   
   if (parmlistvalid) {                                                    //  delete old parms
      delete [] parmlist.name;
      delete [] parmlist.value;
   }

   parmlist.max = max;
   parmlist.count = 0;
   char **names = new char*[max];                                          //  allocate max pointers for names
   double *values = new double[max];                                       //  allocate max doubles for values
   parmlist.name = names;
   parmlist.value = values;
   parmlistvalid = 1;
   return 0;
}


//  Load user parameters if the file exists, else initialize the
//  user parameters file from default application parameters.

int initz_userParms()
{
   if (! parmlistvalid) zappcrash("parmlistvalid = 0");

   int np = loadParms("parameters");
   if (! np) {
      saveParms("parameters");
      zmessageACK(ZTX("Initial parameters file created. \n"
                      "Inspect and revise if necessary."));
   }
   return np;
}


//  load parameters from a file, with file selection dialog

int loadParms()
{
   char     *pfile;
   int      np;
   
   if (! parmlistvalid) zappcrash("parmlistvalid = 0");

   pfile = zgetfile(ZTX("load parameters from a file"),zparmfile,"open","hidden");
   if (! pfile) return 0;
   
   np = loadParms(pfile);
   zfree(pfile);

   return np;
}
   

//  load parameters from a file
//  returns no. parameters loaded

int loadParms(const char *pfile)
{
   FILE        *fid;
   int         Nth, np1, np2 = 0, err;
   char        buff[100], *fgs, *pp;
   cchar       *pname, *pvalue;
   double      dvalue;
   
   if (! parmlistvalid) zappcrash("parmlistvalid = 0");

   if (! pfile) pfile = zparmfile;
   
   if (*pfile != '/') {                                                    //  if parm file name only,
      pp = (char *) strrchr(zparmfile,'/');                                //    make complete absolute path
      if (pp) strcpy(pp+1,pfile);                                          //      in same directory as prior
      pfile = zparmfile;
   }

   fid = fopen(pfile,"r");
   if (! fid) return 0;                                                    //  bad file
   strncpy0(zparmfile,pfile,999);                                          //  set current parm file

   while (true)                                                            //  read file
   {
      fgs = fgets_trim(buff,99,fid,1);
      if (! fgs) break;                                                    //  EOF

      pp = strchr(buff,'#');                                               //  eliminate comments
      if (pp) *pp = 0;

      Nth = 1;                                                             //  parse parm name, value
      pname = strField(buff,' ',Nth++);
      if (! pname) continue;
      pvalue = strField(buff,' ',Nth);
      if (! pvalue) continue;
      err = convSD(pvalue,dvalue);
      if (err) continue;
      np1 = setParm(pname,dvalue);                                         //  set the parameter
      if (! np1) continue;
      np2++;
   }
   
   fclose(fid);                                                            //  close file
   return np2;                                                             //  return parameter count
}


//  save parameters to a file, with file selection dialog

int saveParms()
{
   char     *pfile;
   int      np;

   if (! parmlistvalid) zappcrash("parmlistvalid = 0");

   pfile = zgetfile(ZTX("save parameters to a file"),zparmfile,"save","hidden");
   if (! pfile) return 0;

   np = saveParms(pfile);
   zfree(pfile);

   return np;
}


//  save parameters to a file

int saveParms(const char *pfile)
{
   FILE     *fid;
   int      np;
   char     *pp;

   if (! parmlistvalid) zappcrash("parmlistvalid = 0");

   if (*pfile != '/') {                                                    //  if parm file name only,
      pp = (char *) strrchr(zparmfile,'/');                                //    make complete absolute path
      if (pp) strcpy(pp+1,pfile);                                          //      in same directory as prior
      pfile = zparmfile;
   }

   fid = fopen(pfile,"w");
   if (! fid) { 
      zmessageACK(ZTX("cannot open file %s"),pfile);
      return 0;
   }

   strncpy0(zparmfile,pfile,999);

   for (np = 0; np < parmlist.count; np++)
      fprintf(fid," \"%s\"  %.12g \n",parmlist.name[np],parmlist.value[np]);
   
   fclose(fid);
   return np;
}


//  create a new paramater or change value of existing parameter

int setParm(const char *parmname, double parmval)
{
   int      ii, cc;
   char     *ppname;

   if (! parmlistvalid) zappcrash("parmlistvalid = 0");

   for (ii = 0; ii < parmlist.count; ii++)
      if (strEqu(parmlist.name[ii],parmname)) break;

   if (ii == parmlist.max) return 0;

   if (ii == parmlist.count) {
      parmlist.count++;
      cc = strlen(parmname);
      ppname = new char[cc+1];
      strTrim(ppname,parmname);
      parmlist.name[ii] = ppname;
   }

   parmlist.value[ii] = parmval;
   return parmlist.count;
}


//  get parameter value from parameter name

double getParm(const char *parmname)
{
   if (! parmlistvalid) zappcrash("parmlistvalid = 0");

   for (int ii = 0; ii < parmlist.count; ii++)
   {
      if (strNeq(parmlist.name[ii],parmname)) continue;
      return parmlist.value[ii];
   }

   return NAN;
}


//  get Nth parameter name (zero-based)

char * getParm(int Nth)
{
   if (! parmlistvalid) zappcrash("parmlistvalid = 0");
   if (Nth >= parmlist.count) return null;
   return parmlist.name[Nth];
}


//  list parameters in supplied text entry window

int listParms(GtkWidget *textWin)
{
   int            ii;
   const char     *pname;
   double         pvalue;

   for (ii = 0; ii < parmlist.count; ii++)
   {
      pname = getParm(ii);
      pvalue = getParm(pname);
      wprintf(textWin," %s  %.12g \n",pname,pvalue);
   }
   
   return parmlist.count;
}


//  edit parameters with a GUI
//  textWin != null enables button to list parameters in window
//  addp != 0 enables button to add new parameters
//  return: 0 if cancel, else parameter count                              //  v.2.7

int editParms(GtkWidget *textWin, int addp)
{
   GtkWidget      *peDialog, *peLabel[100], *peEdit[100], *peHbox[100];
   char           ptemp[20];
   const char     *pchval, *pname;
   double         pvalue;
   int            ii, err, iie = -1, zstat, floaded = 0;
   int            bcancel=1, bapply=2, bload=3, bsave=4, blist=5, baddp=6;
   
   if (! parmlistvalid) zappcrash("parmlistvalid = 0");

   build_dialog:                                                           //  build parameter edit dialog

   if (parmlist.count > 100) zappcrash("more than 100 parameters");
   
   if (textWin && addp) 
       peDialog = gtk_dialog_new_with_buttons
         (ZTX("edit parameters"), null, GTK_DIALOG_MODAL, 
          ZTX("load\nfile"),bload, ZTX("save\nfile"),bsave, ZTX("list\nall"),blist, 
          ZTX("add\nnew"),baddp, ZTX("cancel"),bcancel, ZTX("apply"),bapply, null);

   else if (textWin) 
      peDialog = gtk_dialog_new_with_buttons
         (ZTX("edit parameters"), null, GTK_DIALOG_MODAL, 
          ZTX("load\nfile"),bload, ZTX("save\nfile"),bsave, ZTX("list\nall"),blist, 
          ZTX("cancel"),bcancel, ZTX("apply"),bapply, null);

   else if (addp) 
      peDialog = gtk_dialog_new_with_buttons
         (ZTX("edit parameters"), null, GTK_DIALOG_MODAL, 
          ZTX("load\nfile"),bload, ZTX("save\nfile"),bsave,  
          ZTX("add\nnew"),baddp, ZTX("cancel"),bcancel, ZTX("apply"),bapply, null);

   else peDialog = 
      gtk_dialog_new_with_buttons
         (ZTX("edit parameters"), null, GTK_DIALOG_MODAL, 
          ZTX("load\nfile"),bload, ZTX("save\nfile"),bsave,  
          ZTX("cancel"),bcancel, ZTX("apply"),bapply, null);

   gtk_window_set_position(GTK_WINDOW(peDialog),GTK_WIN_POS_MOUSE);        //  v.2.7.1

   for (ii = 0; ii < parmlist.count; ii++)                                 //  labels and edit boxes side by side
   {                                                                       //  (parm names and parm values)
      peLabel[ii] = gtk_label_new(parmlist.name[ii]);
      gtk_misc_set_alignment(GTK_MISC(peLabel[ii]),1,0.5);
      gtk_label_set_width_chars(GTK_LABEL(peLabel[ii]),30);
      peEdit[ii] = gtk_entry_new();
      gtk_entry_set_width_chars(GTK_ENTRY(peEdit[ii]),12);
      sprintf(ptemp,"%.12g",parmlist.value[ii]);
      gtk_entry_set_text(GTK_ENTRY(peEdit[ii]),ptemp);
      peHbox[ii] = gtk_hbox_new(0,0);
      gtk_box_pack_start(GTK_BOX(peHbox[ii]),peLabel[ii],0,0,5);
      gtk_box_pack_start(GTK_BOX(peHbox[ii]),peEdit[ii],0,0,5);
      gtk_box_pack_start(GTK_BOX(GTK_DIALOG(peDialog)->vbox),peHbox[ii],1,1,2);
   }

   run_dialog:                                                             //  display dialog and get inputs
   
   if (iie > -1)
   {
      gtk_editable_select_region(GTK_EDITABLE(peEdit[iie]),0,-1);          //  focus on new or bad parameter
      gtk_widget_grab_focus(peEdit[iie]);
      iie = -1;
   }

   gtk_widget_show_all(peDialog);
   zstat = gtk_dialog_run(GTK_DIALOG(peDialog));

   if (zstat <= bcancel)                                                   //  kill, cancel
   {
      if (floaded) {
         zstat = zmessageYN(ZTX("apply?"));                                //  if file loaded, clarify  v.2.9
         if (! zstat) { 
            gtk_widget_destroy(peDialog);
            return 0;
         }
         zstat = bapply;
      }
   }
   
   if (zstat == bload)                                                     //  load from file
   {
      loadParms();
      gtk_widget_destroy(peDialog);
      floaded = 1;
      goto build_dialog;
   }
   
   for (ii = 0; ii < parmlist.count; ii++)                                 //  capture inputs and check if OK
   {
      pchval = gtk_entry_get_text(GTK_ENTRY(peEdit[ii]));
      err = convSD(pchval,pvalue);
      if (err && iie < 0) iie = ii;                                        //  remember 1st error
   }

   if (iie >= 0) goto run_dialog;                                          //  re-get bad input

   if (zstat == bapply)                                                    //  apply new values
   {
      for (ii = 0; ii < parmlist.count; ii++)                              //  capture inputs and save them  v.2.7
      {
         pchval = gtk_entry_get_text(GTK_ENTRY(peEdit[ii]));
         err = convSD(pchval,parmlist.value[ii]);
      }
      gtk_widget_destroy(peDialog);                                        //  done
      return parmlist.count;
   }

   if (zstat == bsave)                                                     //  save to file
   {
      for (ii = 0; ii < parmlist.count; ii++)                              //  apply new values  v.2.9
      {
         pchval = gtk_entry_get_text(GTK_ENTRY(peEdit[ii]));
         err = convSD(pchval,parmlist.value[ii]);
      }
      saveParms();
      floaded = 0;
      goto run_dialog;
   }
   
   if (zstat == blist)                                                     //  list parameters
   {
      listParms(textWin);
      goto run_dialog;
   }
   
   if (zstat == baddp)                                                     //  add parameter
   {
      pname = dialogText(ZTX("add parameter"),ZTX("(new parm name)"));
      if (! pname) goto run_dialog;
      setParm(pname,0.0);
      floaded = 1;
      iie = parmlist.count - 1;                                            //  focus on new parm
      gtk_widget_destroy(peDialog);
      goto build_dialog;
   }
   
   gtk_widget_destroy(peDialog);                                           //  unknown status
   return 0;
}


/**************************************************************************
      xstring class (dynamic length string)
***************************************************************************/

#define  wmiv  1648734981

int   xstring::tcount = 0;                                                 //  initz. static members
int   xstring::tmem = 0;


xstring::xstring(int cc)                                                   //  new xstring(cc)
{
   wmi = wmiv;
   xmem = (cc & 0x7ffffff8) + 8;                                           //  mod 8 length
   xpp = new char[xmem];                                                   //  allocate
   if (! xpp) appcrash("xstring NEW failure",null);
   tcount++;                                                               //  incr. object count
   tmem += xmem;                                                           //  incr. allocated memory
   xcc = 0;                                                                //  string cc = 0
   *xpp = 0;                                                               //  string = null
}


xstring::xstring(const char *string)                                       //  new xstring("initial string")
{
   wmi = wmiv;
   xcc = 0;
   if (string) xcc = strlen(string);                                       //  string length
   xmem = (xcc & 0x7ffffff8) + 8;                                          //  mod 8 length
   xpp = new char[xmem];                                                   //  allocate
   if (! xpp) appcrash("xstring NEW failure",null);
   tcount++;                                                               //  incr. object count
   tmem += xmem;                                                           //  incr. allocated memory
   *xpp = 0;
   if (xcc) strcpy(xpp,string);                                            //  copy string
}


xstring::xstring(const xstring & xstr)                                     //  new xstring2(xstring1)
{
   wmi = wmiv;
   xmem = xstr.xmem;                                                       //  allocate same length
   xcc = xstr.xcc;
   xpp = new char[xmem];
   if (! xpp) appcrash("xstring NEW failure",null);
   tcount++;                                                               //  incr. object count
   tmem += xmem;                                                           //  incr. allocated memory
   strcpy(xpp,xstr.xpp);                                                   //  copy string
}


xstring::~xstring()                                                        //  delete xstring
{  
   validate();
   delete[] xpp;                                                           //  release allocated memory
   xpp = 0;
   tcount--;                                                               //  decr. object count
   tmem -= xmem;                                                           //  decr. allocated memory
   if (tcount < 0) appcrash("xstring count < 0",null);
   if (tmem < 0) appcrash("xstring memory < 0",null);
   if (tcount == 0 && tmem > 0) appcrash("xstring memory leak",null);
}


xstring xstring::operator= (const xstring & xstr)                          //  xstring2 = xstring1
{
   validate();
   xstr.validate();
   if (this == &xstr) return *this;
   xcc = xstr.xcc;
   if (xmem < xcc+1)
   {
      delete[] xpp;                                                        //  expand memory if needed
      tmem -= xmem;
      xmem = (xcc & 0x7ffffff8) + 8;                                       //  mod 8 length
      xpp = new char[xmem];
      if (! xpp) appcrash("xstring NEW failure",null);
      tmem += xmem;
   }
   strcpy(xpp,xstr.xpp);                                                   //  copy string
   return *this;
}


xstring xstring::operator= (const char *str)                               //  xstring = "some string"
{
   validate();
   xcc = 0;
   *xpp = 0;
   if (str) xcc = strlen(str);
   if (xmem < xcc+1)
   {
      delete[] xpp;                                                        //  expand memory if needed
      tmem -= xmem;
      xmem = (xcc & 0x7ffffff8) + 8;                                       //  mod 8 length
      xpp = new char[xmem];
      if (! xpp) appcrash("xstring NEW failure",null);
      tmem += xmem;
   }
   if (xcc) strcpy(xpp,str);                                               //  copy string
   return *this;
}


xstring operator+ (const xstring & x1, const xstring & x2)                 //  xstring1 + xstring2
{
   x1.validate();
   x2.validate();
   xstring temp(x1.xcc + x2.xcc);                                          //  build temp xstring
   strcpy(temp.xpp,x1.xpp);                                                //    with both input strings
   strcpy(temp.xpp + x1.xcc, x2.xpp);
   temp.xcc = x1.xcc + x2.xcc;
   temp.validate();
   return temp;
}


xstring operator+ (const xstring & x1, const char *s2)                     //  xstring + "some string"
{
   x1.validate();
   int cc2 = 0;
   if (s2) cc2 = strlen(s2);
   xstring temp(x1.xcc + cc2);                                             //  build temp xstring
   strcpy(temp.xpp,x1.xpp);                                                //    with both input strings
   if (s2) strcpy(temp.xpp + x1.xcc, s2);
   temp.xcc = x1.xcc + cc2;
   temp.validate();
   return temp;
}


xstring operator+ (const char *s1, const xstring & x2)                     //  "some string" + xstring
{
   x2.validate();
   int cc1 = 0;
   if (s1) cc1 = strlen(s1);
   xstring temp(cc1 + x2.xcc);                                             //  build temp xstring
   if (s1) strcpy(temp.xpp,s1);                                            //    with both input strings
   strcpy(temp.xpp + cc1, x2.xpp);
   temp.xcc = cc1 + x2.xcc;
   temp.validate();
   return temp;
}


void xstring::insert(int pos, const char *string, int cc)                  //  insert cc chars from string at pos
{                                                                          //  pad if pos > xcc or cc > string
   validate();

   int scc = strlen(string);
   if (! cc) cc = scc;

   int pad = pos - xcc;
   if (pad < 0) pad = 0;                                

   if (xmem < xcc + cc + pad + 1)                                          //  allocate more memory if needed
   {
      int newmem = xcc + cc + pad;
      newmem = (newmem & 0x7ffffff8) + 8;                                  //  mod 8 length
      char * xpp2 = new char[newmem];
      if (! xpp2) appcrash("xstring NEW failure",null);
      strcpy(xpp2,xpp);                                                    //  copy to new space
      delete[] xpp;
      xpp = xpp2;
      tmem += newmem - xmem;
      xmem = newmem;
   }

   if (pad) memset(xpp+xcc,' ',pad);                                       //  add blanks up to pos

   for (int ii = xcc + pad; ii >= pos; ii--)                               //  make hole for inserted string
           *(xpp+ii+cc) = *(xpp+ii);

   if (cc > scc) memset(xpp+pos+scc,' ',cc-scc);                           //  blank pad if cc > string
   if (cc < scc) scc = cc;
   strncpy(xpp+pos,string,scc);                                            //  insert string, without null

   xcc += cc + pad;                                                        //  set new length
   xpp[xcc] = 0;
   validate();
}


void xstring::overlay(int pos, const char *string, int cc)                 //  overlay substring
{
   validate();

   int scc = strlen(string);
   if (! cc) cc = scc;

   if (xmem < pos + cc + 1)                                                //  allocate more memory if needed
   {
      int newmem = pos + cc;
      newmem = (newmem & 0x7ffffff8) + 8;                                  //  mod 8 length
      char * xpp2 = new char[newmem];
      if (! xpp2) appcrash("xstring NEW failure",null);
      strcpy(xpp2,xpp);                                                    //  copy to new space
      delete[] xpp;
      xpp = xpp2;
      tmem += newmem - xmem;
      xmem = newmem;
   }

   if (pos > xcc) memset(xpp+xcc,' ',pos-xcc);                             //  add blanks up to pos
   
   if (cc > scc) memset(xpp+pos+scc,' ',cc-scc);                           //  blank pad if cc > string
   if (cc < scc) scc = cc;
   strncpy(xpp+pos,string,scc);                                            //  insert string, without null

   if (pos + cc > xcc) xcc = pos + cc;                                     //  set new length 
   xpp[xcc] = 0;
   validate();
}


void xstring::getStats(int & tcount2, int & tmem2)                         //  get statistics
{
   tcount2 = tcount;
   tmem2 = tmem;
}


void xstring::validate() const                                             //  validate integrity
{
   if (wmi != wmiv) appcrash("xstring bad wmi",null);
   if (xmem < xcc+1) appcrash("xstring xmem < xcc+1",null);
   if (xcc != (int) strlen(xpp)) appcrash("xstring xcc != strlen(xpp)",null);
}


/**************************************************************************
      Vxstring class (array or vector of xstring)
***************************************************************************/

Vxstring::Vxstring(int ii)                                                 //  constructor
{  
   pdata = 0;
   nd = ii;
   if (nd) pdata = new xstring[nd];
   if (nd && !pdata) appcrash("Vxstring NEW fail",null);
}


Vxstring::~Vxstring()                                                      //  destructor
{
   if (nd) delete[] pdata;
   pdata = 0;
   nd = 0;
}


Vxstring::Vxstring(const Vxstring & pold)                                  //  copy constructor
{
   pdata = 0;
   nd = pold.nd;                                                           //  set size
   if (nd) pdata = new xstring[nd];                                        //  allocate memory
   if (nd && !pdata) appcrash("Vxstring NEW fail");
   for (int ii = 0; ii < nd; ii++) pdata[ii] = pold[ii];                   //  copy defined elements
}


Vxstring Vxstring::operator= (const Vxstring & vdstr)                      //  operator =
{
   if (nd) delete[] pdata;                                                 //  delete old memory
   pdata = 0;
   nd = vdstr.nd;
   if (nd) pdata = new xstring[nd];                                        //  allocate new memory
   if (nd && !pdata) appcrash("Vxstring NEW fail",null);
   for (int ii = 0; ii < nd; ii++) pdata[ii] = vdstr.pdata[ii];            //  copy elements
   return *this;
}


xstring & Vxstring::operator[] (int ii)                                    //  operator []
{
   static xstring xnull(0);
   if (ii < nd) return pdata[ii];                                          //  return reference
   appcrash("Vxstring index invalid %d %d",nd,ii,null);
   return xnull;
}


const xstring & Vxstring::operator[] (int ii) const                        //  operator []
{
   static xstring xnull(0);
   if (ii < nd) return pdata[ii];                                          //  return reference
   appcrash("Vxstring index invalid %d %d",nd,ii,null);
   return xnull;
}


int Vxstring::search(const char *string)                                   //  find element in unsorted Vxstring
{
   for (int ii = 0; ii < nd; ii++)
        if (strEqu(pdata[ii],string)) return ii;
   return -1;
}


int Vxstring::bsearch(const char *string)                                  //  find element in sorted Vxstring
{                                                                          //   (binary search)
   int   nn, ii, jj, kk, rkk;

   nn = nd;
   if (! nn) return 0;                                                     //  empty list

   ii = nn / 2;                                                            //  next element to search
   jj = (ii + 1) / 2;                                                      //  next increment
   nn--;                                                                   //  last element
   rkk = 0;

   while (1)
   {
      kk = strcmp(pdata[ii],string);                                       //  check element

      if (kk > 0) 
      {
         ii -= jj;                                                         //  too high, go down
         if (ii < 0) return -1;
      }

      else if (kk < 0) 
      {
         ii += jj;                                                         //  too low, go up
         if (ii > nn) return -1;
      }

      else if (kk == 0) return ii;                                         //  matched

      jj = jj / 2;                                                         //  reduce increment

      if (jj == 0) 
      {
         jj = 1;                                                           //  step by 1 element
         if (! rkk) rkk = kk;                                              //  save direction
         else 
         {
            if (rkk > 0) { if (kk < 0) return -1; }                        //  if change direction, fail
            else if (kk > 0) return -1;
         }
      }
   }
}


static int  VDsortKeys[10][3], VDsortNK;

int Vxstring::sort(int NK, int keys[][3])                                  //  sort elements by subfields
{                                                                          //  key[ii][0] = position
   int     NR, RL, ii;                                                     //         [1] = length
   HeapSortUcomp  VDsortComp;                                              //         [2] = 1/2 = ascending/desc.
                                                                           //             = 3/4 =  + ignore case
   NR = nd;
   if (NR < 2) return 1;

   RL = sizeof(xstring);

   if (NK < 1) appcrash("Vxstring::sort, bad NK",null);
   if (NK > 10) appcrash("Vxstring::sort, bad NK",null);
   VDsortNK = NK;

   for (ii = 0; ii < NK; ii++)
   {
      VDsortKeys[ii][0] = keys[ii][0];
      VDsortKeys[ii][1] = keys[ii][1];
      VDsortKeys[ii][2] = keys[ii][2];
   }

   HeapSort((char *) pdata,RL,NR,VDsortComp);

   return 1;
}


int VDsortComp(const char *r1, const char *r2)
{
   xstring      *d1, *d2;
   const char   *p1, *p2;
   int          ii, stat, kpos, ktype, kleng;

   d1 = (xstring *) r1;
   d2 = (xstring *) r2;
   p1 = *d1;
   p2 = *d2;

   for (ii = 0; ii < VDsortNK; ii++)                                       //  compare each key
   {
      kpos = VDsortKeys[ii][0];
      kleng = VDsortKeys[ii][1];
      ktype = VDsortKeys[ii][2];

      if (ktype == 1)
      {
         stat = strncmp(p1+kpos,p2+kpos,kleng);
         if (stat) return stat;
         continue;
      }

      else if (ktype == 2)
      {
         stat = strncmp(p1+kpos,p2+kpos,kleng);
         if (stat) return -stat;
         continue;
      }

      else if (ktype == 3)
      {
         stat = strncasecmp(p1+kpos,p2+kpos,kleng);
         if (stat) return stat;
         continue;
      }

      else if (ktype == 4)
      {
         stat = strncasecmp(p1+kpos,p2+kpos,kleng);
         if (stat) return -stat;
         continue;
      }

      appcrash("Vxstring::sort, bad KEYS sort type",null);
   }

   return 0;
}


int Vxstring::sort(int pos, int cc)                                        //  sort elements ascending
{
   int   key[3];

   if (! cc) cc = 999999;
   key[0] = pos;
   key[1] = cc;
   key[2] = 1;

   sort(1,&key);

   return 1;
}


/**************************************************************************
     Hash Table class
***************************************************************************/

//  static members (robust for tables up to 60% full)

int HashTab::trys1 = 100;                                                  //  Add() tries
int HashTab::trys2 = 200;                                                  //  Find() tries


HashTab::HashTab(int _cc, int _cap)                                        //  constructor
{
   cc = 4 * (_cc + 4) / 4;                                                 //  + 1 + mod 4 length
   cap = _cap;
   int len = cc * cap;
   table = new char [len];
   if (! table) appcrash("HashTab() new %d fail",len,null);
   memset(table,0,len);
}


HashTab::~HashTab()                                                        //  destructor
{
   delete [] table;
   table = 0;
}


//  Add a new string to table

int HashTab::Add(const char *string)
{
   int   pos, fpos, trys;

   pos = strHash(string,cap);                                              //  get random position
   pos = pos * cc;

   for (trys = 0, fpos = -1;                                               //  find next free slot 
        trys < trys1;                                                      //   at/after position
        trys++, pos += cc)
   {
      if (pos >= cap * cc) pos = 0;                                        //  last position wraps to 1st

      if (! table[pos])                                                    //  empty slot: string not found
      {
         if (fpos != -1) pos = fpos;                                       //  use prior deleted slot if there
         strncpy(table+pos,string,cc);                                     //  insert new string
         table[pos+cc-1] = 0;                                              //  insure null terminator
         return (pos/cc);                                                  //  return rel. table entry
      }

      if (table[pos] == -1)                                                //  deleted slot
      {
         if (fpos == -1) fpos = pos;                                       //  remember 1st one found
         continue;
      }

      if (strEqu(string,table+pos)) return -2;                             //  string already present
   }   

   return -3;                                                              //  table full (trys1 exceeded)
}


//  Delete a string from table

int HashTab::Del(const char *string)
{
   int   pos, trys;

   pos = strHash(string,cap);                                              //  get random position
   pos = pos * cc;

   for (trys = 0;                                                          //  search for string
        trys < trys2;                                                      //   at/after position
        trys++, pos += cc)
   {
      if (pos >= cap * cc) pos = 0;                                        //  last position wraps to 1st

      if (! table[pos]) return -1;                                         //  empty slot, string not found

      if (strEqu(string,table+pos))                                        //  string found
      {
         table[pos] = -1;                                                  //  delete table entry
         return (pos/cc);                                                  //  return rel. table entry
      }
   }   

   appcrash("HashTab::Del() bug",null);                                    //  exceed trys2, must not happen
   return 0;                                                               //  (table too full to function)
}


//  Find a table entry.

int HashTab::Find(const char *string)
{
   int   pos, trys;

   pos = strHash(string,cap);                                              //  get random position
   pos = pos * cc;

   for (trys = 0;                                                          //  search for string
        trys < trys2;                                                      //   at/after position
        trys++, pos += cc)
   {
      if (pos >= cap * cc) pos = 0;                                        //  last position wraps to 1st
      if (! table[pos]) return -1;                                         //  empty slot, string not found
      if (strEqu(string,table+pos)) return (pos/cc);                       //  string found, return rel. entry
   }   

   appcrash("HashTab::Find() bug",null);                                   //  cannot happen
   return 0;
}


//  return first or next table entry

int HashTab::GetNext(int & ftf, char *string)
{
   static int    pos;

   if (ftf)                                                                //  initial call
   {
      pos = 0;
      ftf = 0;
   }

   while (pos < (cap * cc))
   {
      if ((table[pos] == 0) || (table[pos] == -1))
      {
         pos += cc;
         continue;
      }

      strcpy(string,table+pos);                                            //  return string
      pos += cc;
      return 1;
   }

   return -4;                                                              //  EOF
}


int HashTab::Dump()
{
   int   ii, pos;

   for (ii = 0; ii < cap; ii++)
   {
      pos = ii * cc;
      if (table[pos] > 0) printf("%d %s \n", pos, table + pos);
      if (table[pos] == -1) printf("%d deleted \n", pos);
   }        
   return 1;
}


/**************************************************************************
     class for queue of dynamic strings
***************************************************************************/

Queue::Queue(int cap)                                                      //  constructor
{
   int   err;
   
   err = mutex_init(&qmutex, 0);                                           //  create mutex = queue lock
   if (err) appcrash("Queue(), mutex init fail",null);

   qcap = cap;                                                             //  queue capacity
   ent1 = entN = qcount = 0;                                               //  state = empty
   vd = new Vxstring(qcap);                                                //  create vector of xstring's
   if (! vd) appcrash("Queue(), NEW fail %d",cap,null);
   strcpy(wmi,"queue");
   return;
}


Queue::~Queue()                                                            //  destructor
{
   if (strNeq(wmi,"queue")) appcrash("~Queue wmi fail",null);
   wmi[0] = 0;
   mutex_destroy(&qmutex);                                                 //  destroy mutex
   qcount = qcap = ent1 = entN = -1;
   delete vd;
   vd = 0;
   return;
}


void Queue::lock()                                                         //  lock queue (private)
{
   int   err;
   err = mutex_lock(&qmutex);                                              //  reserve mutex or suspend
   if (err) appcrash("Queue mutex lock fail",null);
   return;
}


void Queue::unlock()                                                       //  unlock queue (private)
{
   int   err;
   err = mutex_unlock(&qmutex);                                            //  release mutex
   if (err) appcrash("Queue mutex unlock fail",null);
   return;
}


int Queue::getCount()                                                      //  get current entry count
{
   if (strNeq(wmi,"queue")) appcrash("Queue getCount wmi fail",null);
   return qcount;
}


int Queue::push(const xstring *newEnt, double wait)                        //  add entry to queue, with max. wait
{
   double  elaps = 0.0;
   int     count;
   
   if (strNeq(wmi,"queue")) appcrash("Queue::push wmi fail",null);

   lock();                                                                 //  lock queue
   while (qcount == qcap) {                                                //  queue full
      unlock();                                                            //  unlock queue
      if (elaps >= wait) return -1;                                        //  too long, return -1 status
      usleep(1000);                                                        //  sleep in 1 millisec. steps
      elaps += 0.001;                                                      //  until queue not full
      lock();                                                              //  lock queue
   }

   (* vd)[entN] = *newEnt;                                                 //  copy new entry into queue
   entN++;                                                                 //  incr. end pointer
   if (entN == qcap) entN = 0;
   qcount++;                                                               //  incr. queue count
   count = qcount;
   unlock();                                                               //  unlock queue
   return count;                                                           //  return curr. queue count
}


xstring *Queue::pop1()                                                     //  get 1st (oldest) entry and remove
{
   xstring    *entry;
   
   if (strNeq(wmi,"queue")) appcrash("Queue::pop1 wmi fail",null);

   lock();                                                                 //  lock queue

   if (qcount == 0) entry = 0;                                             //  queue empty
   else {
      entry = &(* vd)[ent1];                                               //  get first entry
      ent1++;                                                              //  index pointer to next
      if (ent1 == qcap) ent1 = 0;
      qcount--;                                                            //  decr. queue count
   }

   unlock();                                                               //  unlock queue
   return entry;
}


xstring *Queue::popN()                                                     //  get last (newest) entry and remove
{
   xstring   *entry;
   
   if (strNeq(wmi,"queue")) appcrash("Queue::popN wmi fail",null);

   lock();                                                                 //  lock queue

   if (qcount == 0) entry = 0;                                             //  queue empty
   else {
      if (entN == 0) entN = qcap;                                          //  index pointer to prior
      entN--;
      qcount--;                                                            //  decr. queue count
      entry = &(* vd)[entN];                                               //  get last entry
   }

   unlock();                                                               //  unlock queue
   return entry;
}


/**************************************************************************

   Tree class, tree-structured data storage without limits

   Store any amount of data at any depth within a tree-structure with named nodes.
   Data can be found using an ordered list of node names or node numbers.

   Node numbers are in the sequence added using put() with names,
   or the same as those numbers used in put() with numbers.

   Internal code conventions: 
      - caller level is node 0, next level is node 1, etc.
      - node names and numbers in calls to get() and put() refer to next levels
      - number of levels = 1+nn, where nn is max. in calls to put(...nodes[], nn)

***************************************************************************/

#define wmid 1374602859


//  constructor

Tree::Tree(const char *name)
{
   wmi = wmid;
   tname = 0;
   tmem = 0;
   tdata = 0;
   nsub = 0;
   psub = 0;

   if (name) 
   {
      int cc = strlen(name);
      tname = new char[cc+1];
      if (! tname) appcrash("Tree, no memory",null);
      strcpy(tname,name);
   }
}


//  destructor

Tree::~Tree()
{
   if (wmi != wmid) appcrash("not a Tree",null);
   if (tname) delete [] tname;
   tname = 0;
   if (tmem) free(tdata);
   tmem = 0;
   tdata = 0;
   for (int ii = 0; ii < nsub; ii++) delete psub[ii];
   if (psub) free(psub);
   nsub = 0;
   psub = 0;
}


//  put data by node names[]

int Tree::put(void *data, int dd, char *nodes[], int nn)
{
   Tree    *tnode;
   
   if (wmi != wmid) appcrash("not a Tree",null);
   tnode = make(nodes,nn);
   if (tnode->tdata) free(tnode->tdata);
   tnode->tdata = new char[dd];
   if (! tnode->tdata) appcrash("Tree, no memory",null);
   tnode->tmem = dd;
   memmove(tnode->tdata,data,dd);
   return 1;
}


//  put data by node numbers[]

int Tree::put(void *data, int dd, int nodes[], int nn)
{
   Tree    *tnode;
   
   if (wmi != wmid) appcrash("not a Tree",null);
   tnode = make(nodes,nn);
   if (tnode->tdata) free(tnode->tdata);
   tnode->tdata = new char[dd];
   if (! tnode->tdata) appcrash("Tree, no memory",null);
   tnode->tmem = dd;
   memmove(tnode->tdata,data,dd);
   return 1;
}


//  get data by node names[]

int Tree::get(void *data, int dd, char *nodes[], int nn)
{
   Tree *tnode = find(nodes,nn);
   if (! tnode) return 0;
   if (! tnode->tmem) return 0;
   if (dd > tnode->tmem) dd = tnode->tmem;
   memmove(data,tnode->tdata,dd);
   return dd;
}


//  get data by node numbers[]

int Tree::get(void *data, int dd, int nodes[], int nn)
{
   Tree *tnode = find(nodes,nn);
   if (! tnode) return 0;
   if (! tnode->tmem) return 0;
   if (dd > tnode->tmem) dd = tnode->tmem;
   memmove(data,tnode->tdata,dd);
   return dd;
}


//  find a given node by names[]

Tree * Tree::find(char *nodes[], int nn)
{
   int      ii;
   
   for (ii = 0; ii < nsub; ii++)
      if (psub[ii]->tname && strEqu(nodes[0],psub[ii]->tname)) break;
   if (ii == nsub) return 0;
   if (nn == 1) return psub[ii];
   return psub[ii]->find(&nodes[1],nn-1);
}


//  find a given node by numbers[]

Tree * Tree::find(int nodes[], int nn)
{
   int ii = nodes[0];
   if (ii >= nsub) return 0;
   if (! psub[ii]) return 0;
   if (nn == 1) return psub[ii];
   return psub[ii]->find(&nodes[1],nn-1);
}


//  find or create a given node by names[]

Tree * Tree::make(char *nodes[], int nn)
{
   int      ii;
   Tree   **psub2;
   
   for (ii = 0; ii < nsub; ii++)
      if (psub[ii]->tname && strEqu(nodes[0],psub[ii]->tname)) break;

   if (ii == nsub)
   {
      psub2 = new Tree * [nsub+1];
      if (! psub2) appcrash("Tree, no memory",null);
      for (ii = 0; ii < nsub; ii++) psub2[ii] = psub[ii];
      delete [] psub;
      psub = psub2;
      nsub++;
      psub[ii] = new Tree(nodes[0]);
      if (! psub[ii]) appcrash("Tree, no memory",null);
   }

   if (nn == 1) return psub[ii];
   return psub[ii]->make(&nodes[1],nn-1);
}


//  find or create a given node by numbers[]

Tree * Tree::make(int nodes[], int nn)
{
   Tree   **psub2;
   int      ii, jj;

   ii = nodes[0];
   if ((ii < nsub) && psub[ii])
   {
      if (nn == 1) return psub[ii];
      return psub[ii]->make(&nodes[1],nn-1);
   }

   if (ii >= nsub)
   {
      psub2 = new Tree * [ii+1];
      if (psub2 == null) appcrash("Tree, no memory",null);
      for (jj = 0; jj < nsub; jj++) psub2[jj] = psub[jj];
      for (jj = nsub; jj < ii; jj++) psub2[jj] = 0;
      delete [] psub;
      psub = psub2;
      nsub = ii + 1;
   }

   psub[ii] = new Tree("noname");
   if (! psub[ii]) appcrash("Tree, no memory",null);

   if (nn == 1) return psub[ii];
   return psub[ii]->make(&nodes[1],nn-1);
}


//  dump tree data to stdout (call with level 0)

void Tree::dump(int level)
{
   const char    *name;
   
   if (! tname) name = "noname";
   else name = tname;
   printf("%*s level: %d  name: %s  subs: %d  mem: %d \n",
                        level*2,"",level,name,nsub,tmem);
   for (int ii = 0; ii < nsub; ii++) 
         if (psub[ii]) psub[ii]->dump(level+1);
}


//  get node counts and total data per level
//  level 0 + nn more levels, as given in calls to put(...nodes[],nn)
//  caller must initialize counters to zero

void Tree::stats(int nn[], int nd[])
{
   nn[0] += 1;
   nd[0] += tmem;
   for (int ii = 0; ii < nsub; ii++) 
      if (psub[ii]) psub[ii]->stats(&nn[1],&nd[1]);
}



