/*
 * Copyright (C) 2010-2014 Tobias Brunner
 * Copyright (C) 2008 Martin Willi
 * Hochschule fuer Technik Rapperswil
 *
 * 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.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * 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.
 */

#define _GNU_SOURCE
#include <string.h>
#include <stdarg.h>
#include <stdio.h>
#include <errno.h>
#include <limits.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>

#include "settings.h"
#include "settings_types.h"

#include "collections/array.h"
#include "collections/hashtable.h"
#include "collections/linked_list.h"
#include "threading/rwlock.h"
#include "utils/debug.h"

typedef struct private_settings_t private_settings_t;

/**
 * Parse function provided by the generated parser.
 */
bool settings_parser_parse_file(section_t *root, char *name);

/**
 * Private data of settings
 */
struct private_settings_t {

	/**
	 * Public interface
	 */
	settings_t public;

	/**
	 * Top level section
	 */
	section_t *top;

	/**
	 * Contents of replaced settings (char*)
	 *
	 * FIXME: This is required because the pointer returned by get_str()
	 * is not refcounted.  Might cause ever increasing usage stats.
	 */
	array_t *contents;

	/**
	 * Lock to safely access the settings
	 */
	rwlock_t *lock;
};

/**
 * Print a format key, but consume already processed arguments
 */
static bool print_key(char *buf, int len, char *start, char *key, va_list args)
{
	va_list copy;
	char *pos = start;
	bool res;

	va_copy(copy, args);
	while (TRUE)
	{
		pos = memchr(pos, '%', key - pos);
		if (!pos)
		{
			break;
		}
		pos++;
		switch (*pos)
		{
			case 'd':
				va_arg(copy, int);
				break;
			case 's':
				va_arg(copy, char*);
				break;
			case 'N':
				va_arg(copy, enum_name_t*);
				va_arg(copy, int);
				break;
			case '%':
				break;
			default:
				DBG1(DBG_CFG, "settings with %%%c not supported!", *pos);
				break;
		}
		pos++;
	}
	res = vsnprintf(buf, len, key, copy) < len;
	va_end(copy);
	return res;
}

/**
 * Find a section by a given key, using buffered key, reusable buffer.
 * If "ensure" is TRUE, the sections are created if they don't exist.
 */
static section_t *find_section_buffered(section_t *section,
					char *start, char *key, va_list args, char *buf, int len,
					bool ensure)
{
	char *pos;
	section_t *found = NULL;

	if (section == NULL)
	{
		return NULL;
	}
	pos = strchr(key, '.');
	if (pos)
	{
		*pos = '\0';
		pos++;
	}
	if (!print_key(buf, len, start, key, args))
	{
		return NULL;
	}
	if (!strlen(buf))
	{
		found = section;
	}
	else if (array_bsearch(section->sections, buf, settings_section_find,
						   &found) == -1)
	{
		if (ensure)
		{
			found = settings_section_create(strdup(buf));
			settings_section_add(section, found, NULL);
		}
	}
	if (found && pos)
	{
		return find_section_buffered(found, start, pos, args, buf, len, ensure);
	}
	return found;
}

/**
 * Find all sections via a given key considering fallbacks, using buffered key,
 * reusable buffer.
 */
static void find_sections_buffered(section_t *section, char *start, char *key,
						va_list args, char *buf, int len, array_t **sections)
{
	section_t *found = NULL, *fallback;
	char *pos;
	int i;

	if (!section)
	{
		return;
	}
	pos = strchr(key, '.');
	if (pos)
	{
		*pos = '\0';
	}
	if (!print_key(buf, len, start, key, args))
	{
		return;
	}
	if (pos)
	{	/* restore so we can follow fallbacks */
		*pos = '.';
	}
	if (!strlen(buf))
	{
		found = section;
	}
	else
	{
		array_bsearch(section->sections, buf, settings_section_find, &found);
	}
	if (found)
	{
		if (pos)
		{
			find_sections_buffered(found, start, pos+1, args, buf, len,
								   sections);
		}
		else
		{
			array_insert_create(sections, ARRAY_TAIL, found);
			for (i = 0; i < array_count(found->fallbacks); i++)
			{
				array_get(found->fallbacks, i, &fallback);
				array_insert_create(sections, ARRAY_TAIL, fallback);
			}
		}
	}
	if (section->fallbacks)
	{
		for (i = 0; i < array_count(section->fallbacks); i++)
		{
			array_get(section->fallbacks, i, &fallback);
			find_sections_buffered(fallback, start, key, args, buf, len,
								   sections);
		}
	}
}

