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

cfgfile.c (C-Munipack project)
Configuration file parser
Copyright (C) 2004 David Motl, dmotl@volny.cz

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., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

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

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

#include "cmunipack.h"
#include "cfgfile.h"
#include "utils.h"
#include "cmdline.h"

/******************* Constants and definitions **********************/

#define BUFFSIZE 	4096
#define ALLOC_BY	64

/******************* String buffer **********************************/

struct _String
{
	size_t count;		/**< Number of stored chars */
	size_t capacity;	/**< Number of allocated chars */
	char *buf;			/**< Memory buffer */
};
typedef struct _String String;

/* Initialize a string buffer */
static void str_init(String *str)
{
	memset(str, 0, sizeof(String));
}

/* Free allocated memory in a string buffer */
static void str_free(String *str)
{
	if (str->buf) {
		free(str->buf);
		str->buf = NULL;
		str->capacity = str->count = 0;
	}
}

/* Clear a string (do not free the memory) */
static void str_clear(String *str)
{
	str->count = 0;
}

/* Add a character to a string buffer */
static void str_add(String *str, char ch)
{
	if (str->count >= str->capacity) {
		str->capacity += ALLOC_BY;
		str->buf = (char*) realloc(str->buf, str->capacity*sizeof(char));
	}
	str->buf[str->count++] = ch;
}

/* Append a string to a buffer */
static void str_ncat(String *str, const char *buf, size_t len)
{
	if (len>0) {
		if ((str->count+len) > str->capacity) {
			if (str->count+len > str->capacity+ALLOC_BY)
				str->capacity = str->count + len;
			else
				str->capacity = str->capacity + ALLOC_BY;
			str->buf = (char*) realloc(str->buf, str->capacity*sizeof(char));
		}
		memcpy(str->buf + str->count, buf, len*sizeof(char));
		str->count += len;
	}
}	

/* Returns length of the string in characters (without trailing null) */
static size_t str_count(String *str)
{
	return str->count;
}

/* Convert a string buffer to null-terminated string (use free to destroy it) */
static char *str_cstr(String *str)
{
	char *aux = (char*) malloc((str->count+1)*sizeof(char));
	if (aux) {
		memcpy(aux, str->buf, str->count*sizeof(char));
		aux[str->count] = '\0';
	}
	return aux;
}

/* Trim all trailing white characters from the string */
static void str_rtrim(String *str)
{
	int i;

	for (i=(int)str->count-1; i>=0; i--) {
		if (str->buf[i]<=32)
			str->count--;
		else
			break;
	}
}

/********************** Configuration file parser ****************************/

/* Normal line */
typedef int tDataHandler(const char *key, const char *val, const char *str, void *user_data);

/* Context structure for configuration file parser */
struct _tParser
{
	int st;					/**< Automaton state */
	int valid;				/**< Current line contains valid data */
	int row, col;			/**< Current position - row, column index (starting by 1) */
	char lastch;			/**< Previous character */
	String key, val;		/**< String buffers for keyword, value and commentary */
	String str;			/**< String buffer for current line */
	void *user_data;		/**< User data passed through the callbacks */
	tDataHandler *data;		/**< Callback on normal line */
};
typedef struct _tParser tParser;

/* End of line(file) found, process parsed data for current line. */
static void process_line(tParser *p)
{
	char *str, *key, *val;
	
	str = str_cstr(&p->str);
	if (str) {
		if (p->valid && str_count(&p->key)>0) {
			/* Valid parameter line */
			key = str_cstr(&p->key);
			val = str_cstr(&p->val);
			if (p->data) 
				p->data(key, val, str, p->user_data);
			free(key);
			free(val);
		} else {
			/* All other lines */
			if (p->data)
				p->data(NULL, NULL, str, p->user_data);
		}
		free(str);
	}
	str_clear(&p->str);
	str_clear(&p->key);
	str_clear(&p->val);
	p->row++; 
	p->col = 1;
	p->valid = 1;
}	

/* Init parser context */
void cfg_parser_init(tParser *p)
{
	memset(p, 0, sizeof(tParser));
	str_init(&p->key);
	str_init(&p->val);
	str_init(&p->str);
	p->row = p->col = 1;
	p->valid = 1;
}
	
/* Free asociated memory in the context */
void cfg_parser_clear(tParser *p)
{
	str_free(&p->key);	
	str_free(&p->val);	
	str_free(&p->str);	
}
	
/* Set user data */
void cfg_parser_setuserdata(tParser *p, void *data)
{
	if (p)
		p->user_data = data;
}

