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

#include "lib.h"
#include "buffer.h"
#include "str.h"
#include "guid.h"
#include "hex-binary.h"
#include "base64.h"
#include "istream.h"
#include "ostream.h"
#include "dict.h"
#include "fs-api-private.h"

enum fs_dict_value_encoding {
	FS_DICT_VALUE_ENCODING_RAW,
	FS_DICT_VALUE_ENCODING_HEX,
	FS_DICT_VALUE_ENCODING_BASE64
};

struct dict_fs {
	struct fs fs;
	struct dict *dict;
	char *path_prefix;
	enum fs_dict_value_encoding encoding;
};

struct dict_fs_file {
	struct fs_file file;
	pool_t pool;
	const char *key, *value;
	buffer_t *write_buffer;
};

struct dict_fs_iter {
	struct fs_iter iter;
	struct dict_iterate_context *dict_iter;
};

static struct fs *fs_dict_alloc(void)
{
	struct dict_fs *fs;

	fs = i_new(struct dict_fs, 1);
	fs->fs = fs_class_dict;
	return &fs->fs;
}

static int
fs_dict_init(struct fs *_fs, const char *args, const struct fs_settings *set)
{
	struct dict_fs *fs = (struct dict_fs *)_fs;
	struct dict_settings dict_set;
	const char *p, *encoding_str, *error;

	p = strchr(args, ':');
	if (p == NULL) {
		fs_set_error(_fs, "':' missing in args");
		return -1;
	}
	encoding_str = t_strdup_until(args, p++);
	if (strcmp(encoding_str, "raw") == 0)
		fs->encoding = FS_DICT_VALUE_ENCODING_RAW;
	else if (strcmp(encoding_str, "hex") == 0)
		fs->encoding = FS_DICT_VALUE_ENCODING_HEX;
	else if (strcmp(encoding_str, "base64") == 0)
		fs->encoding = FS_DICT_VALUE_ENCODING_BASE64;
	else {
		fs_set_error(_fs, "Unknown value encoding '%s'", encoding_str);
		return -1;
	}

	memset(&dict_set, 0, sizeof(dict_set));
	dict_set.username = set->username;
	dict_set.base_dir = set->base_dir;

	if (dict_init_full(p, &dict_set, &fs->dict, &error) < 0) {
		fs_set_error(_fs, "dict_init(%s) failed: %s", args, error);
		return -1;
	}
	return 0;
}

static void fs_dict_deinit(struct fs *_fs)
{
	struct dict_fs *fs = (struct dict_fs *)_fs;

	dict_deinit(&fs->dict);
	i_free(fs);
}

static enum fs_properties fs_dict_get_properties(struct fs *fs ATTR_UNUSED)
{
	return FS_PROPERTY_ITER | FS_PROPERTY_RELIABLEITER;
}

static struct fs_file *
fs_dict_file_init(struct fs *_fs, const char *path,
		  enum fs_open_mode mode, enum fs_open_flags flags ATTR_UNUSED)
{
	struct dict_fs *fs = (struct dict_fs *)_fs;
	struct dict_fs_file *file;
	guid_128_t guid;
	pool_t pool;

	i_assert(mode != FS_OPEN_MODE_APPEND); /* not supported */
	i_assert(mode != FS_OPEN_MODE_CREATE); /* not supported */

	pool = pool_alloconly_create("fs dict file", 128);
	file = p_new(pool, struct dict_fs_file, 1);
	file->pool = pool;
	file->file.fs = _fs;
	if (mode != FS_OPEN_MODE_CREATE_UNIQUE_128)
		file->file.path = p_strdup(pool, path);
	else {
		guid_128_generate(guid);
		file->file.path = p_strdup_printf(pool, "%s/%s", path,
						  guid_128_to_string(guid));
	}
	file->key = fs->path_prefix == NULL ?
		p_strdup(pool, file->file.path) :
		p_strconcat(pool, fs->path_prefix, file->file.path, NULL);
	return &file->file;
}

static void fs_dict_file_deinit(struct fs_file *_file)
{
	struct dict_fs_file *file = (struct dict_fs_file *)_file;

	i_assert(_file->output == NULL);

	pool_unref(&file->pool);
}

static bool fs_dict_prefetch(struct fs_file *_file ATTR_UNUSED,
			     uoff_t length ATTR_UNUSED)
{
	/* once async dict_lookup() is implemented, we want to start it here */
	return TRUE;
}

static int fs_dict_lookup(struct dict_fs_file *file)
{
	struct dict_fs *fs = (struct dict_fs *)file->file.fs;
	int ret;

	if (file->value != NULL)
		return 0;

	ret = dict_lookup(fs->dict, file->pool, file->key, &file->value);
	if (ret > 0)
		return 0;
	else if (ret < 0) {
		errno = EIO;
		fs_set_error(&fs->fs, "Dict lookup failed");
		return -1;
	} else {
		errno = ENOENT;
		fs_set_error(&fs->fs, "Dict key doesn't exist");
		return -1;
	}
}

