/*
 *  params.c:           Parameter file and command line parsing
 *
 *  Written by:         Stefan Frank
 *                      Ullrich Hafner
 *
 *  This file is part of FIASCO (Fractal Image And Sequence COdec)
 *  Copyright (C) 1994-2000 Ullrich Hafner
 */

/*
 *  $Date: 2000/07/15 17:24:21 $
 *  $Author: hafner $
 *  $Revision: 5.2 $
 *  $State: Exp $
 */

#define _DEFAULT_SOURCE 1 /* New name for SVID & BSD source defines */
#define _BSD_SOURCE 1
    /* Make sure strdup() is in string.h and strcaseeq() is in nstring.h */
#define _XOPEN_SOURCE 500  /* Make sure strdup() is in string.h */

#include "config.h"

#include <stdio.h>
#include <ctype.h>
#include <math.h>                       /* strtod() on SUN sparc */

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

#include <getopt.h>                     /* system or ../lib */

#include "pm_c_util.h"
#include "nstring.h"

#include "types.h"
#include "macros.h"
#include "bit-io.h"
#include "misc.h"
#include "fiasco.h"

#include "binerror.h"

#include "params.h"

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

                                prototypes

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

static void
read_parameter_file (param_t *params, FILE *file);
static int
get_parameter_index (const param_t *params, const char *search_string);
static void
set_parameter (param_t *parameter, const char *value);
static void
usage (const param_t *params, const char *progname, const char *synopsis,
       const char *comment, const char *non_opt_string,
       bool_t show_all_options, const char *sys_file_name,
       const char *usr_file_name);

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

                                public code

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

int
parseargs (param_t *usr_params,
           int argc, char **argv,
           const char *synopsis,
           const char *comment,
           const char *non_opt_string,
           const char *path,
           const char *sys_file_name,
           const char *usr_file_name)
