/**
 * \file confeval.c
 * \ingroup Configuration
 * \brief Configuration helper functions
 * \author Jaroslav Kysela <perex@perex.cz>
 * \date 2021
 *
 * Configuration string evaluation.
 *
 * See the \ref confarg_math page for more details.
 */
/*
 *  Configuration string evaluation
 *  Copyright (c) 2000 by Abramo Bagnara <abramo@alsa-project.org>,
 *			  Jaroslav Kysela <perex@perex.cz>
 *
 *
 *   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.1 of
 *   the License, or (at your option) any later version.
 *
 *   This program 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
 *
 */

#include "local.h"
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <limits.h>

#ifndef DOC_HIDDEN
typedef long long value_type_t;
#endif /* DOC_HIDDEN */

static const char *_find_end_of_expression(const char *s, char begin, char end)
{
	int count = 1;
	while (*s) {
		if (*s == begin) {
			count++;
		} else if (*s == end) {
			count--;
			if (count == 0)
				return s + 1;
		}
		s++;
	}
	return NULL;
}

static int _parse_integer(value_type_t *val, const char **s)
{
	long long v;
	char *end;

	errno = 0;
	v = strtoll(*s, &end, 0);
	if (errno)
		return -errno;
	*val = v;
	if (((long long)*val) != v)
		return -ERANGE;
	*s = end;
	return 0;
}

static int _to_integer(value_type_t *val, snd_config_t *c)
{
	int err;

	switch(snd_config_get_type(c)) {
	case SND_CONFIG_TYPE_INTEGER:
		{
			long v;
			err = snd_config_get_integer(c, &v);
			if (err >= 0)
				*val = v;
		}
		break;
	case SND_CONFIG_TYPE_INTEGER64:
		{
			long long v;
			err = snd_config_get_integer64(c, &v);
			if (err >= 0) {
				*val = v;
				if (((long long)*val) != v)
					return -ERANGE;
				return 0;
			}
		}
		break;
	case SND_CONFIG_TYPE_STRING:
		{
			const char *s;
			long long v;
			err = snd_config_get_string(c, &s);
			if (err >= 0) {
				err = safe_strtoll(s, &v);
				if (err >= 0) {
					*val = v;
					if (((long long)*val) != v)
						return -ERANGE;
					return 0;
				}
			}
		}
		break;
	default:
		return -EINVAL;
	}
	return err;
}

#ifndef DOC_HIDDEN
int _snd_eval_string(snd_config_t **dst, const char *s,
		     snd_config_expand_fcn_t fcn, void *private_data)
{
	snd_config_t *tmp;
	const char *save, *e;
	char *m;
	value_type_t left, right;
	int err, c, op, off;
	enum {
		LEFT,
		OP,
		RIGHT,
		END
	} pos;

	while (*s && *s <= ' ') s++;
	save = s;
	pos = LEFT;
	op = 0;
	while (*s) {
		while (*s && *s <= ' ') s++;
		c = *s;
		if (c == '\0')
			break;
		if (pos == END) {
			snd_error(CORE, "unexpected expression tail '%s'", s);
			return -EINVAL;
		}
		if (pos == OP) {
			switch (c) {
				case '+':
				case '-':
				case '*':
				case '/':
				case '%':
				case '|':
				case '&': op = c; break;
				default:
					snd_error(CORE, "unknown operation '%c'", c);
					return -EINVAL;
			}
			pos = RIGHT;
			s++;
			continue;
		}
		if (c == '(') {
			e = _find_end_of_expression(s + 1, '(', ')');
			off = 1;
			goto _expr;
		} else if (c == '$') {
			if (s[1] == '[') {
				e = _find_end_of_expression(s + 2, '[', ']');
				off = 2;
  _expr:
				if (e == NULL)
					return -EINVAL;
				m = malloc(e - s - (off - 1));
				if (m == NULL)
					return -ENOMEM;
				memcpy(m, s + off, e - s - off);
				m[e - s - (off + 1)] = '\0';
				err = _snd_eval_string(&tmp, m, fcn, private_data);
				free(m);
				if (err < 0)
					return err;
				s = e;
				if (*s)
					s++;
			} else {
				e = s + 1;
				while (*e) {
					if (!isalnum(*e) && *e != '_')
						break;
					e++;
				}
				m = malloc(e - s);
				if (m == NULL)
					return -ENOMEM;
				memcpy(m, s + 1, e - s - 1);
				m[e - s - 1] = '\0';
				err = fcn(&tmp, m, private_data);
				free(m);
				if (err < 0)
					return err;
				if (tmp == NULL) {
					err = snd_config_imake_integer(&tmp, NULL, 0);
					if (err < 0)
						return err;
				}
				s = e;
			}
			err = _to_integer(op == LEFT ? &left : &right, tmp);
			snd_config_delete(tmp);
		} else if (c == '-' || (c >= '0' && c <= '9')) {
			err = _parse_integer(op == LEFT ? &left : &right, &s);
		} else {
			return -EINVAL;
		}
		if (err < 0)
			return err;
		pos = op == LEFT ? OP : END;
	}
	if (pos != OP && pos != END) {
		snd_error(CORE, "incomplete expression '%s'", save);
		return -EINVAL;
	}

	if (pos == END) {
		switch (op) {
		case '+': left = left + right; break;
		case '-': left = left - right; break;
		case '*': left = left * right; break;
		case '/': left = left / right; break;
		case '%': left = left % right; break;
		case '|': left = left | right; break;
		case '&': left = left & right; break;
		default: return -EINVAL;
		}
	}

	if (left > INT_MAX || left < INT_MIN)
		return snd_config_imake_integer64(dst, NULL, left);
	else
		return snd_config_imake_integer(dst, NULL, left);
}
#endif /* DOC_HIDDEN */

/**
 * \brief Evaluate an math expression in the string
 * \param[out] dst The function puts the handle to the new configuration
 *                 node at the address specified by \a dst.
 * \param[in] s A string to evaluate
 * \param[in] fcn A function to get the variable contents
 * \param[in] private_data A private value for the variable contents function
 * \return 0 if successful, otherwise a negative error code.
 */
int snd_config_evaluate_string(snd_config_t **dst, const char *s,
			       snd_config_expand_fcn_t fcn, void *private_data)
{
	assert(dst && s);
	int err;

	if (*s != '$')
		return -EINVAL;
	if (s[1] == '[') {
		err = _snd_eval_string(dst, s, fcn, private_data);
		if (err < 0)
			snd_error(CORE, "wrong expression '%s'", s);
	} else {
		err = fcn(dst, s + 1, private_data);
	}
	return err;
}
