/*
 * App Configuration
 *
 * Copyright (c) 2012 David Herrmann <dh.herrmann@googlemail.com>
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files
 * (the "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/*
 * Configuration
 * Implementation of the configuration parsers.
 */

#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <xkbcommon/xkbcommon.h>
#include "conf.h"
#include "log.h"
#include "shl_misc.h"

#define LOG_SUBSYSTEM "config"

/*
 * Config Contexts
 */

struct conf_ctx {
	struct conf_option *opts;
	size_t onum;
	void *mem;
};

int conf_ctx_new(struct conf_ctx **out, const struct conf_option *opts,
		  size_t onum, void *mem)
{
	struct conf_ctx *ctx;
	size_t size;

	if (!out || !opts || !onum)
		return -EINVAL;

	size = sizeof(*ctx) + onum * sizeof(*opts);
	ctx = malloc(size);
	if (!ctx) {
		log_error("cannot allocate memory for config context");
		return -ENOMEM;
	}
	memset(ctx, 0, size);
	ctx->opts = (void*)((char*)ctx + sizeof(*ctx));
	ctx->onum = onum;
	ctx->mem = mem;
	memcpy(ctx->opts, opts, onum * sizeof(*opts));

	conf_ctx_reset(ctx);

	*out = ctx;
	return 0;
}

void conf_ctx_free(struct conf_ctx *ctx)
{
	if (!ctx)
		return;

	conf_ctx_reset(ctx);
	free(ctx);
}

void conf_ctx_reset(struct conf_ctx *ctx)
{
	unsigned int i;

	if (!ctx)
		return;

	for (i = 0; i < ctx->onum; ++i) {
		if (ctx->opts[i].type->free)
			ctx->opts[i].type->free(&ctx->opts[i]);
		ctx->opts[i].flags = 0;
		if (ctx->opts[i].type->set_default)
			ctx->opts[i].type->set_default(&ctx->opts[i]);
	}
}

void *conf_ctx_get_mem(struct conf_ctx *ctx)
{
	if (!ctx)
		return NULL;

	return ctx->mem;
}

/*
 * Copy all entries from \src into \ctx
 * This calls the "copy" callback for each option inside of \ctx with the
 * corresponding option inside of \src if both have the same type. If the types
 * do not match, nothing is done.
 */
int conf_ctx_parse_ctx(struct conf_ctx *ctx, const struct conf_ctx *src)
{
	unsigned int i;
	struct conf_option *d, *s;
	int ret;

	if (!ctx || !src)
		return -EINVAL;

	for (i = 0; i < ctx->onum && i < src->onum; ++i) {
		d = &ctx->opts[i];
		s = &src->opts[i];

		if (d->type != s->type)
			continue;
		if (d->flags & CONF_LOCKED)
			continue;

		if (s->flags & CONF_LOCKED)
			d->flags |= CONF_LOCKED;

		if (d->type->copy) {
			ret = d->type->copy(d, s);
			if (ret)
				return ret;
		}

		if (d->copy) {
			ret = d->copy(d, s);
			if (ret)
				return ret;
		}
	}

	return 0;
}

/*
 * Parse command line arguments
 * This temporarily allocates the short_options and long_options arrays so we
 * can use the getopt_long() library call. It locks all arguments after they
 * have been set so command-line options will always overwrite config-options.
 */
