/* options.c - Routines to handle the setting of user options.
   Copyright (C) 1991, 1992, 1993 Kristian Nielsen.

   This file is part of XFH, the compressing file system handler.

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.            */

#include "CFS.h"
#include "dossupport.h"

#include <pragma/icon_lib.h>

#include <ctype.h>
#include <string.h>
#include <stdlib.h>

#define EOL '\n'         /* Seperates lines in option files. */
#define STARTUP_SEP '!'  /* Seperates entries ("lines") in startup string. */
#define QUOTE_CHAR '\"'  /* Will be stripped from startup string. ("*"->*) */
#define DEFCHAR '='
#define FAKEDEFCHAR '&'  /* Chars usable in options ('optname * option').  */

struct Library *IconBase;

static BOOL parse_bool_opt(glb glob, char *opt, BOOL *var){
  debug(("parse_bool_opt(): '%s'.\n",opt));
  if(MatchToolValue(opt, "OFF")) *var = FALSE;
  else if(MatchToolValue(opt, "NO")) *var = FALSE;
  else if(MatchToolValue(opt, "ON")) *var = TRUE;
  else if(MatchToolValue(opt, "YES")) *var = TRUE;
  else{
    debug(("Illegal bolean value.\n"));
    return FALSE;
  }
  debug(("Result: %ld.\n",*var));
  return TRUE;
}

/* This function can be used to set a boolean option. */

static BOOL bool_opt(glb glob, char *opt, BOOL *var, BOOL default_val){
   if(!opt){
      *var = default_val;
      return TRUE;
   }else{
      return parse_bool_opt(glob, opt, var);
   }
}

/* These two functions can be used to set/sleanup string options. */

static void string_cleanup(glb glob, char **var){
   if(*var){
      dosfree(*var);
      *var = NULL;
   }
}

static BOOL string_opt(glb glob, char *opt, char **var, char *default_val){
   int len;
   char *p;
   
   if(!opt){
      /* Check for a default value of NULL. */
      if(!default_val){
         string_cleanup(glob, var);
         *var = default_val;
         return TRUE;
      }else{
         opt = default_val;
      }
   }
   len = strlen(opt)+1;
   if(!(p = dosalloc(len))){
      OUTOFMEM;
      return FALSE;
   }
   strcpy(p,opt);
   string_cleanup(glob, var);
   *var = p;
   return TRUE;
}

/* The option setting functions.
 * Each function is passed the remainder of the options string as
 * entered by the user, and is expected to set the appropriate option
 * and return a boolean succes/failure.
 *
 * If called with a NULL argument, should set a default option.
 *
 * NOTE: A cleanup function will not be called without the corresponding
 * set function being called first.
 */
 
static BOOL opt_rootdir(glb glob, char *opt){
   char default_name[MAXFILENAME+4];
   
   /* Get assign name to directory to serve as our default root. Obtained
    * by pruning the first char of the device name (ie. XDH0:->DH0:). */
   if(!glob->devname || !glob->devname[0]
                     || strlen(glob->devname)>MAXFILENAME+1){
      debug(("PANIC: Bad device name.\n"));
      return FALSE;
   }
   strncpy(default_name,&glob->devname[1],MAXFILENAME+2);
   strcat(default_name,":");
   
   return string_opt(glob, opt, &glob->xRootName, default_name);
}

static void cleanup_rootdir(glb glob){
   string_cleanup(glob, &glob->xRootName);
}


/* Option 'VOLUMENAME'. */
static void cleanup_volname(glb glob){
   string_cleanup(glob, &glob->uservolname);
}

static BOOL opt_volname(glb glob, char *opt){
  /* The default is NULL - this will signify to createvolnode() that it
   * should create a default volume name.
   */
  cleanup_volname(glob);
  if(!string_opt(glob, opt, &glob->uservolname, NULL)) return FALSE;
  if(glob->volnode){
    return SetVolumeNameVolNode(glob, glob->uservolname);
  }else{
    return TRUE;   /* createvolnode will set name. */
  }
}


/* Option 'AUTOCOMPRESS'. */
static BOOL opt_autocompress(glb glob, char *opt){
   return bool_opt(glob, opt, &glob->autocompress, FALSE);
}


/* Option 'XSCAN'. */
static BOOL opt_xscan(glb glob, char *opt){
   return bool_opt(glob, opt, &glob->xscan, FALSE);
}


/* Option 'STEPDOWN'. */
static BOOL opt_stepdown(glb glob, char *opt){
   return bool_opt(glob, opt, &glob->stepdown, FALSE);
}


