/**
 * \file control/control.c
 * \brief CTL interface - parse ASCII identifiers and values
 * \author Jaroslav Kysela <perex@perex.cz>
 * \date 2010
 */
/*
 *  Control Interface - ASCII parser
 *  Copyright (c) 2010 by 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 "control_local.h"
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <math.h>

/* Function to convert from percentage to volume. val = percentage */

static inline long int convert_prange1(long perc, long min, long max)
{
	long tmp;

#ifdef HAVE_SOFT_FLOAT
	tmp = perc * (max - min);
	tmp = tmp / 100 + ((tmp % 100) < 50 ? 0 : 1);
#else
	tmp = rint((double)perc * (double)(max - min) * 0.01);
#endif
	if (tmp == 0 && perc > 0)
		tmp++;
	return tmp + min;
}

#define check_range(val, min, max) \
	((val < min) ? (min) : ((val > max) ? (max) : (val)))

static long get_integer(const char **ptr, long min, long max)
{
	long val = min;
	char *p = (char *)*ptr, *s;

	if (*p == ':')
		p++;
	if (*p == '\0' || (!isdigit(*p) && *p != '-'))
		goto out;

	s = p;
	val = strtol(s, &p, 0);
	if (*p == '.') {
		p++;
		(void)strtol(p, &p, 10);
	}
	if (*p == '%') {
		val = (long)convert_prange1(strtod(s, NULL), min, max);
		p++;
	}
	val = check_range(val, min, max);
	if (*p == ',')
		p++;
 out:
	*ptr = p;
	return val;
}

static long long get_integer64(const char **ptr, long long min, long long max)
{
	long long val = min;
	char *p = (char *)*ptr, *s;

	if (*p == ':')
		p++;
	if (*p == '\0' || (!isdigit(*p) && *p != '-'))
		goto out;

	s = p;
	val = strtol(s, &p, 0);
	if (*p == '.') {
		p++;
		(void)strtol(p, &p, 10);
	}
	if (*p == '%') {
		val = (long long)convert_prange1(strtod(s, NULL), min, max);
		p++;
	}
	val = check_range(val, min, max);
	if (*p == ',')
		p++;
 out:
	*ptr = p;
	return val;
}

/**
 * \brief return ASCII CTL element identifier name
 * \param id CTL identifier
 * \return ascii identifier of CTL element
 *
 * The string is allocated using strdup().
 */
char *snd_ctl_ascii_elem_id_get(snd_ctl_elem_id_t *id)
{
	unsigned int numid, index, device, subdevice;
	char buf[256], buf1[32];
	const char *iface;

	numid = snd_ctl_elem_id_get_numid(id);
	iface = snd_ctl_elem_iface_name(snd_ctl_elem_id_get_interface(id));
	if (numid > 0) {
		snprintf(buf, sizeof(buf), "numid=%u,iface=%s,name='%s'",
				numid, iface, snd_ctl_elem_id_get_name(id));
	} else {
		snprintf(buf, sizeof(buf), "iface=%s,name='%s'",
				iface, snd_ctl_elem_id_get_name(id));
	}
	buf[sizeof(buf)-1] = '\0';
	index = snd_ctl_elem_id_get_index(id);
	device = snd_ctl_elem_id_get_device(id);
	subdevice = snd_ctl_elem_id_get_subdevice(id);
	if (index) {
		snprintf(buf1, sizeof(buf1), ",index=%u", index);
		if (strlen(buf) + strlen(buf1) < sizeof(buf))
			strcat(buf, buf1);
	}
	if (device) {
		snprintf(buf1, sizeof(buf1), ",device=%u", device);
		if (strlen(buf) + strlen(buf1) < sizeof(buf))
			strcat(buf, buf1);
	}
	if (subdevice) {
		snprintf(buf1, sizeof(buf1), ",subdevice=%u", subdevice);
		if (strlen(buf) + strlen(buf1) < sizeof(buf))
			strcat(buf, buf1);
	}
	return strdup(buf);
}

