/*
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; either
 *  version 2 of the License, or (at your option) any later version.
 *
 *  This library 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
 *  Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 *  Support for the verb/device/modifier core logic and API,
 *  command line tool and file parser was kindly sponsored by
 *  Texas Instruments Inc.
 *  Support for multiple active modifiers and devices,
 *  transition sequences, multiple client access and user defined use
 *  cases was kindly sponsored by Wolfson Microelectronics PLC.
 *
 *  Copyright (C) 2019-2025 Red Hat Inc.
 *  Authors: Jaroslav Kysela <perex@perex.cz>
 */

#include "ucm_local.h"
#include <stdbool.h>
#include <sys/stat.h>
#include <limits.h>
#include <regex.h>

/* capabilities string like "*a*b*c" will be used to check library extensions */
/* use Needle like "*a*" or regex expression for a match */
#define LIB_CAPS_STRING "*" "*"

static unsigned char _hex_table[16] = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };

static char *rval_lib_caps(snd_use_case_mgr_t *uc_mgr ATTRIBUTE_UNUSED)
{
	if (uc_mgr->conf_format < 8)
		return NULL;
	return strdup(LIB_CAPS_STRING);
}

static char *rval_open_name(snd_use_case_mgr_t *uc_mgr)
{
	const char *name;
	if (uc_mgr->conf_format < 3)
		return NULL;
	name = uc_mgr->card_name;
	if (name) {
		if (strncmp(name, "strict:", 7) == 0)
			name += 7;
		return strdup(name);
	}
	return NULL;
}

static char *rval_conf_libdir(snd_use_case_mgr_t *uc_mgr)
{
	if (uc_mgr->conf_format < 4)
		return NULL;
	return strdup(snd_config_topdir());
}

static char *rval_conf_topdir(snd_use_case_mgr_t *uc_mgr)
{
	const char *dir;

	if (uc_mgr->conf_format < 3)
		return NULL;
	dir = uc_mgr_config_dir(uc_mgr->conf_format);
	if (dir && dir[0])
		return strdup(dir);
	return NULL;
}

static char *rval_conf_dir(snd_use_case_mgr_t *uc_mgr)
{
	if (uc_mgr->conf_format < 3)
		return NULL;
	if (uc_mgr->conf_dir_name && uc_mgr->conf_dir_name[0])
		return strdup(uc_mgr->conf_dir_name);
	return NULL;
}

static char *rval_conf_name(snd_use_case_mgr_t *uc_mgr)
{
	if (uc_mgr->conf_file_name && uc_mgr->conf_file_name[0])
		return strdup(uc_mgr->conf_file_name);
	return NULL;
}

static char *get_card_number(struct ctl_list *ctl_list)
{
	char num[16];

	if (ctl_list == NULL)
		return strdup("");
	snprintf(num, sizeof(num), "%i", snd_ctl_card_info_get_card(ctl_list->ctl_info));
	return strdup(num);
}

static char *rval_card_number(snd_use_case_mgr_t *uc_mgr)
{
	if (uc_mgr->conf_format < 3)
		return NULL;
	return get_card_number(uc_mgr_get_master_ctl(uc_mgr));
}

static char *rval_card_id(snd_use_case_mgr_t *uc_mgr)
{
	struct ctl_list *ctl_list;

	ctl_list = uc_mgr_get_master_ctl(uc_mgr);
	if (ctl_list == NULL)
		return NULL;
	return strdup(snd_ctl_card_info_get_id(ctl_list->ctl_info));
}

static char *rval_card_driver(snd_use_case_mgr_t *uc_mgr)
{
	struct ctl_list *ctl_list;

	ctl_list = uc_mgr_get_master_ctl(uc_mgr);
	if (ctl_list == NULL)
		return NULL;
	return strdup(snd_ctl_card_info_get_driver(ctl_list->ctl_info));
}

static char *rval_card_name(snd_use_case_mgr_t *uc_mgr)
{
	struct ctl_list *ctl_list;

	ctl_list = uc_mgr_get_master_ctl(uc_mgr);
	if (ctl_list == NULL)
		return NULL;
	return strdup(snd_ctl_card_info_get_name(ctl_list->ctl_info));
}

