/** \file configfile.c
 * Define routines to read INI-file like files.
 */

/* This file is part of LCDd, the lcdproc server.
 *
 * This file is released under the GNU General Public License. Refer to the
 * COPYING file distributed with this package.
 *
 * Copyright(c) 2001, Joris Robijn
 *          (c) 2003, Rene Wagner
 *          (c) 2006,2007 Peter Marschall
 *
 */

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

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "shared/report.h"


/** configuration key */
typedef struct _config_key {
	char *name;			/**< name of the config key */
	char *value;			/**< value of the config key */
	struct _config_key *next_key;	/**< pointer to next config key */
} ConfigKey;

/** configuration section */
typedef struct _config_section {
	char *name;			/**< name of the config section */
	ConfigKey *first_key;		/**< config keys in the config section */
	struct _config_section *next_section;	/**< pointer to next config section */
} ConfigSection;


static ConfigSection *first_section = NULL;
/* Yes there is a static. It's C after all :)*/


static ConfigSection *find_section(const char *sectionname);
static ConfigSection *add_section(const char *sectionname);
static ConfigKey *find_key(ConfigSection *s, const char *keyname, int skip);
static ConfigKey *add_key(ConfigSection *s, const char *keyname, const char *value);
#if defined(LCDPROC_CONFIG_READ_STRING)
static char get_next_char_f(FILE *f);
static int process_config(ConfigSection **current_section, char(*get_next_char)(), const char *source_descr, FILE *f);
#else
static int process_config(ConfigSection **current_section, const char *source_descr, FILE *f);
#endif


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

/** Parse configuration from INI-file style config file into memory.
 * \param filename  Name of the config file.
 * \retval 0        config successfully parsed
 * \retval <0       error occurred
 */
int config_read_file(const char *filename)
{
	FILE *f;
	ConfigSection *curr_section = NULL;
	int result = 0;

	report(RPT_NOTICE, "Using Configuration File: %s", filename);

	f = fopen(filename, "r");
	if (f == NULL) {
		return -1;
	}

#if defined(LCDPROC_CONFIG_READ_STRING)
	result = process_config(&curr_section, get_next_char_f, filename, f);
#else
	result = process_config(&curr_section, filename, f);
#endif

	fclose(f);

	return result;
}


#if defined(LCDPROC_CONFIG_READ_STRING)
int config_read_string(const char *sectionname, const char *str)
/* All the config parameters are placed in the given section in memory.*/
{
	int pos = 0;
	ConfigSection *s;

	/* We use a nested function to transfer the characters from buffer to parser*/
	char get_next_char() {
		return str[pos++];
	}

	if ((s = find_section(sectionname)) == NULL)
		s = add_section(sectionname);

	return process_config(&s, get_next_char, "command line", NULL);
}
#endif


/** Get string from configuration in memory.
 *
 * The strings returned are always NUL-terminated.
 * They should never be modified, and used only short-term.
 * In successive calls this function can re-use the data space !
 *
 * You can do some things with the returned string:
 * \li Scan or parse it:
 *     \code
 *     s = config_get_string(...);
 *     sscanf(s, "%dx%d", &w, &h);  // scan format like: 20x4
 *    \endcode
 *    ...and check the w and h values...
 * \li Copy it to a pre-allocated buffer like \c device[256]:
 *     \code
 *     s = config_get_string(...);
 *     strncpy(device, s, sizeof(device));
 *     device[sizeof(device)-1] = '\0';  // make sure it is terminated
 *    \endcode
 * \li Copy it to a newly allocated space in \c char \c *device:
 *     \code
 *     s = config_get_string(...);
 *     device = malloc(strlen(s)+1);
 *     if (device == NULL) return -5; // or whatever < 0
 *     strcpy( device, s );
 *     \endcode
 *
 * \param sectionname   Name of the section where the key is sought.
 * \param keyname       Name of the key to look for.
 * \param skip          Number of values to skip/ignore before returning the value.
 *                      This is used to iterate through the values of a multi-valued key:
 *                      \c 0 for the first value, \c 1 for the 2nd, ... and \c -1 for the last.
 * \param default_value Default value if section/key is not found
 *                      or \c skip exceeds the number of values of the key.
 * \return              Value found / \c default_value
 */
