/* config.c: Configuration gathering functions.
 *
 * $Id: config.c,v 1.9 2003/02/10 06:47:23 andy Exp $
 *
 * Copyright (C) 2002 Andy Goth <unununium@openverse.com>
 * For more information visit http://ioioio.net/devel/install-log/
 * 
 * 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 <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "install-log.h"

/* Description of one option to install-log. */
typedef struct config_data_t {
	char* tag;		/* Name in rc file */
	char* long_opt;		/* Name as a long option */
	char swtch;		/* Name as a commandline switch */
	void* var;		/* Place to store value or function to call */
	int type;		/* Type of variable */
	int priority;		/* Minimum priority that can set this option */
} config_data_t;

/* A handy abbreviation. */
typedef struct option option_t;

/* Types of options to install-log. */
#define ACTION_TYPE 0		/* Call 'var' immediately */
#define STRING_TYPE 1		/* Single string value */
#define STRING_LIST_TYPE 2	/* Colon-separated strings */
#define INT_TYPE 3		/* Integer */
#define BOOL_TYPE 4		/* Yes or no, true or false */

/* Priorities for configuration sources. */
#define DEFAULT_PRIORITY 0	/* From defaults */
#define RC_PRIORITY 1		/* From configuration file */
#define COMMAND_PRIORITY 2	/* From command line */

static void parse_command_line(int argc, char** argv);
static void parse_rc_file(const char* filename);
static char* build_short_options(void);
static option_t* build_long_options(void);
static void set_option(int i, char* val, int p);
static void usage(void);
static void version(void);
static void usage_error(void);
static void remove_space(char* buf);
static void remove_quotes(char* buf);
static void remove_comment(char* buf);
static void remove_trailing_slashes(char* buf);
static void remove_trailing_slashes_wrapper(list_t* n);

/* All valid options to install-log.  The 'priority' field defaults to 0
 * (DEFAULT_PRIORITY) because it's not listed here. */
static config_data_t config_data[] = {
{"EDIT",	"edit",		'e',	&edit,		BOOL_TYPE},
{"EDITOR",	"editor",	'E',	&editor,	STRING_TYPE},
{"FORCE",	"force",	'f',	&force,		BOOL_TYPE},
{NULL,		"help",		'h',	usage,		ACTION_TYPE},
{"INCLUDE",	"include",	'i',	&include,	STRING_LIST_TYPE},
{"LOGDIR",	"logdir",	'l',	&logdir,	STRING_TYPE},
{"QUIET",	"quiet",	'q',	&quiet,		BOOL_TYPE},
{NULL,		"root",		'r',	&root,		STRING_TYPE},
{"CHROOTVAR",	"chrootvar",	'c',	&chrootvar,	STRING_TYPE},
{"VERBOSITY",	"verbosity",	'v',	&verbosity,	INT_TYPE},
{NULL,		"version",	'V',	version,	ACTION_TYPE},
{"EXCLUDE",	"exclude",	'x',	&exclude,	STRING_LIST_TYPE}
};

static const int num_options = sizeof(config_data) / sizeof(config_data_t);

/* Gathers configuration data from all sources, where 'argc' and 'argv'
 * define the command line passed to install-log. */
void get_config(int argc, char** argv)
{
	char* logdir2;
	char* config_file;

	init_globals(argv);
	parse_command_line(argc, argv);

	/* old code:
	config_file = safe_sprintf(NULL, NULL, "%s/etc/install-log.rc", root);
	*/
	config_file = safe_sprintf(NULL, NULL, "%s/etc/install-log.rc",
		root != NULL ? root : ""); /* Fix by cemoreau@hotmail.com */
	parse_rc_file(config_file);
	free(config_file);

	/* Update the root, maybe */
	if (root == NULL) {
		root = getenv(chrootvar);
		if (root == NULL) root = "/";
		root = strdup(root);
	}

	/* Append root to logdir */
	logdir2 = logdir;
	logdir = safe_sprintf(NULL, NULL, "%s/%s", root, logdir2);
	free(logdir2);

	/* Remove trailing slashes from include and exclude lists */
	proc_list(&include, remove_trailing_slashes_wrapper);
	proc_list(&exclude, remove_trailing_slashes_wrapper);
}

