/*
 * Copyright (c) 2014-2020 Alexandre Ratchov <alex@caoua.org>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */
/*
 * the way the sun mixer is designed doesn't let us representing
 * it easily with the sioctl api. For now expose only few
 * white-listed controls the same way as we do in kernel
 * for the wskbd volume keys.
 */
#ifdef USE_SUN_MIXER
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/audioio.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <poll.h>
#include <sndio.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "debug.h"
#include "sioctl_priv.h"
#include "bsd-compat.h"

#define DEVPATH_PREFIX	"/dev/audioctl"
#define DEVPATH_MAX 	(1 +		\
	sizeof(DEVPATH_PREFIX) - 1 +	\
	sizeof(int) * 3)

struct volume
{
	int nch;			/* channels in the level control */
	int level_idx;			/* index of the level control */
	int level_val[8];		/* current value */
	int mute_idx;			/* index of the mute control */
	int mute_val;			/* per channel state of mute control */
	int base_addr;
	char *name;
};

struct sioctl_sun_hdl {
	struct sioctl_hdl sioctl;
	char display[SIOCTL_DISPLAYMAX];
	int display_addr;
	struct volume output, input;
	int fd, events;
};

static void sioctl_sun_close(struct sioctl_hdl *);
static int sioctl_sun_nfds(struct sioctl_hdl *);
static int sioctl_sun_pollfd(struct sioctl_hdl *, struct pollfd *, int);
static int sioctl_sun_revents(struct sioctl_hdl *, struct pollfd *);
static int sioctl_sun_setctl(struct sioctl_hdl *, unsigned int, unsigned int);
static int sioctl_sun_onval(struct sioctl_hdl *);
static int sioctl_sun_ondesc(struct sioctl_hdl *);

/*
 * operations every device should support
 */
struct sioctl_ops sioctl_sun_ops = {
	sioctl_sun_close,
	sioctl_sun_nfds,
	sioctl_sun_pollfd,
	sioctl_sun_revents,
	sioctl_sun_setctl,
	sioctl_sun_onval,
	sioctl_sun_ondesc
};

static int
initmute(struct sioctl_sun_hdl *hdl, struct mixer_devinfo *info)
{
	struct mixer_devinfo mi;
	char name[MAX_AUDIO_DEV_LEN];

	for (mi.index = info->next; mi.index != -1; mi.index = mi.next) {
		if (ioctl(hdl->fd, AUDIO_MIXER_DEVINFO, &mi) < 0)
			break;
		if (strcmp(mi.label.name, AudioNmute) == 0)
			return mi.index;
	}

	/* try "_mute" suffix */
	snprintf(name, sizeof(name), "%s_mute", info->label.name);
	for (mi.index = 0; ; mi.index++) {
		if (ioctl(hdl->fd, AUDIO_MIXER_DEVINFO, &mi) < 0)
			break;
		if (info->mixer_class == mi.mixer_class &&
		    strcmp(mi.label.name, name) == 0)
			return mi.index;
	}
	return -1;
}

static int
initvol(struct sioctl_sun_hdl *hdl, struct volume *vol, char *cn, char *dn)
{
	struct mixer_devinfo dev, cls;

	for (dev.index = 0; ; dev.index++) {
		if (ioctl(hdl->fd, AUDIO_MIXER_DEVINFO, &dev) < 0)
			break;
		if (dev.type != AUDIO_MIXER_VALUE)
			continue;
		cls.index = dev.mixer_class;
		if (ioctl(hdl->fd, AUDIO_MIXER_DEVINFO, &cls) < 0)
			break;
		if (strcmp(cls.label.name, cn) == 0 &&
		    strcmp(dev.label.name, dn) == 0) {
			vol->nch = dev.un.v.num_channels;
			vol->level_idx = dev.index;
			vol->mute_idx = initmute(hdl, &dev);
			DPRINTF("using %s.%s, %d channels, %s\n", cn, dn,
			    vol->nch, vol->mute_idx >= 0 ? "mute" : "no mute");
			return 1;
		}
	}
	vol->level_idx = vol->mute_idx = -1;
	return 0;
}