const char *config_get_string(const char *sectionname, const char *keyname,
		int skip, const char *default_value)
{
	ConfigKey *k = find_key(find_section(sectionname), keyname, skip);

	if (k == NULL)
		return default_value;

	return k->value;

/* This is the safer way:*/

	/* Reallocate memory space for the return value*/
	/*
	string_storage = realloc(string_storage,(strlen(k->value) / 256 + 1) * 256);
	strcpy(string_storage, k->value);

   But then you also need a global static string_storage = NULL;
*/
}


/** Get boolean value from configuration in memory.
 *
 * Legal boolean values are:
 * \li \c 0 , \c false , \c off , \c no or \c n for FALSE.
 * \li \c 1 , \c true , \c on , \c yes or \c y for TRUE
 *
 * \param sectionname   Name of the section where the key is sought.
 * \param keyname       Name of the key to look for.
 * \param skip          Number of values to skip/ignore before returning the value.
 *                      This is used to iterate through the values of a multi-valued key:
 *                      \c 0 for the first value, \c 1 for the 2nd, ... and \c -1 for the last.
 * \param default_value Default value if section/key is not found, value is no legal boolean,
 *                      or \c skip exceeds the number of values of the key.
 * \return              Value found / \c default_value
 */
short config_get_bool(const char *sectionname, const char *keyname,
		int skip, short default_value)
{
	ConfigKey *k = find_key(find_section(sectionname), keyname, skip);

	if (k == NULL)
		return default_value;

	if ((strcasecmp(k->value, "0") == 0) || (strcasecmp(k->value, "false") == 0) ||
	    (strcasecmp(k->value, "n") == 0) || (strcasecmp(k->value, "no") == 0) ||
	    (strcasecmp(k->value, "off") == 0)) {
		return 0;
	}
	if ((strcasecmp(k->value, "1") == 0) || (strcasecmp(k->value, "true") == 0) ||
	    (strcasecmp(k->value, "y") == 0) || (strcasecmp(k->value, "yes") == 0) ||
	    (strcasecmp(k->value, "on") == 0)) {
		return 1;
	}
	return default_value;
}


/** Get tristate value from configuration in memory.
 *
 * Legal tristate values are:
 * \li \c 0 , \c false , \c off , \c no or \c n for 0.
 * \li \c 1 , \c true , \c on , \c yes or \c y for 1
 * \li \c 2 or the given name of the third state for 2
 *
 * \param sectionname   Name of the section where the key is sought.
 * \param keyname       Name of the key to look for.
 * \param skip          Number of values to skip/ignore before returning the value.
 *                      This is used to iterate through the values of a multi-valued key:
 *                      \c 0 for the first value, \c 1 for the 2nd, ... and \c -1 for the last.
 * \param name3rd       Name of the 3rd state
 * \param default_value Default value if section/key is not found, value is no legal boolean,
 *                      or \c skip exceeds the number of values of the key.
 * \return              Value found / \c default_value
 */
short config_get_tristate(const char *sectionname, const char *keyname,
		int skip, const char *name3rd, short default_value)
{
	ConfigKey *k = find_key(find_section(sectionname), keyname, skip);

	if (k == NULL)
		return default_value;

	if ((strcasecmp(k->value, "0") == 0) || (strcasecmp(k->value, "false") == 0) ||
	    (strcasecmp(k->value, "n") == 0) || (strcasecmp(k->value, "no") == 0) ||
	    (strcasecmp(k->value, "off") == 0)) {
		return 0;
	}
	if ((strcasecmp(k->value, "1") == 0) || (strcasecmp(k->value, "true") == 0) ||
	    (strcasecmp(k->value, "y") == 0) || (strcasecmp(k->value, "yes") == 0) ||
	    (strcasecmp(k->value, "on") == 0)) {
		return 1;
	}
	if ((strcasecmp(k->value, "2") == 0) ||
	    ((name3rd != NULL) && (strcasecmp(k->value, name3rd) == 0))) {
		return 2;
	}
	return default_value;
}