/*
 *  Perform the command line parsing.
 *  List of allowed parameters is given by 'usr_params'.
 *  Command line and number of parameters are given by 'argv' and 'argc'.
 *  'synopsis' contains a brief description of the program and
 *  'comment' may contain some additional advice.
 *  Initialization order of parameters:
 *      1.) Default values given by the param_t struct
 *      2.) System parameter-file ('path'/'sys_file_name')
 *      3.) User parameter-file ($HOME/'usr_file_name')
 *      4.) Command line parameters
 *      5.) Parameter-file forced by option -f (--config-file)
 *
 *  Return value:
 *      index in ARGV of the first ARGV-element that is not an option.
 *
 *  Side effects:
 *      the elements of ARGV are permuted
 *      usr_params [].value is modified
 */
{
   extern int optind;                   /* index in ARGV of the 1st element
                                           that is not an option */
   bool_t     detailed_help = NO;       /* NO if all parameters can be modified
                                           with short options too */
   unsigned   n1;                       /* number of user parameters */
   unsigned   n2;                       /* number of system parameters */
   bool_t     read_config_file = NO;    /* will override command line */
   param_t    *params;                  /* array of user and system params */
   param_t    *sys_params;              /* array of system parameters */
   param_t    detailed_sys_params [] =  /* detailed system parameters */
   {
      {"version", NULL, 'v', PFLAG, {0}, NULL,
       "Print program version number, then exit."},
      {"verbose", "NUM", 'V', PINT, {0}, "1",
       "Set level of verbosity to `%s'."},
      {"config", "FILE", 'f', PSTR, {0}, NULL,
       "Load `%s' to initialize parameters."},
      {"info", NULL, 'h', PFLAG, {0}, NULL,
       "Print brief help, then exit."},
      {"help", NULL, 'H', PFLAG, {0}, NULL,
       "Print detailed help, then exit."},
      {NULL, NULL, 0, PSTR, {0}, NULL, NULL }
   };
   param_t    short_sys_params [] =     /* short system parameters */
   {
      {"version", NULL, 'v', PFLAG, {0}, NULL,
       "Print program version number, then exit."},
      {"verbose", "NUM", 'V', PINT, {0}, "1",
       "Set level of verbosity to `%s'."},
      {"config", "FILE", 'f', PSTR, {0}, NULL,
       "Load `%s' to initialize parameters."},
      {"help", NULL, 'h', PFLAG, {0}, NULL,
       "Print this help, then exit."},
      {NULL, NULL, 0, PSTR, {0}, NULL, NULL }
   };
   char *sys_path;                      /* path to system config file */

   sys_path = calloc (strlen (path) + strlen (sys_file_name) + 2,
                      sizeof (char));
   if (!sys_path)
      error ("Out of memory.");
   sprintf (sys_path, "%s/%s", path, sys_file_name);

   /*
    *  Set parameters defaults
    */
   {
       param_t *p;

       for (p = usr_params; p->name != NULL; p++)
       {
           set_parameter (p, p->default_value);
           if (p->optchar == '\0')
               detailed_help = YES;
       }

      sys_params = detailed_help ? detailed_sys_params : short_sys_params;

      for (p = sys_params; p->name != NULL; p++)
          set_parameter (p, p->default_value);
   }
   /*
    *  Append system command line option to user parameters
    */
   for (n1 = 0; usr_params [n1].name != NULL; n1++)
      ;
   for (n2 = 0; sys_params [n2].name != NULL; n2++)
      ;
   params = calloc (n1 + n2 + 1, sizeof (param_t));
   if (!params)
      error ("Out of memory.");

   memcpy (params, usr_params, n1 * sizeof (param_t));
   memcpy (params + n1, sys_params, (n2 + 1) * sizeof (param_t));
   /*
    *  Try to open the system resource file 'path'/'sys_file_name'
    */
   {
      FILE *parameter_file = open_file (sys_path, NULL, READ_ACCESS);
      if (parameter_file == NULL)
/*
         warning ("No system resource file found.");
*/ {}
      else
      {
         read_parameter_file (params, parameter_file);
         fclose (parameter_file);
      }
   }
   /*
    *  Try to read user resource file $HOME/'usr_file_name'
    */
   {
      FILE *parameter_file = open_file (usr_file_name, "HOME", READ_ACCESS);
      if (parameter_file != NULL)
      {
         read_parameter_file (params, parameter_file);
         fclose (parameter_file);
      }
   }
   /*
    *  Parse command line options
    */
   {
      extern char   *optarg;            /* argument of current option */
      struct option *long_options;      /* array of long options */
      int            option_index = 0;
      char           optstr [MAXSTRLEN]; /* string containing the legitimate
                                            option characters */
      int            optchar;           /* found option character */

      /*
       *  Build short option string for getopt_long ().
       */
      {
         param_t *p;                    /* counter */
         char    *ptr_optstr;           /* pointer to position in string */

         ptr_optstr = optstr;
         for (p = params; p->name != NULL; p++)
            if (p->optchar != '\0')
            {
               *ptr_optstr++ = p->optchar;
               if (p->type == POSTR)
               {
                  *ptr_optstr++ = ':';
                  *ptr_optstr++ = ':';
               }
               else if (p->type != PFLAG)
                  *ptr_optstr++ = ':';
            }
         *ptr_optstr = '\0';
      }

      /*
       *  Build long option string for getopt_long ().
       */
      {
         int i;

         long_options = calloc (n1 + n2 + 1, sizeof (struct option));
         if (!long_options)
            error ("Out of memory.");
         for (i = 0; params [i].name != NULL; i++)
         {
            long_options [i].name    = params [i].name;
            switch (params [i].type)
            {
               case PFLAG:
                  long_options [i].has_arg = 0;
                  break;
               case POSTR:
                  long_options [i].has_arg = 2;
                  break;
               case PINT:
               case PSTR:
               case PFLOAT:
               default:
                  long_options [i].has_arg = 1;
                  break;
            }
            long_options [i].has_arg = params [i].type != PFLAG;
            long_options [i].flag    = NULL;
            long_options [i].val     = 0;
         }
      }

      /*
       *  Parse command line
       */
      while ((optchar = getopt_long (argc, argv, optstr, long_options,
                                     &option_index)) != EOF)
      {
         int param_index = -1;

         switch (optchar)
         {
            case 0:
               param_index = option_index;
               break;
            case ':':
               if (detailed_help)
                  fprintf (stderr,
                           "Try `%s -h' or `%s --help' for "
                           "more information.\n",
                           argv [0], argv [0]);
               else
                  fprintf (stderr, "Try `%s --help' for more information.\n",
                           argv [0]);
               exit (2);
               break;
            case '?':
               if (detailed_help)
                  fprintf (stderr,
                           "Try `%s -h' or `%s --help' "
                           "for more information.\n",
                           argv [0], argv [0]);
               else
                  fprintf (stderr, "Try `%s --help' for more information.\n",
                           argv [0]);
               exit (2);
               break;
            default:
               {
                  int i;

                  for (i = 0; params [i].name != NULL; i++)
                     if (params [i].optchar == optchar)
                     {
                        param_index = i;
                        break;
                     }
               }
         }
         /*
          *  Check for system options
          */
         if (param_index >= 0)
         {
            set_parameter (params + param_index, optarg ? optarg : "");
            if (streq (params [param_index].name, "help"))
               usage (params, argv [0], synopsis, comment, non_opt_string,
                      YES, sys_path, usr_file_name);
            else if (streq (params [param_index].name, "info"))
               usage (params, argv [0], synopsis, comment, non_opt_string,
                      NO, sys_path, usr_file_name);
            else if (streq (params [param_index].name, "version"))
            {
           fprintf (stderr, "%s " VERSION "\n", argv [0]);
           {
              /* Kludge for standard Netpbm version announcement */
              char * modified_argv[2];
              int argc;
              modified_argv[0] = argv[0];
              modified_argv[1] = (char *) "--version";
              argc = 2;
              pm_proginit(&argc, (const char **) modified_argv);
           }
           exit (2);
            }
            else if (streq (params [param_index].name, "verbose"))
               fiasco_set_verbosity (
               * (fiasco_verbosity_e *) parameter_value (params,
                                                         "verbose"));
            else if (streq (params [param_index].name, "config"))
               read_config_file = YES;
            param_index = -1;           /* clear index flag */
         }
      }

      free (long_options);
   }

   /*
    *  Read config-file if specified by option -f
    */
   if (read_config_file)
   {
      char *filename;

      if ((filename = (char *) parameter_value (params, "config")) != NULL)
      {
         FILE *parameter_file;          /* input file */

         warning ("Options set in file `%s' will override"
                  " command line options.", filename);
         parameter_file = open_file (filename, NULL, READ_ACCESS);
         if (parameter_file != NULL)
         {
            read_parameter_file (params, parameter_file);
            fclose (parameter_file);
         }
         else
            file_error (filename);
      }
      else
         error ("Invalid config filename.");
   }

   memcpy (usr_params, params, n1 * sizeof (param_t)); /* fill user struct */
   free (sys_path);

   return optind;
}