int conf_ctx_parse_argv(struct conf_ctx *ctx, int argc, char **argv)
{
	char *short_options;
	struct option *long_options;
	struct option *opt;
	struct conf_option *o;
	size_t i, pos;
	int c, ret;

	if (!ctx || !argv)
		return -EINVAL;

	short_options = malloc(sizeof(char) * (ctx->onum + 1) * 2);
	if (!short_options) {
		log_error("out of memory to parse cmd-line arguments (%d): %m",
			  errno);
		return -ENOMEM;
	}

	long_options = malloc(sizeof(struct option) * ctx->onum * 2);
	if (!long_options) {
		log_error("out of memory to parse cmd-line arguments (%d): %m",
			  errno);
		free(short_options);
		return -ENOMEM;
	}

	pos = 0;
	short_options[pos++] = ':';
	opt = long_options;
	for (i = 0; i < ctx->onum; ++i) {
		if (ctx->opts[i].short_name) {
			short_options[pos++] = ctx->opts[i].short_name;
			if (ctx->opts[i].type->flags & CONF_HAS_ARG)
				short_options[pos++] = ':';
		}

		if (ctx->opts[i].long_name) {
			/* skip the "no-" prefix */
			opt->name = &ctx->opts[i].long_name[3];
			opt->has_arg = !!(ctx->opts[i].type->flags &
								CONF_HAS_ARG);
			opt->flag = NULL;
			opt->val = 100000 + i;
			++opt;

			/* boolean args are also added with "no-" prefix */
			if (!(ctx->opts[i].type->flags & CONF_HAS_ARG)) {
				opt->name = ctx->opts[i].long_name;
				opt->has_arg = 0;
				opt->flag = NULL;
				opt->val = 200000 + i;
				++opt;
			}
		}
	}
	short_options[pos++] = 0;

	opterr = 0;
	while (1) {
		c = getopt_long(argc, argv, short_options,
				long_options, NULL);
		if (c <= 0) {
			break;
		} else if (c == ':') {
			fprintf(stderr, "Missing argument for: %s\n",
				argv[optind - 1]);
			return -EFAULT;
		} else if (c == '?') {
			if (optopt && optopt < 100000)
				fprintf(stderr, "Unknown argument: -%c\n",
					optopt);
			else if (!optopt)
				fprintf(stderr, "Unknown argument: %s\n",
					argv[optind - 1]);
			else
				fprintf(stderr, "Option takes no arg: %s\n",
					argv[optind - 1]);
			return -EFAULT;
		} else if (c < 100000) {
			for (i = 0; i < ctx->onum; ++i) {
				o = &ctx->opts[i];

				if (o->short_name != c)
					continue;

				if (o->type->parse) {
					ret = o->type->parse(o, true, optarg);
					if (ret)
						return ret;
				}

				o->flags |= CONF_LOCKED;
				o->flags |= CONF_DONE;
				break;
			}
		} else if (c < 200000) {
			i = c - 100000;
			o = &ctx->opts[i];

			if (o->type->parse) {
				ret = o->type->parse(o, true, optarg);
				if (ret)
					return ret;
			}

			o->flags |= CONF_LOCKED;
			o->flags |= CONF_DONE;
		} else {
			i = c - 200000;
			o = &ctx->opts[i];

			if (o->type->parse) {
				ret = o->type->parse(o, false, NULL);
				if (ret)
					return ret;
			}

			o->flags |= CONF_LOCKED;
			o->flags |= CONF_DONE;
		}
	}

	free(long_options);
	free(short_options);

	/* Perform aftercheck:
	 * All arguments that provide an aftercheck will be passed the remaining
	 * arguments in order. If they return a negative error code, it is
	 * interpreted as fatal error and returned to the caller. A positive
	 * error code is interpreted as the amount of remaining arguments that
	 * have been consumed by this aftercheck. 0 means nothing has been
	 * consumed.
	 * The next argument's aftercheck will only get the now remaning
	 * arguments passed in. If not all arguments are consumed, then this
	 * function will report an error to the caller. */
	for (i = 0; i < ctx->onum; ++i) {
		o = &ctx->opts[i];
		if (o->aftercheck) {
			ret = o->aftercheck(o, argc, argv, optind);
			if (ret < 0)
				return ret;
			optind += ret;
		}
	}

	if (optind < argc) {
		fprintf(stderr, "Unparsed remaining args starting with: %s\n",
			argv[optind]);
		return -EFAULT;
	}

	return 0;
}