/* Option 'PACKMODE'. */
static BOOL opt_packmode(glb glob, char *opt){
   return string_opt(glob, opt, &glob->packmode, "NUKE");
}

static void cleanup_packmode(glb glob){
   string_cleanup(glob, &glob->packmode);
}


/* Option 'PASSWORD'. */
static BOOL opt_password(glb glob, char *opt){
   /* The default is NULL - no password (valid acc. to 'xpk.doc'). */
   return string_opt(glob, opt, &glob->xpkpassword, NULL);
}

static void cleanup_password(glb glob){
   string_cleanup(glob, &glob->xpkpassword);
}


/* Option 'TRUNCATEONPACK'. */
static BOOL opt_truncateonpack(glb glob, char *opt){
   return bool_opt(glob, opt, &glob->truncateonpack, FALSE);
}


/* Option 'XPKPRIORITY'. */
static BOOL opt_xpkpriority(glb glob, char *opt){
   if(opt){
      glob->xpksetpri = TRUE;
      glob->xpkpri = atoi(opt);
   }else{
      glob->xpksetpri = FALSE;
   }
   return TRUE;
}


/* Option 'CREATEVOLUME'. */
static BOOL opt_createvol(glb glob, char *opt){
   return bool_opt(glob, opt, &glob->createvolnode, TRUE);
}


/* Option 'FAILONEXNEXT'. */
static BOOL opt_failonexnext(glb glob, char *opt){
   return bool_opt(glob, opt, &glob->FailOnExNext, TRUE);
}


/* Option 'COMPRESSREADWRITE'. */
static BOOL opt_compressrw(glb glob, char *opt){
   return bool_opt(glob, opt, &glob->compressreadwrite, TRUE);
}
      
     
/* Option 'ALLOWAPPEND'. */
static BOOL opt_allowappend(glb glob, char *opt){
   return bool_opt(glob, opt, &glob->allowappend, TRUE);
}
      
     
/* Option 'KILLSTARTUP'. */
static void cleanup_kstartup(glb glob){
   DevNode_Stuff_Startup_String(glob, glob->bcplstartup);
}


static BOOL opt_killstartup(glb glob, char *opt){
   BOOL tmp;

   if(!bool_opt(glob, opt, &tmp, TRUE)){
      return FALSE;
   }
   if(tmp){
      DevNode_Stuff_Startup_String(glob, 0L);
   }else{
      cleanup_kstartup(glob);
   }
   return TRUE;
}


/* Option 'PORTNAME'. */
static BOOL opt_arexxportname(glb glob, char *opt){
   return string_opt(glob, opt, &glob->userarexxportname, NULL);
}

/* Option 'ENVOYKLUDGE'. */
static BOOL opt_envoykludge(glb glob, char *opt){
   return bool_opt(glob, opt, &glob->EnvoyKludge, FALSE);
}


static void cleanup_arexxpname(glb glob){
   string_cleanup(glob, &glob->userarexxportname);
}


/* The 'options table'. */

static struct opttable{
   char *opt;
   BOOL (*set)(glb, char *);     /* 'set' function. */
   void (*cleanup)(glb);         /* 'cleanup' function. */
   BOOL persistent;              /* True -> option can be set only once. */
} opttable[] = {
   "ROOTDIR",           opt_rootdir,         cleanup_rootdir,   TRUE,
   "VOLUMENAME",        opt_volname,         cleanup_volname,   FALSE,
   "AUTOCOMPRESS",      opt_autocompress,    NULL,              FALSE,
   "XSCAN",             opt_xscan,           NULL,              FALSE,
   "STEPDOWN",          opt_stepdown,        NULL,              FALSE,
   "PACKMODE",          opt_packmode,        cleanup_packmode,  FALSE,
   "PASSWORD",          opt_password,        cleanup_password,  FALSE,
   "TRUNCATEONPACK",    opt_truncateonpack,  NULL,              FALSE,
   "XPKPRIORITY",       opt_xpkpriority,     NULL,              FALSE,
   "CREATEVOLUME",      opt_createvol,       NULL,              TRUE,
   "FAILONEXNEXT",      opt_failonexnext,    NULL,              FALSE,
   "KILLSTARTUP",	opt_killstartup,     cleanup_kstartup,  FALSE,
   "COMPRESSREADWRITE", opt_compressrw,      NULL,              FALSE,
   "ALLOWAPPEND",       opt_allowappend,     NULL,              FALSE,
   "PORTNAME",          opt_arexxportname,   cleanup_arexxpname,TRUE,
   "ENVOYKLUDGE",       opt_envoykludge,     NULL,              FALSE,
   NULL
};


