/*
 * 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.
 */
#include <errno.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sndio.h>
#include <unistd.h>
#include <arpa/inet.h>
#include "debug.h"
#include "aucat.h"
#include "sioctl_priv.h"
#include "bsd-compat.h"

struct sioctl_aucat_hdl {
	struct sioctl_hdl sioctl;
	struct aucat aucat;
	struct sioctl_desc desc;
	struct amsg_ctl_desc buf[16];
	size_t buf_wpos;
	int dump_wait;
};

static void sioctl_aucat_close(struct sioctl_hdl *);
static int sioctl_aucat_nfds(struct sioctl_hdl *);
static int sioctl_aucat_pollfd(struct sioctl_hdl *, struct pollfd *, int);
static int sioctl_aucat_revents(struct sioctl_hdl *, struct pollfd *);
static int sioctl_aucat_setctl(struct sioctl_hdl *, unsigned int, unsigned int);
static int sioctl_aucat_onval(struct sioctl_hdl *);
static int sioctl_aucat_ondesc(struct sioctl_hdl *);

/*
 * operations every device should support
 */
struct sioctl_ops sioctl_aucat_ops = {
	sioctl_aucat_close,
	sioctl_aucat_nfds,
	sioctl_aucat_pollfd,
	sioctl_aucat_revents,
	sioctl_aucat_setctl,
	sioctl_aucat_onval,
	sioctl_aucat_ondesc
};

static int
sioctl_aucat_rdata(struct sioctl_aucat_hdl *hdl)
{
	struct sioctl_desc desc;
	struct amsg_ctl_desc *c;
	size_t rpos;
	int n;

	while (hdl->aucat.rstate == RSTATE_DATA) {

		/* read entries */
		while (hdl->buf_wpos < sizeof(hdl->buf) &&
		    hdl->aucat.rstate == RSTATE_DATA) {
			n = _aucat_rdata(&hdl->aucat,
			    (unsigned char *)hdl->buf + hdl->buf_wpos,
			    sizeof(hdl->buf) - hdl->buf_wpos,
			    &hdl->sioctl.eof);
			if (n == 0 || hdl->sioctl.eof)
				return 0;
			hdl->buf_wpos += n;
		}

		/* parse entries */
		c = hdl->buf;
		rpos = 0;
		while (rpos < hdl->buf_wpos) {
			strlcpy(desc.group, c->group, SIOCTL_NAMEMAX);
			strlcpy(desc.node0.name, c->node0.name, SIOCTL_NAMEMAX);
			desc.node0.unit = (int16_t)ntohs(c->node0.unit);
			strlcpy(desc.node1.name, c->node1.name, SIOCTL_NAMEMAX);
			desc.node1.unit = (int16_t)ntohs(c->node1.unit);
			strlcpy(desc.func, c->func, SIOCTL_NAMEMAX);
			strlcpy(desc.display, c->display, SIOCTL_DISPLAYMAX);
			desc.type = c->type;
			desc.addr = ntohs(c->addr);
			desc.maxval = ntohs(c->maxval);
			_sioctl_ondesc_cb(&hdl->sioctl,
			    &desc, ntohs(c->curval));
			rpos += sizeof(struct amsg_ctl_desc);
			c++;
		}
		hdl->buf_wpos = 0;
	}
	return 1;
}

/*
 * execute the next message, return 0 if blocked
 */
static int
sioctl_aucat_runmsg(struct sioctl_aucat_hdl *hdl)
{
	if (!_aucat_rmsg(&hdl->aucat, &hdl->sioctl.eof))
		return 0;
	switch (ntohl(hdl->aucat.rmsg.cmd)) {
	case AMSG_DATA:
		hdl->buf_wpos = 0;
		if (!sioctl_aucat_rdata(hdl))
			return 0;
		break;
	case AMSG_CTLSET:
		DPRINTF("sioctl_aucat_runmsg: got CTLSET\n");
		_sioctl_onval_cb(&hdl->sioctl,
		    ntohs(hdl->aucat.rmsg.u.ctlset.addr),
		    ntohs(hdl->aucat.rmsg.u.ctlset.val));
		break;
	case AMSG_CTLSYNC:
		DPRINTF("sioctl_aucat_runmsg: got CTLSYNC\n");
		hdl->dump_wait = 0;
		_sioctl_ondesc_cb(&hdl->sioctl, NULL, 0);
		break;
	default:
		DPRINTF("sio_aucat_runmsg: unhandled message %u\n",
		    hdl->aucat.rmsg.cmd);
		hdl->sioctl.eof = 1;
		return 0;
	}
	hdl->aucat.rstate = RSTATE_MSG;
	hdl->aucat.rtodo = sizeof(struct amsg);
	return 1;
}

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

	hdl = malloc(sizeof(struct sioctl_aucat_hdl));
	if (hdl == NULL)
		return NULL;
	if (!_aucat_open(&hdl->aucat, str, mode))
		goto bad;
	_sioctl_create(&hdl->sioctl, &sioctl_aucat_ops, mode, nbio);
	if (!_aucat_setfl(&hdl->aucat, 1, &hdl->sioctl.eof))
		goto bad;
	hdl->dump_wait = 0;
	return (struct sioctl_hdl *)hdl;