/**
 * Ensure that the section with the given key exists (thread-safe).
 */
static section_t *ensure_section(private_settings_t *this, section_t *section,
								 const char *key, va_list args)
{
	char buf[128], keybuf[512];
	section_t *found;

	if (snprintf(keybuf, sizeof(keybuf), "%s", key) >= sizeof(keybuf))
	{
		return NULL;
	}
	/* we might have to change the tree */
	this->lock->write_lock(this->lock);
	found = find_section_buffered(section, keybuf, keybuf, args, buf,
								  sizeof(buf), TRUE);
	this->lock->unlock(this->lock);
	return found;
}

/**
 * Find a section by a given key with its fallbacks (not thread-safe!).
 * Sections are returned in depth-first order (array is allocated). NULL is
 * returned if no sections are found.
 */
static array_t *find_sections(private_settings_t *this, section_t *section,
							  char *key, va_list args)
{
	char buf[128], keybuf[512];
	array_t *sections = NULL;

	if (snprintf(keybuf, sizeof(keybuf), "%s", key) >= sizeof(keybuf))
	{
		return NULL;
	}
	find_sections_buffered(section, keybuf, keybuf, args, buf,
						   sizeof(buf), &sections);
	return sections;
}

/**
 * Check if the given fallback section already exists
 */
static bool fallback_exists(section_t *section, section_t *fallback)
{
	if (section == fallback)
	{
		return TRUE;
	}
	else if (section->fallbacks)
	{
		section_t *existing;
		int i;

		for (i = 0; i < array_count(section->fallbacks); i++)
		{
			array_get(section->fallbacks, i, &existing);
			if (existing == fallback)
			{
				return TRUE;
			}
		}
	}
	return FALSE;
}

/**
 * Ensure that the section with the given key exists and add the given fallback
 * section (thread-safe).
 */
static void add_fallback_to_section(private_settings_t *this,
							section_t *section, const char *key, va_list args,
							section_t *fallback)
{
	char buf[128], keybuf[512];
	section_t *found;

	if (snprintf(keybuf, sizeof(keybuf), "%s", key) >= sizeof(keybuf))
	{
		return;
	}
	this->lock->write_lock(this->lock);
	found = find_section_buffered(section, keybuf, keybuf, args, buf,
								  sizeof(buf), TRUE);
	if (!fallback_exists(found, fallback))
	{
		/* to ensure sections referred to as fallback are not purged, we create
		 * the array there too */
		if (!fallback->fallbacks)
		{
			fallback->fallbacks = array_create(0, 0);
		}
		array_insert_create(&found->fallbacks, ARRAY_TAIL, fallback);
	}
	this->lock->unlock(this->lock);
}

/**
 * Find the key/value pair for a key, using buffered key, reusable buffer
 * If "ensure" is TRUE, the sections (and key/value pair) are created if they
 * don't exist.
 * Fallbacks are only considered if "ensure" is FALSE.
 */
static kv_t *find_value_buffered(section_t *section, char *start, char *key,
								 va_list args, char *buf, int len, bool ensure)
{
	int i;
	char *pos;
	kv_t *kv = NULL;
	section_t *found = NULL;

	if (section == NULL)
	{
		return NULL;
	}

	pos = strchr(key, '.');
	if (pos)
	{
		*pos = '\0';
		if (!print_key(buf, len, start, key, args))
		{
			return NULL;
		}
		/* restore so we can retry for fallbacks */
		*pos = '.';
		if (!strlen(buf))
		{
			found = section;
		}
		else if (array_bsearch(section->sections, buf, settings_section_find,
							   &found) == -1)
		{
			if (ensure)
			{
				found = settings_section_create(strdup(buf));
				settings_section_add(section, found, NULL);
			}
		}
		if (found)
		{
			kv = find_value_buffered(found, start, pos+1, args, buf, len,
									 ensure);
		}
		if (!kv && !ensure && section->fallbacks)
		{
			for (i = 0; !kv && i < array_count(section->fallbacks); i++)
			{
				array_get(section->fallbacks, i, &found);
				kv = find_value_buffered(found, start, key, args, buf, len,
										 ensure);
			}
		}
	}
	else
	{
		if (!print_key(buf, len, start, key, args))
		{
			return NULL;
		}
		if (array_bsearch(section->kv, buf, settings_kv_find, &kv) == -1)
		{
			if (ensure)
			{
				kv = settings_kv_create(strdup(buf), NULL);
				settings_kv_add(section, kv, NULL);
			}
			else if (section->fallbacks)
			{
				for (i = 0; !kv && i < array_count(section->fallbacks); i++)
				{
					array_get(section->fallbacks, i, &found);
					kv = find_value_buffered(found, start, key, args, buf, len,
											 ensure);
				}
			}
		}
	}
	return kv;
}

