/*-
 * Copyright (c) 2021 Christos Margiolis <christos@FreeBSD.org>
 *
 * 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.
 */

#include <sys/sysctl.h>
#include <sys/wait.h>

#include <err.h>
#include <errno.h>
#include <mixer.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

enum {
	C_VOL = 0,
	C_MUT,
	C_SRC,
};

static void usage(void) __dead2;
static void initctls(struct mixer *);
static void printall(struct mixer *, int);
static void printminfo(struct mixer *, int);
static void printdev(struct mixer *, int);
static void printrecsrc(struct mixer *, int); /* XXX: change name */
static int set_dunit(struct mixer *, int, char *);
/* Control handlers */
static int mod_volume(struct mix_dev *, void *);
static int mod_mute(struct mix_dev *, void *);
static int mod_recsrc(struct mix_dev *, void *);
static int print_volume(struct mix_dev *, void *);
static int print_mute(struct mix_dev *, void *);
static int print_recsrc(struct mix_dev *, void *);

int
main(int argc, char *argv[])
{
	struct mixer *m;
	mix_ctl_t *cp;
	char *name = NULL, buf[NAME_MAX], *vctl = NULL;
	char *p, *q, *devstr, *ctlstr, *valstr = NULL;
	int dunit, i, n, pall = 1, shorthand;
	int aflag = 0, dflag = 0, oflag = 0, sflag = 0;
	int ch;

	while ((ch = getopt(argc, argv, "ad:f:hosV:")) != -1) {
		switch (ch) {
		case 'a':
			aflag = 1;
			break;
		case 'd':
			if (strncmp(optarg, "pcm", 3) == 0)
				optarg += 3;
			errno = 0;
			dunit = strtol(optarg, NULL, 10);
			if (errno == EINVAL || errno == ERANGE)
				err(1, "strtol(%s)", optarg);
			dflag = 1;
			break;
		case 'f':
			name = optarg;
			break;
		case 'o':
			oflag = 1;
			break;
		case 's':
			sflag = 1;
			break;
		case 'V':
			vctl = optarg;
			break;
		case 'h': /* FALLTHROUGH */
		case '?':
		default:
			usage();
		}
	}
	argc -= optind;
	argv += optind;

	/* Print all mixers and exit. */
	if (aflag) {
		if ((n = mixer_get_nmixers()) < 0)
			errx(1, "no mixers present in the system");
		for (i = 0; i < n; i++) {
			(void)mixer_get_path(buf, sizeof(buf), i);
			if ((m = mixer_open(buf)) == NULL)
				continue;
			initctls(m);
			if (sflag)
				printrecsrc(m, oflag);
			else {
				printall(m, oflag);
				if (oflag)
					printf("\n");
			}
			(void)mixer_close(m);
		}
		return (0);
	}

	if ((m = mixer_open(name)) == NULL)
		errx(1, "%s: no such mixer", name);

	initctls(m);

	if (dflag) {
		if (set_dunit(m, dunit, vctl) < 0)
			goto parse;
		else {
			/*
			 * Open current mixer since we changed the default
			 * unit, otherwise we'll print and apply changes to the
			 * old one.
			 */
			(void)mixer_close(m);
			if ((m = mixer_open(NULL)) == NULL)
				errx(1, "cannot open default mixer");
			initctls(m);
		}
	}
	if (sflag) {
		printrecsrc(m, oflag);
		(void)mixer_close(m);
		return (0);
	}

parse:
	while (argc > 0) {
		char *orig;

		if ((orig = p = strdup(*argv)) == NULL)
			err(1, "strdup(%s)", *argv);

		/* Check if we're using the shorthand syntax for volume setting. */
		shorthand = 0;
		for (q = p; *q != '\0'; q++) {
			if (*q == '=') {
				q++;
				shorthand = ((*q >= '0' && *q <= '9') ||
				    *q == '+' || *q == '-' || *q == '.');
				break;
			} else if (*q == '.')
				break;
		}

		/* Split the string into device, control and value. */
		devstr = strsep(&p, ".=");
		if ((m->dev = mixer_get_dev_byname(m, devstr)) == NULL) {
			warnx("%s: no such device", devstr);
			goto next;
		}
		/* Input: `dev`. */
		if (p == NULL) {
			printdev(m, 1);
			pall = 0;
			goto next;
		} else if (shorthand) {
			/*
			 * Input: `dev=N` -> shorthand for `dev.volume=N`.
			 *
			 * We don't care what the rest of the string contains as
			 * long as we're sure the very beginning is right,
			 * mod_volume() will take care of parsing it properly.
			 */
			cp = mixer_get_ctl(m->dev, C_VOL);
			cp->mod(cp->parent_dev, p);
			goto next;
		}
		ctlstr = strsep(&p, "=");
		if ((cp = mixer_get_ctl_byname(m->dev, ctlstr)) == NULL) {
			warnx("%s.%s: no such control", devstr, ctlstr);
			goto next;
		}
		/* Input: `dev.control`. */
		if (p == NULL) {
			(void)cp->print(cp->parent_dev, cp->name);
			pall = 0;
			goto next;
		}
		valstr = p;
		/* Input: `dev.control=val`. */
		cp->mod(cp->parent_dev, valstr);
next:
		free(orig);
		argc--;
		argv++;
	}

	if (pall)
		printall(m, oflag);
	(void)mixer_close(m);

	return (0);
}

