/*
 * options.c
 * 
 * Copyright (C) 2006-2007  Josef Kufner <jk@myserver.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.
 *
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <errno.h>
#include "options.h"
#include "options-defaults.h"
#include "message.h"
#include "dirs.h"

#define DEBUG 0
#include "debug.h"

#define FILE_PERROR(action, filename)	fprintf(stderr, "ERROR: %s `%s': %s\n", action, filename, strerror(errno))


typedef struct {
	char cat[OPTION_MAX_CAT_LEN],	/* category */
	     mod[OPTION_MAX_MOD_LEN],	/* module */
	     opt[OPTION_MAX_OPT_LEN];	/* option */
	
	OPTION_TYPE type;
	struct {
		char *string;
		long integer;
	} value;

	OPTION_ITEM_UPDATE_CALLBACK update_cb;
	void *update_cb_data;
	int update_cb_done;
} OPTION_ITEM;

int option_last_error = 0;

static OPTION_ITEM *option = NULL;
static int option_size = 1; /* half of initial size */
static int option_is_sorted = 0;
static int option_freezed_callbacks = 0;
static OPTION_UPDATE_CALLBACK option_update_callback = NULL;

static const char *option_type_name[] = {
	"none",
	"integer",
	"string",
	NULL
};

static void option_enlarge(void)
{
	int old_size = option_size;
	option_size <<= 1;
	DBG_MSG(2, "resizing array from %d to %d items", old_size, option_size);
	option = realloc(option, sizeof(OPTION_ITEM) * option_size);
	memset(option + old_size, 0, sizeof(OPTION_ITEM) * old_size);
	if (option == NULL) {
		perror("Option array realloc failed");
		abort();
	}
}


static int option_item_compare(const void *va, const void *vb)
{
	OPTION_ITEM *a = (OPTION_ITEM *)va;
	OPTION_ITEM *b = (OPTION_ITEM *)vb;
	int r;

	r = strcmp(a->cat, b->cat);
	if (r == 0) {
		r = strcmp(a->mod, b->mod);
		return r != 0 ? r : strcmp(a->opt, b->opt);
	} else {
		return r;
	}
}


static void option_sort(void)
{
	if (!option_is_sorted)
		qsort(option, option_size, sizeof(OPTION_ITEM), option_item_compare);
	option_is_sorted = 1;
}


static OPTION_ITEM *option_item_lookup(const char *cat, const char *mod, const char *opt)
{
	OPTION_ITEM key;
	strncpy(key.cat, cat, OPTION_MAX_CAT_LEN - 1);
	strncpy(key.mod, mod, OPTION_MAX_MOD_LEN - 1);
	strncpy(key.opt, opt, OPTION_MAX_OPT_LEN - 1);
	key.cat[OPTION_MAX_CAT_LEN - 1] = 0;
	key.cat[OPTION_MAX_MOD_LEN - 1] = 0;
	key.opt[OPTION_MAX_OPT_LEN - 1] = 0;
	
	DBG_MSG(4, "[ %s :: %s :: %s ]", key.cat, key.mod, key.opt);

	option_sort();
	return (OPTION_ITEM *) bsearch(&key, option, option_size, sizeof(OPTION_ITEM), option_item_compare);
}


static OPTION_ITEM *option_item_add(const char *cat, const char *mod, const char *opt)
{
	OPTION_ITEM *a = NULL;
	
	DBG_MSG(4, "[ %s :: %s :: %s ]", cat, mod, opt);

	if ((a = option_item_lookup(cat, mod, opt)))
		return a;
	
	if (option[0].cat[0] == 0 && option[0].mod[0] == 0 && option[0].opt[0] == 0) {
		DBG_MSG(4, "have empty slot");
		a = &(option[0]);
	} else {
		DBG_MSG(4, "no empty slot");
		option_enlarge();
		a = &(option[option_size - 1]);
	}
	
	strncpy(a->cat, cat, OPTION_MAX_CAT_LEN - 1);
	strncpy(a->mod, mod, OPTION_MAX_MOD_LEN - 1);
	strncpy(a->opt, opt, OPTION_MAX_OPT_LEN - 1);
	a->cat[OPTION_MAX_CAT_LEN - 1] = 0;
	a->mod[OPTION_MAX_MOD_LEN - 1] = 0;
	a->opt[OPTION_MAX_OPT_LEN - 1] = 0;
	a->type = OPTION_NONE;
	a->value.string = NULL;
	a->value.integer = 0;
	a->update_cb = NULL;
	a->update_cb_data = NULL;
	a->update_cb_done = 0;
	option_is_sorted = 0;
	return a;
}