/* Set callback functions */
void cfg_parser_sethandler(tParser *p, tDataHandler *data)
{
	if (p) {
		p->data = data;
	}
}
	
/* Parse buffer */
int cfg_parser_parse(tParser *p, const char *buf, size_t buflen, int eof)
{
	size_t i, sline;
	char ch;

	if (p==NULL || buf==NULL)
		return 0; 
	
	sline = 0;
	for (i=0; i<buflen; i++) {
		/* Take next character from the buffer */
		ch = buf[i]; 
		/* Finite automaton which processes the characters */
		switch (p->st) 
		{
		case 0:
			/* Start of new line */
			sline = i;
			p->col = 1;	
			/* First character on the line */
		 	if ((ch>='A' && ch<='Z') || (ch>='a' && ch<='z') || (ch=='_') || (ch=='-')) {
				/* Start of a keyword */
				str_add(&p->key, ch);
				p->st = 10;
			} else
			if (ch=='#') {
				/* Start of a comment */
				p->st = 30;
			} else
			if (ch==13) {
				/* Empty line */
				process_line(p);
			} else
			if (ch==10) {
				if (p->lastch!=13) 
					process_line(p);
			} else {
				/* Write space */
				p->st = 1;
			}
		 	break;
		 	
		case 1: 
			/* Skip leading spaces */
		 	if ((ch>='A' && ch<='Z') || (ch>='a' && ch<='z') || (ch=='_') || (ch=='-')) {
				/* Start of a keyword */
				str_add(&p->key, ch);
				p->st = 10;
			} else
			if (ch=='#') {
				/* Start of a comment */
				p->st = 30;
			} else
			if (ch==13 || ch==10) {
				/* Line with spaces only */
				str_ncat(&p->str, buf+sline, i-sline);
				process_line(p);
				p->st = 0;
			}
		 	break;
		 	
		case 10: 
		 	/* Reading keyword */
		 	if ((ch>='A' && ch<='Z') || (ch>='a' && ch<='z') || (ch>='0' && ch<='9') || (ch=='_') || (ch=='-')) {
			 	str_add(&p->key, ch);
		 	} else 
		 	if (ch=='=') {
				/* End of keyword, wait for value */
				p->st = 12;
			} else
		 	if (ch=='#') {
				/* End of keyword, start of a comment */
				p->st = 30;
			} else
			if (ch==13 || ch==10) {
				/* End of line (ignore it) */
				p->valid = 0;
				str_ncat(&p->str, buf+sline, i-sline);
				process_line(p);
				p->st = 0;
			} else {
				/* End of keyword, wait for equal sign */
				p->st = 11;
			}
			break;
			
		 case 11:
		 	/* Spaces before eq. sign */
		 	if (ch=='=') {
				/* Equal sign, wait for value */
				p->st = 12;
			} else
			if (ch=='#') {
				/* Start a comment */
				p->st = 30;
			} else
			if (ch==13 || ch==10) {
				/* End of line (ignore it) */
				p->valid = 0;
				str_ncat(&p->str, buf+sline, i-sline);
				process_line(p);
				p->st = 0;
			}
		 	break;
		 	
		 case 12:
		 	/* Spaces after eq. sign */
		 	if (ch=='"') {
				/* Start of "value" */
				p->st = 14;
			} else
			if (ch=='#') {
				/* Start of comment */
				p->st = 30;
			} else
			if (ch==13 || ch==10) {
				/* End of line */
				str_ncat(&p->str, buf+sline, i-sline);
				process_line(p);
				p->st = 0;
			} else
		 	if (ch!=' ' && ch!='\t') {
				/* Start of value (unquoted) */
				str_add(&p->val, ch); 
				p->st = 13;
			}
		 	break;
		 	
		 case 13: 
		 	/* Reading value (unquoted) */
		 	if (ch=='#') {
				/* Start of comment */
				str_rtrim(&p->val);
				p->st = 30;
			} else
			if (ch==13 || ch==10) {
				/* End of line */
				str_rtrim(&p->val);
				str_ncat(&p->str, buf+sline, i-sline);
				process_line(p);
				p->st = 0;
			} else {
				str_add(&p->val, ch);
			}
		 	break;
		 	
		 case 14: 
		 	/* Reading "value" */
		 	if (ch=='\\') {
				/* Escape character */
				p->st = 15;
			} else
			if (ch=='"') {
				/* End of value */
				p->st = 16;
			} else
			if (ch==13 || ch==10) {
				/* End of line (ignore it) */
				p->valid = 0;
				str_ncat(&p->str, buf+sline, i-sline);
				process_line(p);
				p->st = 0;
			} else {
				str_add(&p->val, ch);
			}
		 	break;
		 	
		case 15: 
		 	/* Escape character */
			if (ch=='n' || ch=='r' || ch=='t') {
				/* No, single quote or backslash */
				if (ch=='n') str_add(&p->val, 0x0a);
				if (ch=='r') str_add(&p->val, 0x0d);
				if (ch=='t') str_add(&p->val, 0x09);
				p->st = 14;
			} else
			if (ch=='"' || ch=='\\') {
				str_add(&p->val, ch);
				p->st = 14;
			} else
			if (ch==13 || ch==10) {
				/* End of line */
				p->valid = 0;
				str_ncat(&p->str, buf+sline, i-sline);
				process_line(p);
				p->st = 0;
			} else {
				/* Yes, end of "value" */
				p->st = 14;
			}
		 	break;
		 	
		case 16:	
			/* Waiting for sharp sign */
			if (ch=='#') {
				/* Sharp sign */
				p->st = 30;
			} else
			if (ch==13 || ch==10) {
				/* End of line */
				str_ncat(&p->str, buf+sline, i-sline);
				process_line(p);
				p->st = 0;
			}
			break;

		case 30:
			/* Spaces after sharp sign */
			if (ch==13 || ch==10) {
				/* End of line */
				str_ncat(&p->str, buf+sline, i-sline);
				process_line(p);
				p->st = 0;
			}
			break;
		}
		/* Remember last character */
		p->lastch = ch;
	}

	/* Copy rest of the buffer to the current line */
	if (p->st!=0 && i>sline)
		str_ncat(&p->str, buf+sline, i-sline);

	if (eof && p->st!=0) {
		/* Last line, which doesn't end with line delimiter */
		p->valid = p->st==1 || p->st==12 || p->st==13 || p->st==16 || p->st==23 || p->st==30;
		process_line(p);
		p->st = 0;
	}
	return 0;
}

