/* Copyright (c) 2008-2016 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "array.h"
#include "str.h"
#include "settings.h"
#include "dict-sql-settings.h"

#include <ctype.h>

enum section_type {
	SECTION_ROOT = 0,
	SECTION_MAP,
	SECTION_FIELDS
};

struct dict_sql_map_field {
	struct dict_sql_field sql_field;
	const char *variable;
};

struct setting_parser_ctx {
	pool_t pool;
	struct dict_sql_settings *set;
	enum section_type type;

	struct dict_sql_map cur_map;
	ARRAY(struct dict_sql_map_field) cur_fields;
};

#define DEF_STR(name) DEF_STRUCT_STR(name, dict_sql_map)
#define DEF_BOOL(name) DEF_STRUCT_BOOL(name, dict_sql_map)

static const struct setting_def dict_sql_map_setting_defs[] = {
	DEF_STR(pattern),
	DEF_STR(table),
	DEF_STR(username_field),
	DEF_STR(value_field),
	DEF_STR(value_type),
	DEF_BOOL(value_hexblob),

	{ 0, NULL, 0 }
};

static const char *pattern_read_name(const char **pattern)
{
	const char *p = *pattern, *name;

	if (*p == '{') {
		/* ${name} */
		name = ++p;
		p = strchr(p, '}');
		if (p == NULL) {
			/* error, but allow anyway */
			*pattern += strlen(*pattern);
			return "";
		}
		*pattern = p + 1;
	} else {
		/* $name - ends at the first non-alnum_ character */
		name = p;
		for (; *p != '\0'; p++) {
			if (!i_isalnum(*p) && *p != '_')
				break;
		}
		*pattern = p;
	}
	name = t_strdup_until(name, p);
	return name;
}

static const char *dict_sql_fields_map(struct setting_parser_ctx *ctx)
{
	struct dict_sql_map_field *fields;
	string_t *pattern;
	const char *p, *name;
	unsigned int i, count;

	/* go through the variables in the pattern, replace them with plain
	   '$' character and add its sql field */
	pattern = t_str_new(strlen(ctx->cur_map.pattern) + 1);
	fields = array_get_modifiable(&ctx->cur_fields, &count);

	p_array_init(&ctx->cur_map.sql_fields, ctx->pool, count);
	for (p = ctx->cur_map.pattern; *p != '\0';) {
		if (*p != '$') {
			str_append_c(pattern, *p);
			p++;
			continue;
		}
		p++;
		str_append_c(pattern, '$');

		name = pattern_read_name(&p);
		for (i = 0; i < count; i++) {
			if (fields[i].variable != NULL &&
			    strcmp(fields[i].variable, name) == 0)
				break;
		}
		if (i == count) {
			return t_strconcat("Missing SQL field for variable: ",
					   name, NULL);
		}

		/* mark this field as used */
		fields[i].variable = NULL;
		array_append(&ctx->cur_map.sql_fields,
			     &fields[i].sql_field, 1);
	}

	/* make sure there aren't any unused fields */
	for (i = 0; i < count; i++) {
		if (fields[i].variable != NULL) {
			return t_strconcat("Unused variable: ",
					   fields[i].variable, NULL);
		}
	}

	if (ctx->set->max_field_count < count)
		ctx->set->max_field_count = count;
	ctx->cur_map.pattern = p_strdup(ctx->pool, str_c(pattern));
	return NULL;
}

static const char *dict_sql_map_finish(struct setting_parser_ctx *ctx)
{
	if (ctx->cur_map.pattern == NULL)
		return "Missing setting: pattern";
	if (ctx->cur_map.table == NULL)
		return "Missing setting: table";
	if (ctx->cur_map.value_field == NULL)
		return "Missing setting: value_field";
	if (ctx->cur_map.value_type != NULL) {
		if (strcmp(ctx->cur_map.value_type, "string") != 0 &&
		    strcmp(ctx->cur_map.value_type, "hexblob") != 0 &&
		    strcmp(ctx->cur_map.value_type, "uint") != 0)
			return "Invalid value in value_type";
	}

	if (ctx->cur_map.username_field == NULL) {
		/* not all queries require this */
		ctx->cur_map.username_field = "'username_field not set'";
	}

	if (!array_is_created(&ctx->cur_map.sql_fields)) {
		/* no fields besides value. allocate the array anyway. */
		p_array_init(&ctx->cur_map.sql_fields, ctx->pool, 1);
		if (strchr(ctx->cur_map.pattern, '$') != NULL)
			return "Missing fields for pattern variables";
	}
	array_append(&ctx->set->maps, &ctx->cur_map, 1);
	memset(&ctx->cur_map, 0, sizeof(ctx->cur_map));
	return NULL;
}

