
/*
 * dc_decl.h - Dotconf functions and types for Speech Dispatcher
 *
 * Copyright (C) 2001, 2002, 2003 Brailcom, o.p.s.
 *
 * This 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, or (at your option)
 * any later version.
 *
 * This software 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 <https://www.gnu.org/licenses/>.
 *
 * $Id: config.c,v 1.18 2009-05-14 08:11:33 hanke Exp $
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <glib.h>

#include <dotconf.h>

#include "speechd.h"
#include "configuration.h"
#include "symbols.h"
#include <fdsetconv.h>

configoption_t *spd_options;
int spd_num_options;

static TFDSetClientSpecific *cl_spec_section;

/* So that gcc doesn't comply about casts to char* */

/* == CONFIGURATION MANAGEMENT FUNCTIONS */

/* Add dotconf configuration option */
configoption_t *add_config_option(configoption_t * options,
				  int *num_config_options, const char *name, int type,
				  dotconf_callback_t callback, info_t * info,
				  unsigned long context)
{
	configoption_t *opts;

	(*num_config_options)++;
	opts =
	    (configoption_t *) g_realloc(options,
					 (*num_config_options) *
					 sizeof(configoption_t));
	opts[*num_config_options - 1].name = g_strdup(name);
	opts[*num_config_options - 1].type = type;
	opts[*num_config_options - 1].callback = callback;
	opts[*num_config_options - 1].info = info;
	opts[*num_config_options - 1].context = context;
	return opts;
}

/* Free all configuration options. */
void free_config_options(configoption_t * opts, int *num)
{
	int i = 0;

	if (opts == NULL)
		return;

	for (i = 0; i <= (*num) - 1; i++) {
		g_free((char *)opts[i].name);
	}
	g_free(opts);
	*num = 0;
	opts = NULL;
}

/* == CALLBACK DEFINITION MACROS == */