/** Get integer from configuration in memory.
 * \param sectionname   Name of the section where the key is sought.
 * \param keyname       Name of the key to look for.
 * \param skip          Number of values to skip/ignore before returning the value.
 *                      This is used to iterate through the values of a multi-valued key:
 *                      \c 0 for the first value, \c 1 for the 2nd, ... and \c -1 for the last.
 * \param default_value Default value if section/key is not found, value is no integer,
 *                      or \c skip exceeds the number of values of the key.
 * \return              Value found / \c default_value
 */
long int config_get_int(const char *sectionname, const char *keyname,
		int skip, long int default_value)
{
	ConfigKey *k = find_key(find_section(sectionname), keyname, skip);

	if (k != NULL) {
		char *end;
		long int v = strtol(k->value, &end, 0);

		if ((end != NULL) && (end != k->value) && (*end == '\0'))
			/* Conversion successful */
			return v;
	}
	return default_value;
}


/** Get floating point number from configuration in memory.
 * \param sectionname   Name of the section where the key is sought.
 * \param keyname       Name of the key to look for.
 * \param skip          Number of values to skip/ignore before returning the value.
 *                      This is used to iterate through the values of a multi-valued key:
 *                      \c 0 for the first value, \c 1 for the 2nd, ... and \c -1 for the last.
 * \param default_value Default value if section/key is not found, value is no floating point number
 *                      or \c skip exceeds the number of values of the key.
 * \return              Value found / \c default_value
 */
double config_get_float(const char *sectionname, const char *keyname,
		int skip, double default_value)
{
	ConfigKey *k = find_key(find_section(sectionname), keyname, skip);

	if (k != NULL) {
		char *end;
		double v = strtod(k->value, &end);

		if ((end != NULL) && (end != k->value) && (*end == '\0'))
			/* Conversion successful*/
			return v;
	}
	return default_value;
}


/** Test whether the configuration contains a specific section.
 * \param sectionname  Name of the section to look for.
 * \retval 0           section not in config
 * \retval 1           section in config
 */
int config_has_section(const char *sectionname)
{
	return (find_section(sectionname) != NULL) ? 1 : 0;
}


/** Test whether the configuration contains a specific key in a specific section.
 * \param sectionname  Name of the section where the key is sought.
 * \param keyname      Name of the key to look for.
 * \retval 0           key or section not found
 * \retval n           key found with \c n values (\c n > 0)
 */
int config_has_key(const char *sectionname, const char *keyname)
{
	ConfigSection *s = find_section(sectionname);
	int count = 0;

	if (s != NULL) {
		ConfigKey *k;

		for (k = s->first_key; k != NULL; k = k->next_key) {
			/* Did we find the right key ?*/
			if (strcasecmp(k->name, keyname) == 0)
				count++;
		}
	}
	return count;
}


/** Clear configuration. */
void config_clear(void)
{
	ConfigSection *s;
	ConfigSection *next_s;

	for (s = first_section; s != NULL; s = next_s) {
		ConfigKey *k;
		ConfigKey *next_k;

		for (k = s->first_key; k != NULL; k = next_k) {
			/* Advance before we destroy the current key */
			next_k = k->next_key;

			free(k->name);
			free(k->value);
			free(k);
		}
		/* Advance before we destroy the current section */
		next_s = s->next_section;

		/* And destroy it */
		free(s->name);
		free(s);
	}
	/* Finally make everything inaccessible */
	first_section = NULL;
}


/**** INTERNAL FUNCTIONS ****/

static ConfigSection *find_section(const char *sectionname)
{
	ConfigSection *s;

	for (s = first_section; s != NULL; s = s->next_section) {
		if (strcasecmp(s->name, sectionname) == 0) {
			return s;
		}
	}
	return NULL; /* not found */
}


static ConfigSection *add_section(const char *sectionname)
{
	ConfigSection *s;
	ConfigSection **place = &first_section;

	for (s = first_section; s != NULL; s = s->next_section)
		place = &(s->next_section);

	*place = (ConfigSection *) malloc(sizeof(ConfigSection));
	if (*place != NULL) {
		(*place)->name = strdup(sectionname);
		(*place)->first_key = NULL;
		(*place)->next_section = NULL;
	}

	return(*place);
}