static void
init(struct sioctl_sun_hdl *hdl)
{
	static struct {
		char *cn, *dn;
	} output_names[] = {
		{AudioCoutputs, AudioNmaster},
		{AudioCinputs,  AudioNdac},
		{AudioCoutputs, AudioNdac},
		{AudioCoutputs, AudioNoutput}
	}, input_names[] = {
		{AudioCrecord, AudioNrecord},
		{AudioCrecord, AudioNvolume},
		{AudioCinputs, AudioNrecord},
		{AudioCinputs, AudioNvolume},
		{AudioCinputs, AudioNinput}
	};
	struct audio_device getdev;
	int i;

	for (i = 0; i < sizeof(output_names) / sizeof(output_names[0]); i++) {
		if (initvol(hdl, &hdl->output,
			output_names[i].cn, output_names[i].dn)) {
			hdl->output.name = "output";
			hdl->output.base_addr = 0;
			break;
		}
	}
	for (i = 0; i < sizeof(input_names) / sizeof(input_names[0]); i++) {
		if (initvol(hdl, &hdl->input,
			input_names[i].cn, input_names[i].dn)) {
			hdl->input.name = "input";
			hdl->input.base_addr = 64;
			break;
		}
	}

	hdl->display_addr = 128;
	if (ioctl(hdl->fd, AUDIO_GETDEV, &getdev) == -1)
		strlcpy(hdl->display, "unknown", SIOCTL_DISPLAYMAX);
	else
		strlcpy(hdl->display, getdev.name, SIOCTL_DISPLAYMAX);
	DPRINTF("init: server.device: display = %s\n", hdl->display);
}

static int
setvol(struct sioctl_sun_hdl *hdl, struct volume *vol, int addr, int val)
{
	struct mixer_ctrl ctrl;
	int i;

	addr -= vol->base_addr;
	if (vol->level_idx >= 0 && addr >= 0 && addr < vol->nch) {
		if (vol->level_val[addr] == val) {
			DPRINTF("level %d, no change\n", val);
			return 1;
		}
		vol->level_val[addr] = val;
		ctrl.dev = vol->level_idx;
		ctrl.type = AUDIO_MIXER_VALUE;
		ctrl.un.value.num_channels = vol->nch;
		for (i = 0; i < vol->nch; i++)
			ctrl.un.value.level[i] = vol->level_val[i];
		DPRINTF("vol %d setting to %d\n", addr, vol->level_val[addr]);
		if (ioctl(hdl->fd, AUDIO_MIXER_WRITE, &ctrl) < 0) {
			DPRINTF("level write failed\n");
			return 0;
		}
		_sioctl_onval_cb(&hdl->sioctl, vol->base_addr + addr, val);
		return 1;
	}

	addr -= 32;
	if (vol->mute_idx >= 0 && addr >= 0 && addr < vol->nch) {
		val = val ? 1 : 0;
		if (vol->mute_val == val) {
			DPRINTF("mute %d, no change\n", val);
			return 1;
		}
		vol->mute_val = val;
		ctrl.dev = vol->mute_idx;
		ctrl.type = AUDIO_MIXER_ENUM;
		ctrl.un.ord = val;
		DPRINTF("mute setting to %d\n", val);
		if (ioctl(hdl->fd, AUDIO_MIXER_WRITE, &ctrl) < 0) {
			DPERROR("mute write\n");
			return 0;
		}
		for (i = 0; i < vol->nch; i++) {
			_sioctl_onval_cb(&hdl->sioctl,
			    vol->base_addr + 32 + i, val);
		}
		return 1;
	}
	return 1;
}

static int
scanvol(struct sioctl_sun_hdl *hdl, struct volume *vol)
{
	struct sioctl_desc desc;
	struct mixer_ctrl ctrl;
	int i, val;

	memset(&desc, 0, sizeof(struct sioctl_desc));
	if (vol->level_idx >= 0) {
		ctrl.dev = vol->level_idx;
		ctrl.type = AUDIO_MIXER_VALUE;
		ctrl.un.value.num_channels = vol->nch;
		if (ioctl(hdl->fd, AUDIO_MIXER_READ, &ctrl) < 0) {
			DPRINTF("level read failed\n");
			return 0;
		}
		desc.type = SIOCTL_NUM;
		desc.maxval = AUDIO_MAX_GAIN;
		desc.node1.name[0] = 0;
		desc.node1.unit = -1;
		strlcpy(desc.func, "level", SIOCTL_NAMEMAX);
		strlcpy(desc.node0.name, vol->name, SIOCTL_NAMEMAX);
		for (i = 0; i < vol->nch; i++) {
			desc.node0.unit = i;
			desc.addr = vol->base_addr + i;
			val = ctrl.un.value.level[i];
			vol->level_val[i] = val;
			_sioctl_ondesc_cb(&hdl->sioctl, &desc, val);
		}
	}
	if (vol->mute_idx >= 0) {
		ctrl.dev = vol->mute_idx;
		ctrl.type = AUDIO_MIXER_ENUM;
		if (ioctl(hdl->fd, AUDIO_MIXER_READ, &ctrl) < 0) {
			DPRINTF("mute read failed\n");
			return 0;
		}
		desc.type = SIOCTL_SW;
		desc.maxval = 1;
		desc.node1.name[0] = 0;
		desc.node1.unit = -1;
		strlcpy(desc.func, "mute", SIOCTL_NAMEMAX);
		strlcpy(desc.node0.name, vol->name, SIOCTL_NAMEMAX);
		val = ctrl.un.ord ? 1 : 0;
		vol->mute_val = val;
		for (i = 0; i < vol->nch; i++) {
			desc.node0.unit = i;
			desc.addr = vol->base_addr + 32 + i;
			_sioctl_ondesc_cb(&hdl->sioctl, &desc, val);
		}
	}
	return 1;
}