/* Handles the command line, defined by 'argc' and 'argv'. */
static void parse_command_line(int argc, char** argv)
{
	int c;
	int i;
	int option_index = 0;
	char* short_options;
	option_t* long_options;

	/* Build option strings */
	short_options = build_short_options();
	long_options = build_long_options();

	/* Parse switches */
	while ((c = getopt_long(argc, argv, short_options, long_options,
			&option_index)) != -1) {
		/* Find the corresponding config_data entry */
		for (i = 0; i < num_options; i++) {
			if (config_data[i].swtch == c) break;
			if (c != 0) continue;
			if (strcmp(config_data[i].long_opt,
			long_options[option_index].name) == 0) break;
		}
		if (i == num_options) usage_error();
	
		set_option(i, optarg, COMMAND_PRIORITY);
	}

	free(long_options);
	free(short_options);
	
	/* Find the package name */
	if (optind != argc - 1) usage_error();
	else package = strdup(argv[optind]);
}

/* Handles the rc file, named 'filename'. */
static void parse_rc_file(const char* filename)
{
	int i;
	int line_num = 0;
	int buf_len = 256;
	char* buf;	/* The whole line */
	char* value;	/* After the = */
	FILE* file;

	if ((file = fopen(filename, "r")) == NULL) return;
	buf = xmalloc(buf_len);
	
	while (1) {
		/* Read the line */
		line_num++;
		getline(&buf, &buf_len, file);
		if (feof(file)) break;
		remove_comment(buf);
		remove_space(buf);

		/* Determine if the line should be skipped */
		if (*buf == 0) continue;
		
		/* Split the string at the `=' */
		value = strchr(buf, '=');
		if (value != NULL) {
			*value = 0;
			value++;
			remove_space(value);
			remove_quotes(value);
		}
		
		remove_space(buf);
		remove_quotes(buf);

		/* Find the index which corresponds to the key */
		for (i = 0; i < num_options; i++) {
			if (config_data[i].tag == NULL) continue;
			if (strcasecmp(config_data[i].tag, buf) == 0) break;
		}
		if (i == num_options) fail("Parse error in %s on line %i\n",
				filename, line_num);

		set_option(i, value, RC_PRIORITY);
	}

	free(buf);
	fclose(file);
}

/* Prepares a list of short options for getopt. */
static char* build_short_options(void)
{
	int i;
	char* o_p;
	char* o = xmalloc(num_options * 3 + 1);

	for (i = 0, o_p = o; i < num_options; i++) {
		/* See if this config item has a short option */
		if (config_data[i].swtch == 0) continue;

		/* Place this config item in the option list */
		*(o_p++) = config_data[i].swtch;

		/* See if there's a parameter */
		switch (config_data[i].type) {
		case BOOL_TYPE:
			*(o_p++) = ':';
			/* Fallthrough intentional */
		case STRING_TYPE:
		case STRING_LIST_TYPE:
		case INT_TYPE:
			*(o_p++) = ':';
		}
	}
	*o_p = 0;

	return o;
}

/* Prepares a list of long options for getopt_long. */
static option_t* build_long_options(void)
{
	int i;
	option_t* o_p;
	option_t* o = xmalloc((num_options + 1) * sizeof(option_t));

	for (i = 0, o_p = o; i < num_options; i++) {
		/* See if this config item has a long option */
		if (config_data[i].long_opt == NULL) continue;

		/* Build an entry in the long option table */
		o_p->name = config_data[i].long_opt;
		switch (config_data[i].type) {
		case STRING_TYPE:
		case STRING_LIST_TYPE:
		case INT_TYPE:
			o_p->has_arg = required_argument;
			break;
		case BOOL_TYPE:
			o_p->has_arg = optional_argument;
			break;
		case ACTION_TYPE:
			o_p->has_arg = no_argument;
		}
		o_p->flag = NULL;
		o_p->val = config_data[i].swtch;
		o_p++;
	}

	/* Make the terminating entry */
	o_p->name = NULL;
	o_p->has_arg = no_argument;
	o_p->flag = NULL;
	o_p->val = 0;

	return o;
}