static char *rval_card_longname(snd_use_case_mgr_t *uc_mgr)
{
	struct ctl_list *ctl_list;

	ctl_list = uc_mgr_get_master_ctl(uc_mgr);
	if (ctl_list == NULL)
		return NULL;
	return strdup(snd_ctl_card_info_get_longname(ctl_list->ctl_info));
}

static char *rval_card_components(snd_use_case_mgr_t *uc_mgr)
{
	struct ctl_list *ctl_list;

	ctl_list = uc_mgr_get_master_ctl(uc_mgr);
	if (ctl_list == NULL)
		return NULL;
	return strdup(snd_ctl_card_info_get_components(ctl_list->ctl_info));
}

static struct ctl_list *get_ctl_list_by_name(snd_use_case_mgr_t *uc_mgr, const char *id)
{
	char *name, *index;
	long idx = 0;

	name = alloca(strlen(id) + 1);
	strcpy(name, id);
	index = strchr(name, '#');
	if (index) {
		*index = '\0';
		if (safe_strtol(index + 1, &idx))
			return NULL;
	}
	return uc_mgr_get_ctl_by_name(uc_mgr, name, idx);
}

static char *rval_card_number_by_name(snd_use_case_mgr_t *uc_mgr, const char *id)
{
	if (uc_mgr->conf_format < 3) {
		snd_error(UCM, "CardNumberByName substitution is supported in v3+ syntax");
		return NULL;
	}

	snd_error(UCM, "${CardNumberByName} substitution is obsolete - use ${find-card}!");

	return get_card_number(get_ctl_list_by_name(uc_mgr, id));
}

static char *rval_card_id_by_name(snd_use_case_mgr_t *uc_mgr, const char *id)
{
	struct ctl_list *ctl_list;

	if (uc_mgr->conf_format < 3) {
		snd_error(UCM, "CardIdByName substitution is supported in v3+ syntax");
		return NULL;
	}

	snd_error(UCM, "${CardIdByName} substitution is obsolete - use ${find-card}!");

	ctl_list = get_ctl_list_by_name(uc_mgr, id);
	if (ctl_list == NULL)
		return NULL;
	return strdup(snd_ctl_card_info_get_id(ctl_list->ctl_info));
}

#ifndef DOC_HIDDEN
typedef struct lookup_iterate *(*lookup_iter_fcn_t)
			(snd_use_case_mgr_t *uc_mgr, struct lookup_iterate *iter);
typedef const char *(*lookup_fcn_t)(void *);

struct lookup_fcn {
	char *name;
	const char *(*fcn)(void *opaque);
};

struct lookup_iterate {
	int (*init)(snd_use_case_mgr_t *uc_mgr, struct lookup_iterate *iter,
		    snd_config_t *config);
	void (*done)(struct lookup_iterate *iter);
	lookup_iter_fcn_t first;
	lookup_iter_fcn_t next;
	char *(*retfcn)(struct lookup_iterate *iter, snd_config_t *config);
	struct lookup_fcn *fcns;
	lookup_fcn_t fcn;
	struct ctl_list *ctl_list;
	void *info;
};
#endif /* DOC_HIDDEN */

static char *rval_lookup_main(snd_use_case_mgr_t *uc_mgr,
			      const char *query,
			      struct lookup_iterate *iter)
{
	snd_config_t *config, *d;
	struct lookup_fcn *fcn;
	struct lookup_iterate *curr;
	const char *s;
	char *result;
	regmatch_t match[1];
	regex_t re;
	int err;

	if (uc_mgr->conf_format < 4) {
		snd_error(UCM, "Lookups are supported in v4+ syntax");
		return NULL;
	}

	err = snd_config_load_string(&config, query, 0);
	if (err < 0) {
		snd_error(UCM, "The lookup arguments '%s' are invalid", query);
		return NULL;
	}
	if (iter->init && iter->init(uc_mgr, iter, config))
		goto null;
	if (snd_config_search(config, "field", &d)) {
		snd_error(UCM, "Lookups require field!");
		goto null;
	}
	if (snd_config_get_string(d, &s))
		goto null;
	for (fcn = iter->fcns ; fcn; fcn++) {
		if (strcasecmp(fcn->name, s) == 0) {
			iter->fcn = fcn->fcn;
			break;
		}
	}
	if (iter->fcn == NULL) {
		snd_error(UCM, "Unknown field value '%s'", s);
		goto null;
	}
	if (snd_config_search(config, "regex", &d)) {
		snd_error(UCM, "Lookups require regex!");
		goto null;
	}
	if (snd_config_get_string(d, &s))
		goto null;
	err = regcomp(&re, s, REG_EXTENDED | REG_ICASE);
	if (err) {
		snd_error(UCM, "Regex '%s' compilation failed (code %d)", s, err);
		goto null;
	}