void *
parameter_value (const param_t *params, const char *name)
/*
 *  Extract value of parameter 'name.' of the given parameters 'params'.
 *
 *  Return value:
 *      value of given parameter
 */
{
   int pind = get_parameter_index (params, name);

   if (pind < 0)
      error ("Invalid parameter `%s'.", name);

   if (params [pind].type == PSTR || params [pind].type == POSTR)
      return (void *) params [pind].value.s;

   return (void *) &(params [pind].value);
}

void
ask_and_set (param_t *params, const char *name, const char *msg)
/*
 *  Ask user (print given message 'msg') for missing mandatory
 *  parameter 'name' of the given parameters 'params'.
 *
 *  No return value.
 *
 *  Side effects:
 *      'params ['name'].value' is changed
 */
{
   char answer [MAXSTRLEN];
   int  index = get_parameter_index (params, name);

   if (index < 0)
      error ("Invalid parameter %s.", name);

   if (msg)
      fprintf (stderr, "%s\n", msg);

   switch (params [index].type)
   {
      case PFLAG:                       /* Unusual, at least. */
         warning ("Flags should be initialized and set on demand, "
                  "not request");
      case PINT:
      case PSTR:
      case POSTR:
      case PFLOAT:
         scanf (MAXSTRLEN_SCANF, answer);
         set_parameter (&params [index], answer);
         break;
      default:
         error ("Invalid parameter type for %s", name);
   }
}