static int parse_kv_pair(struct conf_option *opts, size_t len,
			 const char *key, const char *value)
{
	unsigned int i;
	int ret;
	bool set;
	struct conf_option *opt;

	for (i = 0; i < len; ++i) {
		opt = &opts[i];
		if (!opt->long_name)
			continue;

		if (!strcmp(key, opt->long_name))
			set = false;
		else if (!strcmp(key, &opt->long_name[3]))
			set = true;
		else
			continue;

		/* ignore if already set by command-line arguments */
		if (opt->flags & CONF_LOCKED)
			return 0;

		if (opt->file) {
			ret = opt->file(opt, set, value);
			if (ret)
				return ret;
			opt->flags |= CONF_DONE;
			return 0;
		}

		if (opt->type->flags & CONF_HAS_ARG && !value) {
			log_error("config option '%s' requires an argument",
				  key);
			return -EFAULT;
		} else if (!(opt->type->flags & CONF_HAS_ARG) && value) {
			log_error("config option '%s' does not take arguments",
				  key);
			return -EFAULT;
		}

		if (opt->type->parse) {
			ret = opt->type->parse(opt, set, value);
			if (ret)
				return ret;
		}

		opt->flags |= CONF_DONE;
		return 0;
	}

	log_error("unknown config option '%s'", key);
	return -EFAULT;
}

static void strip_spaces(char **buf)
{
	char *tail;

	while (**buf == ' ' || **buf == '\r' || **buf == '\t')
		++*buf;

	if (!**buf)
		return;

	tail = *buf;
	while (*tail)
		++tail;

	--tail;

	while (*tail == ' ' || *tail == '\r' || *tail == '\t')
		*tail-- = 0;
}

static int parse_line(struct conf_option *opts, size_t olen,
		      char **buf, size_t *size)
{
	char *key;
	char *value = NULL;
	char *line;
	char c;
	size_t len, klen;
	int ret;

	line = *buf;
	len = *size;

	/* parse key */
	key = line;
	while (len) {
		c = *line;
		if (c == '\n' ||
		    c == '#' ||
		    c == '=')
			break;
		++line;
		--len;
	}

	if (!len) {
		*line = 0;
		goto done;
	} else if (*line == '\n') {
		*line = 0;
		goto done;
	} else if (*line == '#') {
		*line = 0;
		goto skip_comment;
	} else if (*line != '=') {
		return -EFAULT;
	}

	*line++ = 0;
	--len;

	/* parse value */
	value = line;
	while (len) {
		c = *line;
		if (c == '\n' ||
		    c == '#')
			break;
		++line;
		--len;
	}

	if (!len) {
		*line = 0;
		goto done;
	} else if (*line == '\n') {
		*line = 0;
		goto done;
	} else if (*line == '#') {
		*line = 0;
		goto skip_comment;
	} else {
		return -EFAULT;
	}

skip_comment:
	while (len) {
		c = *line;
		if (c == '\n')
			break;
		++line;
		--len;
	}

done:
	strip_spaces(&key);
	
	klen = strlen(key);
	if (klen > 0) {
		if (value)
			strip_spaces(&value);

		ret = parse_kv_pair(opts, olen, key, value);
		if (ret)
			return ret;
	}

	if (!len) {
		*buf = NULL;
		*size = 0;
	} else {
		*buf = ++line;
		*size = --len;
	}

	return 0;
}

static int parse_buffer(struct conf_option *opts, size_t len,
			char *buf, size_t size)
{
	int ret = 0;
	struct conf_option *o;
	unsigned int i;

	while (!ret && size > 0)
		ret = parse_line(opts, len, &buf, &size);

	if (ret)
		return ret;

	for (i = 0; i < len; ++i) {
		o = &opts[i];
		if (o->aftercheck) {
			ret = o->aftercheck(o, 0, NULL, 0);
			if (ret < 0)
				return ret;
		}
	}

	return 0;
}