/**
 * Find the string value for a key (thread-safe).
 */
static char *find_value(private_settings_t *this, section_t *section,
						char *key, va_list args)
{
	char buf[128], keybuf[512], *value = NULL;
	kv_t *kv;

	if (snprintf(keybuf, sizeof(keybuf), "%s", key) >= sizeof(keybuf))
	{
		return NULL;
	}
	this->lock->read_lock(this->lock);
	kv = find_value_buffered(section, keybuf, keybuf, args, buf, sizeof(buf),
							 FALSE);
	if (kv)
	{
		value = kv->value;
	}
	this->lock->unlock(this->lock);
	return value;
}

/**
 * Set a value to a copy of the given string (thread-safe).
 */
static void set_value(private_settings_t *this, section_t *section,
					  char *key, va_list args, char *value)
{
	char buf[128], keybuf[512];
	kv_t *kv;

	if (snprintf(keybuf, sizeof(keybuf), "%s", key) >= sizeof(keybuf))
	{
		return;
	}
	this->lock->write_lock(this->lock);
	kv = find_value_buffered(section, keybuf, keybuf, args, buf, sizeof(buf),
							 TRUE);
	if (kv)
	{
		settings_kv_set(kv, strdupnull(value), this->contents);
	}
	this->lock->unlock(this->lock);
}

METHOD(settings_t, get_str, char*,
	private_settings_t *this, char *key, char *def, ...)
{
	char *value;
	va_list args;

	va_start(args, def);
	value = find_value(this, this->top, key, args);
	va_end(args);
	if (value)
	{
		return value;
	}
	return def;
}

/**
 * Described in header
 */
inline bool settings_value_as_bool(char *value, bool def)
{
	if (value)
	{
		if (strcaseeq(value, "1") ||
			strcaseeq(value, "yes") ||
			strcaseeq(value, "true") ||
			strcaseeq(value, "enabled"))
		{
			return TRUE;
		}
		else if (strcaseeq(value, "0") ||
				 strcaseeq(value, "no") ||
				 strcaseeq(value, "false") ||
				 strcaseeq(value, "disabled"))
		{
			return FALSE;
		}
	}
	return def;
}

METHOD(settings_t, get_bool, bool,
	private_settings_t *this, char *key, bool def, ...)
{
	char *value;
	va_list args;

	va_start(args, def);
	value = find_value(this, this->top, key, args);
	va_end(args);
	return settings_value_as_bool(value, def);
}

/**
 * Described in header
 */
inline int settings_value_as_int(char *value, int def)
{
	int intval;
	if (value)
	{
		errno = 0;
		intval = strtol(value, NULL, 10);
		if (errno == 0)
		{
			return intval;
		}
	}
	return def;
}

METHOD(settings_t, get_int, int,
	private_settings_t *this, char *key, int def, ...)
{
	char *value;
	va_list args;

	va_start(args, def);
	value = find_value(this, this->top, key, args);
	va_end(args);
	return settings_value_as_int(value, def);
}

/**
 * Described in header
 */
inline double settings_value_as_double(char *value, double def)
{
	double dval;
	if (value)
	{
		errno = 0;
		dval = strtod(value, NULL);
		if (errno == 0)
		{
			return dval;
		}
	}
	return def;
}

METHOD(settings_t, get_double, double,
	private_settings_t *this, char *key, double def, ...)
{
	char *value;
	va_list args;

	va_start(args, def);
	value = find_value(this, this->top, key, args);
	va_end(args);
	return settings_value_as_double(value, def);
}

/**
 * Described in header
 */
inline u_int32_t settings_value_as_time(char *value, u_int32_t def)
{
	char *endptr;
	u_int32_t timeval;
	if (value)
	{
		errno = 0;
		timeval = strtoul(value, &endptr, 10);
		if (errno == 0)
		{
			switch (*endptr)
			{
				case 'd':		/* time in days */
					timeval *= 24 * 3600;
					break;
				case 'h':		/* time in hours */
					timeval *= 3600;
					break;
				case 'm':		/* time in minutes */
					timeval *= 60;
					break;
				case 's':		/* time in seconds */
				default:
					break;
			}
			return timeval;
		}
	}
	return def;
}