#define GLOBAL_FDSET_OPTION_CB_STR(name, arg) \
	DOTCONF_CB(cb_ ## name) \
	{ \
		assert(cmd->data.str != NULL); \
		if (!cl_spec_section) \
			GlobalFDSet.arg = g_strdup(cmd->data.str); \
		else \
			cl_spec_section->val.arg = g_strdup(cmd->data.str); \
		return NULL; \
	}

#define GLOBAL_FDSET_OPTION_CB_INT(name, arg, cond, str) \
	DOTCONF_CB(cb_ ## name) \
	{ \
		int val = cmd->data.value; \
		if (!(cond)) FATAL(str); \
		if (!cl_spec_section) \
			GlobalFDSet.arg = val; \
		else \
			cl_spec_section->val.arg = val; \
		return NULL; \
	}

#define GLOBAL_FDSET_OPTION_CB_SPECIAL(name, arg, type, fconv) \
	DOTCONF_CB(cb_ ## name) \
	{ \
		char *val_str; \
		type val; \
		val_str = g_ascii_strdown(cmd->data.str, strlen(cmd->data.str)); \
		if (val_str == NULL) FATAL("Invalid parameter in configuration"); \
		val = fconv(val_str); \
		g_free(val_str); \
		if (val == -1) FATAL("Invalid parameter in configuration."); \
		if (!cl_spec_section) \
			GlobalFDSet.arg = val; \
		else \
			cl_spec_section->val.arg = val; \
		return NULL; \
	}

#define SPEECHD_OPTION_CB_STR_M(name, arg) \
	DOTCONF_CB(cb_ ## name) \
	{ \
		if (cl_spec_section) \
			FATAL("This command isn't allowed in a client specific section!"); \
		if (!SpeechdOptions.arg ## _set) SpeechdOptions.arg = g_strdup(cmd->data.str); \
		return NULL; \
	}

#define SPEECHD_OPTION_CB_INT_M(name, arg, cond, str) \
	DOTCONF_CB(cb_ ## name) \
	{ \
		int val = cmd->data.value; \
		if (cl_spec_section) \
			FATAL("This command isn't allowed in a client specific section!"); \
		if (!(cond)) FATAL(str); \
		if (!SpeechdOptions.arg ## _set) SpeechdOptions.arg = val; \
		return NULL; \
	}

#define SPEECHD_OPTION_CB_STR(name, arg) \
	DOTCONF_CB(cb_ ## name) \
	{ \
		if (cl_spec_section) \
			FATAL("This command isn't allowed in a client specific section!"); \
		SpeechdOptions.arg = g_strdup(cmd->data.str); \
		return NULL; \
	}

#define SPEECHD_OPTION_CB_INT(name, arg, cond, str) \
	DOTCONF_CB(cb_ ## name) \
	{ \
		int val = cmd->data.value; \
		if (cl_spec_section) \
			FATAL("This command isn't allowed in a client specific section!"); \
		if (!(cond)) FATAL(str); \
		SpeechdOptions.arg = val; \
		return NULL; \
	}

#define GLOBAL_SET_LOGLEVEL(name, arg, cond, str) \
	DOTCONF_CB(cb_ ## name) \
	{ \
		int val = cmd->data.value; \
		if (cl_spec_section) \
			FATAL("This command isn't allowed in a client specific section!"); \
		if (!(cond)) FATAL(str); \
		if (!SpeechdOptions.arg ## _set){ \
			SpeechdOptions.arg = val; \
			GlobalFDSet.arg = val; \
		} \
		return NULL; \
	}

/* == CALLBACK DEFINITIONS == */
GLOBAL_FDSET_OPTION_CB_STR(DefaultModule, output_module)
    GLOBAL_FDSET_OPTION_CB_STR(DefaultLanguage, msg_settings.voice.language)
    GLOBAL_FDSET_OPTION_CB_STR(DefaultClientName, client_name)

    GLOBAL_FDSET_OPTION_CB_STR(AudioOutputMethod, audio_output_method)
    GLOBAL_FDSET_OPTION_CB_STR(AudioOSSDevice, audio_oss_device)
    GLOBAL_FDSET_OPTION_CB_STR(AudioALSADevice, audio_alsa_device)
    GLOBAL_FDSET_OPTION_CB_STR(AudioNASServer, audio_nas_server)
    GLOBAL_FDSET_OPTION_CB_STR(AudioPulseServer, audio_pulse_server)
    GLOBAL_FDSET_OPTION_CB_STR(AudioPulseDevice, audio_pulse_device)
    GLOBAL_FDSET_OPTION_CB_INT(AudioPulseMinLength, audio_pulse_min_length, 1, "")

    GLOBAL_FDSET_OPTION_CB_INT(DefaultRate, msg_settings.rate, (val >= -100)
			       && (val <= +100), "Rate out of range.")
    GLOBAL_FDSET_OPTION_CB_INT(DefaultPitch, msg_settings.pitch, (val >= -100)
			       && (val <= +100), "Pitch out of range.")
    GLOBAL_FDSET_OPTION_CB_INT(DefaultPitchRange, msg_settings.pitch_range,
			   (val >= -100)
			       && (val <= +100), "Pitch range out of range.")
    GLOBAL_FDSET_OPTION_CB_INT(DefaultVolume, msg_settings.volume, (val >= -100)
			       && (val <= +100), "Volume out of range.")
    GLOBAL_FDSET_OPTION_CB_INT(DefaultSpelling, msg_settings.spelling_mode, 1,
			   "Invalid spelling mode")
    GLOBAL_FDSET_OPTION_CB_INT(DefaultPauseContext, pause_context, 1, "")

    GLOBAL_FDSET_OPTION_CB_SPECIAL(DefaultPriority, priority, SPDPriority,
			       str2intpriority)
    GLOBAL_FDSET_OPTION_CB_SPECIAL(DefaultVoiceType, msg_settings.voice_type,
			       SPDVoiceType, str2EVoice)
    GLOBAL_FDSET_OPTION_CB_SPECIAL(DefaultPunctuationMode,
			       msg_settings.punctuation_mode, SPDPunctuation,
			       str2EPunctMode)
    GLOBAL_FDSET_OPTION_CB_SPECIAL(SymbolsPreproc, symbols_preprocessing,
			       SymLvl,str2SymLvl)
    GLOBAL_FDSET_OPTION_CB_SPECIAL(DefaultCapLetRecognition,
			       msg_settings.cap_let_recogn, SPDCapitalLetters,
			       str2ECapLetRecogn)

    SPEECHD_OPTION_CB_STR_M(CommunicationMethod, communication_method)
    SPEECHD_OPTION_CB_STR_M(SocketPath, socket_path)
    SPEECHD_OPTION_CB_INT_M(Port, port, val >= 0, "Invalid port number!")
    SPEECHD_OPTION_CB_INT_M(LocalhostAccessOnly, localhost_access_only, val >= 0,
			"Invalid access control mode!")
    GLOBAL_SET_LOGLEVEL(LogLevel, log_level, (val >= 0)
			&& (val <= 5), "Invalid log (verbosity) level!")
    SPEECHD_OPTION_CB_INT(MaxHistoryMessages, max_history_messages, val >= 0,
		      "Invalid parameter!")
    SPEECHD_OPTION_CB_INT_M(Timeout, server_timeout, val >= 0, "Invalid timeout value!")

    DOTCONF_CB(cb_LanguageDefaultModule)
{
	char *key;
	char *value;

	if (cmd->data.list[0] == NULL)
		FATAL("No language specified for LanguageDefaultModule");
	if (cmd->data.list[0] == NULL)
		FATAL("No module specified for LanguageDefaultModule");

	key = g_strdup(cmd->data.list[0]);
	value = g_strdup(cmd->data.list[1]);

	g_hash_table_insert(language_default_modules, key, value);

	return NULL;
}

DOTCONF_CB(cb_LogFile)
{
	/* This option is DEPRECATED. If it is specified, get the directory. */
	assert(cmd->data.str != NULL);
	SpeechdOptions.log_dir = g_path_get_dirname(cmd->data.str);
	logging_init();

	MSG(1,
	    "WARNING: The LogFile option is deprecated. Directory accepted but filename ignored");
	return NULL;
}

DOTCONF_CB(cb_LogDir)
{
	assert(cmd->data.str != NULL);

	if (strcmp(cmd->data.str, "default")
	    && !SpeechdOptions.log_dir_set) {
		// cmd->data.str different from "default"
		SpeechdOptions.log_dir = g_strdup(cmd->data.str);
	}
	logging_init();
	return NULL;
}

DOTCONF_CB(cb_CustomLogFile)
{
	char *kind;
	char *file;

	if (cmd->data.list[0] == NULL)
		FATAL("No log kind specified in CustomLogFile");
	if (cmd->data.list[1] == NULL)
		FATAL("No log file specified in CustomLogFile");
	kind = g_strdup(cmd->data.list[0]);
	assert(kind != NULL);
	file = g_strdup(cmd->data.list[1]);
	assert(file != NULL);

	custom_log_kind = kind;
	if (!strncmp(file, "stdout", 6)) {
		custom_logfile = stdout;
		return NULL;
	}
	if (!strncmp(file, "stderr", 6)) {
		custom_logfile = stderr;
		return NULL;
	}
	custom_logfile = fopen(file, "a");
	if (custom_logfile == NULL) {
		fprintf(stderr,
			"Error: can't open custom log file, using stdout\n");
		custom_logfile = stdout;
	}

	MSG(2, "Speech Dispatcher custom logging to file %s", file);
	return NULL;
}

DOTCONF_CB(cb_SymbolsPreprocFile)
{
	if (cmd->data.list[0] == NULL) {
		MSG(3,
		    "No symbol preprocessing name specified in configuration under SymbolsPreprocFile");
		return NULL;
	}

	symbols_preprocessing_add_file(cmd->data.list[0]);

	return NULL;
}

DOTCONF_CB(cb_AddModule)
{
	if (cmd->data.list[0] == NULL) {
		MSG(3,
		    "No output module name specified in configuration under AddModule");
		return NULL;
	}

	module_add_load_request(g_strdup(cmd->data.list[0]),
				g_strdup(cmd->data.list[1]),
				g_strdup(cmd->data.list[2]),
				g_strdup_printf("%s/%s.log",
						SpeechdOptions.log_dir,
						cmd->data.list[0]));

	return NULL;
}

/* == CLIENT SPECIFIC CONFIGURATION == */

#define SET_PAR(name, value) cl_spec->val.name = value;
#define SET_PAR_STR(name) cl_spec->val.name = NULL;
DOTCONF_CB(cb_BeginClient)
{
	TFDSetClientSpecific *cl_spec;

	if (cl_spec_section != NULL)
		FATAL
		    ("Configuration: Already in client specific section, can't open a new one!");

	if (cmd->data.str == NULL)
		FATAL
		    ("Configuration: You must specify some client's name for BeginClient");

	cl_spec =
	    (TFDSetClientSpecific *) g_malloc(sizeof(TFDSetClientSpecific));
	cl_spec->pattern = g_strdup(cmd->data.str);
	cl_spec_section = cl_spec;

	MSG(4, "Reading configuration for pattern %s", cl_spec->pattern);

	/*  Warning: If you modify this, you must also modify update_cl_settings() in set.c ! */
	SET_PAR(msg_settings.rate, -101)
	    SET_PAR(msg_settings.pitch, -101)
	    SET_PAR(msg_settings.pitch_range, -101)
	    SET_PAR(msg_settings.volume, -101)
	    SET_PAR(msg_settings.punctuation_mode, -1)
	    SET_PAR(msg_settings.spelling_mode, -1)
	    SET_PAR(msg_settings.voice_type, -1)
	    SET_PAR(msg_settings.cap_let_recogn, -1)
	    SET_PAR(pause_context, -1);
	SET_PAR(ssml_mode, -1);
	SET_PAR(symbols_preprocessing, -1);
	SET_PAR_STR(msg_settings.voice.language)
	    SET_PAR_STR(output_module)

	    return NULL;
}

#undef SET_PAR
#undef SET_PAR_STR

DOTCONF_CB(cb_EndClient)
{
	if (cl_spec_section == NULL)
		FATAL
		    ("Configuration: Already outside the client specific section!");

	client_specific_settings =
	    g_list_append(client_specific_settings, cl_spec_section);

	cl_spec_section = NULL;

	return NULL;
}

/* == CALLBACK FOR UNKNOWN OPTIONS == */

DOTCONF_CB(cb_unknown)
{
	MSG(2, "Unknown option in configuration!");
	return NULL;
}

/* == Auto-spawn no-action callback == */
DOTCONF_CB(cb_DisableAutoSpawn)
{
	/* DisableAutoSpawn option is handled earlier during startup, not via the DotConf
	   mechanism. This callback here is a hack to ensure DotConf doesn't complain about
	   an unknown option */
	return NULL;
}

/* == LOAD CALLBACKS == */

#define ADD_CONFIG_OPTION(name, arg_type) \
	options = add_config_option(options, num_options, #name, arg_type, cb_ ## name, 0, 0);

#define ADD_LAST_OPTION() \
	options = add_config_option(options, num_options, "", 0, NULL, NULL, 0);

configoption_t *load_config_options(int *num_options)
{
	configoption_t *options = NULL;

	cl_spec_section = NULL;

	ADD_CONFIG_OPTION(CommunicationMethod, ARG_STR);
	ADD_CONFIG_OPTION(SocketPath, ARG_STR);
	ADD_CONFIG_OPTION(Port, ARG_INT);
	ADD_CONFIG_OPTION(DisableAutoSpawn, ARG_NONE);
	ADD_CONFIG_OPTION(LocalhostAccessOnly, ARG_INT);
	ADD_CONFIG_OPTION(LogFile, ARG_STR);
	ADD_CONFIG_OPTION(LogDir, ARG_STR);
	ADD_CONFIG_OPTION(CustomLogFile, ARG_LIST);
	ADD_CONFIG_OPTION(LogLevel, ARG_INT);
	ADD_CONFIG_OPTION(DefaultModule, ARG_STR);
	ADD_CONFIG_OPTION(LanguageDefaultModule, ARG_LIST);
	ADD_CONFIG_OPTION(DefaultRate, ARG_INT);
	ADD_CONFIG_OPTION(DefaultPitch, ARG_INT);
	ADD_CONFIG_OPTION(DefaultPitchRange, ARG_INT);
	ADD_CONFIG_OPTION(DefaultVolume, ARG_INT);
	ADD_CONFIG_OPTION(DefaultLanguage, ARG_STR);
	ADD_CONFIG_OPTION(DefaultPriority, ARG_STR);
	ADD_CONFIG_OPTION(MaxHistoryMessages, ARG_INT);
	ADD_CONFIG_OPTION(DefaultPunctuationMode, ARG_STR);
	ADD_CONFIG_OPTION(SymbolsPreproc, ARG_STR);
	ADD_CONFIG_OPTION(SymbolsPreprocFile, ARG_STR);
	ADD_CONFIG_OPTION(DefaultClientName, ARG_STR);
	ADD_CONFIG_OPTION(DefaultVoiceType, ARG_STR);
	ADD_CONFIG_OPTION(DefaultSpelling, ARG_TOGGLE);
	ADD_CONFIG_OPTION(DefaultCapLetRecognition, ARG_STR);
	ADD_CONFIG_OPTION(DefaultPauseContext, ARG_INT);
	ADD_CONFIG_OPTION(Timeout, ARG_INT);
	ADD_CONFIG_OPTION(AddModule, ARG_LIST);

	ADD_CONFIG_OPTION(AudioOutputMethod, ARG_STR);
	ADD_CONFIG_OPTION(AudioOSSDevice, ARG_STR);
	ADD_CONFIG_OPTION(AudioALSADevice, ARG_STR);
	ADD_CONFIG_OPTION(AudioNASServer, ARG_STR);
	ADD_CONFIG_OPTION(AudioPulseServer, ARG_STR);
	ADD_CONFIG_OPTION(AudioPulseDevice, ARG_STR);
	ADD_CONFIG_OPTION(AudioPulseMinLength, ARG_INT);

	ADD_CONFIG_OPTION(BeginClient, ARG_STR);
	ADD_CONFIG_OPTION(EndClient, ARG_NONE);
	return options;

}

/* == DEFAULT OPTIONS == */

void load_default_global_set_options()
{
	GlobalFDSet.priority = SPD_MESSAGE;
	GlobalFDSet.msg_settings.punctuation_mode = SPD_PUNCT_NONE;
	GlobalFDSet.symbols_preprocessing = SYMLVL_NONE;
	GlobalFDSet.msg_settings.spelling_mode = 0;
	GlobalFDSet.msg_settings.rate = 0;
	GlobalFDSet.msg_settings.pitch = 0;
	GlobalFDSet.msg_settings.pitch_range = 0;
	GlobalFDSet.msg_settings.volume = 0;
	GlobalFDSet.client_name = g_strdup("unknown:unknown:unknown");
	GlobalFDSet.msg_settings.voice.language = g_strdup("en-US");
	GlobalFDSet.output_module = NULL;
	GlobalFDSet.msg_settings.voice_type = SPD_MALE1;
	GlobalFDSet.msg_settings.cap_let_recogn = SPD_CAP_NONE;
	GlobalFDSet.min_delay_progress = 2000;
	GlobalFDSet.pause_context = 0;
	GlobalFDSet.ssml_mode = SPD_DATA_TEXT;
	GlobalFDSet.notification = 0;

	GlobalFDSet.audio_output_method = g_strdup(DEFAULT_AUDIO_METHOD);
	GlobalFDSet.audio_oss_device = g_strdup("/dev/dsp");
	GlobalFDSet.audio_alsa_device = g_strdup("default");
	GlobalFDSet.audio_nas_server = g_strdup("tcp/localhost:5450");
	GlobalFDSet.audio_pulse_server = g_strdup("default");
	GlobalFDSet.audio_pulse_device = g_strdup("default");
	GlobalFDSet.audio_pulse_min_length = 10;

	SpeechdOptions.max_history_messages = 10000;

	/* Options which are accessible from command line must be handled
	   specially to make sure we don't overwrite them */
	if (!SpeechdOptions.log_level_set)
		SpeechdOptions.log_level = 3;
	if (!SpeechdOptions.communication_method_set)
		SpeechdOptions.communication_method = g_strdup("unix_socket");
	if (!SpeechdOptions.socket_path_set)
		SpeechdOptions.socket_path = g_strdup("default");
	if (!SpeechdOptions.port_set)
		SpeechdOptions.port = SPEECHD_DEFAULT_PORT;
	if (!SpeechdOptions.localhost_access_only_set)
		SpeechdOptions.localhost_access_only = 1;
	if (!SpeechdOptions.server_timeout_set)
		SpeechdOptions.server_timeout = 5;
	

	logfile = stderr;
	custom_logfile = NULL;
}
