/********************************************************************
 *                                                                  *
 * THIS FILE IS PART OF THE OggVorbis SOFTWARE CODEC SOURCE CODE.   *
 * USE, DISTRIBUTION AND REPRODUCTION OF THIS SOURCE IS GOVERNED BY *
 * THE GNU PUBLIC LICENSE 2, WHICH IS INCLUDED WITH THIS SOURCE.    *
 * PLEASE READ THESE TERMS BEFORE DISTRIBUTING.                     *
 *                                                                  *
 * THE Ogg123 SOURCE CODE IS (C) COPYRIGHT 2000-2001                *
 * by Kenneth C. Arnold <ogg@arnoldnet.net> AND OTHER CONTRIBUTORS  *
 * http://www.xiph.org/                                             *
 *                                                                  *
 ********************************************************************

 last mod: $Id: cfgfile_options.c,v 1.1 2002/04/02 15:31:50 root Exp root $

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


/* if strcasecmp is giving you problems, switch to strcmp or the appropriate
 * function for your platform / compiler.
 */

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <limits.h> /* for LONG_MAX / LONG_MIN */
#include <errno.h>

#include "cfgfile_options.h"
#include "status.h"

/* ------------------- Private Functions ---------------------- */

int print_space (FILE *f, int s, int c)
{
  int tmp = 0;
  do {
    fputc (c, f);
    tmp++;
  } while (--s > 0);
  return tmp;
}


int parse_error (parse_code_t pcode, int lineno, const char *filename,
		 char *line)
{
  if (pcode == parse_syserr) {
    if (errno != EEXIST && errno != ENOENT)
      perror ("System error");
    return -1;
  } else {
    status_error ("=== Parse error: %s on line %d of %s (%s)\n", 
		  parse_error_string(pcode), 
		  lineno, filename, line);
    return 0;
  }
}


/* ------------------- Public Interface ----------------------- */

void file_options_init (file_option_t opts[])
{

  while (opts && opts->name) {

    opts->found = 0;

    if (opts->dfl) {
      switch (opts->type) {
      case opt_type_none:
	/* do nothing */
	break;
	
      case opt_type_char:
	*(char *) opts->ptr = *(char*) opts->dfl;
	break;
	
      case opt_type_string:
	*(char **) opts->ptr = *(char **) opts->dfl;
	break;

      case opt_type_bool:
      case opt_type_int:
	*(long int *) opts->ptr = *(int *) opts->dfl;
	break;
	
      case opt_type_float:
	*(float *) opts->ptr = *(float *) opts->dfl;
	break;
	
      case opt_type_double:
	*(double *) opts->ptr = *(double *) opts->dfl;
	break;
      }
    }

    opts++;
  }
}


/* DescribeOptions - describe available options to outfile */
void file_options_describe (file_option_t opts[], FILE *f)
{
  /* name | description | type | default */
  int colWidths[] = {0, 0, 7, 7};
  int totalWidth = 0;
  file_option_t *opt = opts;

  while (opt->name) {
    int len = strlen (opt->name) + 1;
    if (len  > colWidths[0])
      colWidths[0] = len;
    opt++;
  }

  opt = opts;
  while (opt->name) {
    int len = strlen (opt->desc) + 1;
    if (len > colWidths[1])
      colWidths[1] = len;
    opt++;
  }

  /* Column headers */
  /* Name */
  totalWidth += fprintf (f, "Name");
  totalWidth += print_space (f, (colWidths[0] - 4), ' ');
  
  /* Description */
  totalWidth += fprintf (f, "Description");
  totalWidth += print_space (f, (colWidths[1] - 11), ' ');
  
  /* Type */
  totalWidth += fprintf (f, "Type");
  totalWidth += print_space (f, (colWidths[2] - 4), ' ');
  
  /* Default */
  totalWidth += fprintf (f, "Default");
  totalWidth += print_space (f, (colWidths[3] - 7), ' ');

  fputc ('\n', f);
  
  /* Divider */
  print_space (f, totalWidth, '-');

  fputc ('\n', f);

  opt = opts;
  while (opt->name)
    {
      /* name */
      int w = colWidths[0];
      w -= fprintf (f, "%s", opt->name);
      print_space (f, w, ' ');

      /* description */
      w = colWidths[1];
      w -= fprintf (f, "%s", opt->desc);
      print_space (f, w, ' ');

      /* type */
      w = colWidths[2];
      switch (opt->type) {
      case opt_type_none:
	w -= fprintf (f, "none");
	break;
      case opt_type_bool:
	w -= fprintf (f, "bool");
	break;
      case opt_type_char:
	w -= fprintf (f, "char");
	break;
      case opt_type_string:
	w -= fprintf (f, "string");
	break;
      case opt_type_int:
	w -= fprintf (f, "int");
	break;
      case opt_type_float:
	w -= fprintf (f, "float");
	break;
      case opt_type_double:
	w -= fprintf (f, "double");
	break;
      default:
	w -= fprintf (f, "other");
      }
      print_space (f, w, ' ');

      /* default */
      if (opt->dfl == NULL)
	fputs ("(NULL)", f);
      else {
	switch (opt->type) {
	case opt_type_none:
	  fputs ("(none)", f);
	  break;
	case opt_type_char:
	  fputc (*(char *) opt->dfl, f);
	  break;
	case opt_type_string:
	  fputs (*(char **) opt->dfl, f);
	  break;
	case opt_type_bool:
	case opt_type_int:
	  fprintf (f, "%ld", *(long int *) opt->dfl);
	  break;
	case opt_type_float:
	  fprintf (f, "%f", (double) (*(float *) opt->dfl));
	  break;
	case opt_type_double:
	  fprintf (f, "%f", *(double *) opt->dfl);
	  break;
	}
      }
      fputc ('\n', f);
      opt++;
    }
}