static void option_item_reset(OPTION_ITEM *item) {
	assert(item != NULL);
	if (item->value.string) {
		free(item->value.string);
		item->value.string = NULL;
	}
	item->value.integer = 0;
	item->type = OPTION_NONE;
}


static void option_run_update_callback(OPTION_ITEM *item, int run_global)
{
	if (item)
		item->update_cb_done = 0;

	if (!option_freezed_callbacks) {
		/* execute item's update callback */
		if (item && item->update_cb) {
			(*(item->update_cb))(item - option, item->update_cb_data);
			item->update_cb_done = 1;
		}
		/* execute global update callback */
		if (option_update_callback && run_global) {
			option_sort();
			(*option_update_callback)();
		}
	}
}


int option_init(void) {
	DBG_MSG(1, "");
	option_enlarge();
	memset(option, 0, sizeof(OPTION_ITEM) * option_size / 2);
	return 1;
}


void option_freeze_callbacks(void)
{
	option_freezed_callbacks = 1;
}


void option_thaw_callbacks(void)
{
	int i;
	option_freezed_callbacks = 0;
	
	option_sort();
	for (i = 0; i <= option_size; i++) {
		if (!option[i].update_cb_done) {
			option_run_update_callback(&(option[i]), 0);
		}
	}
}


int option_load_defaults(void) {
	const struct s_options_defualts *p = NULL;

	DBG_MSG(1, "");
	
	for (p = options_defaults; p->cat && p->mod && p->opt; p++) {
		if (option_get_type(p->cat, p->mod, p->opt) == OPTION_NONE) {
			if (p->string)
				option_set_string(p->cat, p->mod, p->opt, p->string);
			else
				option_set_integer(p->cat, p->mod, p->opt, p->integer);
		}
	}
	DBG_MSG(1, "Loading defaults... done.");
	
	return 1;
}


void option_set_update_callback(OPTION_UPDATE_CALLBACK callback)
{
	option_update_callback = callback;
}


int option_set_item_update_callback(const char *cat, const char *mod, const char *opt, OPTION_ITEM_UPDATE_CALLBACK cb, void *data)
{
	OPTION_ITEM *item;

	assert(cat != NULL);
	assert(mod != NULL);
	assert(opt != NULL);

	DBG_MSG(3, "[ %s :: %s :: %s ] ... %p, %p", cat, mod, opt, cb, data);
	item = option_item_lookup(cat, mod, opt);
	if (!item) {
		item = option_item_add(cat, mod, opt);
	}
	if (item) {
		item->update_cb = cb;
		item->update_cb_data = data;
		return 1;
	}
	return 0;
}


int option_set_integer(const char *cat, const char *mod, const char *opt, long value)
{
	OPTION_ITEM *item = NULL;

	assert(cat != NULL);
	assert(mod != NULL);
	assert(opt != NULL);

	DBG_MSG(3, "[ %s :: %s :: %s ] = %ld", cat, mod, opt, value);
	item = option_item_add(cat, mod, opt);
	if (item) {
		option_item_reset(item);
		item->type = OPTION_INTEGER;
		item->value.integer = value;
		option_run_update_callback(item, 1);
		return 1;
	}
	return 0;
}


int option_set_string(const char *cat, const char *mod, const char *opt, const char *value)
{
	OPTION_ITEM *item = NULL;

	assert(cat != NULL);
	assert(mod != NULL);
	assert(opt != NULL);

	DBG_MSG(3, "[ %s :: %s :: %s ] = \"%s\"", cat, mod, opt, value);
	item = option_item_add(cat, mod, opt);
	if (item) {
		option_item_reset(item);
		item->type = OPTION_STRING;
		if (value)
			item->value.string = strdup(value);
		else
			item->value.string = strdup("");
		option_run_update_callback(item, 1);
		return 1;
	}
	return 0;
}


void option_unset_by_id(int id)
{
	DBG_MSG(4, "id = %d", id);
	if (option[id].cat[0]) {
		option_item_reset(&(option[id]));
		if (option[id].update_cb == NULL) {
			option[id].cat[0] = 0;
			option[id].mod[0] = 0;
			option[id].opt[0] = 0;
			option_is_sorted = 0;
		}
		option_run_update_callback(NULL, 1);
	}
}