#ifndef DOC_HIDDEN
/* used by UCM parser, too */
int __snd_ctl_ascii_elem_id_parse(snd_ctl_elem_id_t *dst, const char *str,
				  const char **ret_ptr)
{
	int c, size, numid;
	int err = -EINVAL;
	char *ptr;

	while (isspace(*str))
		str++;
	if (!(*str))
		goto out;
	snd_ctl_elem_id_set_interface(dst, SND_CTL_ELEM_IFACE_MIXER);	/* default */
	while (*str) {
		if (!strncasecmp(str, "numid=", 6)) {
			str += 6;
			numid = atoi(str);
			if (numid <= 0) {
				fprintf(stderr, "amixer: Invalid numid %d\n", numid);
				goto out;
			}
			snd_ctl_elem_id_set_numid(dst, atoi(str));
			while (isdigit(*str))
				str++;
		} else if (!strncasecmp(str, "iface=", 6)) {
			str += 6;
			if (!strncasecmp(str, "card", 4)) {
				snd_ctl_elem_id_set_interface(dst, SND_CTL_ELEM_IFACE_CARD);
				str += 4;
			} else if (!strncasecmp(str, "mixer", 5)) {
				snd_ctl_elem_id_set_interface(dst, SND_CTL_ELEM_IFACE_MIXER);
				str += 5;
			} else if (!strncasecmp(str, "pcm", 3)) {
				snd_ctl_elem_id_set_interface(dst, SND_CTL_ELEM_IFACE_PCM);
				str += 3;
			} else if (!strncasecmp(str, "rawmidi", 7)) {
				snd_ctl_elem_id_set_interface(dst, SND_CTL_ELEM_IFACE_RAWMIDI);
				str += 7;
			} else if (!strncasecmp(str, "timer", 5)) {
				snd_ctl_elem_id_set_interface(dst, SND_CTL_ELEM_IFACE_TIMER);
				str += 5;
			} else if (!strncasecmp(str, "sequencer", 9)) {
				snd_ctl_elem_id_set_interface(dst, SND_CTL_ELEM_IFACE_SEQUENCER);
				str += 9;
			} else {
				goto out;
			}
		} else if (!strncasecmp(str, "name=", 5)) {
			char buf[64];
			str += 5;
			ptr = buf;
			size = 0;
			if (*str == '\'' || *str == '\"') {
				c = *str++;
				while (*str && *str != c) {
					if (size < (int)sizeof(buf)) {
						*ptr++ = *str;
						size++;
					}
					str++;
				}
				if (*str == c)
					str++;
			} else {
				while (*str && *str != ',') {
					if (size < (int)sizeof(buf)) {
						*ptr++ = *str;
						size++;
					}
					str++;
				}
			}
			*ptr = '\0';
			snd_ctl_elem_id_set_name(dst, buf);
		} else if (!strncasecmp(str, "index=", 6)) {
			str += 6;
			snd_ctl_elem_id_set_index(dst, atoi(str));
			while (isdigit(*str))
				str++;
		} else if (!strncasecmp(str, "device=", 7)) {
			str += 7;
			snd_ctl_elem_id_set_device(dst, atoi(str));
			while (isdigit(*str))
				str++;
		} else if (!strncasecmp(str, "subdevice=", 10)) {
			str += 10;
			snd_ctl_elem_id_set_subdevice(dst, atoi(str));
			while (isdigit(*str))
				str++;
		}
		if (*str == ',') {
			str++;
		} else {
			/* when ret_ptr is given, allow to terminate gracefully
			 * at the next space letter
			 */
			if (ret_ptr && isspace(*str))
				break;
			if (*str)
				goto out;
		}
	}			
	err = 0;

 out:
	if (ret_ptr)
		*ret_ptr = str;
	return err;
}
#endif

/**
 * \brief parse ASCII string as CTL element identifier
 * \param dst destination CTL identifier
 * \param str source ASCII string
 * \return zero on success, otherwise a negative error code
 */
int snd_ctl_ascii_elem_id_parse(snd_ctl_elem_id_t *dst, const char *str)
{
	return __snd_ctl_ascii_elem_id_parse(dst, str, NULL);
}

static int get_ctl_enum_item_index(snd_ctl_t *handle,
				   snd_ctl_elem_info_t *info,
				   const char **ptrp)
{ 
	char *ptr = (char *)*ptrp;
	int items, i, len;
	const char *name;
	char end;
  
	items = snd_ctl_elem_info_get_items(info);
	if (items <= 0)
		return -1;

	end = *ptr;
	if (end == '\'' || end == '"')
		ptr++;
	else
		end = '\0';

	for (i = 0; i < items; i++) {
		snd_ctl_elem_info_set_item(info, i);
		if (snd_ctl_elem_info(handle, info) < 0)
			return -1;
		name = snd_ctl_elem_info_get_item_name(info);
		len = strlen(name);
		if (strncmp(name, ptr, len))
			continue;
		if (end == '\0' && (ptr[len] == '\0' || ptr[len] == ',' || ptr[len] == '\n')) {
			*ptrp = ptr + len;
			return i;
		}
		if (end != '\0' && ptr[len] == end) {
			*ptrp = ptr + len + 1;
			return i;
		}
	}
	return -1;
}