static struct istream *
fs_dict_read_stream(struct fs_file *_file, size_t max_buffer_size ATTR_UNUSED)
{
	struct dict_fs_file *file = (struct dict_fs_file *)_file;
	struct istream *input;

	if (fs_dict_lookup(file) < 0)
		input = i_stream_create_error_str(errno, "%s", fs_last_error(_file->fs));
	else
		input = i_stream_create_from_data(file->value, strlen(file->value));
	i_stream_set_name(input, file->key);
	return input;
}

static void fs_dict_write_stream(struct fs_file *_file)
{
	struct dict_fs_file *file = (struct dict_fs_file *)_file;

	i_assert(_file->output == NULL);

	file->write_buffer = buffer_create_dynamic(file->pool, 128);
	_file->output = o_stream_create_buffer(file->write_buffer);
	o_stream_set_name(_file->output, file->key);
}

static int fs_dict_write_stream_finish(struct fs_file *_file, bool success)
{
	struct dict_fs_file *file = (struct dict_fs_file *)_file;
	struct dict_fs *fs = (struct dict_fs *)_file->fs;
	struct dict_transaction_context *trans;

	o_stream_destroy(&_file->output);
	if (!success)
		return -1;

	trans = dict_transaction_begin(fs->dict);
	switch (fs->encoding) {
	case FS_DICT_VALUE_ENCODING_RAW:
		dict_set(trans, file->key, str_c(file->write_buffer));
		break;
	case FS_DICT_VALUE_ENCODING_HEX: {
		string_t *hex = t_str_new(file->write_buffer->used * 2 + 1);
		binary_to_hex_append(hex, file->write_buffer->data,
				     file->write_buffer->used);
		dict_set(trans, file->key, str_c(hex));
		break;
	}
	case FS_DICT_VALUE_ENCODING_BASE64: {
		const unsigned int base64_size =
			MAX_BASE64_ENCODED_SIZE(file->write_buffer->used);
		string_t *base64 = t_str_new(base64_size);
		base64_encode(file->write_buffer->data,
			      file->write_buffer->used, base64);
		dict_set(trans, file->key, str_c(base64));
	}
	}
	if (dict_transaction_commit(&trans) < 0) {
		errno = EIO;
		fs_set_error(_file->fs, "Dict transaction commit failed");
		return -1;
	}
	return 1;
}

static int fs_dict_stat(struct fs_file *_file, struct stat *st_r)
{
	struct dict_fs_file *file = (struct dict_fs_file *)_file;

	memset(st_r, 0, sizeof(*st_r));

	if (fs_dict_lookup(file) < 0)
		return -1;
	st_r->st_size = strlen(file->value);
	return 0;
}

static int fs_dict_delete(struct fs_file *_file)
{
	struct dict_fs_file *file = (struct dict_fs_file *)_file;
	struct dict_fs *fs = (struct dict_fs *)_file->fs;
	struct dict_transaction_context *trans;

	trans = dict_transaction_begin(fs->dict);
	dict_unset(trans, file->key);
	if (dict_transaction_commit(&trans) < 0) {
		errno = EIO;
		fs_set_error(_file->fs, "Dict transaction commit failed");
		return -1;
	}
	return 0;
}

static struct fs_iter *
fs_dict_iter_init(struct fs *_fs, const char *path, enum fs_iter_flags flags)
{
	struct dict_fs *fs = (struct dict_fs *)_fs;
	struct dict_fs_iter *iter;

	iter = i_new(struct dict_fs_iter, 1);
	iter->iter.fs = _fs;
	iter->iter.flags = flags;
	if (fs->path_prefix != NULL)
		path = t_strconcat(fs->path_prefix, path, NULL);

	iter->dict_iter = dict_iterate_init(fs->dict, path, 0);
	return &iter->iter;
}

static const char *fs_dict_iter_next(struct fs_iter *_iter)
{
	struct dict_fs_iter *iter = (struct dict_fs_iter *)_iter;
	const char *key, *value;

	if (!dict_iterate(iter->dict_iter, &key, &value))
		return NULL;
	return key;
}

static int fs_dict_iter_deinit(struct fs_iter *_iter)
{
	struct dict_fs_iter *iter = (struct dict_fs_iter *)_iter;
	int ret;

	ret = dict_iterate_deinit(&iter->dict_iter);
	if (ret < 0)
		fs_set_error(_iter->fs, "Dict iteration failed");
	i_free(iter);
	return ret;
}

const struct fs fs_class_dict = {
	.name = "dict",
	.v = {
		fs_dict_alloc,
		fs_dict_init,
		fs_dict_deinit,
		fs_dict_get_properties,
		fs_dict_file_init,
		fs_dict_file_deinit,
		NULL,
		NULL,
		NULL, NULL,
		NULL, NULL,
		fs_dict_prefetch,
		NULL,
		fs_dict_read_stream,
		NULL,
		fs_dict_write_stream,
		fs_dict_write_stream_finish,
		NULL,
		NULL,
		NULL,
		fs_dict_stat,
		fs_default_copy,
		NULL,
		fs_dict_delete,
		fs_dict_iter_init,
		fs_dict_iter_next,
		fs_dict_iter_deinit
	}
};