static void __dead2
usage(void)
{
	fprintf(stderr, "usage: %1$s [-f device] [-d pcmN | N "
	    "[-V voss_device:mode]] [-os] [dev[.control[=value]]] ...\n"
	    "       %1$s [-os] -a\n"
	    "       %1$s -h\n", getprogname());
	exit(1);
}

static void
initctls(struct mixer *m)
{
	struct mix_dev *dp;
	int rc = 0;

	TAILQ_FOREACH(dp, &m->devs, devs) {
		rc += mixer_add_ctl(dp, C_VOL, "volume", mod_volume, print_volume);
		rc += mixer_add_ctl(dp, C_MUT, "mute", mod_mute, print_mute);
		rc += mixer_add_ctl(dp, C_SRC, "recsrc", mod_recsrc, print_recsrc);
	}
	if (rc) {
		(void)mixer_close(m);
		errx(1, "cannot make mixer controls");
	}
}

static void
printall(struct mixer *m, int oflag)
{
	struct mix_dev *dp;

	printminfo(m, oflag);
	TAILQ_FOREACH(dp, &m->devs, devs) {
		m->dev = dp;
		printdev(m, oflag);
	}
}

static void
printminfo(struct mixer *m, int oflag)
{
	int playrec = MIX_MODE_PLAY | MIX_MODE_REC;

	if (oflag)
		return;
	printf("%s:", m->mi.name);
	if (*m->ci.longname != '\0')
		printf(" <%s>", m->ci.longname);
	if (*m->ci.hw_info != '\0')
		printf(" %s", m->ci.hw_info);

	if (m->mode != 0)
		printf(" (");
	if (m->mode & MIX_MODE_PLAY)
		printf("play");
	if ((m->mode & playrec) == playrec)
		printf("/");
	if (m->mode & MIX_MODE_REC)
		printf("rec");
	if (m->mode != 0)
		printf(")");

	if (m->f_default)
		printf(" (default)");
	printf("\n");
}

static void
printdev(struct mixer *m, int oflag)
{
	struct mix_dev *d = m->dev;
	mix_ctl_t *cp;

	if (!oflag) {
		printf("    %-10s= %.2f:%.2f    ",
		    d->name, d->vol.left, d->vol.right);
		if (!MIX_ISREC(m, d->devno))
			printf(" pbk");
		if (MIX_ISREC(m, d->devno))
			printf(" rec");
		if (MIX_ISRECSRC(m, d->devno))
			printf(" src");
		if (MIX_ISMUTE(m, d->devno))
			printf(" mute");
		printf("\n");
	} else {
		TAILQ_FOREACH(cp, &d->ctls, ctls) {
			(void)cp->print(cp->parent_dev, cp->name);
		}
	}
}

static void
printrecsrc(struct mixer *m, int oflag)
{
	struct mix_dev *dp;
	int n = 0;

	if (!m->recmask)
		return;
	if (!oflag)
		printf("%s: ", m->mi.name);
	TAILQ_FOREACH(dp, &m->devs, devs) {
		if (MIX_ISRECSRC(m, dp->devno)) {
			if (n++ && !oflag)
				printf(", ");
			printf("%s", dp->name);
			if (oflag)
				printf(".%s=+%s",
				    mixer_get_ctl(dp, C_SRC)->name, n ? " " : "");
		}
	}
	printf("\n");
}