METHOD(settings_t, get_time, u_int32_t,
	private_settings_t *this, char *key, u_int32_t def, ...)
{
	char *value;
	va_list args;

	va_start(args, def);
	value = find_value(this, this->top, key, args);
	va_end(args);
	return settings_value_as_time(value, def);
}

METHOD(settings_t, set_str, void,
	private_settings_t *this, char *key, char *value, ...)
{
	va_list args;
	va_start(args, value);
	set_value(this, this->top, key, args, value);
	va_end(args);
}

METHOD(settings_t, set_bool, void,
	private_settings_t *this, char *key, bool value, ...)
{
	va_list args;
	va_start(args, value);
	set_value(this, this->top, key, args, value ? "1" : "0");
	va_end(args);
}

METHOD(settings_t, set_int, void,
	private_settings_t *this, char *key, int value, ...)
{
	char val[16];
	va_list args;
	va_start(args, value);
	if (snprintf(val, sizeof(val), "%d", value) < sizeof(val))
	{
		set_value(this, this->top, key, args, val);
	}
	va_end(args);
}

METHOD(settings_t, set_double, void,
	private_settings_t *this, char *key, double value, ...)
{
	char val[64];
	va_list args;
	va_start(args, value);
	if (snprintf(val, sizeof(val), "%f", value) < sizeof(val))
	{
		set_value(this, this->top, key, args, val);
	}
	va_end(args);
}

METHOD(settings_t, set_time, void,
	private_settings_t *this, char *key, u_int32_t value, ...)
{
	char val[16];
	va_list args;
	va_start(args, value);
	if (snprintf(val, sizeof(val), "%u", value) < sizeof(val))
	{
		set_value(this, this->top, key, args, val);
	}
	va_end(args);
}

METHOD(settings_t, set_default_str, bool,
	private_settings_t *this, char *key, char *value, ...)
{
	char *old;
	va_list args;

	va_start(args, value);
	old = find_value(this, this->top, key, args);
	va_end(args);

	if (!old)
	{
		va_start(args, value);
		set_value(this, this->top, key, args, value);
		va_end(args);
		return TRUE;
	}
	return FALSE;
}

/**
 * Data for enumerators
 */
typedef struct {
	/** settings_t instance */
	private_settings_t *settings;
	/** sections to enumerate */
	array_t *sections;
	/** sections/keys that were already enumerated */
	hashtable_t *seen;
} enumerator_data_t;

/**
 * Destroy enumerator data
 */
static void enumerator_destroy(enumerator_data_t *this)
{
	this->settings->lock->unlock(this->settings->lock);
	this->seen->destroy(this->seen);
	array_destroy(this->sections);
	free(this);
}

/**
 * Enumerate section names, not sections
 */
static bool section_filter(hashtable_t *seen, section_t **in, char **out)
{
	*out = (*in)->name;
	if (seen->get(seen, *out))
	{
		return FALSE;
	}
	seen->put(seen, *out, *out);
	return TRUE;
}

/**
 * Enumerate sections of the given section
 */
static enumerator_t *section_enumerator(section_t *section,
										enumerator_data_t *data)
{
	return enumerator_create_filter(
			array_create_enumerator(section->sections_order),
				(void*)section_filter, data->seen, NULL);
}

METHOD(settings_t, create_section_enumerator, enumerator_t*,
	private_settings_t *this, char *key, ...)
{
	enumerator_data_t *data;
	array_t *sections;
	va_list args;

	this->lock->read_lock(this->lock);
	va_start(args, key);
	sections = find_sections(this, this->top, key, args);
	va_end(args);

	if (!sections)
	{
		this->lock->unlock(this->lock);
		return enumerator_create_empty();
	}
	INIT(data,
		.settings = this,
		.sections = sections,
		.seen = hashtable_create(hashtable_hash_str, hashtable_equals_str, 8),
	);
	return enumerator_create_nested(array_create_enumerator(sections),
					(void*)section_enumerator, data, (void*)enumerator_destroy);
}

/**
 * Enumerate key and values, not kv_t entries
 */