	result = NULL;
	for (curr = iter->first(uc_mgr, iter); curr; curr = iter->next(uc_mgr, iter)) {
		s = curr->fcn(iter->info);
		if (s == NULL)
			continue;
		if (regexec(&re, s, ARRAY_SIZE(match), match, 0) == 0) {
			result = curr->retfcn(iter, config);
			break;
		}
	}
	regfree(&re);
fin:
	snd_config_delete(config);
	if (iter->done)
		iter->done(iter);
	return result;
null:
	result = NULL;
	goto fin;
}

static struct lookup_iterate *rval_card_lookup1(snd_use_case_mgr_t *uc_mgr,
						struct lookup_iterate *iter,
						int card)
{
	if (snd_card_next(&card) < 0 || card < 0)
		return NULL;
	iter->ctl_list = uc_mgr_get_ctl_by_card(uc_mgr, card);
	if (iter->ctl_list == NULL)
		return NULL;
	iter->info = iter->ctl_list->ctl_info;
	return iter;
}

static struct lookup_iterate *rval_card_lookup_first(snd_use_case_mgr_t *uc_mgr,
						     struct lookup_iterate *iter)
{
	return rval_card_lookup1(uc_mgr, iter, -1);
}

static struct lookup_iterate *rval_card_lookup_next(snd_use_case_mgr_t *uc_mgr,
						    struct lookup_iterate *iter)
{
	return rval_card_lookup1(uc_mgr, iter, snd_ctl_card_info_get_card(iter->info));
}

static char *rval_card_lookup_return(struct lookup_iterate *iter, snd_config_t *config)
{
	snd_config_t *d;
	const char *s;

	if (snd_config_search(config, "return", &d))
		return strdup(snd_ctl_card_info_get_id(iter->info));
	else if (snd_config_get_string(d, &s))
		return NULL;
	else if (strcasecmp(s, "id") == 0)
		return strdup(snd_ctl_card_info_get_id(iter->info));
	else if (strcasecmp(s, "number") == 0) {
		char num[16];
		snprintf(num, sizeof(num), "%d", snd_ctl_card_info_get_card(iter->info));
		return strdup(num);
	} else {
		snd_error(UCM, "Unknown return type '%s'", s);
		return NULL;
	}
}

static char *rval_card_lookup(snd_use_case_mgr_t *uc_mgr, const char *query)
{
	static struct lookup_fcn fcns[] = {
		{ .name = "id", (lookup_fcn_t)snd_ctl_card_info_get_id },
		{ .name = "driver", (lookup_fcn_t)snd_ctl_card_info_get_driver },
		{ .name = "name", (lookup_fcn_t)snd_ctl_card_info_get_name },
		{ .name = "longname", (lookup_fcn_t)snd_ctl_card_info_get_longname },
		{ .name = "mixername", (lookup_fcn_t)snd_ctl_card_info_get_mixername },
		{ .name = "components", (lookup_fcn_t)snd_ctl_card_info_get_components },
		{ 0 },
	};
	struct lookup_iterate iter = {
		.first = rval_card_lookup_first,
		.next = rval_card_lookup_next,
		.retfcn = rval_card_lookup_return,
		.fcns = fcns,
	};
	return rval_lookup_main(uc_mgr, query, &iter);
}

static struct lookup_iterate *rval_pcm_lookup1(struct lookup_iterate *iter,
					       int device)
{
	snd_pcm_info_t *pcminfo;
	snd_ctl_t *ctl = iter->ctl_list->ctl;
	int err;

next:
	if (snd_ctl_pcm_next_device(ctl, &device) < 0 || device < 0)
		return NULL;
	pcminfo = iter->info;
	snd_pcm_info_set_device(pcminfo, device);
	err = snd_ctl_pcm_info(ctl, pcminfo);
	if (err < 0) {
		if (err == -ENOENT)
			goto next;
		snd_error(UCM, "Unable to obtain PCM info (device %d)", device);
		return NULL;
	}
	return iter;
}