/* chunk size when reading config files */
#define CONF_BUFSIZE 4096

/* This reads the file at \path in memory and parses it as if it was given as
 * command line options. */
static int conf_parse_file(struct conf_option *opts, size_t len,
			   const char *path)
{
	int fd, ret;
	size_t size, pos;
	char *buf, *tmp;

	if (!opts || !len || !path)
		return -EINVAL;

	if (access(path, F_OK))
		return 0;

	if (access(path, R_OK)) {
		log_error("read access to config file %s denied", path);
		return -EACCES;
	}

	log_info("reading config file %s", path);
	fd = open(path, O_RDONLY | O_CLOEXEC | O_NOCTTY);
	if (fd < 0) {
		log_error("cannot open %s (%d): %m", path, errno);
		return -EFAULT;
	}

	buf = NULL;
	size = 0;
	pos = 0;

	do {
		if (size - pos < CONF_BUFSIZE) {
			tmp = realloc(buf, size + CONF_BUFSIZE + 1);
			if (!tmp) {
				log_error("cannot allocate enough memory to parse config file %s (%d): %m",
					  path, errno);
				ret = -ENOMEM;
				goto out_free;
			}
			buf = tmp;
			size += CONF_BUFSIZE;
		}

		ret = read(fd, &buf[pos], CONF_BUFSIZE);
		if (ret < 0) {
			log_error("cannot read from config file %s (%d): %m",
				  path, errno);
			ret = -EFAULT;
			goto out_free;
		}
		pos += ret;
	} while (ret > 0);

	buf[pos] = 0;
	ret = parse_buffer(opts, len, buf, pos);

out_free:
	free(buf);
	close(fd);
	return ret;
}

int conf_ctx_parse_file(struct conf_ctx *ctx, const char *format, ...)
{
	va_list list;
	char *path;
	int ret;

	if (!ctx || !format)
		return -EINVAL;

	va_start(list, format);
	ret = vasprintf(&path, format, list);
	va_end(list);

	if (ret < 0) {
		log_error("cannot allocate memory for config-file path");
		return -ENOMEM;
	}

	ret = conf_parse_file(ctx->opts, ctx->onum, path);
	free(path);
	return ret;
}

/*
 * Config Types
 * Each option that can be parsed must be a specific config-type. A config-type
 * is used to parse, free and reset the value. It must implement the following
 * callbacks:
 *   set_default: This should link opt->mem to opt->def and must not fail. It is
 *                called during initialization and reset.
 *   free: This should free any allocated memory and reset the option to the
 *         initial state. It must not fail.
 *   parse: This should parse a command-line option. Return 0 on success.
 *   copy: Copy data from source into destination. Return 0 on success.
 *
 * The backing memory is zeroed on reset so a config-type must be able to handle
 * this as "not set". Also, the "free" callback should reset it to zero (which
 * is the initial state).
 */

/* Miscellaneous helper */

static void conf_free_value(struct conf_option *opt)
{
	if (*(void**)opt->mem) {
		if (*(void**)opt->mem != opt->def)
			free(*(void**)opt->mem);
		*(void**)opt->mem = NULL;
	}
}

/* Boolean Option */

static void conf_default_bool(struct conf_option *opt)
{
	*(bool*)opt->mem = (bool)opt->def;
}

static void conf_free_bool(struct conf_option *opt)
{
	*(bool*)opt->mem = false;
}

static int conf_parse_bool(struct conf_option *opt, bool on, const char *arg)
{
	*(bool*)opt->mem = on;
	return 0;
}

static int conf_copy_bool(struct conf_option *opt,
			  const struct conf_option *src)
{
	*(bool*)opt->mem = *(bool*)src->mem;
	return 0;
}

const struct conf_type conf_bool = {
	.flags = 0,
	.set_default = conf_default_bool,
	.free = conf_free_bool,
	.parse = conf_parse_bool,
	.copy = conf_copy_bool,
};