/* This function sets the options according to an option file (in
 * char ** form as returned by buf2array()). The first time it is called
 * initial should be TRUE to set default and persistent options.
 * And initial should be TRUE ONLY the first time this is called!!!
 */
static BOOL SetOptions(glb glob, char **opts, BOOL initial){
  struct opttable *p,*r;
  char *q;
  
  {char **p;for(p=opts;*p;p++) debug(("'%s'\n",*p));}
  for(p=opttable;p->opt;p++){
    q = FindToolType((UBYTE **)opts, p->opt);
    debug(("Setting option '%s': (%ld).\n",p->opt,!!q));
    if((q && !p->persistent) || initial){
      if( !(*p->set)(glob, q) ){
	debug(("Error occured seting option '%s'.\n",p->opt));
	goto optionerror;
      }
    }
  }
  /* All options successfull. */
  if(initial) glob->optionsset = TRUE;
  return TRUE;
  
 optionerror:
  /* An error occured during option setting. Cleanup any set options
   * if nessesary and return, reporting failure.
   */
  glob->badoption = p->opt;     /* For error report. */
  if(initial){
    glob->optionsset = FALSE;  /* Just to be safe. */
    for(r=opttable;r<p;r++){
      if(r->cleanup) (*r->cleanup)(glob);
    }
  }
  return FALSE;
}


/* Set a single option. This is done by faking an option file with a single
 * entry.
 */
BOOL set_option(glb glob, char *p){
   char *array[2];
   
   debug(("set_option() received '%s'.\n",p));
   array[0]=p;
   array[1]=NULL;
   return SetOptions(glob, array, FALSE);
}


/* Set a single option using an opt/val pair. */
BOOL set_option_value(glb glob, char *opt, char *val){
  char *p;
  BOOL res;

  if(!(p=dosalloc(strlen(opt)+strlen("=")+strlen(val)+1))){
    debug(("set_option_value(): no mem.\n"));
    OUTOFMEM;
    return FALSE;
  }
  strcpy(p,opt);
  strcat(p,"=");
  strcat(p,val);
  res = set_option(glob, p);
  dosfree(p);
  return res;
}
static void CleanupAllOptions(glb glob){
   struct opttable *p;
   
   if(glob->optionsset){
      for(p=opttable; p->opt; p++){
         if(p->cleanup) (*p->cleanup)(glob);
      }
      glob->optionsset = FALSE;
   }
}

/* Create string array (char**) from buffer (changing "sep" chars to '\0').
 * Also removes whitespace at end of lines.
 * The buffer MUST be (at least) 1 byte longer than 'len'.
 * Returns NULL if not enough memory available.
 */
static char **buf2array(glb glob, char *buf, int len, char sep){
  int i,j;
  int numlines=0;
  char **p,**q;
  
  debug(("String: '%s'.\n",buf));
  for(i=0;i<len;i++) if(buf[i]==sep) numlines++;
  /* The array size is numlines+2: one extra 'safe' line, and a 
   * NULL-terminator. */
  if(!(p=q=dosalloc(sizeof(*p)*(numlines+2)))){
    OUTOFMEM;
    return NULL;
  }
  *q++ = buf;
  for(i=0;i<len;i++){
    if(buf[i]==sep){
      for(j=i-1; (j>=0) && buf[j] && isspace(buf[j]); j--){
	debug(("Skipping '%lc'.\n",buf[j]));
      }
      buf[j+1]='\0';
      *q++=&buf[i+1];
    }
  }
  /* Fix the last line (may not be "sep" terminated). */
  for(j=i-1; (j>=0) && buf[j] && isspace(buf[j]); j--);
  buf[j+1]='\0';
  if(!**(q-1)) *(q-1) = NULL; /* Remove last line if empty. */
  return p;
}


static void deleteoptionfromarray(glb glob, char **array, char *opt){
  char *p;
  char **q;

  p = FindToolType((UBYTE **)array, opt);
  if(!p) return;
  /* Delete the entry corresponding to p. */
  for(q=array; *q; q++){
    if(*q<=p && (!q[1] || q[1] > p)) goto deleteit;
  }
  debug(("Strange: Bugus pointer returned by FindToolType().\n"));
  return;
 deleteit:
  while(q[0] = q[1]) q++;
  return;
}