static struct lookup_iterate *rval_pcm_lookup_first(snd_use_case_mgr_t *uc_mgr ATTRIBUTE_UNUSED,
						    struct lookup_iterate *iter)
{
	return rval_pcm_lookup1(iter, -1);
}

static struct lookup_iterate *rval_pcm_lookup_next(snd_use_case_mgr_t *uc_mgr ATTRIBUTE_UNUSED,
						   struct lookup_iterate *iter)
{
	return rval_pcm_lookup1(iter, snd_pcm_info_get_device(iter->info));
}

static char *rval_pcm_lookup_return(struct lookup_iterate *iter,
				    snd_config_t *config ATTRIBUTE_UNUSED)
{
	char num[16];
	snprintf(num, sizeof(num), "%d", snd_pcm_info_get_device(iter->info));
	return strdup(num);
}

static int rval_pcm_lookup_init(struct lookup_iterate *iter,
				snd_config_t *config)
{
	static struct lookup_fcn pcm_fcns[] = {
		{ .name = "id", (lookup_fcn_t)snd_pcm_info_get_id },
		{ .name = "name", (lookup_fcn_t)snd_pcm_info_get_name },
		{ .name = "subname", (lookup_fcn_t)snd_pcm_info_get_subdevice_name },
		{ 0 },
	};
	snd_config_t *d;
	const char *s;
	snd_pcm_info_t *pcminfo;
	snd_pcm_stream_t stream = SND_PCM_STREAM_PLAYBACK;

	if (snd_config_search(config, "stream", &d) == 0 &&
	    snd_config_get_string(d, &s) == 0) {
		if (strcasecmp(s, "playback") == 0)
			stream = SND_PCM_STREAM_PLAYBACK;
		else if (strcasecmp(s, "capture") == 0)
			stream = SND_PCM_STREAM_CAPTURE;
		else {
			snd_error(UCM, "Unknown stream type '%s'", s);
			return -EINVAL;
		}
	}
	if (snd_pcm_info_malloc(&pcminfo))
		return -ENOMEM;
	snd_pcm_info_set_device(pcminfo, 0);
	snd_pcm_info_set_subdevice(pcminfo, 0);
	snd_pcm_info_set_stream(pcminfo, stream);
	iter->first = rval_pcm_lookup_first;
	iter->next = rval_pcm_lookup_next;
	iter->retfcn = rval_pcm_lookup_return;
	iter->fcns = pcm_fcns;
	iter->info = pcminfo;
	return 0;
}

static int rval_device_lookup_init(snd_use_case_mgr_t *uc_mgr,
				   struct lookup_iterate *iter,
				   snd_config_t *config)
{
	static struct {
		const char *name;
		int (*init)(struct lookup_iterate *iter, snd_config_t *config);
	} *t, types[] = {
		{ .name = "pcm", .init = rval_pcm_lookup_init },
		{ 0 }
	};
	snd_config_t *d;
	const char *s;
	int err;

	if (snd_config_search(config, "ctl", &d) || snd_config_get_string(d, &s)) {
		iter->ctl_list = uc_mgr_get_master_ctl(uc_mgr);
		if (iter->ctl_list == NULL) {
			snd_error(UCM, "Control device is not defined!");
			return -EINVAL;
		}
	} else {
		err = uc_mgr_open_ctl(uc_mgr, &iter->ctl_list, s, 1);
		if (err < 0) {
			snd_error(UCM, "Control device '%s' not found", s);
			return -EINVAL;
		}
	}
	if (snd_config_search(config, "type", &d) || snd_config_get_string(d, &s)) {
		snd_error(UCM, "Missing device type!");
		return -EINVAL;
	}
	for (t = types; t->name; t++)
		if (strcasecmp(t->name, s) == 0)
			return t->init(iter, config);
	snd_error(UCM, "Device type '%s' is invalid", s);
	return -EINVAL;
}

static void rval_device_lookup_done(struct lookup_iterate *iter)
{
	free(iter->info);
}

static char *rval_device_lookup(snd_use_case_mgr_t *uc_mgr, const char *query)
{
	struct lookup_iterate iter = {
		.init = rval_device_lookup_init,
		.done = rval_device_lookup_done,
	};
	return rval_lookup_main(uc_mgr, query, &iter);
}

static char *rval_env(snd_use_case_mgr_t *uc_mgr ATTRIBUTE_UNUSED, const char *id)
{
	char *e;

	if (*id == '-') {
		e = getenv(id + 1);
		if (e == NULL)
			e = "";
	} else {
		e = getenv(id);
	}
	if (e)
		return strdup(e);
	return NULL;
}