static int
updatevol(struct sioctl_sun_hdl *hdl, struct volume *vol, int idx)
{
	struct mixer_ctrl ctrl;
	int val, i;

	if (idx == vol->mute_idx)
		ctrl.type = AUDIO_MIXER_ENUM;
	else {
		ctrl.type = AUDIO_MIXER_VALUE;
		ctrl.un.value.num_channels = vol->nch;
	}
	ctrl.dev = idx;
	if (ioctl(hdl->fd, AUDIO_MIXER_READ, &ctrl) == -1) {
		DPERROR("sioctl_sun_revents: ioctl\n");
		hdl->sioctl.eof = 1;
		return 0;
	}
	if (idx == vol->mute_idx) {
		val = ctrl.un.ord ? 1 : 0;
		if (vol->mute_val == val)
			return 1;
		vol->mute_val = val;
		for (i = 0; i < vol->nch; i++) {
			_sioctl_onval_cb(&hdl->sioctl,
			    vol->base_addr + 32 + i, val);
		}
	} else {
		for (i = 0; i < vol->nch; i++) {
			val = ctrl.un.value.level[i];
			if (vol->level_val[i] == val)
				continue;
			vol->level_val[i] = val;
			_sioctl_onval_cb(&hdl->sioctl,
			    vol->base_addr + i, val);
		}
	}
	return 1;
}

int
sioctl_sun_getfd(const char *str, unsigned int mode, int nbio)
{
	const char *p;
	char path[DEVPATH_MAX];
	unsigned int devnum;
	int fd, flags;

#ifdef DEBUG
	_sndio_debug_init();
#endif
	p = _sndio_parsetype(str, "rsnd");
	if (p == NULL) {
		DPRINTF("sioctl_sun_getfd: %s: \"rsnd\" expected\n", str);
		return -1;
	}
	switch (*p) {
	case '/':
		p++;
		break;
	default:
		DPRINTF("sioctl_sun_getfd: %s: '/' expected\n", str);
		return -1;
	}
	if (strcmp(p, "default") == 0) {
		devnum = 0;
	} else {
		p = _sndio_parsenum(p, &devnum, 255);
		if (p == NULL || *p != '\0') {
			DPRINTF("sioctl_sun_getfd: %s: number expected after '/'\n", str);
			return -1;
		}
	}
	snprintf(path, sizeof(path), DEVPATH_PREFIX "%u", devnum);
	if (mode == (SIOCTL_READ | SIOCTL_WRITE))
		flags = O_RDWR;
	else
		flags = (mode & SIOCTL_WRITE) ? O_WRONLY : O_RDONLY;
	while ((fd = open(path, flags | O_NONBLOCK | O_CLOEXEC)) < 0) {
		if (errno == EINTR)
			continue;
		DPERROR(path);
		return -1;
	}
	return fd;
}

struct sioctl_hdl *
sioctl_sun_fdopen(int fd, unsigned int mode, int nbio)
{
	struct sioctl_sun_hdl *hdl;

#ifdef DEBUG
	_sndio_debug_init();
#endif
	hdl = malloc(sizeof(struct sioctl_sun_hdl));
	if (hdl == NULL)
		return NULL;
	_sioctl_create(&hdl->sioctl, &sioctl_sun_ops, mode, nbio);
	hdl->fd = fd;
	init(hdl);
	return (struct sioctl_hdl *)hdl;
}

