/*
 *  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) 2020 Red Hat Inc.
 *  Authors: Jaroslav Kysela <perex@perex.cz>
 */

#include "ucm_local.h"

static int get_string(snd_config_t *compound, const char *key, const char **str)
{
	snd_config_t *node;
	int err;

	err = snd_config_search(compound, key, &node);
	if (err < 0)
		return err;
	return snd_config_get_string(node, str);
}

static int include_eval_one(snd_use_case_mgr_t *uc_mgr,
			    snd_config_t *inc,
			    snd_config_t **result,
			    snd_config_t **before,
			    snd_config_t **after)
{
	snd_config_t *opt;
	const char *file;
	bool opt_bool = false;
	char *s;
	int err;

	*result = NULL;

	if (snd_config_get_type(inc) != SND_CONFIG_TYPE_COMPOUND) {
		snd_error(UCM, "compound type expected for Include.1");
		return -EINVAL;
	}

	err = get_string(inc, "File", &file);
	if (err < 0) {
		snd_error(UCM, "file expected (Include)");
		return -EINVAL;
	}

	err = snd_config_search(inc, "Before", before);
	if (err < 0 && err != -ENOENT) {
		snd_error(UCM, "before block identifier error");
		return -EINVAL;
	}

	err = snd_config_search(inc, "After", after);
	if (err < 0 && err != -ENOENT) {
		snd_error(UCM, "before block identifier error");
		return -EINVAL;
	}

	err = snd_config_search(inc, "Optional", &opt);
	if (err < 0 && err != -ENOENT) {
		snd_error(UCM, "optional error");
		return -EINVAL;
	} else if (err == 0) {
		err = snd_config_get_bool(opt);
		if (err < 0) {
			snd_error(UCM, "optional format error");
			return -EINVAL;
		}
		opt_bool = err > 0;
	}

	err = uc_mgr_get_substituted_value(uc_mgr, &s, file);
	if (err < 0)
		return err;
	if (opt_bool && access(s, R_OK) != 0) {
		snd_trace(UCM, "optional file '%s' not found", s);
		err = 0;
	} else {
		err = uc_mgr_config_load_file(uc_mgr, s, result);
	}
	free(s);
	return err;
}

#if 0
static void config_dump(snd_config_t *cfg)
{
	snd_output_t *out;
	snd_output_stdio_attach(&out, stderr, 0);
	snd_output_printf(out, "-----\n");
	snd_config_save(cfg, out);
	snd_output_close(out);
}
#endif

static int find_position_node(snd_use_case_mgr_t *uc_mgr,
			      snd_config_t **res, snd_config_t *dst,
			      const char *id, snd_config_t *pos)
{
	const char *s;
	char *s1;
	int err;

	err = get_string(pos, id, &s);
	if (err < 0 && err != -ENOENT)
		return err;
	if (err == 0) {
		err = uc_mgr_get_substituted_value(uc_mgr, &s1, s);
		if (err < 0)
			return err;
		err = snd_config_search(dst, s1, res);
		free(s1);
		if (err < 0 && err != -ENOENT)
			return err;
	}
	return 0;
}

static int merge_it(snd_config_t *dst, snd_config_t *n, snd_config_t **_dn)
{
	snd_config_t *dn;
	const char *id;
	int err;

	err = snd_config_get_id(n, &id);
	if (err < 0)
		return err;
	err = snd_config_search(dst, id, &dn);
	if (err < 0)
		return err;
	err = snd_config_merge(dn, n, 0); /* merge / append mode */
	if (err < 0)
		snd_config_delete(n);
	else
		*_dn = dn;
	return err;
}