/********************** Local functions ****************************/

/* Find parameter definition */
static CfgParam *find(CfgParam *opts, const char *name)
{
	int i;

	if (opts) {
		for (i=0; opts[i].name!=NULL; i++) {
			if (strcmp(opts[i].name, name)==0) {
				/* Found */
				return opts+i;
			}
		}
	}
	/* Not found */
	return NULL;
}

static int set_attr(AppInfo *info, CfgParam *opt, const char *val, const char *str)
{
	int x, *intv, count;
	size_t len;
	double d, *dblv;
	char *endptr;

	len = strlen(str)+1;
	opt->str = (char*) malloc(len*sizeof(char));
	if (opt->str)
		strcpy(opt->str, str);

	switch (opt->type)
	{
	case CFG_PTYPE_INT:
		x = strtol(val, &endptr, 10);
		if (endptr==val) {
			fprintf(stderr, "%s: Invalid format of option value %s: '%s' (should be integer)\n", 
				info->program->name, opt->name, val);
			return 0;
		} else {
			opt->count = 1;
			opt->present = 1;
			opt->val.int_val = x;
			return 1;
		}
		break;

	case CFG_PTYPE_DOUBLE:
		d = strtod(val, &endptr);
		if (endptr==val) {
			fprintf(stderr, "%s: Invalid format of option value %s: '%s' (should be real number)\n", 
				info->program->name, opt->name, val);
			return 0;
		} else {
			opt->count = 1;
			opt->present = 1;
			opt->val.dbl_val = d;
			return 1;
		}
		break;

	case CFG_PTYPE_STRING:
		opt->count = 1;
		opt->present = 1;
		len = strlen(val)+1;
		opt->val.str_val = (char*) malloc(len*sizeof(char));
		if (opt->val.str_val)
			strcpy(opt->val.str_val, val);
		return 1;

	case CFG_PTYPE_INT_V:
		StrToIntV(val, &intv, &count);
		if (!intv) {
			fprintf(stderr, "%s: Invalid format of option value %s: '%s' (should be array of integers)\n", 
				info->program->name, opt->name, val);
			return 0;
		} else {
			opt->count = count;
			opt->present = 1;
			opt->val.int_vec = intv;
			return 1;
		}
		break;

	case CFG_PTYPE_DOUBLE_V:
		StrToDoubleV(val, &dblv, &count);
		if (!dblv) {
			fprintf(stderr, "%s: Invalid format of option value %s: '%s' (should be array of real numbers)\n", 
				info->program->name, opt->name, val);
			return 0;
		} else {
			opt->count = count;
			opt->present = 1;
			opt->val.dbl_vec = dblv;
			return 1;
		}
		break;
		
	default:
	    break;
	}

	return 0;
}