static ConfigKey *find_key(ConfigSection *s, const char *keyname, int skip)
{
	ConfigKey *k;
	int count = 0;
	ConfigKey *last_key = NULL;

	/* Check for NULL section*/
	if (s == NULL)
		return NULL;

	for (k = s->first_key; k != NULL; k = k->next_key) {

		/* Did we find the right key ?*/
		if (strcasecmp(k->name, keyname) == 0) {
			if (count == skip)
				return k;

			count++;
			last_key = k;
		}
	}
	if (skip == -1)
		return last_key;

	return NULL; /* not found*/
}


static ConfigKey *add_key(ConfigSection *s, const char *keyname, const char *value)
{
	if (s != NULL) {
		ConfigKey *k;
		ConfigKey **place = &(s->first_key);

		for (k = s->first_key; k != NULL; k = k->next_key)
			place = &(k->next_key);

		*place = (ConfigKey *) malloc(sizeof(ConfigKey));
		if (*place != NULL) {
			(*place)->name = strdup(keyname);
			(*place)->value = strdup(value);
			(*place)->next_key = NULL;
		}

		return(*place);
	}
	return NULL;
}


#if defined(LCDPROC_CONFIG_READ_STRING)
static char get_next_char_f(FILE *f)
{
	int c = fgetc(f);

	return((c == EOF) ? '\0' : c);
}
#endif


/* Parser states */
#define ST_INITIAL		0
#define ST_COMMENT		257
#define ST_SECTIONLABEL		258
#define ST_KEYNAME		259
#define ST_ASSIGNMENT		260
#define ST_VALUE		261
#define ST_QUOTEDVALUE		262
#define ST_SECTIONLABEL_DONE	263
#define ST_VALUE_DONE		264
#define ST_INVALID_SECTIONLABEL	265
#define ST_INVALID_KEYNAME	266
#define ST_INVALID_ASSIGNMENT	267
#define ST_INVALID_VALUE	268
#define ST_END			999

/* Limits */
#define MAXSECTIONLABELLENGTH	40
#define MAXKEYNAMELENGTH	40
#define MAXVALUELENGTH		200