void
write_parameters (const param_t *params, FILE *output)
/*
 *  Write all parameter settings to 'output'.
 *
 *  No return value.
 */
{
   int pind;

   if (!params || !output)
      error ("Parameters must be not NULL.");

   for (pind = 0; params [pind].name != NULL; pind++)
   {
      fprintf (output, "# %s = ", params [pind].name);
      switch (params [pind].type)
      {
         case PFLAG:
            fprintf (output, "%s\n", params [pind].value.b ? "TRUE" : "FALSE");
            break;
         case PINT:
            fprintf (output, "%d\n", params [pind].value.i);
            break;
         case PFLOAT:
            fprintf (output, "%.4f\n", (double) params [pind].value.f);
            break;
         case PSTR:
         case POSTR:
            fprintf (output, "%s\n", params [pind].value.s);
            break;
         default:
            error ("Invalid type %d for parameter %s",
                   params [pind].type, params [pind].name);
      }
   }
   fputc ('\n', output);
}

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

                                private code

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

static void
set_parameter (param_t *parameter, const char *value)
/*
 *  Set value of 'parameter' to 'value'.
 *
 *  No return value.
 *
 *  Side effects:
 *      'parameter.value' is changed accordingly
 */
{
   assert (parameter);

   switch (parameter->type)
   {
      case PFLAG:
         if (value != NULL && *value != '\0')
         {
            if (strcaseeq (value, "TRUE"))
               parameter->value.b = YES;
            else if (strcaseeq (value, "FALSE"))
               parameter->value.b = NO;
            else if (strcaseeq (value, "YES"))
               parameter->value.b = YES;
            else if (strcaseeq (value, "NO"))
               parameter->value.b = NO;
            else
            {
               long int data;
               char     *endptr;

               data = strtol (value, &endptr, 0);
               if (*endptr != '\0' || endptr == value)
                  warning ("Invalid value `%s' converted to %d",
                           value, (int) data);
               parameter->value.b = data ? YES : NO;
            }
         }
         else
            parameter->value.b = !parameter->value.b;
         break;
      case PINT:
         {
            long int  data;
            char     *endptr;

            data = strtol (value, &endptr, 0);
            if (*endptr != '\0' || endptr == value)
               warning ("Invalid value `%s' converted to %d",
                        value, (int) data);
            parameter->value.i = data;
         }
         break;
      case PFLOAT:
         {
            double      data;
            char        *endptr;

            data = strtod (value, &endptr);
            if (*endptr != '\0' || endptr == value)
               warning ("Invalid value `%s' converted to %f",
                        value, (double) data);
            parameter->value.f = data;
         }
         break;
      case PSTR:
      case POSTR:
         parameter->value.s = value ? strdup (value) : NULL;
         break;
      default:
         error ("Invalid parameter type for %s", parameter->name);
   }
}

static int
get_parameter_index (const param_t *params, const char *search_string)
/*
 *  Search for parameter with name 'search_string' in parameter struct.
 *
 *  Return value:
 *      index of parameter or -1 if no matching parameter has been found
 */
{
   int n;
   int index = -1;

   assert (params && search_string);

   for (n = 0; params [n].name != NULL; n++)
      if (strcaseeq (params [n].name, search_string))
      {
         index = n;
         break;
      }

   return index;
}

static void
read_parameter_file (param_t *params, FILE *file)
/*
 *  Read parameter settings from 'file'.
 *
 *  No return value.
 *
 *  Side effects:
 *      'params [].value' are changed if specified in 'file'
 */
{
   char buffer [MAXSTRLEN];
   int  n = 0;

   assert (params && file);

   while (fgets (buffer, MAXSTRLEN, file) != NULL)
   {
      char *b;                          /* temporary variable */
      char *name;                       /* parameter name */
      char *value;                      /* parameter value */
      int   pind;                       /* current argument number */

      b = strchr (buffer, '#');
      if (b != NULL)                    /* Strip comments. */
         *b = '\0';

      b = strchr (buffer, '=');
      if (b == NULL)                    /* Strip lines that contain no '=' */
         continue;
      *b = '\0';                        /* Replace '=' by string terminator */

      /*
       *  Extract value of parameter
       */
      for (value = b + 1; ISSPACE (*value); value++)
         ;                              /* Delete leading spaces */

      for (b = value + strlen (value) - 1; b >= value && ISSPACE (*b); b--)
         *b = '\0';                     /* Delete trailing spaces. */

      /*
       *  Extract parameter name
       */
      for (name = buffer; ISSPACE (*name); name++)
         ;                              /* Delete leading spaces */

      for (b = name + strlen (name) - 1; b >= name && ISSPACE (*b); b--)
         *b = '\0';                     /* Delete trailing spaces. */

      pind = get_parameter_index (params, name);
      if (pind >= 0)
         set_parameter (&params [pind], value);

      n++;
   }
}