static int cfg_parser_data(const char *key, const char *val, const char *str, void *user_data)
{
	AppInfo *f = (AppInfo*) user_data;

	if (f && key) {
		CfgParam *param = find(f->cfg, key);
		if (param)
			set_attr(f, param, val, str);
	}
	return 0;
}


/* Loads a configuration file. Returns zero on success or one of MPK_xxx error codes. */
static int file_load(AppInfo *f, FILE *from)
{
	size_t len;
	int res = 0, done;
	char buff[BUFFSIZE];
	tParser p;

	cfg_parser_init(&p);
	cfg_parser_setuserdata(&p, f);
	cfg_parser_sethandler(&p, cfg_parser_data);
	do {
		len = fread(buff, 1, BUFFSIZE, from);
		if (ferror(from)) {
			res = CMPACK_ERR_CFG_READ_ERROR;
			break;
		}
		done = feof(from);
		res = cfg_parser_parse(&p, buff, len, done);
		if (res!=0) 
			break;
	} while (!done);
	cfg_parser_clear(&p);
	return res;
}

/*****************   PUBLIC FUNCTIONS   ******************************/

/* Parses the command line and fills the information to 'info' structure */
void cfgfile_init(AppInfo *info)
{
	int i;

	/* Init variables */
	if (info->cfg) {
		for (i=0; info->cfg[i].name; i++)
			info->cfg[i].present = 0;
	}
}

/* Parses the command line and fills the information to 'info' structure */
int cfgfile_read(AppInfo *info, const char *filename)
{
	if (info) {
		/* Open file */
		FILE *f = fopen(filename, "rb");
		if (!f)
			return CMPACK_ERR_CFG_OPEN_ERROR;
		/* Load file */
		if (file_load(info, f)) {
			fclose(f);
			return CMPACK_ERR_CFG_READ_ERROR;
		}
		fclose(f);
		/* Debug output */
		if (cmdline_isdef(info, "verbose")) {
			cfgfile_dump(info);
		}
	}
	/* Normal return */
	return 0;
}

/* Free memory allocated in the 'info' structure */
void cfgfile_clean(AppInfo *info)
{
}

/* Returns nonzero if the parameter is present */
int cfgfile_isdef(AppInfo *info, const char *name)
{
	CfgParam *opt = find(info->cfg, name);
	if (opt) {
		if (cmdline_iscfgdef(info, name))
			return 1;
		else
			return opt->present;
	}
	return 0;
}

/* Get parameter attribute value */
int cfgfile_geti(AppInfo *info, const char *name, int *value)
{
	CfgParam *opt = find(info->cfg, name);
	if (opt) {
		if (cmdline_getcfgi(info, opt->name, value))
			return 1;
		if (opt->present) {
			switch (opt->type) 
			{
			case CFG_PTYPE_INT:
				if (value) *value = opt->val.int_val;
				return 1;
			case CFG_PTYPE_INT_V:
				if (value) *value = opt->val.int_vec[0];
				return 1;
			case CFG_PTYPE_DOUBLE:
				if (value) *value = (int)opt->val.dbl_val;
				return 1;
			case CFG_PTYPE_DOUBLE_V:
				if (value) *value = (int)opt->val.dbl_vec[0];
				return 1;
			case CFG_PTYPE_STRING:
				if (value) *value = atol(opt->str);
				return 1;
			default:
			    break;
			}
		}
	}
	if (value) *value = 0;
	return 0;
}

/* Get parameter attribute value */
int cfgfile_getd(AppInfo *info, const char *name, double *value)
{
	CfgParam *opt = find(info->cfg, name);
	if (opt) {
		if (cmdline_getcfgd(info, opt->name, value))
			return 1;
		if (opt->present) {
			switch (opt->type) 
			{
			case CFG_PTYPE_INT:
				if (value) *value = (double)opt->val.int_val;
				return 1;
			case CFG_PTYPE_INT_V:
				if (value) *value = (double)opt->val.int_vec[0];
				return 1;
			case CFG_PTYPE_DOUBLE:
				if (value) *value = opt->val.dbl_val;
				return 1;
			case CFG_PTYPE_DOUBLE_V:
				if (value) *value = opt->val.dbl_vec[0];
				return 1;
			case CFG_PTYPE_STRING:
				if (value) *value = atof(opt->str);
				return 1;
			default:
			    break;
			}
		}
	}
	if (value) *value = 0.0;
	return 0;
}