parse_code_t parse_line (file_option_t opts[], char *line)
{
  char *equals, *value = "";
  file_option_t *opt;
  int len;

  /* skip leading whitespace */
  while (line[0] == ' ')
    line++;

  /* remove comments */
  equals = strchr (line, '#');
  if (equals)
    *equals = '\0';

  /* return if only whitespace on line */
  if (!line[0] || line[0] == '#')
    return parse_ok;

  /* check for an '=' and set to \0 */
  equals = strchr (line, '=');
  if (equals) {
    value = equals + 1;
    *equals = '\0';
  }

  /* cut trailing whitespace from key (line = key now) */
  while ((equals = strrchr(line, ' ')))
    *equals = '\0';

  /* remove this if you want a zero-length key */
  if (strlen(line) == 0)
    return parse_nokey;
  
  if (value) {
    /* cut leading whitespace from value */
    while (*value == ' ')
      value++;
    
    /* cut trailing whitespace from value */
    len = strlen (value);
    while (len > 0 && value[len-1] == ' ') {
      len--;
      value[len] = '\0';
    }
  }

  /* now key is in line and value is in value. Search for a matching option. */
  opt = opts;
  while (opt->name) {
    if (!strcasecmp (opt->name, line)) {
      long tmpl;
      char *endptr;

      /* found the key. now set the value. */
      switch (opt->type) {
      case opt_type_none:
	if (value != NULL || strlen(value) > 0)
	  return parse_badvalue;
	opt->found++;
	break;

      case opt_type_bool:
	if (!value || *value == '\0')
	  return parse_badvalue;
	
	/* Maybe this is a numeric bool */
	tmpl = strtol (value, &endptr, 0);

	if ( strncasecmp(value, "y", 1) == 0
	     || strcasecmp(value, "true")
	     || (*endptr == '\0' && tmpl) )
	  *(long int *) opt->ptr = 1;
	else if ( strncasecmp(value, "n", 1) == 0
		  || strcasecmp(value, "false")
		  || (*endptr == '\0' && !tmpl) )
	  *(long int *) opt->ptr = 0;
	else
	  return parse_badvalue;
	break;

      case opt_type_char:
	if (strlen(value) != 1)
	  return parse_badvalue;
	opt->found++;
	*(char *) opt->ptr = value[0];
	break;

      case opt_type_string:
	opt->found++;
	if (*(char **)opt->ptr) free(*(char **)opt->ptr);
	*(char **) opt->ptr = strdup (value);
	break;

      case opt_type_int:
	if (!value || *value == '\0')
	  return parse_badvalue;
	tmpl = strtol (value, &endptr, 0);
	if (((tmpl == LONG_MIN || tmpl == LONG_MAX) && errno == ERANGE)
	    || (*endptr != '\0'))
	  return parse_badvalue;
	opt->found++;
	*(long int *) opt->ptr = tmpl;
	break;
	
      case opt_type_float:
	if (!value || *value == '\0')
	  return parse_badvalue;
	opt->found++;
	*(float *) opt->ptr = atof (value);
	break;

      case opt_type_double:
	if (!value || *value == '\0')
	  return parse_badvalue;
	opt->found++;
	*(double *) opt->ptr = atof (value);
	break;

      default:
	return parse_badtype;
      }
      return parse_ok;
    }
    opt++;
  }
  return parse_keynotfound;
}


parse_code_t parse_config_file (file_option_t opts[], const char *filename)
{
  unsigned int len=80;
  char *line = malloc(len);
  int readoffset, thischar, lineno;
  FILE *file;
  parse_code_t pcode;
  char empty[] = "";

  if (!line) {
      parse_error(parse_syserr, 0, empty, empty);
      return parse_syserr;
  }

  file = fopen (filename, "r");
  if (!file) {
      parse_error (parse_syserr, 0, empty, empty);
      free (line);
      return parse_syserr;
  }

  lineno = 0;
  while (!feof (file)) {

    lineno++;
    readoffset = 0;
    memset (line, 0, len);

    while ((thischar = fgetc(file)) != EOF) {

      if (readoffset + 1 > len) {
	len *= 2;
	line = realloc (line, len);
	if (!line)
	  {
	    parse_error(parse_syserr, 0, empty, empty);
	    fclose (file);
	    return parse_syserr;
	  }
      }

      if (thischar == '\n') {
	line[readoffset] = '\0';
	break;
      }
      else
	line[readoffset] = (unsigned char) thischar;
      readoffset++;

    }

    pcode = parse_line (opts, line);

    if (pcode != parse_ok)
      if (!parse_error(pcode, lineno, filename, line)) {
	free (line);
	return pcode;
      }

  }

  free (line);
  return parse_ok;
}

/* ParseErr - returns a string corresponding to parse code pcode */
const char *parse_error_string (parse_code_t pcode)
{
  switch (pcode) {
  case parse_ok:
    return "Success";
  case parse_syserr:
    return strerror(errno);
  case parse_keynotfound:
    return "Key not found";
  case parse_nokey:
    return "No key";
  case parse_badvalue:
    return "Bad value";
  case parse_badtype:
    return "Bad type in options list";
  default:
    return "Unknown error";
  }
}


void parse_std_configs (file_option_t opts[])
{
  char filename[FILENAME_MAX];
  char *homedir = getenv("HOME");

  parse_config_file(opts, "/etc/ogg123rc");
  if (homedir && strlen(homedir) < FILENAME_MAX - 10) {
    /* Try ~/.ogg123 */
    strncpy(filename, homedir, FILENAME_MAX);
    strcat(filename, "/.ogg123rc");
    parse_config_file(opts, filename);
  }
}