static BOOL writearraytofile(glb glob, char **array, struct FileHandle *fh){
  char **p;

  for(p=array; *p; p++){
    if(!xWriteStr(glob, fh, *p)) return FALSE;
    if(!xWriteStr(glob, fh, "\n")) return FALSE;
  }
  return TRUE;
}


/* Attempt to read an option file. If the file could be read, it sets
 * the arg to point to a buffer containing the data. This buffer must
 * be deallocated by the caller (using dosfree()).
 * Returns TRUE if the file was found, FALSE otherwise.
 * NOTE: The arg may be returned NULL even if TRUE is returned.
 * This signifies an 'out of memory' condition.
 */
static BOOL ReadOptFile(glb glob, struct FileLock *parent, char *name,
                        char **buf, LONG *size){
   struct FileHandle *fh;
   
   *buf=NULL;
   if(!(fh = xOpen(glob, parent, name, MODE_OLDFILE))) return FALSE;
   *size = xSeek(glob, fh, 0, OFFSET_END);
   if(*size==-1L){
      xClose(glob, fh);
      return FALSE;
   }
   *size = xSeek(glob, fh, 0, OFFSET_BEGINNING);
   if(*size==-1L){
      xClose(glob, fh);
      return FALSE;
   }
   if(!(*buf = dosalloc(*size+1))){
      OUTOFMEM;
      xClose(glob, fh);
      return TRUE;
   }
   if(*size != xRead(glob, fh, *buf, *size)){
      xClose(glob, fh);
      dosfree(buf);
      *buf = NULL;
      return FALSE;
   }
   xClose(glob, fh);
   return TRUE;
}


static BOOL writeoption(glb glob, struct FileHandle *fh, char *opt, char *val){
  if(!xWriteStr(glob, fh, opt)) return FALSE;
  if(!xWriteStr(glob, fh, "=")) return FALSE;
  if(!xWriteStr(glob, fh, val)) return FALSE;
  if(!xWriteStr(glob, fh, "\n"))return FALSE;
  return TRUE;
}


/* Set a single option, but make it permanent (in :.xfhrc).
 *
 * A little magic is neede here since we share the .xfhrc file with
 * the user. So, first read it, find existing entry (if any), then
 * write back the file, changing the entry in the process.
 */
BOOL SetOptionPermanent(glb glob, char *opt, char *val){
  struct FileHandle *fh;
  char *buf;
  LONG len;
  char **array;
  BOOL res;
  
  if(!ReadOptFile(glob, glob->xrootlock, ALTOPTIONPATH, &buf, &len)){
    debug((".xfhrc file nonexisting, creating new.\n"));
    if(!(fh=xOpen(glob, glob->xrootlock, ALTOPTIONPATH, MODE_NEWFILE))){
      debug(("Error: Unable to open file: %ld.\n",glob->ioerr));
      return FALSE;
    }
    if(writeoption(glob, fh, opt, val)){
      if(!xClose(glob, fh)) return FALSE;
      return set_option_value(glob, opt, val);
    }else{
      SAVEIOERR;
      xClose(glob, fh);
      RESTIOERR;
      return FALSE;
    }
  }
  if(!buf) return FALSE;
  /* Ok, there's a file there already. Change it. */
  if(!(array = buf2array(glob, buf, len, EOL))){
    dosfree(buf);
    return FALSE;
  }
  deleteoptionfromarray(glob, array, opt);
  if(!(fh=xOpen(glob, glob->xrootlock, ALTOPTIONPATH, MODE_NEWFILE))){
    debug(("Error: Unable to open file again: %ld.\n",glob->ioerr));
    dosfree(array);
    dosfree(buf);
    return FALSE;
  }
  res = writearraytofile(glob, array, fh);
  dosfree(array);
  dosfree(buf);
  if(!res){
    SAVEIOERR;
    xClose(glob, fh);
    RESTIOERR;
    return FALSE;
  }
  if(!writeoption(glob, fh, opt, val)){
    SAVEIOERR;
    xClose(glob, fh);
    RESTIOERR;
    return FALSE;
  }
  if(!xClose(glob, fh)) return FALSE;
  return set_option_value(glob, opt, val);
}


/* Called to read additional option files (after setting the initial
 * options).
 */
BOOL SetOptionsFromFile(glb glob, struct FileLock *lock, char *name){
   char *buf;
   LONG len;
   char **array;
   BOOL res;
   
   if(!ReadOptFile(glob, lock, name, &buf, &len)) return TRUE;
   if(!buf) return FALSE;
   if(!(array = buf2array(glob, buf, len, EOL))){
      dosfree(buf);
      return FALSE;
   }
   res = SetOptions(glob, array, FALSE);
   dosfree(array);
   dosfree(buf);
   return res;
}