#define RANGE_TYPE_ASCII 0
#define RANGE_TYPE_HEX   1

static int parse_position(snd_config_t *config, const char *name, ssize_t *pos, bool optional)
{
	snd_config_t *d;
	const char *s;
	long v;

	if (snd_config_search(config, name, &d)) {
		if (optional) {
			*pos = -1;
			return 0;
		}
		snd_error(UCM, "Unable to find field '%s'", name);
		return -1;
	}
	if (!snd_config_get_integer(d, &v))
		goto fin;
	if (snd_config_get_string(d, &s))
		return -1;
	if (safe_strtol(s, &v)) {
		snd_error(UCM, "Unable to parse position '%s'", s);
		return -1;
	}
fin:
	*pos = v;
	return 0;
}

static int parse_range(const char *cfg, int *type, ssize_t *pos, ssize_t *size)
{
	snd_config_t *config, *d;
	int err, retval = 0;
	const char *s;

	err = snd_config_load_string(&config, cfg, 0);
	if (err < 0) {
		snd_error(UCM, "The range arguments '%s' are invalid", cfg);
		return -1;
	}
	if (snd_config_search(config, "type", &d)) {
		*type = RANGE_TYPE_ASCII;
	} else {
		if (snd_config_get_string(d, &s))
			goto null;
		if (strcasecmp(s, "ascii") == 0) {
			*type = RANGE_TYPE_ASCII;
		} else if (strcasecmp(s, "hex") == 0) {
			*type = RANGE_TYPE_HEX;
		} else {
			snd_error(UCM, "Unknown range type '%s'", s);
		}
	}
	*pos = 0;
	*size = -1;
	if (parse_position(config, "pos", pos, false) ||
	    parse_position(config, "size", size, true)) {
		retval = -1;
		goto null;
	}

	if (*size <= 0)
		*size = 1;
	if (*pos < 0) {
		snd_error(UCM, "Invalid start position");
		retval = -1;
		goto null;
	}

null:
	snd_config_delete(config);
	return retval;
}

static char *rval_sysfs_main(snd_use_case_mgr_t *uc_mgr, const char *top_path, const char *id)
{
	char path[PATH_MAX], link[PATH_MAX + 1];
	struct stat64 sb;
	ssize_t len, range_start = -1, range_size = -1;
	const char *e, *s;
	int fd, type = RANGE_TYPE_ASCII;

	e = uc_mgr_sysfs_root();
	if (e == NULL)
		return NULL;
	if (id[0] == '[') {
		if (uc_mgr->conf_format < 8) {
			snd_error(UCM, "Sysfs ranges are supported in v8+ syntax");
			return NULL;
		}
		s = strchr(id, ']');
		if (s == NULL)
			return NULL;
		len = s - id - 1;
		if ((size_t)len > sizeof(link) - 1)
			return NULL;
		strncpy(link, id + 1, len);
		link[len] = '\0';
		if (parse_range(link, &type, &range_start, &range_size)) {
			snd_error(UCM, "sysfs: cannot parse hex range '%s'", link);
			return NULL;
		}
		id = s + 1;
	}
	if (id[0] == '/')
		id++;
	if (top_path)
		snprintf(path, sizeof(path), "%s/%s/%s", e, top_path, id);
	else
		snprintf(path, sizeof(path), "%s/%s", e, id);
	if (lstat64(path, &sb) != 0)
		return NULL;
	if (S_ISLNK(sb.st_mode)) {
		len = readlink(path, link, sizeof(link) - 1);
		if (len <= 0) {
			snd_error(UCM, "sysfs: cannot read link '%s' (%d)", path, errno);
			return NULL;
		}
		link[len] = '\0';
		e = strrchr(link, '/');
		if (e)
			return strdup(e + 1);
		return NULL;
	}
	if (S_ISDIR(sb.st_mode))
		return NULL;
	if ((sb.st_mode & S_IRUSR) == 0)
		return NULL;

	fd = open(path, O_RDONLY);
	if (fd < 0) {
		snd_error(UCM, "sysfs open failed for '%s' (%d)", path, errno);
		return NULL;
	}
	len = sizeof(path) - 1;
	if (range_start > 0 && lseek(fd, range_start, SEEK_SET) != range_start) {
		snd_error(UCM, "sysfs seek failed (%d)", errno);
		close(fd);
		return NULL;
	}
	if (range_size > 0) {
		if (range_size > len) {
			snd_error(UCM, "sysfs EOB for '%s'", path);
			close(fd);
			return NULL;
		} else {
			len = range_size;
		}
	}
	len = read(fd, path, len);
	close(fd);
	if (len < 0) {
		snd_error(UCM, "sysfs unable to read value '%s' (%d)", path, errno);
		return NULL;
	}
	if (type == RANGE_TYPE_HEX && range_start >= 0) {
		char *m = malloc(len * 2 + 1);
		ssize_t idx;
		if (m == NULL)
			return NULL;
		for (idx = 0; idx < len; idx++) {
			m[(idx * 2) + 0] = _hex_table[((unsigned char)path[idx]) >> 4];
			m[(idx * 2) + 1] = _hex_table[((unsigned char)path[idx]) & 0x0f];
		}
		m[len * 2] = '\0';
		return m;
	}
	while (len > 0 && path[len-1] == '\n')
		len--;
	path[len] = '\0';
	return strdup(path);
}