/* Get parameter attribute value */
int cfgfile_gets(AppInfo *info, const char *name, char **value)
{
	CfgParam *opt = find(info->cfg, name);
	if (opt) {
		if (cmdline_getcfgs(info, name, value))
			return 1;
		if (opt->present) {
			if (value) *value = opt->str;
			return 1;
		}
	}
	if (value) *value = NULL;
	return 0;
}

/* Get parameter attribute value */
int cfgfile_getiv(AppInfo *info, const char *name, int **intv, int *intc)
{
	CfgParam *opt = find(info->cfg, name);
	if (opt) {
		if (cmdline_getcfgiv(info, opt->name, intv, intc))
			return 1;
		if (opt->present) {
			switch (opt->type) 
			{
			case CFG_PTYPE_INT:
				if (intv) *intv = &opt->val.int_val;
				if (intc) *intc = 1;
				return 1;
			case CFG_PTYPE_INT_V:
				if (intv) *intv = opt->val.int_vec;
				if (intc) *intc = opt->count;
				return 1;
			default:
			    break;
			}
		}
	}
	if (intv) *intv = NULL;
	if (intc) *intc = 0;
	return 0;
}

/* Get parameter attribute value */
int cfgfile_getdv(AppInfo *info, const char *name, double **dblv, int *dblc)
{
	CfgParam *opt = find(info->cfg, name);
	if (opt) {
		if (cmdline_getcfgdv(info, opt->name, dblv, dblc))
			return 1;
		if (opt->present) {
			switch (opt->type) 
			{
			case CFG_PTYPE_DOUBLE:
				if (dblv) *dblv = &opt->val.dbl_val;
				if (dblc) *dblc = 1;
				return 1;
			case CFG_PTYPE_DOUBLE_V:
				if (dblv) *dblv = opt->val.dbl_vec;
				if (dblc) *dblc = opt->count;
				return 1;
			default:
			    break;
			}
		}
	}
	if (dblv) *dblv = NULL;
	if (dblc) *dblc = 0;
	return 0;
}

void cfgfile_dump(AppInfo *info)
{
	int i, j, k;

	if (!info->cfg)
		return;

	j = 0;
	printf("Configuration file parameters:\n");
	for (i=0; info->cfg[i].name; i++) {
		if (info->cfg[i].present) {
			printf("\t%s : ", info->cfg[i].name);
			switch (info->cfg[i].type)
			{
			case CFG_PTYPE_INT:
				printf("%d", info->cfg[i].val.int_val);
				break;
			case CFG_PTYPE_DOUBLE:
				printf("%.5f", info->cfg[i].val.dbl_val);
				break;
			case CFG_PTYPE_STRING:
				printf("'%s'", info->cfg[i].val.str_val);
				break;
			case CFG_PTYPE_INT_V:
				for (k=0; k<info->cfg[i].count; k++) {
					if (k>0) printf(",");
					printf("%d", info->cfg[i].val.int_vec[k]);
				}
				break;
			case CFG_PTYPE_DOUBLE_V:
				for (k=0; k<info->cfg[i].count; k++) {
					if (k>0) printf(",");
					printf("%d", info->cfg[i].val.int_vec[k]);
				}
				break;
			}
			printf("\n");
			j++;
		}
	}
	if (j==0) {
		printf("\tNone.\n");
	}
}

/* Prints help */
void cfgfile_help(AppInfo *info)
{
	int i;

	if (info->cfg) {
		printf("Configuration file parameters:\n");
		for (i=0; info->cfg[i].name; i++) {
			printf("\t%s = ", info->cfg[i].name);
			switch (info->cfg[i].type)
			{
			case CFG_PTYPE_INT:
				printf("integer");
				break;
			case CFG_PTYPE_INT_V:
				printf("integer, integer, ...");
				break;
			case CFG_PTYPE_DOUBLE:
				printf("double"); 
				break;
			case CFG_PTYPE_DOUBLE_V:
				printf("double, double, ..."); 
				break;
			case CFG_PTYPE_STRING:
				printf("string");
				break;
			default:
				printf("(undefined type");
				break;
			}
			printf("\n\t\t%s\n", info->cfg[i].description);
		}
		printf("\n");
	}
}