static int compound_merge(snd_use_case_mgr_t *uc_mgr, const char *id,
			  snd_config_t *dst, snd_config_t *src,
			  snd_config_t *before, snd_config_t *after)
{
	snd_config_iterator_t i, next;
	snd_config_t *n, *_before = NULL, *_after = NULL;
	char tmpid[32];
	int err, array, idx;

	if (snd_config_get_type(src) != SND_CONFIG_TYPE_COMPOUND) {
		snd_error(UCM, "compound type expected for the merged block");
		return -EINVAL;
	}

	if (before) {
		err = find_position_node(uc_mgr, &_before, dst, id, before);
		if (err < 0)
			return err;
	}
	if (after) {
		err = find_position_node(uc_mgr, &_after, dst, id, after);
		if (err < 0)
			return err;
	}

	/* direct merge? */
	if (!_before && !_after)
		return snd_config_merge(dst, src, 0);	/* merge / append mode */

	if (_before && _after) {
		snd_error(UCM, "defined both before and after identifiers in the If or Include block");
		return -EINVAL;
	}

	array = snd_config_is_array(dst);
	if (array < 0) {
		snd_error(UCM, "destination configuration node is not a compound");
		return array;
	}
	if (array && snd_config_is_array(src) <= 0) {
		snd_error(UCM, "source configuration node is not an array");
		return -EINVAL;
	}

	idx = 0;

	/* for array, use a temporary non-clashing identifier */
	if (array > 0) {
		snd_config_for_each(i, next, dst) {
			n = snd_config_iterator_entry(i);
			snprintf(tmpid, sizeof(tmpid), "_tmp_%d", idx++);
			err = snd_config_set_id(n, tmpid);
			if (err < 0)
				return err;
		}
	}

	snd_config_for_each(i, next, src) {
		n = snd_config_iterator_entry(i);
		err = snd_config_remove(n);
		if (err < 0)
			return err;
		/* for array, use a temporary non-clashing identifier */
		if (array > 0) {
			snprintf(tmpid, sizeof(tmpid), "_tmp_%d", idx++);
			err = snd_config_set_id(n, tmpid);
			if (err < 0)
				return err;
		}
		if (_before) {
			err = snd_config_add_before(_before, n);
			if (err == -EEXIST)
				err = merge_it(dst, n, &n);
			if (err < 0)
				return err;
			_before = NULL;
			_after = n;
		} else if (_after) {
			err = snd_config_add_after(_after, n);
			if (err == -EEXIST)
				err = merge_it(dst, n, &n);
			if (err < 0)
				return err;
			_after = n;
		}
	}

	/* set new indexes for the final array */
	if (array > 0) {
		idx = 0;
		snd_config_for_each(i, next, dst) {
			n = snd_config_iterator_entry(i);
			snprintf(tmpid, sizeof(tmpid), "%d", idx++);
			err = snd_config_set_id(n, tmpid);
			if (err < 0)
				return err;
		}
	}

	snd_config_delete(src);
	return 0;
}

int uc_mgr_config_tree_merge(snd_use_case_mgr_t *uc_mgr,
			     snd_config_t *parent, snd_config_t *new_ctx,
			     snd_config_t *before, snd_config_t *after)
{
	snd_config_iterator_t i, next;
	snd_config_t *n, *parent2;
	const char *id;
	int err;

	err = uc_mgr_substitute_tree(uc_mgr, new_ctx);
	if (err < 0)
		return err;

	snd_config_for_each(i, next, new_ctx) {
		n = snd_config_iterator_entry(i);
		err = snd_config_remove(n);
		if (err < 0)
			return err;
		err = snd_config_get_id(n, &id);
		if (err < 0) {
__add:
			err = snd_config_add(parent, n);
			if (err < 0)
				return err;
		} else {
			err = snd_config_search(parent, id, &parent2);
			if (err == -ENOENT)
				goto __add;
			err = compound_merge(uc_mgr, id, parent2, n, before, after);
			if (err < 0) {
				snd_config_delete(n);
				return err;
			}
		}
	}
	return 0;
}

/*
 * put back the included configuration to the parent
 */
int uc_mgr_evaluate_include(snd_use_case_mgr_t *uc_mgr,
			      snd_config_t *parent,
			      snd_config_t *inc)
{
	snd_config_iterator_t i, next;
	snd_config_t *a, *n, *before, *after;
	int err;

	if (uc_mgr->conf_format < 3) {
		snd_error(UCM, "in-place include is supported in v3+ syntax");
		return -EINVAL;
	}

	if (snd_config_get_type(inc) != SND_CONFIG_TYPE_COMPOUND) {
		snd_error(UCM, "compound type expected for Include");
		return -EINVAL;
	}

	snd_config_for_each(i, next, inc) {
		n = snd_config_iterator_entry(i);
		before = after = NULL;
		err = include_eval_one(uc_mgr, n, &a, &before, &after);
		if (err < 0)
			return err;
		if (a == NULL)
			continue;
		err = uc_mgr_evaluate_inplace(uc_mgr, a);
		if (err < 0) {
			snd_config_delete(a);
			return err;
		}
		err = uc_mgr_config_tree_merge(uc_mgr, parent, a, before, after);
		snd_config_delete(a);
		if (err < 0)
			return err;
	}
	return 0;
}