static char *rval_sysfs(snd_use_case_mgr_t *uc_mgr, const char *id)
{
	return rval_sysfs_main(uc_mgr, NULL, id);
}

static char *rval_sysfs_card(snd_use_case_mgr_t *uc_mgr, const char *id)
{
	char top_path[32], *s;

	if (uc_mgr->conf_format < 8) {
		snd_error(UCM, "sys-card is supported in v8+ syntax");
		return NULL;
	}
	s = get_card_number(uc_mgr_get_master_ctl(uc_mgr));
	snprintf(top_path, sizeof(top_path), "class/sound/card%s", s);
	free(s);
	return rval_sysfs_main(uc_mgr, top_path, id);
}

static char *rval_var(snd_use_case_mgr_t *uc_mgr, const char *id)
{
	const char *v;
	bool ignore_not_found = false;

	if (uc_mgr->conf_format < 3) {
		snd_error(UCM, "variable substitution is supported in v3+ syntax");
		return NULL;
	}

	if (id[0] == '-') {
		ignore_not_found = true;
		id++;
	} else if (id[0] == '@') {
		ignore_not_found = true;
	}
	v = uc_mgr_get_variable(uc_mgr, id);
	if (v == NULL && ignore_not_found)
		v = "";
	if (v)
		return strdup(v);
	return NULL;
}

int _snd_eval_string(snd_config_t **dst, const char *s,
		     snd_config_expand_fcn_t fcn, void *private_data);

static int rval_eval_var_cb(snd_config_t **dst, const char *s, void *private_data)
{
	snd_use_case_mgr_t *uc_mgr = private_data;
	const char *v;

	v = uc_mgr_get_variable(uc_mgr, s);
	if (v == NULL)
		return -ENOENT;
	return snd_config_imake_string(dst, NULL, v);
}

static char *rval_eval(snd_use_case_mgr_t *uc_mgr, const char *e)
{
	snd_config_t *dst;
	char *r;
	int err;

	if (uc_mgr->conf_format < 5) {
		snd_error(UCM, "variable evaluation is supported in v5+ syntax");
		return NULL;
	}
	err = _snd_eval_string(&dst, e, rval_eval_var_cb, uc_mgr);
	if (err < 0) {
		snd_error(UCM, "unable to evaluate '%s'", e);
		return NULL;
	}
	err = snd_config_get_ascii(dst, &r);
	snd_config_delete(dst);
	if (err < 0)
		return NULL;
	return r;
}

static int rval_evali(snd_use_case_mgr_t *uc_mgr, snd_config_t *node, const char *e)
{
	snd_config_t *dst;
	const char *id;
	char *s;
	size_t l;
	int err;

	if (uc_mgr->conf_format < 6) {
		snd_error(UCM, "variable evaluation is supported in v6+ syntax");
		return -EINVAL;
	}
	err = snd_config_get_id(node, &id);
	if (err < 0)
		return err;
	l = strlen(e);
	if (e[l-1] != '}')
		return -EINVAL;
	s = malloc(l + 1);
	if (s == NULL)
		return -ENOMEM;
	strcpy(s, e);
	s[l-1] = '\0';
	err = _snd_eval_string(&dst, s + 8, rval_eval_var_cb, uc_mgr);
	free(s);
	if (err < 0) {
		snd_error(UCM, "unable to evaluate '%s'", e);
		return err;
	}
	err = snd_config_set_id(dst, id);
	if (err < 0)
		return err;
	return snd_config_substitute(node, dst);
}