static bool kv_filter(hashtable_t *seen, kv_t **in, char **key,
					  void *none, char **value)
{
	*key = (*in)->key;
	if (seen->get(seen, *key) || !(*in)->value)
	{
		return FALSE;
	}
	*value = (*in)->value;
	seen->put(seen, *key, *key);
	return TRUE;
}

/**
 * Enumerate key/value pairs of the given section
 */
static enumerator_t *kv_enumerator(section_t *section, enumerator_data_t *data)
{
	return enumerator_create_filter(array_create_enumerator(section->kv_order),
					(void*)kv_filter, data->seen, NULL);
}

METHOD(settings_t, create_key_value_enumerator, enumerator_t*,
	private_settings_t *this, char *key, ...)
{
	enumerator_data_t *data;
	array_t *sections;
	va_list args;

	this->lock->read_lock(this->lock);
	va_start(args, key);
	sections = find_sections(this, this->top, key, args);
	va_end(args);

	if (!sections)
	{
		this->lock->unlock(this->lock);
		return enumerator_create_empty();
	}
	INIT(data,
		.settings = this,
		.sections = sections,
		.seen = hashtable_create(hashtable_hash_str, hashtable_equals_str, 8),
	);
	return enumerator_create_nested(array_create_enumerator(sections),
					(void*)kv_enumerator, data, (void*)enumerator_destroy);
}

METHOD(settings_t, add_fallback, void,
	private_settings_t *this, const char *key, const char *fallback, ...)
{
	section_t *section;
	va_list args;

	/* find/create the fallback */
	va_start(args, fallback);
	section = ensure_section(this, this->top, fallback, args);
	va_end(args);

	va_start(args, fallback);
	add_fallback_to_section(this, this->top, key, args, section);
	va_end(args);
}

/**
 * Load settings from files matching the given file pattern.
 * All sections and values are added relative to "parent".
 * All files (even included ones) have to be loaded successfully.
 * If merge is FALSE the contents of parent are replaced with the parsed
 * contents, otherwise they are merged together.
 */
static bool load_files_internal(private_settings_t *this, section_t *parent,
								char *pattern, bool merge)
{
	section_t *section;

	if (pattern == NULL || !pattern[0])
	{	/* TODO: Clear parent if merge is FALSE? */
		return FALSE;
	}

	section = settings_section_create(NULL);
	if (!settings_parser_parse_file(section, pattern))
	{
		settings_section_destroy(section, NULL);
		return FALSE;
	}

	this->lock->write_lock(this->lock);
	settings_section_extend(parent, section, this->contents, !merge);
	this->lock->unlock(this->lock);

	settings_section_destroy(section, NULL);
	return TRUE;
}

METHOD(settings_t, load_files, bool,
	private_settings_t *this, char *pattern, bool merge)
{
	return load_files_internal(this, this->top, pattern, merge);
}

METHOD(settings_t, load_files_section, bool,
	private_settings_t *this, char *pattern, bool merge, char *key, ...)
{
	section_t *section;
	va_list args;

	va_start(args, key);
	section = ensure_section(this, this->top, key, args);
	va_end(args);

	if (!section)
	{
		return FALSE;
	}
	return load_files_internal(this, section, pattern, merge);
}

METHOD(settings_t, destroy, void,
	private_settings_t *this)
{
	settings_section_destroy(this->top, NULL);
	array_destroy_function(this->contents, (void*)free, NULL);
	this->lock->destroy(this->lock);
	free(this);
}

/*
 * see header file
 */
settings_t *settings_create(char *file)
{
	private_settings_t *this;

	INIT(this,
		.public = {
			.get_str = _get_str,
			.get_int = _get_int,
			.get_double = _get_double,
			.get_time = _get_time,
			.get_bool = _get_bool,
			.set_str = _set_str,
			.set_int = _set_int,
			.set_double = _set_double,
			.set_time = _set_time,
			.set_bool = _set_bool,
			.set_default_str = _set_default_str,
			.create_section_enumerator = _create_section_enumerator,
			.create_key_value_enumerator = _create_key_value_enumerator,
			.add_fallback = _add_fallback,
			.load_files = _load_files,
			.load_files_section = _load_files_section,
			.destroy = _destroy,
		},
		.top = settings_section_create(NULL),
		.contents = array_create(0, 0),
		.lock = rwlock_create(RWLOCK_TYPE_DEFAULT),
	);

	load_files(this, file, FALSE);

	return &this->public;
}
