/*
 * Brutus source file implementing configuration file helper functions.
 * Copyright (C) 2004-2007 OMC Denmark ApS.
 * 
 * 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 3 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, see <http://www.gnu.org/licenses/>.
 */


#include "config.h"
#include <string.h>
#include <stdio.h>

#include <fcntl.h>
#include <unistd.h>
#include <sys/stat.h>


#ifdef COMMENT_CHAR
#undef COMMENT_CHAR
#endif
#define COMMENT_CHAR '#'

#ifdef MAX_CONFIG_FILE_SIZE
#undef MAX_CONFIG_FILE_SIZE
#endif
#define MAX_CONFIG_FILE_SIZE 10240


// whitespace?
static unsigned char wspace(char c)
{
	switch (c) {
	case ' ' :
	case '\t' :
	case '\r' :
		return 1;
	default:
		return 0;
	}
};

/* Reads all of file_name into *buf, as a set of adjoined 
 * strings, correcting for CRLF. 
 *
 * *size is the size of the file in bytes, not the size 
 * of the buffer *buf. Callee must free() *buf if zero 
 * is returned, otherwise not. 
 *
 * I am reading one byte of the file at a time. This is to 
 * avoid a second copy/allocation. Not that this method is 
 * particular fast, but it makes the purpose so much clearer.
 * This function is not a bottleneck, therefore clarity before 
 * speed.
 *
 * Returns -1 on error. */
static int read_conf(const char *file_name, size_t *size, void **buf)
{
	FILE *file;
	int retv = 0;
	ssize_t len;
	char *tmp;
	char *first;
	struct stat fstatus;


        retv = stat(file_name, &fstatus);
	if (retv)
		return -1;

	if (fstatus.st_size > MAX_CONFIG_FILE_SIZE)
		return -1;

	tmp = (char*)malloc(fstatus.st_size);
	if (!tmp)
		return -1;
	memset((void*)tmp, '\0', fstatus.st_size);
	first = tmp;

	file = fopen(file_name, "r");
	if (!file) {
		free(tmp);
		return -1;
	}

	char c;
	off_t pos = 0;
	while (1) {
		// For clarity, not speed
                len = fread((void*)&c, 1, 1, file);
                if (!len) {
			if (ferror (file))
				retv = -1;
                        break;
                }

		// create null-terminated string
		if (c == '\n') {
			c = '\0';
		}

		// skip CR
		if (c != '\r') {
			*tmp = c;
			pos++;
			if (pos < fstatus.st_size)
				tmp++;
		}
	}
	fclose(file);

	if (retv)
		free(tmp);
	else {
		*buf = first;
		*size = pos;
	}

	return retv;
}

/* *str is a pointer to the first character 
 * of the next configuration line skipping comments and
 * empty lines.
 *
 * *str is NULL on end-of-buffer. 
 *
 * Returns (-1) on error or zero on success. */
static int get_next_line(char **str,
			 struct Config *cfg)
{
	int retv = 0;
	size_t offset = *str - cfg->_fbuf;

	do {
		// skip printable characters
		while (**str != '\0') {
			(*str)++;

			offset++;
			if (offset == cfg->_fsize) {
				*str = NULL;
				goto out;
			}
		}

		// skip '\0's
		while (**str == '\0') {
			cfg->_lnum++;
			(*str)++;

			offset++;
			if (offset == cfg->_fsize) {
				*str = NULL;
				goto out;
			}
		}

		// quick check for floating text (<WHITESPACE>TEXT)
		if (wspace(**str)) {
			while (wspace(**str)) {
				(*str)++;
				offset++;
				if (offset == cfg->_fsize) {
					*str = NULL;
					goto out;
				}
			}

			// floating text
			if (**str != '\0') { 
				retv = -1;
				goto out;
			}
			continue;
		}

		// configuration line?
		if (**str != COMMENT_CHAR)
			break;
	} while (1);

out:
	return retv;
}

/* Upon return val points to the first character in the null-
 * or whitespace-terminated value string. 
 *
 * *str is a pointer to the first character in a null-terminated 
 * configuration line. 
 * 
 * *str is modified by this function to point to the first 
 * character in the value string. 
 *
 * Will return (-1) on error or zero on success. */
static int point_to_value(char **str, char **val)
{
	// not possible
	if (wspace(**str))
		return -1;

	// skip past token 
        while ((!wspace(**str)) && (**str != '\0')) {
		(*str)++;
        }
	if (**str == '\0')
		return -1;

	// skip past whitespace to value
	while (wspace(**str)) {
		**str = '\0';
		(*str)++;
	}
	if (**str == '\0')
		return -1;

	*val = *str;

	// remove trailing whitespace
	char *tmp = *val;
	while (*tmp != '\0')
		tmp++;
	tmp--;
	while (wspace(*tmp)) {
		*tmp = '\0';
		tmp--;
	}

        return 0;
}

static int itemize(struct Config *cfg)
{
	size_t count = 0;
	char *p = cfg->_fbuf;

	// count maximum number of effective configuration lines
	size_t n;
	for (n = 0; n < cfg->_fsize; n++, p++) {
		// newline?
		if (*p == '\0')
			count++;
	}
	cfg->_items = (struct item*)malloc(sizeof(struct item) * (count + 1));

	// get token/value pairs
	p = cfg->_fbuf;
	count = 0;
	if ((wspace(*p)) || (*p == COMMENT_CHAR)) 
		if (get_next_line(&p, cfg))
			return cfg->_lnum;
	while (1) {
		if (!p)
			break;

		cfg->_items[count].token = p;

		// p will be somewhere within the value string when the function returns
		if (point_to_value(&p, &cfg->_items[count++].value)) 
			return cfg->_lnum;

		if (get_next_line(&p, cfg))
			return cfg->_lnum;
	}
	cfg->_items[count].token = NULL;
	cfg->_items[count].value = NULL;

	return 0;
}

char *get_config_value(const char *token,
		       struct Config *cfg)
{
	size_t n = 0;

	if (cfg->_items) {
		while (cfg->_items[n].value) {
			if ((cfg->_items[n].token) && !strcmp(token, cfg->_items[n].token)) {
				if (cfg->stack_token && !strcmp(token, cfg->stack_token))
					cfg->_items[n].token = NULL;
				return strdup(cfg->_items[n].value);
			}
			n++;
		}
	}

	return NULL;
}

int init_config(struct Config *cfg)
{
	if (!cfg->config_file)
		return -1;

	// initialize
	if (cfg->_items) {
		free(cfg->_items);
		cfg->_items = NULL;
	}
	if (cfg->_fbuf) {
		free(cfg->_fbuf);
		cfg->_fbuf = NULL;
	}
	cfg->_lnum = 1;

	// read file into buffer
	if (read_conf(cfg->config_file, &cfg->_fsize, (void**)&cfg->_fbuf)) 
		return -1;

	return itemize(cfg);
}

void free_config(struct Config *cfg)
{
	if (!cfg)
		return;

	free(cfg->_fbuf);
	free(cfg->_items);
}