#define MATCH_VARIABLE(name, id, fcn, empty_ok)				\
	if (strncmp((name), (id), sizeof(id) - 1) == 0) { 		\
		rval = fcn(uc_mgr);					\
		idsize = sizeof(id) - 1;				\
		allow_empty = (empty_ok);				\
		goto __rval;						\
	}

#define MATCH_VARIABLE2(name, id, fcn, empty_ok)			\
	if (strncmp((name), (id), sizeof(id) - 1) == 0) {		\
		idsize = sizeof(id) - 1;				\
		allow_empty = (empty_ok);				\
		fcn2 = (fcn);						\
		goto __match2;						\
	}

/*
 * skip escaped } character (simple version)
 */
static inline const char *strchr_with_escape(const char *str, char c)
{
	char *s;

	while (1) {
		s = strchr(str, c);
		if (s && s != str) {
			if (*(s - 1) == '\\') {
				str = s + 1;
				continue;
			}
		}
		return s;
	}
}

/*
 * remove escaped } character (simple version)
 */
static inline void strncpy_with_escape(char *dst, const char *src, size_t len)
{
	char c;

	c = *src++;
	while (c != '\0' && len > 0) {
		if (c == '\\' && *src == '}') {
			c = *src++;
			len--;
		}
		*dst++ = c;
		len--;
		c = *src++;
	}
	*dst = '\0';
}

int uc_mgr_get_substituted_value(snd_use_case_mgr_t *uc_mgr,
				 char **_rvalue,
				 const char *value)
{
	size_t size, nsize, idsize, rvalsize, dpos = 0;
	const char *tmp;
	char *r, *nr, *rval, v2[128];
	bool ignore_error, allow_empty;
	char *(*fcn2)(snd_use_case_mgr_t *, const char *id);
	int err;

	if (value == NULL)
		return -ENOENT;

	size = strlen(value) + 1;
	r = malloc(size);
	if (r == NULL)
		return -ENOMEM;

	while (*value) {
		if (*value != '$') {
__std:
			r[dpos++] = *value;
			value++;
			continue;
		}
		ignore_error = false;
		if (value[1] == '$' && value[2] == '{' && uc_mgr->conf_format >= 3) {
			value++;
			ignore_error = true;
		} else if (value[1] != '{') {
			goto __std;
		}
		fcn2 = NULL;
		MATCH_VARIABLE(value, "${LibCaps}", rval_lib_caps, false);
		MATCH_VARIABLE(value, "${OpenName}", rval_open_name, false);
		MATCH_VARIABLE(value, "${ConfLibDir}", rval_conf_libdir, false);
		MATCH_VARIABLE(value, "${ConfTopDir}", rval_conf_topdir, false);
		MATCH_VARIABLE(value, "${ConfDir}", rval_conf_dir, false);
		MATCH_VARIABLE(value, "${ConfName}", rval_conf_name, false);
		MATCH_VARIABLE(value, "${CardNumber}", rval_card_number, true);
		MATCH_VARIABLE(value, "${CardId}", rval_card_id, false);
		MATCH_VARIABLE(value, "${CardDriver}", rval_card_driver, false);
		MATCH_VARIABLE(value, "${CardName}", rval_card_name, false);
		MATCH_VARIABLE(value, "${CardLongName}", rval_card_longname, false);
		MATCH_VARIABLE(value, "${CardComponents}", rval_card_components, true);
		MATCH_VARIABLE2(value, "${env:", rval_env, false);
		MATCH_VARIABLE2(value, "${sys:", rval_sysfs, false);
		MATCH_VARIABLE2(value, "${sys-card:", rval_sysfs_card, false);
		MATCH_VARIABLE2(value, "${var:", rval_var, true);
		MATCH_VARIABLE2(value, "${eval:", rval_eval, false);
		MATCH_VARIABLE2(value, "${find-card:", rval_card_lookup, false);
		MATCH_VARIABLE2(value, "${find-device:", rval_device_lookup, false);
		MATCH_VARIABLE2(value, "${CardNumberByName:", rval_card_number_by_name, false);
		MATCH_VARIABLE2(value, "${CardIdByName:", rval_card_id_by_name, false);
__merr:
		err = -EINVAL;
		tmp = strchr(value, '}');
		if (tmp) {
			strncpy(r, value, tmp + 1 - value);
			r[tmp + 1 - value] = '\0';
			snd_error(UCM, "variable '%s' is not known!", r);
		} else {
			snd_error(UCM, "variable reference '%s' is not complete", value);
		}
		goto __error;
__match2:
		tmp = strchr_with_escape(value + idsize, '}');
		if (tmp) {
			rvalsize = tmp - (value + idsize);
			if (rvalsize >= sizeof(v2)) {
				err = -ENOMEM;
				goto __error;
			}
			strncpy_with_escape(v2, value + idsize, rvalsize);
			idsize += rvalsize + 1;
			if (*v2 == '$' && uc_mgr->conf_format >= 3) {
				if (strncmp(value, "${eval:", 7) == 0)
					goto __direct_fcn2;
				tmp = uc_mgr_get_variable(uc_mgr, v2 + 1);
				if (tmp == NULL) {
					snd_error(UCM, "define '%s' is not reachable in this context!", v2 + 1);
					rval = NULL;
				} else {
					rval = fcn2(uc_mgr, tmp);
				}
			} else {
__direct_fcn2:
				rval = fcn2(uc_mgr, v2);
			}
			goto __rval;
		}
		goto __merr;
__rval:
		if (rval == NULL || (!allow_empty && rval[0] == '\0')) {
			free(rval);
			if (ignore_error) {
				value += idsize;
				continue;
			}
			strncpy(r, value, idsize);
			r[idsize] = '\0';
			snd_error(UCM, "variable '%s' is %s in this context!", r,
				       rval ? "empty" : "not defined");

			err = -EINVAL;
			goto __error;
		}
		value += idsize;
		rvalsize = strlen(rval);
		nsize = size + rvalsize - idsize;
		if (nsize > size) {
			nr = realloc(r, nsize);
			if (nr == NULL) {
				free(rval);
				err = -ENOMEM;
				goto __error;
			}
			size = nsize;
			r = nr;
		}
		strcpy(r + dpos, rval);
		dpos += rvalsize;
		free(rval);
	}
	r[dpos] = '\0';

	*_rvalue = r;
	return 0;

__error:
	free(r);
	return err;
}