static void
usage (const param_t *params, const char *progname, const char *synopsis,
       const char *comment, const char *non_opt_string,
       bool_t show_all_options, const char *sys_file_name,
       const char *usr_file_name)
/*
 *  Generates and prints command line description from param_t struct 'params'.
 *  'progname' is the name of the executable, 'synopsis' a short program
 *  description, and 'comment' some more advice.
 *  If flag 'show_all_options' is set then print also options that are not
 *  associated with a short option character.
 *  'sys_file_name' and 'usr_file_name' are filenames to parameter files.
 *
 *  No return value.
 */
{
    int   i;
    size_t width = 0;

    fprintf (stderr, "Usage: %s [OPTION]...%s\n", progname,
             non_opt_string ? non_opt_string : " ");
    if (synopsis != NULL)
        fprintf (stderr, "%s", synopsis);
    fprintf (stderr, "\n\n");
    fprintf (stderr, "Mandatory or optional arguments to long options "
             "are mandatory or optional\nfor short options too. "
             "Default values are surrounded by {}.\n");
    for (i = 0; params [i].name != NULL; i++)
        if (params [i].optchar != '\0' || show_all_options)
        {
            if (params [i].type == POSTR)
                width = MAX(width, (strlen (params [i].name)
                                     + strlen (params [i].argument_name) + 2));
            else if (params [i].type != PFLAG)
                width = MAX(width, (strlen (params [i].name)
                                     + strlen (params [i].argument_name)));
            else
                width = MAX(width, (strlen (params [i].name)) - 1);
        }

    for (i = 0; params [i].name != NULL; i++)
        if (params [i].optchar != '\0' || show_all_options)
        {
            if (params [i].optchar != '\0')
                fprintf (stderr, "  -%c, --", params [i].optchar);
            else
                fprintf (stderr, "      --");

            if (params [i].type == POSTR)
                fprintf (stderr, "%s=[%s]%-*s  ", params [i].name,
                         params [i].argument_name,
                         (unsigned)
                         MAX(0, (width - 2 - strlen (params [i].name)
                                 - strlen (params [i].argument_name))), "");
            else if (params [i].type != PFLAG)
                fprintf (stderr, "%s=%-*s  ", params [i].name,
                         (unsigned)(width - strlen (params [i].name)),
                         params [i].argument_name);
            else
                fprintf (stderr, "%-*s  ",
                         (unsigned)(width + 1), params [i].name);

            fprintf (stderr, params [i].use, params [i].argument_name);

            switch (params [i].type)
            {
            case PFLAG:
                break;
            case PINT:
                fprintf (stderr, "{%d}", params [i].value.i);
                break;
            case PFLOAT:
                fprintf (stderr, "{%.2f}", (double) params [i].value.f);
                break;
            case PSTR:
            case POSTR:
                if (params [i].value.s)
                    fprintf (stderr, "{%s}", params [i].value.s);
                break;
            default:
                error ("type %d for %s invalid",
                       params [i].type, params [i].name);
            }
            fprintf (stderr, "\n");
        }
    fprintf (stderr, "\n");
    fprintf (stderr, "Parameter initialization order:\n");
    fprintf (stderr,
             "1.) %s\n2.) $HOME/%s\t 3.) command line\t 4.) --config=file",
             sys_file_name, usr_file_name);
    fprintf (stderr, "\n\n");
    if (comment != NULL)
        fprintf (stderr, "%s\n", comment);

    exit (1);
}