/* Int Option */

static int conf_parse_int(struct conf_option *opt, bool on, const char *arg)
{
	*(int*)opt->mem = atoi(arg);
	return 0;
}

static void conf_free_int(struct conf_option *opt)
{
	*(int*)opt->mem = 0;
}

static void conf_default_int(struct conf_option *opt)
{
	*(int*)opt->mem = (int)(unsigned long)opt->def;
}

static int conf_copy_int(struct conf_option *opt,
			 const struct conf_option *src)
{
	*(int*)opt->mem = *(int*)src->mem;
	return 0;
}

const struct conf_type conf_int = {
	.flags = CONF_HAS_ARG,
	.set_default = conf_default_int,
	.free = conf_free_int,
	.parse = conf_parse_int,
	.copy = conf_copy_int,
};

/* Unsigned Int Option */

static void conf_default_uint(struct conf_option *opt)
{
	*(unsigned int*)opt->mem = (unsigned int)(unsigned long)opt->def;
}

static void conf_free_uint(struct conf_option *opt)
{
	*(unsigned int*)opt->mem = 0;
}

static int conf_parse_uint(struct conf_option *opt, bool on, const char *arg)
{
	*(unsigned int*)opt->mem = atoi(arg);
	return 0;
}

static int conf_copy_uint(struct conf_option *opt,
			  const struct conf_option *src)
{
	*(unsigned int*)opt->mem = *(unsigned int*)src->mem;
	return 0;
}

const struct conf_type conf_uint = {
	.flags = CONF_HAS_ARG,
	.set_default = conf_default_uint,
	.free = conf_free_uint,
	.parse = conf_parse_uint,
	.copy = conf_copy_uint,
};

/* String Option */

static void conf_default_string(struct conf_option *opt)
{
	opt->type->free(opt);
	*(void**)opt->mem = opt->def;
}

static int conf_parse_string(struct conf_option *opt, bool on, const char *arg)
{
	char *val = strdup(arg);
	if (!val)
		return -ENOMEM;

	opt->type->free(opt);
	*(void**)opt->mem = val;
	return 0;
}

static int conf_copy_string(struct conf_option *opt,
			    const struct conf_option *src)
{
	char *val;

	if (!*(void**)src->mem) {
		val = NULL;
	} else {
		val = strdup(*(void**)src->mem);
		if (!val)
			return -ENOMEM;
	}

	opt->type->free(opt);
	*(void**)opt->mem = val;
	return 0;
}

const struct conf_type conf_string = {
	.flags = CONF_HAS_ARG,
	.set_default = conf_default_string,
	.free = conf_free_value,
	.parse = conf_parse_string,
	.copy = conf_copy_string,
};

/* Stringlist Option */

static void conf_default_string_list(struct conf_option *opt)
{
	opt->type->free(opt);
	*(void**)opt->mem = opt->def;
}

static int conf_parse_string_list(struct conf_option *opt, bool on,
				  const char *arg)
{
	int ret;
	char **list;

	ret = shl_split_string(arg, &list, NULL, ',', true);
	if (ret)
		return ret;

	opt->type->free(opt);
	*(char***)opt->mem = list;
	return 0;
}

static int conf_copy_string_list(struct conf_option *opt,
				 const struct conf_option *src)
{
	int ret;
	char **t;

	if (!(void***)src->mem) {
		t = NULL;
	} else {
		ret = shl_dup_array(&t, *(char***)src->mem);
		if (ret)
			return ret;
	}

	opt->type->free(opt);
	*(char***)opt->mem = t;
	return 0;
}

const struct conf_type conf_string_list = {
	.flags = CONF_HAS_ARG,
	.set_default = conf_default_string_list,
	.free = conf_free_value,
	.parse = conf_parse_string_list,
	.copy = conf_copy_string_list,
};