static unsigned int get_ctl_type_max_elements(snd_ctl_elem_type_t type)
{
	struct snd_ctl_elem_value value;

	switch (type) {
	case SND_CTL_ELEM_TYPE_BOOLEAN:
	case SND_CTL_ELEM_TYPE_INTEGER:
		return ARRAY_SIZE(value.value.integer.value);
	case SND_CTL_ELEM_TYPE_INTEGER64:
		return ARRAY_SIZE(value.value.integer64.value);
	case SND_CTL_ELEM_TYPE_ENUMERATED:
		return ARRAY_SIZE(value.value.enumerated.item);
	case SND_CTL_ELEM_TYPE_BYTES:
		return ARRAY_SIZE(value.value.bytes.data);
	default:
		return 0;
	}
}

/**
 * \brief parse ASCII string as CTL element value
 * \param handle CTL handle
 * \param dst destination CTL element value
 * \param info CTL element info structure
 * \param value source ASCII string
 * \return zero on success, otherwise a negative error code
 *
 * Note: For toggle command, the dst must contain previous (current)
 * state (do the #snd_ctl_elem_read call to obtain it).
 */
int snd_ctl_ascii_value_parse(snd_ctl_t *handle,
			      snd_ctl_elem_value_t *dst,
			      snd_ctl_elem_info_t *info,
			      const char *value)
{
	const char *ptr = value;
	snd_ctl_elem_id_t myid = {0};
	snd_ctl_elem_type_t type;
	unsigned int idx, count;
	long tmp;
	long long tmp64;

	snd_ctl_elem_info_get_id(info, &myid);
	type = snd_ctl_elem_info_get_type(info);
	count = snd_ctl_elem_info_get_count(info);
	snd_ctl_elem_value_set_id(dst, &myid);

	if (count > get_ctl_type_max_elements(type))
		count = get_ctl_type_max_elements(type);
	
	for (idx = 0; idx < count && ptr && *ptr; idx++) {
		if (*ptr == ',')
			goto skip;
		switch (type) {
		case SND_CTL_ELEM_TYPE_BOOLEAN:
			tmp = 0;
			if (!strncasecmp(ptr, "on", 2) ||
			    !strncasecmp(ptr, "up", 2)) {
				tmp = 1;
				ptr += 2;
			} else if (!strncasecmp(ptr, "yes", 3)) {
				tmp = 1;
				ptr += 3;
			} else if (!strncasecmp(ptr, "toggle", 6)) {
				tmp = snd_ctl_elem_value_get_boolean(dst, idx);
				tmp = tmp > 0 ? 0 : 1;
				ptr += 6;
			} else if (isdigit(*ptr)) {
				tmp = atoi(ptr) > 0 ? 1 : 0;
				while (isdigit(*ptr))
					ptr++;
			} else {
				while (*ptr && *ptr != ',')
					ptr++;
			}
			snd_ctl_elem_value_set_boolean(dst, idx, tmp);
			break;
		case SND_CTL_ELEM_TYPE_INTEGER:
			tmp = get_integer(&ptr,
					  snd_ctl_elem_info_get_min(info),
					  snd_ctl_elem_info_get_max(info));
			snd_ctl_elem_value_set_integer(dst, idx, tmp);
			break;
		case SND_CTL_ELEM_TYPE_INTEGER64:
			tmp64 = get_integer64(&ptr,
					  snd_ctl_elem_info_get_min64(info),
					  snd_ctl_elem_info_get_max64(info));
			snd_ctl_elem_value_set_integer64(dst, idx, tmp64);
			break;
		case SND_CTL_ELEM_TYPE_ENUMERATED:
			tmp = get_ctl_enum_item_index(handle, info, &ptr);
			if (tmp < 0)
				tmp = get_integer(&ptr, 0,
					snd_ctl_elem_info_get_items(info) - 1);
			snd_ctl_elem_value_set_enumerated(dst, idx, tmp);
			break;
		case SND_CTL_ELEM_TYPE_BYTES:
			tmp = get_integer(&ptr, 0, 255);
			snd_ctl_elem_value_set_byte(dst, idx, tmp);
			break;
		default:
			break;
		}
	skip:
		if (!strchr(value, ','))
			ptr = value;
		else if (*ptr == ',')
			ptr++;
	}
	return 0;
}