/* Set options given in the "Startup" keyword in the mountlist.
 * NOTE: this function will modify "startup".
 * ONLY called by SetOptionsDayOne() !!!
 */
static BOOL SetOptionsFromStartup(glb glob, char *startup){
  char **array;
  char *p;
  BOOL res;

  /* Change FAKEDEFCHAR to DEFCHAR (hack to fix bug in mount cmd). */
  for(p=startup;*p;p++){
    if(*p==FAKEDEFCHAR) *p=DEFCHAR;
  }
  if(!(array = buf2array(glob, startup, strlen(startup), STARTUP_SEP))){
    return FALSE;
  }
  res = SetOptions(glob, array, TRUE);
  debug(("SetOptions(): result %ld\n",res));
  dosfree(array);
  return res;
}


/* Set all options to their default, since there wasn't an option file.
  * ONLY called by SetOptionsDayOne() !!!
 */
static BOOL SetDefaultOptions(glb glob){
   static char *fakeopts[] = {NULL};
   
   /* Just pass SetOptions a fake, empty option file. */
   return SetOptions(glob, fakeopts, TRUE);
}


/* Set options from primary option file.
 * ONLY called by SetOptionsDayOne() !!!
 */
static BOOL SetOptionsFromPrimary(glb glob, char *filename){
   char *buf;
   LONG len;
   char **array;
   struct MsgPort *procid;
   struct FileLock *lock;
   BPTR bcpllock;
   BOOL res;

   /* ToDo: This won't work with multi-assigns. */
   procid = DoDeviceProc( (LONG *)&bcpllock, filename, glob );
   /* ToDo: Nasty case if we get a non-null procid but a null lock.
    * For now this is regarded as an error.
    */
   if( !procid || !bcpllock ){
      return SetDefaultOptions(glob);
   }
   lock = b2c(bcpllock);

   if(!ReadOptFile(glob, lock, filename, &buf, &len)){
       return SetDefaultOptions(glob);
   }
   if(!buf) return FALSE;
   if(!(array = buf2array(glob, buf, len, EOL))){
      dosfree(buf);
      return FALSE;
   }
   res = SetOptions(glob, array, TRUE);
   dosfree(array);
   dosfree(buf);
   return res;
}


/* Called ONCE to set the initial options.
 * These may either come directly in the "Startup" string, or in an
 * option file. */
static BOOL SetOptionsDayOne(glb glob){
   char *startup, *s2;
   BOOL res;

   /* Check for missing startup string. */
   if(!glob->bcplstartup) return SetDefaultOptions(glob);
   if(!(startup = copybstr(glob->bcplstartup))){
      debug(("Error: No memory for STARTUP string.\n"));
      OUTOFMEM;
      return FALSE;
   }
   /* Check for an empty startup string (""). */
   if(!strcmp("",startup) || !strcmp("\"\"",startup)){
      res = SetDefaultOptions(glob);
   }else{
      /* Check for a quoted string (ie. "foo" -> foo). */
      if(startup[0]==startup[strlen(startup)-1] && startup[0]==QUOTE_CHAR){
         startup[strlen(startup)-1] = '\0';
         s2 = &startup[1];
      }else{
         s2 = startup;
      }
      debug(("Startup string: '%s'\n",s2));

      /* If the first character of the STARTUP string is STARTUP_SEP, the
       * string is taken to be a list of option commands seperated by
       * STARTUP_SEP. Otherwise, it is taken to be the name of an option
       * file (which should have an absolute path).
       */
      if(s2[0]==STARTUP_SEP){
         debug(("Setting options from string.\n"));
         res = SetOptionsFromStartup(glob, &s2[1]);
      }else{
         debug(("Setting options from option file '%s'.\n",s2));
         res = SetOptionsFromPrimary(glob, s2);
      }
   }
   freestr(startup);
   return res;
}


BOOL InitOptions(glb glob){

   glob->optionsset = FALSE;
   
   if(!(glob->IconBase = OpenLibrary("icon.library",0L))){
      debug(("Error: InitOptions: Cannot open icon.library.\n"));
      return FALSE;
   }
   IconBase = glob->IconBase;
   
   SetOptionsDayOne(glob);
}

void CleanupOptions(glb glob){
   
   CleanupAllOptions(glob);
   
   if(glob->IconBase){
      CloseLibrary(IconBase);
      glob->IconBase = NULL;
   }
}


/* End of options.c */