static const char *
parse_setting(const char *key, const char *value,
	      struct setting_parser_ctx *ctx)
{
	struct dict_sql_map_field *field;
	unsigned int value_len;

	switch (ctx->type) {
	case SECTION_ROOT:
		if (strcmp(key, "connect") == 0) {
			ctx->set->connect = p_strdup(ctx->pool, value);
			return NULL;
		}
		break;
	case SECTION_MAP:
		return parse_setting_from_defs(ctx->pool,
					       dict_sql_map_setting_defs,
					       &ctx->cur_map, key, value);
	case SECTION_FIELDS:
		if (*value != '$') {
			return t_strconcat("Value is missing '$' for field: ",
					   key, NULL);
		}
		field = array_append_space(&ctx->cur_fields);
		field->sql_field.name = p_strdup(ctx->pool, key);
		value_len = strlen(value);
		if (strncmp(value, "${hexblob:", 10) == 0 &&
		    value[value_len-1] == '}') {
			field->variable = p_strndup(ctx->pool, value + 10,
						    value_len-10-1);
			field->sql_field.value_type = DICT_SQL_TYPE_HEXBLOB;
		} else if (strncmp(value, "${uint:", 7) == 0 &&
			   value[value_len-1] == '}') {
			field->variable = p_strndup(ctx->pool, value + 7,
						    value_len-7-1);
			field->sql_field.value_type = DICT_SQL_TYPE_UINT;
		} else {
			field->variable = p_strdup(ctx->pool, value + 1);
		}
		return NULL;
	}
	return t_strconcat("Unknown setting: ", key, NULL);
}

static bool
parse_section(const char *type, const char *name ATTR_UNUSED,
	      struct setting_parser_ctx *ctx, const char **error_r)
{
	switch (ctx->type) {
	case SECTION_ROOT:
		if (type == NULL)
			return FALSE;
		if (strcmp(type, "map") == 0) {
			array_clear(&ctx->cur_fields);
			ctx->type = SECTION_MAP;
			return TRUE;
		}
		break;
	case SECTION_MAP:
		if (type == NULL) {
			ctx->type = SECTION_ROOT;
			*error_r = dict_sql_map_finish(ctx);
			return FALSE;
		}
		if (strcmp(type, "fields") == 0) {
			ctx->type = SECTION_FIELDS;
			return TRUE;
		}
		break;
	case SECTION_FIELDS:
		if (type == NULL) {
			ctx->type = SECTION_MAP;
			*error_r = dict_sql_fields_map(ctx);
			return FALSE;
		}
		break;
	}
	*error_r = t_strconcat("Unknown section: ", type, NULL);
	return FALSE;
}

struct dict_sql_settings *
dict_sql_settings_read(pool_t pool, const char *path, const char **error_r)
{
	struct setting_parser_ctx ctx;

	memset(&ctx, 0, sizeof(ctx));
	ctx.pool = pool;
	ctx.set = p_new(pool, struct dict_sql_settings, 1);
	t_array_init(&ctx.cur_fields, 16);
	p_array_init(&ctx.set->maps, pool, 8);

	if (!settings_read(path, NULL, parse_setting, parse_section,
			   &ctx, error_r))
		return NULL;

	if (ctx.set->connect == NULL) {
		*error_r = t_strdup_printf("Error in configuration file %s: "
					   "Missing connect setting", path);
		return NULL;
	}

	return ctx.set;
}