bad:
	free(hdl);
	return NULL;
}

static void
sioctl_aucat_close(struct sioctl_hdl *addr)
{
	struct sioctl_aucat_hdl *hdl = (struct sioctl_aucat_hdl *)addr;

	if (!hdl->sioctl.eof)
		_aucat_setfl(&hdl->aucat, 0, &hdl->sioctl.eof);
	_aucat_close(&hdl->aucat, hdl->sioctl.eof);
	free(hdl);
}

static int
sioctl_aucat_ondesc(struct sioctl_hdl *addr)
{
	struct sioctl_aucat_hdl *hdl = (struct sioctl_aucat_hdl *)addr;

	while (hdl->aucat.wstate != WSTATE_IDLE) {
		if (!_sioctl_psleep(&hdl->sioctl, POLLOUT))
			return 0;
	}
	AMSG_INIT(&hdl->aucat.wmsg);
	hdl->aucat.wmsg.cmd = htonl(AMSG_CTLSUB);
	hdl->aucat.wmsg.u.ctlsub.desc = 1;
	hdl->aucat.wmsg.u.ctlsub.val = 0;
	hdl->aucat.wtodo = sizeof(struct amsg);
	if (!_aucat_wmsg(&hdl->aucat, &hdl->sioctl.eof))
		return 0;
	hdl->dump_wait = 1;
	while (hdl->dump_wait) {
		DPRINTF("psleeping...\n");
		if (!_sioctl_psleep(&hdl->sioctl, 0))
			return 0;
		DPRINTF("psleeping done\n");
	}
	DPRINTF("done\n");
	return 1;
}

static int
sioctl_aucat_onval(struct sioctl_hdl *addr)
{
	struct sioctl_aucat_hdl *hdl = (struct sioctl_aucat_hdl *)addr;

	while (hdl->aucat.wstate != WSTATE_IDLE) {
		if (!_sioctl_psleep(&hdl->sioctl, POLLOUT))
			return 0;
	}
	AMSG_INIT(&hdl->aucat.wmsg);
	hdl->aucat.wmsg.cmd = htonl(AMSG_CTLSUB);
	hdl->aucat.wmsg.u.ctlsub.desc = 1;
	hdl->aucat.wmsg.u.ctlsub.val = 1;
	hdl->aucat.wtodo = sizeof(struct amsg);
	if (!_aucat_wmsg(&hdl->aucat, &hdl->sioctl.eof))
		return 0;
	return 1;
}

static int
sioctl_aucat_setctl(struct sioctl_hdl *addr, unsigned int a, unsigned int v)
{
	struct sioctl_aucat_hdl *hdl = (struct sioctl_aucat_hdl *)addr;

	hdl->aucat.wstate = WSTATE_MSG;
	hdl->aucat.wtodo = sizeof(struct amsg);
	hdl->aucat.wmsg.cmd = htonl(AMSG_CTLSET);
	hdl->aucat.wmsg.u.ctlset.addr = htons(a);
	hdl->aucat.wmsg.u.ctlset.val = htons(v);
	while (hdl->aucat.wstate != WSTATE_IDLE) {
		if (_aucat_wmsg(&hdl->aucat, &hdl->sioctl.eof))
			break;
		if (hdl->sioctl.nbio || !_sioctl_psleep(&hdl->sioctl, POLLOUT))
			return 0;
	}
	return 1;
}

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

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

	return _aucat_pollfd(&hdl->aucat, pfd, events | POLLIN);
}

static int
sioctl_aucat_revents(struct sioctl_hdl *addr, struct pollfd *pfd)
{
	struct sioctl_aucat_hdl *hdl = (struct sioctl_aucat_hdl *)addr;
	int revents;

	revents = _aucat_revents(&hdl->aucat, pfd);
	if (revents & POLLIN) {
		while (1) {
			if (hdl->aucat.rstate == RSTATE_MSG) {
				if (!sioctl_aucat_runmsg(hdl))
					break;
			}
			if (hdl->aucat.rstate == RSTATE_DATA) {
				if (!sioctl_aucat_rdata(hdl))
					break;
			}
		}
		revents &= ~POLLIN;
	}
	if (hdl->sioctl.eof)
		return POLLHUP;
	DPRINTFN(3, "sioctl_aucat_revents: revents = 0x%x\n", revents);
	return revents;
}