static inline int uc_mgr_substitute_check(const char *s)
{
	return s && strstr(s, "${") != NULL;
}

int uc_mgr_substitute_tree(snd_use_case_mgr_t *uc_mgr, snd_config_t *node)
{
	snd_config_iterator_t i, next;
	snd_config_t *n;
	const char *id, *s2;
	char *s;
	int err;

	err = snd_config_get_id(node, &id);
	if (err < 0)
		return err;
	if (uc_mgr_substitute_check(id)) {
		err = uc_mgr_get_substituted_value(uc_mgr, &s, id);
		if (err < 0)
			return err;
		err = snd_config_set_id(node, s);
		if (err < 0) {
			snd_error(UCM, "unable to set substituted id '%s' (old id '%s')", s, id);
			free(s);
			return err;
		}
		free(s);
	}
	if (snd_config_get_type(node) != SND_CONFIG_TYPE_COMPOUND) {
		if (snd_config_get_type(node) == SND_CONFIG_TYPE_STRING) {
			err = snd_config_get_string(node, &s2);
			if (err < 0)
				return err;
			if (!uc_mgr_substitute_check(s2))
				return 0;
			if (strncmp(s2, "${evali:", 8) == 0)
				return rval_evali(uc_mgr, node, s2);
			err = uc_mgr_get_substituted_value(uc_mgr, &s, s2);
			if (err < 0)
				return err;
			err = snd_config_set_string(node, s);
			free(s);
			if (err < 0)
				return err;
		}
		return 0;
	}
	/* exception - macros are evaluated when instantied */
	err = snd_config_get_id(node, &id);
	if (err < 0)
		return err;
	if (id && strcmp(id, "DefineMacro") == 0)
		return 0;
	snd_config_for_each(i, next, node) {
		n = snd_config_iterator_entry(i);
		err = uc_mgr_substitute_tree(uc_mgr, n);
		if (err < 0)
			return err;
	}
	return 0;
}