#if defined(LCDPROC_CONFIG_READ_STRING)
static int process_config(ConfigSection **current_section, char(*get_next_char)(), const char *source_descr, FILE *f)
#else
static int process_config(ConfigSection **current_section, const char *source_descr, FILE *f)
#endif
{
	int state = ST_INITIAL;
	int ch;
	char sectionname[MAXSECTIONLABELLENGTH+1];
	int sectionname_pos = 0;
	char keyname[MAXKEYNAMELENGTH+1];
	int keyname_pos = 0;
	char value[MAXVALUELENGTH+1];
	int value_pos = 0;
	int escape = 0;
	int line_nr = 1;
	int error = 0;

#if !defined(LCDPROC_CONFIG_READ_STRING)
	if (f == NULL)
		return(0);
#endif

	while (state != ST_END) {

#if defined(LCDPROC_CONFIG_READ_STRING)
		ch = (f != NULL)
			? get_next_char(f)
			: get_next_char();
#else
		ch = fgetc(f);
		if (ch == EOF)
			ch = '\0';
#endif

		/* Secretly keep count of the line numbers */
		if (ch == '\n')
			line_nr++;

		switch (state) {
		  case ST_INITIAL:
			switch (ch) {
			  case '#':
			  case ';':
				/* comment start */
				state = ST_COMMENT;
				/* fall through */
			  case '\0':
			  case '\n':
			  case '\r':
			  case '\t':
			  case ' ':
				/* ignore spaces */
				break;
			  case '[':
				/* section name */
				state = ST_SECTIONLABEL;
				sectionname_pos = 0;
				sectionname[sectionname_pos] = '\0';
				break;
			  default:
				/* key word */
				state = ST_KEYNAME;
				keyname_pos = 0;
				keyname[keyname_pos++] = ch;
				keyname[keyname_pos] = '\0';
			}
			break;
		  case ST_SECTIONLABEL:
			/* section label: "["{non-space chars}+"]" */
			switch (ch) {
			  case '\0':
			  case '\n':
				/* premature end of label */
				report(RPT_WARNING, "Unterminated section label on line %d of %s: %s",
						line_nr, source_descr, sectionname);
				error = 1;
				state = ST_INITIAL;	/* already at the end, no resync required */
				break;
			  case ']':
				/* label terminated: find/create section */
				if (!(*current_section = find_section(sectionname))) {
					*current_section = add_section(sectionname);
				}
				state = ST_SECTIONLABEL_DONE;
				break;
			//  case '\r':
			//  case '\t':
			//  case ' ':
			//	/* no spaces allowed in section labels WHY? */
			//	report(RPT_WARNING, "Invalid character in section label on line %d of %s: %s",
			//			line_nr, source_descr, sectionname);
			//	error = 1;
			//	state = ST_INVALID_SECTIONLABEL;	/* resync required */
			//	break;
			  default:
				/* append char to section label */
				if (sectionname_pos < MAXSECTIONLABELLENGTH) {
					sectionname[sectionname_pos++] = ch;
					sectionname[sectionname_pos] = '\0';
					break;
				}
				report(RPT_WARNING, "Section name too long on line %d of %s: %s",
						line_nr, source_descr, sectionname);
				error = 1;
				state = ST_INVALID_SECTIONLABEL;	/* resync required */
			}
			break;
		  case ST_KEYNAME:
			/* key name: {non-space chars}+ */
			switch (ch) {
			  case '\r':
			  case '\t':
			  case ' ':
				/* ignore trailing spaces */
				if (keyname_pos != 0)
					state = ST_ASSIGNMENT;
				break;
			  case '\0':
			  case '\n':
				/* premature end of line */
				report(RPT_WARNING, "Loose word found on line %d of %s: %s",
						line_nr, source_descr, keyname);
				error = 1;
				state = ST_INITIAL;	/* already at the end; no resync required */
				break;
			  case '=':
				/* end of key reached, "=" found, now we need a value */
				state = ST_VALUE;
				value[0] = '\0';
				value_pos = 0;
				break;
			//  case '"':
			//  case '[':
			//  case ']':
			//	/* character invalid in key names WHY ? */
			//	report(RPT_WARNING, "Invalid character in key name on line %d of %s: %s",
			//			line_nr, source_descr, keyname);
			//	error = 1;
			//	state = ST_INVALID_KEYNAME;	/* resync required */
			//	break;
			  default:
				/* append char to key name */
				if (keyname_pos < MAXKEYNAMELENGTH) {
					keyname[keyname_pos++] = ch;
					keyname[keyname_pos] = '\0';
					break;
				}
				report(RPT_WARNING, "Key name too long on line %d of %s: %s",
						line_nr, source_descr, keyname);
				error = 1;
				state = ST_INVALID_KEYNAME;	/* resync required */
			}
			break;
		  case ST_ASSIGNMENT:
			/* assignment: "=" */
			switch (ch) {
			  case '\t':
			  case ' ':
				/* ignore leading spaces */
				break;
			  case '=':
				/* "=" found, now we need a value */
				state = ST_VALUE;
				value[0] = '\0';
				value_pos = 0;
				break;
			  default:
				report(RPT_WARNING, "Assignment expected on line %d of %s: %s",
						line_nr, source_descr, keyname);
				error = 1;
				state = ST_INVALID_ASSIGNMENT;
			}
			break;
		  case ST_VALUE:
			/* value: {non-space char}+ | "\""{any potentially-quoted char}+"\"" */
			switch (ch) {
			  case '#':
			  case ';':
				/* allow comment if we already had a value */
				/* WHY ONLY THEN ? 'xx=' can be seen as equivalent to 'xx=""' */
				if (value_pos > 0) {
					state = ST_COMMENT;
					break;
				}
				/* fall through */
			  case '[':
			  case ']':
			  case '=':
				/* illegal characters WHY? */
				report(RPT_WARNING, "Invalid character '%c' in value on line %d of %s, at key: %s",
						ch, line_nr, source_descr, keyname);
				error = 1;
				state = ST_INVALID_VALUE;
				break;
			  case '\t':
			  case ' ':
				/* ignore leading spaces */
				if (value_pos == 0)
					break;
				/* fall through */
			  case '\0':
			  case '\n':
			  case '\r':
				/* value complete */
				if (!*current_section) {
					report(RPT_WARNING, "Data outside sections on line %d of %s with key: %s",
							line_nr, source_descr, keyname);
					error = 1;
				}
				else {
					/* Store the value*/
					(void) add_key(*current_section, keyname, value);
				}
				/* And be ready for next thing...*/
				state = ((ch == ' ') || (ch == '\t')) ? ST_VALUE_DONE : ST_INITIAL;
				break;
			  case '"':
				/* quoted string */
				state = ST_QUOTEDVALUE;
				break;
			  default:
				/* append char to value */
				if (value_pos < MAXVALUELENGTH) {
					value[value_pos++] = ch;
					value[value_pos] = '\0';
					break;
				}
				report(RPT_WARNING, "Value too long on line %d of %s, at key: %s",
						line_nr, source_descr, keyname);
				error = 1;
				state = ST_INVALID_VALUE;
			}
			break;
		  case ST_QUOTEDVALUE:
			/* a quoted part of a string */
			switch (ch) {
			  case '\0':
			  case '\n':
				report(RPT_WARNING, "Premature end of quoted string on line %d of %s: %s",
						line_nr, source_descr, keyname);
				error = 1;
				state = ST_INITIAL;
				break;
			  case '\\':
				if (!escape) {
					escape = 1;
					break;
				}
				/* fall through */
			  case '"':
				if (!escape) {
					state = ST_VALUE;
					break;
				}
				/* fall through */
			  default:
				if (escape) {
					switch (ch) {
					  case 'a': ch = '\a'; break;
					  case 'b': ch = '\b'; break;
					  case 'f': ch = '\f'; break;
					  case 'n': ch = '\n'; break;
					  case 'r': ch = '\r'; break;
					  case 't': ch = '\t'; break;
					  case 'v': ch = '\v'; break;
					  /* default: literal char  (i.e. ignore '\') */
					}
					escape = 0;
				}
				value[value_pos++] = ch;
				value[value_pos] = '\0';
			}
			break;
		  case ST_SECTIONLABEL_DONE:
		  case ST_VALUE_DONE:
			switch (ch) {
			  case ';':
			  case '#':
				state = ST_COMMENT;
				break;
			  case '\0':
			  case '\n':
				state = ST_INITIAL;
				break;
			  case '\t':
			  case ' ':
				break;
			  default:
				/* illegal characters */
				report(RPT_WARNING, "Invalid character '%c' on line %d of %s",
						ch, line_nr, source_descr);
				error = 1;
				state = ST_INVALID_VALUE;
			 }
		  case ST_INVALID_SECTIONLABEL:
			/* invalid section label: resync up to end of label/next line */
			if (ch == ']')
				state = ST_INITIAL;
			/* fall through */
		  case ST_INVALID_ASSIGNMENT:
		  case ST_INVALID_KEYNAME:
		  case ST_INVALID_VALUE:
		  case ST_COMMENT:
			/* comment or error: ignore anything up to the next line */
			if (ch == '\n')
				state = ST_INITIAL;
		}
		if (ch == '\0') {
			if ((!error) && (state != ST_INITIAL) && (state != ST_COMMENT) &&
			    (state != ST_SECTIONLABEL_DONE) && (state != ST_VALUE_DONE)) {
				report(RPT_WARNING, "Premature end of configuration on line %d of %s: %d",
						line_nr, source_descr, state);

				error = 1;
			}
			state = ST_END;
		}

	}
	return (error) ? -1 : 0;
}


#if CONFIGFILE_DEBUGTEST
void config_dump(void)
{
ConfigSection *s;

	for (s = first_section; s != NULL; s = s->next_section) {
		ConfigKey *k;

		fprintf(stderr, "[%s]\n", s->name);

		for (k = s->first_key; k != NULL; k = k->next_key)
			fprintf(stderr, "%s = \"%s\"\n", k->name, k->value);

		fprintf(stderr, "\n");
	}
}


int main(int argc, char *argv[])
{
	if (argc > 0)
		config_read_file(argv[1]);
	config_dump();
}
#endif