int option_unset(const char *cat, const char *mod, const char *opt)
{
	OPTION_ITEM *item = NULL;

	assert(cat != NULL);
	assert(mod != NULL);
	assert(opt != NULL);

	DBG_MSG(4, "[ %s :: %s :: %s ]", cat, mod, opt);
	item = option_item_lookup(cat, mod, opt);
	if (item) {
		option_unset_by_id(item - option);
		return 1;
	}
	return 0;
}


long option_get_integer_by_id(int id)
{
	long v;

	if (option[id].type == OPTION_INTEGER) {
		DBG_MSG(4, "id = %d: %ld", id, option[id].value.integer);
		return option[id].value.integer;
	} else if (option[id].type == OPTION_STRING) {
		v = strtol(option[id].value.string, NULL, 0);
		DBG_MSG(4, "id = %d: \"%s\" => %ld", id, option[id].value.string, v);
		return v;
	}
	return 0;
}


long option_get_integer(const char *cat, const char *mod, const char *opt)
{
	OPTION_ITEM *item;

	assert(cat != NULL);
	assert(mod != NULL);
	assert(opt != NULL);

	DBG_MSG(3, "[ %s :: %s :: %s ]", cat, mod, opt);
	item = option_item_lookup(cat, mod, opt);
	if(item) {
		return option_get_integer_by_id(item - option);
	}
	return 0;
}


const char *option_get_string_by_id(int id)
{
	if (option[id].type == OPTION_INTEGER) {
		if (!option[id].value.string) {
			option[id].value.string = (char *) malloc(OPTION_INT_TO_STRING_LEN);
			option[id].value.string[OPTION_INT_TO_STRING_LEN - 1] = 0;
			snprintf(option[id].value.string, OPTION_INT_TO_STRING_LEN - 1, "%ld", option[id].value.integer);
		}
		DBG_MSG(4, "id = %d: %ld => \"%s\"", id, option[id].value.integer, option[id].value.string);
		return option[id].value.string;
	} else if (option[id].type == OPTION_STRING) {
		DBG_MSG(4, "id = %d: \"%s\"", id, option[id].value.string);
		return option[id].value.string;
	}
	return NULL;
}


const char *option_get_string(const char *cat, const char *mod, const char *opt)
{
	OPTION_ITEM *item;

	assert(cat != NULL);
	assert(mod != NULL);
	assert(opt != NULL);

	DBG_MSG(3, "[ %s :: %s :: %s ]", cat, mod, opt);
	item = option_item_lookup(cat, mod, opt);
	if(item) {
		return option_get_string_by_id(item - option);
	}
	return NULL;
}


OPTION_TYPE option_get_type(const char *cat, const char *mod, const char *opt)
{
	OPTION_ITEM *item;

	assert(cat != NULL);
	assert(mod != NULL);
	assert(opt != NULL);

	DBG_MSG(4, "[ %s :: %s :: %s ]", cat, mod, opt);
	item = option_item_lookup(cat, mod, opt);
	if(item) {
		DBG_MSG(4, "[ %s :: %s :: %s ] is type %d", cat, mod, opt, item->type);
		return item->type;
	}
	return OPTION_NONE;
}


const char *option_get_type_name_by_id(int id)
{
	return option_type_name[option[id].type];
}


const char *option_get_type_name(const char *cat, const char *mod, const char *opt)
{

	assert(cat != NULL);
	assert(mod != NULL);
	assert(opt != NULL);

	return option_type_name[option_get_type(cat, mod, opt)];
}


int option_store_to_default(void)
{
	dir_update();
	return option_store_file(NULL, dir_main_config);
}
	
int option_store_file(const char *cat, const char *filename)
{
	FILE *f;

	DBG_MSG(1, "writing cat \"%s\" to file %s", cat, filename);
	assert(filename != NULL);
	
	f = fopen(filename, "wt");
	if (f) {
		option_store(cat, f, filename);
		if (fclose(f) != 0) {
			option_last_error = errno;
			FILE_PERROR("Can't close", filename);
		}
		option_last_error = 0;
		return 1;
	} else {
		option_last_error = errno;
		FILE_PERROR("Can't open", filename);
		return 0;
	}
}