struct sioctl_hdl *
_sioctl_sun_open(const char *str, unsigned int mode, int nbio)
{
	struct sioctl_hdl *hdl;
	int fd;

	fd = sioctl_sun_getfd(str, mode, nbio);
	if (fd < 0)
		return NULL;
	hdl = sioctl_sun_fdopen(fd, mode, nbio);
	if (hdl != NULL)
		return hdl;
	while (close(fd) < 0 && errno == EINTR)
		; /* retry */
	return NULL;
}

static void
sioctl_sun_close(struct sioctl_hdl *addr)
{
	struct sioctl_sun_hdl *hdl = (struct sioctl_sun_hdl *)addr;

	close(hdl->fd);
	free(hdl);
}

static int
sioctl_sun_ondesc(struct sioctl_hdl *addr)
{
	struct sioctl_sun_hdl *hdl = (struct sioctl_sun_hdl *)addr;
	struct sioctl_desc desc;

	if (!scanvol(hdl, &hdl->output) ||
	    !scanvol(hdl, &hdl->input)) {
		hdl->sioctl.eof = 1;
		return 0;
	}

	/* report "server.device" control */
	memset(&desc, 0, sizeof(struct sioctl_desc));
	desc.type = SIOCTL_SEL;
	desc.maxval = 1;
	strlcpy(desc.func, "device", SIOCTL_NAMEMAX);
	strlcpy(desc.node0.name, "server", SIOCTL_NAMEMAX);
	desc.node0.unit = -1;
	strlcpy(desc.node1.name, "0", SIOCTL_NAMEMAX);
	desc.node1.unit = -1;
	strlcpy(desc.display, hdl->display, SIOCTL_DISPLAYMAX);
	desc.addr = hdl->display_addr;
	_sioctl_ondesc_cb(&hdl->sioctl, &desc, 1);

	_sioctl_ondesc_cb(&hdl->sioctl, NULL, 0);
	return 1;
}

static int
sioctl_sun_onval(struct sioctl_hdl *addr)
{
	return 1;
}

static int
sioctl_sun_setctl(struct sioctl_hdl *arg, unsigned int addr, unsigned int val)
{
	struct sioctl_sun_hdl *hdl = (struct sioctl_sun_hdl *)arg;

	if (!setvol(hdl, &hdl->output, addr, val) ||
	    !setvol(hdl, &hdl->input, addr, val)) {
		hdl->sioctl.eof = 1;
		return 0;
	}
	return 1;
}

static int
sioctl_sun_nfds(struct sioctl_hdl *addr)
{
	return 1;
}

static int
sioctl_sun_pollfd(struct sioctl_hdl *addr, struct pollfd *pfd, int events)
{
	struct sioctl_sun_hdl *hdl = (struct sioctl_sun_hdl *)addr;

	hdl->events = events;

	/*
	 * The audio(4) driver doesn't support POLLOUT, so if it is
	 * requested, don't set the struct pollfd. The AUDIO_MIXER_WRITE
	 * ioctl never blocks, so just return POLLOUT in sioctl_sun_revents().
	 */
	if (events & POLLOUT)
		return 0;

	pfd->fd = hdl->fd;
	pfd->events = POLLIN;
	return 1;
}

static int
sioctl_sun_revents(struct sioctl_hdl *arg, struct pollfd *pfd)
{
	struct sioctl_sun_hdl *hdl = (struct sioctl_sun_hdl *)arg;
	struct volume *vol;
	int idx, n;

	if (hdl->events & POLLOUT)
		return POLLOUT;

	if (pfd->revents & POLLIN) {
		while (1) {
			n = read(hdl->fd, &idx, sizeof(int));
			if (n == -1) {
				if (errno == EINTR || errno == EAGAIN)
					break;
				DPERROR("read");
				hdl->sioctl.eof = 1;
				return POLLHUP;
			}
			if (n < sizeof(int)) {
				DPRINTF("sioctl_sun_revents: short read\n");
				hdl->sioctl.eof = 1;
				return POLLHUP;
			}

			if (idx == hdl->output.level_idx ||
			    idx == hdl->output.mute_idx) {
				vol = &hdl->output;
			} else if (idx == hdl->input.level_idx ||
			    idx == hdl->input.mute_idx) {
				vol = &hdl->input;
			} else
				continue;

			if (!updatevol(hdl, vol, idx))
				return POLLHUP;
		}
	}
	return 0;
}
#endif