/* Grab Option */

static void conf_default_grab(struct conf_option *opt)
{
	opt->type->free(opt);
	*(void**)opt->mem = opt->def;
}

static void conf_free_grab(struct conf_option *opt)
{
	struct conf_grab *grab;
	unsigned int i;

	grab = *(void**)opt->mem;
	*(void**)opt->mem = NULL;

	if (!grab || grab == opt->def)
		return;

	for (i = 0; i < grab->num; ++i)
		free(grab->keysyms[i]);

	free(grab->keysyms);
	free(grab->num_syms);
	free(grab->mods);
	free(grab);
}

static int parse_single_grab(char *arg, unsigned int *mods,
			     uint32_t *keysym, bool allow_mods)
{
	char *tmp, *start, *end;
	char buf[128];

	tmp = arg;
	do {
		while (*tmp == ' ')
			++tmp;
		if (!allow_mods)
			break;
		if (*tmp != '<')
			break;

		start = tmp;
		while (*tmp && *tmp != '>')
			++tmp;

		if (*tmp != '>') {
			log_error("missing '>' near '%s'", start);
			return -EFAULT;
		}

		*tmp++ = 0;
		++start;
		if (!strcasecmp(start, "shift")) {
			*mods |= SHL_SHIFT_MASK;
		} else if (!strcasecmp(start, "lock")) {
			*mods |= SHL_LOCK_MASK;
		} else if (!strcasecmp(start, "control") ||
			   !strcasecmp(start, "ctrl")) {
			*mods |= SHL_CONTROL_MASK;
		} else if (!strcasecmp(start, "alt")) {
			*mods |= SHL_ALT_MASK;
		} else if (!strcasecmp(start, "logo")) {
			*mods |= SHL_LOGO_MASK;
		} else {
			log_error("invalid modifier '%s'", start);
			return -EFAULT;
		}
	} while (1);

	while (*tmp == ' ')
		++tmp;

	start = tmp;
	end = start;
	do {
		while (*tmp && *tmp != ' ')
			++tmp;
		end = tmp;
		if (!*tmp)
			break;
		while (*tmp == ' ')
			++tmp;
	} while (1);

	if (start == end)
		return 0;
	if (*end)
		*end = 0;

	*keysym = xkb_keysym_from_name(start, 0);
	if (!*keysym) {
		*keysym = xkb_keysym_from_name(start,
					       XKB_KEYSYM_CASE_INSENSITIVE);
		if (!*keysym) {
			log_error("invalid key '%s'", start);
			return -EFAULT;
		}

		xkb_keysym_get_name(*keysym, buf, sizeof(buf));
		log_warning("invalid keysym '%s', did you mean '%s'? (keysyms are case-sensitive)",
			    start, buf);
		return -EFAULT;
	}

	return 1;
}