/* Sets variable index 'i' to 'val'. */
static void set_option(int i, char* val, int p)
{
	/* Lower priorities cannot override higher priorities */
	if (p < config_data[i].priority) return;
	config_data[i].priority = p;

	switch (config_data[i].type) {
	case STRING_TYPE: {
		char** string = (char**)config_data[i].var;
		free(*string);
		*string = strdup(val);
		break;
	} case STRING_LIST_TYPE: {
		char* val_p;
		list_t* node = (list_t*)config_data[i].var;
		clear_list(node);
		do {
			/* Turn : into \0 */
			val_p = strchr(val, ':');
			if (val_p != NULL) *val_p = 0;

			/* Add this string to the list if not zero-length */
			if (*val != 0) add_string_node(&node, val);

			/* Skip to the next character in the input */
			val = val_p + 1;
		} while (val_p != NULL);
		break;
	} case INT_TYPE:
		*(int*)config_data[i].var = atoi(val);
		break;
	case BOOL_TYPE:
		*(bool*)config_data[i].var = val == NULL ||
				strcasecmp(val, "yes") == 0 ||
				strcasecmp(val, "true") == 0;
		break;
	case ACTION_TYPE:
		((void(*)(void))config_data[i].var)();
	}
}

/* Prints usage information and exits. */
static void usage(void)
{
	printf(
"Usage: %s <package> [OPTIONS]\n"
"Manage package installation history database\n"
"\n"
"  -e, --edit                  Edit log.\n"
"  -E, --editor=CMD            Use CMD with --edit.\n"
"  -f, --force                 Replace existing installation log.\n"
"  -h, --help                  Print this message.\n"
"  -i, --include=DIR1:DIR2:... Scan these directories.\n"
"  -l, --logdir=DIR            Keep logs in DIR.\n"
"  -q, --quiet                 Do not output messages (except errors).\n"
"  -r, --root=DIR              Use DIR as root.\n"
"  -c, --chrootvar=NAME        Use ${NAME} as root.\n"
"  -v, --verbosity=LEVEL       Report progress with given level of detail.\n"
"  -V, --version               Print version information and then exit.\n"
"  -x, --exclude=DIR1:DIR2:... Do not scan these directories.\n",
	program_name);
	exit(EXIT_SUCCESS);
}

/* Prints version and exits. */
static void version(void)
{
	printf(
"install-log " VERSION_STR ", " DATE_STR "\n"
"Copyright 2001-2003 Andy Goth <unununium@openverse.com>.\n"
"Check for updates at <http://ioioio.net/devel/install-log/>.\n"
"install-log is free software, covered by the GNU General Public License;\n"
"you are welcome to change and/or distribute it under certain conditions.\n"
"There is absolutely no warranty for install-log.  See COPYING for details.\n");
	exit(EXIT_SUCCESS);
}

/* Prints a usage information and exits. */
static void usage_error(void)
{
	fprintf(stderr, "Try `%s --help' for more information.\n",
			program_name);
	exit(EXIT_FAILURE);
}

/* Trims whitespace from the beginning and end of 'buf'. */
static void remove_space(char* buf)
{
	char* buf_p;

	/* Remove trailing whitespace */
	for (buf_p = strchr(buf, 0) - 1; isspace(*buf_p); buf_p--);
	buf_p[1] = 0;

	/* Remove leading whitespace */
	for (buf_p = buf; *buf_p != 0 && isspace(*buf_p); buf_p++);
	if (buf != buf_p) memmove(buf, buf_p, strlen(buf_p) + 1);
}

/* Removes all quote characters from 'buf'. */
static void remove_quotes(char* buf)
{
	int len = strlen(buf) + 1;
	for (; *buf != 0; buf++, len--)
		if (*buf == '"') memmove(buf, buf + 1, --len);
}

/* Removes all text from 'buf' starting with the first # not between quotes. */
static void remove_comment(char* buf)
{
	bool in_quotes = 0;
	for (; *buf != 0; buf++) {
		switch (*buf) {
		case '"': in_quotes = !in_quotes; break;
		case '#': if (!in_quotes) {*buf = 0; return;}
		}
	}
}

/* Removes all trailing slashes from 'buf'. */
static void remove_trailing_slashes(char* buf)
{
	char* buf_p = strchr(buf, 0) - 1;
	while (buf_p >= buf && *buf_p == '/') buf_p--;
	buf_p[1] = 0;
}

/* Wrapper for remove_trailing_slashes for use with proc_list. */
static void remove_trailing_slashes_wrapper(list_t* n)
{
	remove_trailing_slashes(n->data);
}

/* EOF */