static int
set_dunit(struct mixer *m, int dunit, char *vctl)
{
	const char *opt;
	char *dev, *mode;
	char buf[32];
	size_t size;
	int n, rc;

	/*
	 * Issue warning in case of hw.snd.basename_clone being unset. Omit the
	 * check and warning if the -V flag is used, since the user is most
	 * likely to be aware of this, and the warning might be confusing.
	 */
	if (vctl == NULL) {
		size = sizeof(int);
		if (sysctlbyname("hw.snd.basename_clone", &n, &size,
		    NULL, 0) < 0) {
			warn("hw.snd.basename_clone failed");
			return (-1);
		}
		if (n == 0) {
			warnx("warning: hw.snd.basename_clone not set. "
			    "/dev/dsp is managed externally and does not "
			    "change with the default unit change here.");
		}
	}

	if ((n = mixer_get_dunit()) < 0) {
		warn("cannot get default unit");
		return (-1);
	}
	if (mixer_set_dunit(m, dunit) < 0) {
		warn("cannot set default unit to %d", dunit);
		return (-1);
	}
	printf("default_unit: %d -> %d\n", n, dunit);

	/* Hot-swap in case virtual_oss exists and is running. */
	if (vctl != NULL) {
		dev = strsep(&vctl, ":");
		mode = vctl;
		if (dev == NULL || mode == NULL) {
			warnx("voss_device:mode tuple incomplete");
			return (-1);
		}
		if (strcmp(mode, "all") == 0)
			opt = "-f";
		else if (strcmp(mode, "play") == 0)
			opt = "-P";
		else if (strcmp(mode, "rec") == 0)
			opt = "-R";
		else {
			warnx("please use one of the following modes: "
			    "all, play, rec");
			return (-1);
		}
		snprintf(buf, sizeof(buf), "/dev/dsp%d", dunit);
		switch (fork()) {
		case -1:
			warn("fork");
			break;
		case 0:
			rc = execl("/usr/local/sbin/virtual_oss_cmd",
			    "virtual_oss_cmd", dev, opt, buf, NULL);
			if (rc < 0)
				warn("virtual_oss_cmd");
			_exit(0);
		default:
			if (wait(NULL) < 0)
				warn("wait");
			break;
		}
	}

	return (0);
}

static int
mod_volume(struct mix_dev *d, void *p)
{
	struct mixer *m;
	mix_ctl_t *cp;
	mix_volume_t v;
	const char *val;
	char *endp, lstr[8], rstr[8];
	float lprev, rprev, lrel, rrel;
	int n;

	m = d->parent_mixer;
	cp = mixer_get_ctl(m->dev, C_VOL);
	val = p;
	n = sscanf(val, "%7[^:]:%7s", lstr, rstr);
	if (n == EOF) {
		warnx("invalid volume value: %s", val);
		return (-1);
	}
	lrel = rrel = 0;
	if (n > 0) {
		if (*lstr == '+' || *lstr == '-')
			lrel = 1;
		v.left = strtof(lstr, &endp);
		if (*endp != '\0' && (*endp != '%' || *(endp + 1) != '\0')) {
			warnx("invalid volume value: %s", lstr);
			return (-1);
		}

		if (*endp == '%')
			v.left /= 100.0f;
	}
	if (n > 1) {
		if (*rstr == '+' || *rstr == '-')
			rrel = 1;
		v.right = strtof(rstr, &endp);
		if (*endp != '\0' && (*endp != '%' || *(endp + 1) != '\0')) {
			warnx("invalid volume value: %s", rstr);
			return (-1);
		}

		if (*endp == '%')
			v.right /= 100.0f;
	}
	switch (n) {
	case 1:
		v.right = v.left; /* FALLTHROUGH */
		rrel = lrel;
	case 2:
		if (lrel)
			v.left += m->dev->vol.left;
		if (rrel)
			v.right += m->dev->vol.right;

		if (v.left < MIX_VOLMIN)
			v.left = MIX_VOLMIN;
		else if (v.left > MIX_VOLMAX)
			v.left = MIX_VOLMAX;
		if (v.right < MIX_VOLMIN)
			v.right = MIX_VOLMIN;
		else if (v.right > MIX_VOLMAX)
			v.right = MIX_VOLMAX;

		lprev = m->dev->vol.left;
		rprev = m->dev->vol.right;
		if (mixer_set_vol(m, v) < 0)
			warn("%s.%s=%.2f:%.2f",
			    m->dev->name, cp->name, v.left, v.right);
		else
			printf("%s.%s: %.2f:%.2f -> %.2f:%.2f\n",
			   m->dev->name, cp->name, lprev, rprev, v.left, v.right);
	}

	return (0);
}