static int conf_parse_grab(struct conf_option *opt, bool on, const char *arg)
{
	char **list, **keys;
	unsigned int list_num, key_num, i, j, k, l;
	int ret;
	struct conf_grab *grab;

	ret = shl_split_string(arg, &list, &list_num, ',', false);
	if (ret)
		return ret;

	grab = malloc(sizeof(*grab));
	if (!grab) {
		ret = -ENOMEM;
		goto err_list;
	}
	memset(grab, 0, sizeof(*grab));

	if (list_num) {
		grab->mods = malloc(sizeof(*grab->mods) * list_num);
		if (!grab->mods) {
			ret = -ENOMEM;
			goto err_grab;
		}
		memset(grab->mods, 0, sizeof(*grab->mods) * list_num);

		grab->num_syms = malloc(sizeof(*grab->num_syms) * list_num);
		if (!grab->num_syms) {
			ret = -ENOMEM;
			goto err_grab;
		}
		memset(grab->num_syms, 0, sizeof(*grab->num_syms) * list_num);

		grab->keysyms = malloc(sizeof(*grab->keysyms) * list_num);
		if (!grab->keysyms) {
			ret = -ENOMEM;
			goto err_grab;
		}
		memset(grab->keysyms, 0, sizeof(*grab->keysyms) * list_num);
	}

	l = 0;
	for (i = 0; i < list_num; ++i) {
		ret = shl_split_string(list[i], &keys, &key_num, '+', false);
		if (ret)
			goto err_all;
		if (!key_num) {
			free(keys);
			continue;
		}

		grab->keysyms[l] = malloc(sizeof(*grab->keysyms[l]) * key_num);
		if (!grab->keysyms[l]) {
			ret = -ENOMEM;
			free(keys);
			goto err_all;
		}

		k = 0;
		for (j = 0; j < key_num; ++j) {
			ret = parse_single_grab(keys[j], &grab->mods[l],
						&grab->keysyms[l][k],
						j == 0);
			if (ret < 0) {
				log_error("cannot parse grab '%s' in '%s'",
					  list[i], arg);
				free(keys);
				goto err_all;
			}
			k += ret;
		}

		free(keys);
		if (!k)
			continue;
		grab->num_syms[l] = k;
		++l;
		++grab->num;
	}

	free(list);
	opt->type->free(opt);
	*(void**)opt->mem = grab;
	return 0;

err_all:
	for (i = 0; i < list_num; ++i)
		free(grab->keysyms[i]);
err_grab:
	free(grab->keysyms);
	free(grab->num_syms);
	free(grab->mods);
	free(grab);
err_list:
	free(list);
	return ret;
}

static int conf_copy_grab(struct conf_option *opt,
			  const struct conf_option *src)
{
	struct conf_grab *grab, *s;
	int ret;
	unsigned int i;

	s = *(void**)src->mem;

	if (!s) {
		opt->type->free(opt);
		*(void**)opt->mem = NULL;
		return 0;
	}

	grab = malloc(sizeof(*grab));
	if (!grab)
		return -ENOMEM;
	memset(grab, 0, sizeof(*grab));
	grab->num = s->num;

	if (grab->num) {
		grab->mods = malloc(sizeof(*grab->mods) * grab->num);
		if (!grab->mods) {
			ret = -ENOMEM;
			goto err_grab;
		}
		memcpy(grab->mods, s->mods, sizeof(*grab->mods) * grab->num);

		grab->num_syms = malloc(sizeof(*grab->num_syms) * grab->num);
		if (!grab->num_syms) {
			ret = -ENOMEM;
			goto err_grab;
		}
		memcpy(grab->num_syms, s->num_syms,
		       sizeof(*grab->num_syms) * grab->num);

		grab->keysyms = malloc(sizeof(*grab->keysyms) * grab->num);
		if (!grab->keysyms) {
			ret = -ENOMEM;
			goto err_grab;
		}
		memset(grab->keysyms, 0, sizeof(*grab->keysyms) * grab->num);
	}

	for (i = 0; i < grab->num; ++i) {
		grab->keysyms[i] = malloc(sizeof(*s->keysyms[i]) *
					  s->num_syms[i]);
		if (!grab->keysyms[i]) {
			ret = -ENOMEM;
			goto err_all;
		}
		memcpy(grab->keysyms[i], s->keysyms[i],
		       sizeof(*s->keysyms[i]) * s->num_syms[i]);
	}

	opt->type->free(opt);
	*(void**)opt->mem = grab;
	return 0;

err_all:
	for (i = 0; i < grab->num; ++i)
		free(grab->keysyms[i]);
err_grab:
	free(grab->keysyms);
	free(grab->num_syms);
	free(grab->mods);
	free(grab);
	return ret;
}

const struct conf_type conf_grab = {
	.flags = CONF_HAS_ARG,
	.set_default = conf_default_grab,
	.free = conf_free_grab,
	.parse = conf_parse_grab,
	.copy = conf_copy_grab,
};