int option_store(const char *cat, FILE *file, const char *filename_to_show)
{
	int i;
	char *str;
	MESSAGE *m = NULL;

	DBG_MSG(4, "cat = \"%s\", filename = \"%s\"", cat, filename_to_show);
	
	/* unset flag, that options were loaded */
	option_unset("main", "runtime", "option_load_complete");
	
	option_sort();

	for (i = 0; i <= option_size; i++) {
		if (i < option_size) {
			/* ordinary items */
			if (cat && strcmp(cat, option[i].cat) != 0)
				continue;

			if (!m)
				m = message_create('O', 0);

			if (option[i].type == OPTION_STRING)
				message_append(m, MESSAGE_PART_DATA, MESSAGE_STRING, "Os",
						MESSAGE_STRING, option[i].cat,
						MESSAGE_STRING, option[i].mod,
						MESSAGE_STRING, option[i].opt,
						MESSAGE_STRING, option[i].value.string,
						MESSAGE_END);
			else if (option[i].type == OPTION_INTEGER)
				message_append(m, MESSAGE_PART_DATA, MESSAGE_STRING, "Os",
						MESSAGE_STRING, option[i].cat,
						MESSAGE_STRING, option[i].mod,
						MESSAGE_STRING, option[i].opt,
						MESSAGE_INTEGER, option[i].value.integer,
						MESSAGE_END);
			else
				continue;
		} else {
			/* after last */
			m = message_create('O', 0);
			message_append(m, MESSAGE_PART_DATA, MESSAGE_STRING, "Os",
					MESSAGE_STRING, "main",
					MESSAGE_STRING, "runtime",
					MESSAGE_STRING, "option_load_complete",
					MESSAGE_INTEGER, 1,
					MESSAGE_END);
		}

		str = message_to_string(m, 1);
		if (fputs(str, file) == EOF) {
			option_last_error = errno;
			FILE_PERROR("Can't write to `%s': %s\n", filename_to_show);
			free(str);
			break;
		}
		free(str);
		message_destroy(m);
		m = NULL;
	}
	if (m)
		message_destroy(m);
	option_last_error = 0;
	return 1;
}


int option_load_file(const char *filename)
{
	FILE *f;

	DBG_MSG(1, "loading file %s", filename);
	assert(filename != NULL);

	f = fopen(filename, "rt");
	if (f) {
		option_load(f, filename);
		if (fclose(f) != 0) {
			option_last_error = errno;
			FILE_PERROR("Can't close", filename);
		}
		option_last_error = 0;
		return 1;
	} else {
		option_last_error = errno;
		FILE_PERROR("Can't open", filename);
		return 0;
	}
}


int option_load(FILE *file, const char *filename_to_show)
{
	int ch, parsed;
	MESSAGE *msg = NULL;
	char *cmd = NULL;
	int err_shown = 0, msg_count = 0;
	
	DBG_MSG(4, "filename = \"%s\"", filename_to_show);
	assert(file != NULL);

	/* set flag, that options were not loaded, last command in the file sets this to 1 */
	option_set_integer("main", "runtime", "option_load_complete", 0);
	
	errno = 0;
	while ((ch = getc(file)) != EOF) {
		parsed = message_from_stream('O', '?', ch, &msg);
		if (parsed == 1 && msg) {
			msg_count++;
			err_shown = 0;
			if (message_get(msg, MESSAGE_PART_DATA, MESSAGE_STRING, &cmd, MESSAGE_END) && cmd)
				message_edit(msg, 0, cmd[0]);
			message_send(msg);
			message_destroy(msg);
			msg = NULL;
		} else if (parsed == -1 && !err_shown) {
			fprintf(stderr, "%s:%d: Syntax error after %d messages.\n", filename_to_show, msg_count, msg_count);
			err_shown = 1;
		}
	}
	if (errno) {
		option_last_error = errno;
		FILE_PERROR("Can't read from", filename_to_show);
	}
	return 0;
}


int option_list_opt(const char *cat_query, int *iter, char **cat, char **mod, char **opt, int *have_cb)
{
	assert(iter != NULL);
	assert(cat != NULL);
	assert(opt != NULL);
	assert(mod != NULL);
	
	DBG_MSG(6, "cat_query = %s, iter = %d", cat_query, *iter);
	while (*iter < option_size && (!option[*iter].opt[0] || (cat_query && strncmp(cat_query, option[*iter].cat, OPTION_MAX_CAT_LEN) != 0))) {
		(*iter)++;
	}
	if (*iter < option_size) {
		DBG_MSG(7, "next item found, iter = %d", *iter);
		*cat = option[*iter].cat;
		*mod = option[*iter].mod;
		*opt = option[*iter].opt;
		if (have_cb)
			*have_cb = option[*iter].update_cb != NULL;
	} else {
		DBG_MSG(7, "no more items");
		*cat = NULL;
		*mod = NULL;
		*opt = NULL;
		if (have_cb)
			*have_cb = 0;
	}
	DBG_MSG(7, "data returned");
	return *iter < option_size;
}