static int
mod_mute(struct mix_dev *d, void *p)
{
	struct mixer *m;
	mix_ctl_t *cp;
	const char *val;
	int n, opt = -1;

	m = d->parent_mixer;
	cp = mixer_get_ctl(m->dev, C_MUT);
	val = p;
	if (strncmp(val, "off", strlen(val)) == 0 ||
	    strncmp(val, "0", strlen(val)) == 0)
		opt = MIX_UNMUTE;
	else if (strncmp(val, "on", strlen(val)) == 0 ||
	    strncmp(val, "1", strlen(val)) == 0)
		opt = MIX_MUTE;
	else if (strncmp(val, "toggle", strlen(val)) == 0 ||
	    strncmp(val, "^", strlen(val)) == 0)
		opt = MIX_TOGGLEMUTE;
	else {
		warnx("%s: no such modifier", val);
		return (-1);
	}
	n = MIX_ISMUTE(m, m->dev->devno);
	if (mixer_set_mute(m, opt) < 0)
		warn("%s.%s=%s", m->dev->name, cp->name, val);
	else
		printf("%s.%s: %s -> %s\n",
		    m->dev->name, cp->name,
		    n ? "on" : "off",
		    MIX_ISMUTE(m, m->dev->devno) ? "on" : "off");

	return (0);
}

static int
mod_recsrc(struct mix_dev *d, void *p)
{
	struct mixer *m;
	mix_ctl_t *cp;
	const char *val;
	int n, opt = -1;

	m = d->parent_mixer;
	cp = mixer_get_ctl(m->dev, C_SRC);
	val = p;
	if (strncmp(val, "add", strlen(val)) == 0 ||
	    strncmp(val, "+", strlen(val)) == 0)
		opt = MIX_ADDRECSRC;
	else if (strncmp(val, "remove", strlen(val)) == 0 ||
	    strncmp(val, "-", strlen(val)) == 0)
		opt = MIX_REMOVERECSRC;
	else if (strncmp(val, "set", strlen(val)) == 0 ||
	    strncmp(val, "=", strlen(val)) == 0)
		opt = MIX_SETRECSRC;
	else if (strncmp(val, "toggle", strlen(val)) == 0 ||
	    strncmp(val, "^", strlen(val)) == 0)
		opt = MIX_TOGGLERECSRC;
	else {
		warnx("%s: no such modifier", val);
		return (-1);
	}
	n = MIX_ISRECSRC(m, m->dev->devno);
	if (mixer_mod_recsrc(m, opt) < 0)
		warn("%s.%s=%s", m->dev->name, cp->name, val);
	else
		printf("%s.%s: %s -> %s\n",
		    m->dev->name, cp->name,
		    n ? "add" : "remove",
		    MIX_ISRECSRC(m, m->dev->devno) ? "add" : "remove");

	return (0);
}

static int
print_volume(struct mix_dev *d, void *p)
{
	struct mixer *m = d->parent_mixer;
	const char *ctl_name = p;

	printf("%s.%s=%.2f:%.2f\n",
	    m->dev->name, ctl_name, m->dev->vol.left, m->dev->vol.right);

	return (0);
}

static int
print_mute(struct mix_dev *d, void *p)
{
	struct mixer *m = d->parent_mixer;
	const char *ctl_name = p;

	printf("%s.%s=%s\n", m->dev->name, ctl_name,
	    MIX_ISMUTE(m, m->dev->devno) ? "on" : "off");

	return (0);
}

static int
print_recsrc(struct mix_dev *d, void *p)
{
	struct mixer *m = d->parent_mixer;
	const char *ctl_name = p;

	if (!MIX_ISRECSRC(m, m->dev->devno))
		return (-1);
	printf("%s.%s=add\n", m->dev->name, ctl_name);

	return (0);
}
