/*	$OpenBSD$	*/
/*
 * Copyright (c) 2010 Jacob Meuser <jakemsr@sdf.lonestar.org>
 * Copyright (c) 2008,2012-2013 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.
 */
#ifdef USE_ALSA
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/stat.h>

#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <values.h>
#include <alsa/asoundlib.h>

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

#define DEVNAME_PREFIX "hw:"

#ifdef DEBUG
static snd_output_t *output = NULL;
#define DALSA(str, err) fprintf(stderr, "%s: %s\n", str, snd_strerror(err))
#else
#define DALSA(str, err) do {} while (0)
#endif

struct sio_alsa_hdl {
	struct sio_hdl sio;
	struct sio_par par;
	char *devname;
	snd_pcm_t *opcm;
	snd_pcm_t *ipcm;
	unsigned ibpf, obpf;		/* bytes per frame */
	int iused, oused;		/* frames used in hardware fifos */
	int idelta, odelta;		/* position reported to client */
	int nfds, infds, onfds;
	int running;
	int initialized;
	int events;
	int ipartial, opartial;
	char *itmpbuf, *otmpbuf;
};

static void sio_alsa_onmove(struct sio_alsa_hdl *);
static int sio_alsa_revents(struct sio_hdl *, struct pollfd *);
static void sio_alsa_close(struct sio_hdl *);
static int sio_alsa_start(struct sio_hdl *);
static int sio_alsa_flush(struct sio_hdl *);
static int sio_alsa_setpar(struct sio_hdl *, struct sio_par *);
static int sio_alsa_getpar(struct sio_hdl *, struct sio_par *);
static int sio_alsa_getcap(struct sio_hdl *, struct sio_cap *);
static size_t sio_alsa_read(struct sio_hdl *, void *, size_t);
static size_t sio_alsa_write(struct sio_hdl *, const void *, size_t);
static int sio_alsa_nfds(struct sio_hdl *);
static int sio_alsa_pollfd(struct sio_hdl *, struct pollfd *, int);
static int sio_alsa_revents(struct sio_hdl *, struct pollfd *);

static struct sio_ops sio_alsa_ops = {
	sio_alsa_close,
	sio_alsa_setpar,
	sio_alsa_getpar,
	sio_alsa_getcap,
	sio_alsa_write,
	sio_alsa_read,
	sio_alsa_start,
	NULL,
	sio_alsa_flush,
	sio_alsa_nfds,
	sio_alsa_pollfd,
	sio_alsa_revents,
	NULL,
	NULL
};

#define CAP_NFMTS	(sizeof(cap_fmts)  / sizeof(cap_fmts[0]))
#define CAP_NCHANS	(sizeof(cap_chans) / sizeof(cap_chans[0]))
#define CAP_NRATES	(sizeof(cap_rates) / sizeof(cap_rates[0]))

static unsigned int cap_chans[] = {
	1, 2, 4, 6, 8, 10, 12, 16
};
static unsigned int cap_rates[] = {
	 8000, 11025, 12000, 16000, 22050, 24000,
	32000, 44100, 48000, 64000, 88200, 96000
};
static snd_pcm_format_t cap_fmts[] = {
	/* XXX add s24le3 and s24be3 */
	SND_PCM_FORMAT_S32_LE,	SND_PCM_FORMAT_S32_BE,
	SND_PCM_FORMAT_S24_LE,	SND_PCM_FORMAT_S24_BE,
	SND_PCM_FORMAT_S16_LE,	SND_PCM_FORMAT_S16_BE,
	SND_PCM_FORMAT_U8
};

/*
 * convert ALSA format to sio_par encoding
 */
static int
sio_alsa_fmttopar(struct sio_alsa_hdl *hdl, snd_pcm_format_t fmt,
    unsigned int *bits, unsigned int *sig, unsigned int *le)
{
	switch (fmt) {
	case SND_PCM_FORMAT_U8:
		*bits = 8;
		*sig = 0;
		break;
	case SND_PCM_FORMAT_S8:
		*bits = 8;
		*sig = 1;
		break;
	case SND_PCM_FORMAT_S16_LE:
		*bits = 16;
		*sig = 1;
		*le = 1;
		break;
	case SND_PCM_FORMAT_S16_BE:
		*bits = 16;
		*sig = 1;
		*le = 0;
		break;
	case SND_PCM_FORMAT_U16_LE:
		*bits = 16;
		*sig = 0;
		*le = 1;
		break;
	case SND_PCM_FORMAT_U16_BE:
		*bits = 16;
		*sig = 0;
		*le = 0;
		break;
	case SND_PCM_FORMAT_S24_LE:
		*bits = 24;
		*sig = 1;
		*le = 1;
		break;
	case SND_PCM_FORMAT_S24_BE:
		*bits = 24;
		*sig = 1;
		*le = 0;
		break;
	case SND_PCM_FORMAT_U24_LE:
		*bits = 24;
		*sig = 0;
		*le = 1;
		break;
	case SND_PCM_FORMAT_U24_BE:
		*bits = 24;
		*sig = 0;
		*le = 0;
		break;
	case SND_PCM_FORMAT_S32_LE:
		*bits = 32;
		*sig = 1;
		*le = 1;
		break;
	case SND_PCM_FORMAT_S32_BE:
		*bits = 32;
		*sig = 1;
		*le = 0;
		break;
	case SND_PCM_FORMAT_U32_LE:
		*bits = 32;
		*sig = 0;
		*le = 1;
		break;
	case SND_PCM_FORMAT_U32_BE:
		*bits = 32;
		*sig = 0;
		*le = 0;
		break;
	default:
		DPRINTF("sio_alsa_fmttopar: 0x%x: unsupported format\n", fmt);
		hdl->sio.eof = 1;
		return 0;
	}
	return 1;
}


/*
 * convert sio_par encoding to ALSA format
 */
static void
sio_alsa_enctofmt(struct sio_alsa_hdl *hdl, snd_pcm_format_t *rfmt,
    unsigned int bits, unsigned int sig, unsigned int le)
{
	if (bits == 8) {
		if (sig == ~0U || !sig)
			*rfmt = SND_PCM_FORMAT_U8;
		else
			*rfmt = SND_PCM_FORMAT_S8;
	} else if (bits == 16) {
		if (sig == ~0U || sig) {
			if (le == ~0U) {
				*rfmt = SIO_LE_NATIVE ?
				    SND_PCM_FORMAT_S16_LE :
				    SND_PCM_FORMAT_S16_BE;
			} else if (le)
				*rfmt = SND_PCM_FORMAT_S16_LE;
			else
				*rfmt = SND_PCM_FORMAT_S16_BE;
		} else {
			if (le == ~0U) {
				*rfmt = SIO_LE_NATIVE ?
				    SND_PCM_FORMAT_U16_LE :
				    SND_PCM_FORMAT_U16_BE;
			} else if (le)
				*rfmt = SND_PCM_FORMAT_U16_LE;
			else
				*rfmt = SND_PCM_FORMAT_U16_BE;
		}
	} else if (bits == 24) {
		if (sig == ~0U || sig) {
			if (le == ~0U) {
				*rfmt = SIO_LE_NATIVE ?
				    SND_PCM_FORMAT_S24_LE :
				    SND_PCM_FORMAT_S24_BE;
			 } else if (le)
				*rfmt = SND_PCM_FORMAT_S24_LE;
			else
				*rfmt = SND_PCM_FORMAT_S24_BE;
		} else {
			if (le == ~0U) {
				*rfmt = SIO_LE_NATIVE ?
				    SND_PCM_FORMAT_U24_LE :
				    SND_PCM_FORMAT_U24_BE;
			} else if (le)
				*rfmt = SND_PCM_FORMAT_U24_LE;
			else
				*rfmt = SND_PCM_FORMAT_U24_BE;
		}
	} else if (bits == 32) {
		if (sig == ~0U || sig) {
			if (le == ~0U) {
				*rfmt = SIO_LE_NATIVE ?
				    SND_PCM_FORMAT_S32_LE :
				    SND_PCM_FORMAT_S32_BE;
			 } else if (le)
				*rfmt = SND_PCM_FORMAT_S32_LE;
			else
				*rfmt = SND_PCM_FORMAT_S32_BE;
		} else {
			if (le == ~0U) {
				*rfmt = SIO_LE_NATIVE ?
				    SND_PCM_FORMAT_U32_LE :
				    SND_PCM_FORMAT_U32_BE;
			} else if (le)
				*rfmt = SND_PCM_FORMAT_U32_LE;
			else
				*rfmt = SND_PCM_FORMAT_U32_BE;
		}
	} else {
		*rfmt = SIO_LE_NATIVE ?
		    SND_PCM_FORMAT_S16_LE : SND_PCM_FORMAT_S16_BE;
	}
}

static int
sio_alsa_init(struct sio_alsa_hdl *hdl)
{
	struct sio_par par;

	if (hdl->initialized)
		return 1;

	/*
	 * Default parameters may not be compatible with libsndio (eg. mulaw
	 * encodings, different playback and recording parameters, etc...), so
	 * set parameters to a random value. If the requested parameters are
	 * not supported by the device, then sio_setpar() will pick supported
	 * ones.
	 */
	sio_initpar(&par);
	par.bits = 16;
	par.le = SIO_LE_NATIVE;
	par.rate = 48000;
	if (hdl->sio.mode & SIO_PLAY)
		par.pchan = 2;
	if (hdl->sio.mode & SIO_REC)
		par.rchan = 2;
	if (!sio_alsa_setpar(&hdl->sio, &par))
		return 0;

	return 1;
}

struct sio_hdl *
_sio_alsa_open(const char *str, unsigned mode, int nbio)
{
	const char *p;
	struct sio_alsa_hdl *hdl;
	size_t len;
	int err;

	p = _sndio_parsetype(str, "rsnd");
	if (p == NULL) {
		DPRINTF("_sio_alsa_open: %s: \"rsnd\" expected\n", str);
		return NULL;
	}
	switch (*p) {
	case '/':
		p++;
		break;
	default:
		DPRINTF("_sio_alsa_open: %s: '/' expected\n", str);
		return NULL;
	}
	hdl = malloc(sizeof(struct sio_alsa_hdl));
	if (hdl == NULL)
		return NULL;
	_sio_create(&hdl->sio, &sio_alsa_ops, mode, nbio);

#ifdef DEBUG
	err = snd_output_stdio_attach(&output, stderr, 0);
	if (err < 0)
		DALSA("couldn't attach to stderr", err);
#endif
	if (strcmp(p, "default") == 0)
		p = "0";
	len = strlen(p);
	hdl->devname = malloc(len + sizeof(DEVNAME_PREFIX));
	if (hdl->devname == NULL)
		goto bad_free_hdl;
	memcpy(hdl->devname, DEVNAME_PREFIX, sizeof(DEVNAME_PREFIX) - 1);
	memcpy(hdl->devname + sizeof(DEVNAME_PREFIX) - 1, p, len + 1);
	if (mode & SIO_PLAY) {
		err = snd_pcm_open(&hdl->opcm, hdl->devname,
		    SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
		if (err < 0) {
			DALSA("couldn't open play stream", err);
			goto bad_free;
		}
	}
	if (mode & SIO_REC) {
		err = snd_pcm_open(&hdl->ipcm, hdl->devname,
		    SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK);
		if (err < 0) {
			DALSA("couldn't open rec stream", err);
			goto bad_free_opcm;
		}
	}
	hdl->initialized = 0;

	/*
	 * snd_pcm_poll_descriptors_count returns a small value
	 * that grows later, after the stream is started
	 */
	hdl->nfds = SIO_MAXNFDS;

	return (struct sio_hdl *)hdl;
bad_free_opcm:
	if (mode & SIO_PLAY)
		snd_pcm_close(hdl->opcm);
bad_free:
	free(hdl->devname);
bad_free_hdl:
	free(hdl);
	return NULL;
}

static void
sio_alsa_close(struct sio_hdl *sh)
{
	struct sio_alsa_hdl *hdl = (struct sio_alsa_hdl *)sh;

	if (hdl->sio.mode & SIO_PLAY)
		snd_pcm_close(hdl->opcm);
	if (hdl->sio.mode & SIO_REC)
		snd_pcm_close(hdl->ipcm);
	free(hdl->devname);
	free(hdl);
}

static int
sio_alsa_start(struct sio_hdl *sh)
{
	struct sio_alsa_hdl *hdl = (struct sio_alsa_hdl *)sh;
	int err;

	DPRINTFN(2, "sio_alsa_start:\n");

	/*
	 * Starting with neither sio_setpar() nor sio_getpar() doesn't
	 * make sense, so we could just fail instead of initializing
	 * the device. Buf for now, let's do what other platforms do.
	 */
	if (!sio_alsa_init(hdl))
		return 0;

	hdl->ibpf = hdl->par.rchan * hdl->par.bps;
	hdl->obpf = hdl->par.pchan * hdl->par.bps;
	hdl->iused = 0;
	hdl->oused = 0;
	hdl->idelta = 0;
	hdl->odelta = 0;
	hdl->infds = 0;
	hdl->onfds = 0;
	hdl->running = 0;

	if (hdl->sio.mode & SIO_PLAY) {
		err = snd_pcm_prepare(hdl->opcm);
		if (err < 0) {
			DALSA("couldn't prepare play stream", err);
			hdl->sio.eof = 1;
			return 0;
		}
		hdl->otmpbuf = malloc(hdl->obpf);
		if (hdl->otmpbuf == NULL) {
			hdl->sio.eof = 1;
			return 0;
		}
		hdl->opartial = 0;
	}
	if (hdl->sio.mode & SIO_REC) {
		err = snd_pcm_prepare(hdl->ipcm);
		if (err < 0) {
			DALSA("couldn't prepare rec stream", err);
			hdl->sio.eof = 1;
			return 0;
		}
		hdl->itmpbuf = malloc(hdl->ibpf);
		if (hdl->itmpbuf == NULL) {
			hdl->sio.eof = 1;
			return 0;
		}
		hdl->ipartial = 0;
	}
	if ((hdl->sio.mode & SIO_PLAY) && (hdl->sio.mode & SIO_REC)) {
		err = snd_pcm_link(hdl->ipcm, hdl->opcm);
		if (err < 0) {
			DALSA("couldn't link streams", err);
			hdl->sio.eof = 1;
			return 0;
		}
	}
	if (!(hdl->sio.mode & SIO_PLAY)) {
		err = snd_pcm_start(hdl->ipcm);
		if (err < 0) {
			DALSA("couldn't start rec stream", err);
			hdl->sio.eof = 1;
			return 0;
		}
	}
	return 1;
}

static int
sio_alsa_flush(struct sio_hdl *sh)
{
	struct sio_alsa_hdl *hdl = (struct sio_alsa_hdl *)sh;
	int err;

	if (hdl->sio.mode & SIO_PLAY) {
		err = snd_pcm_drop(hdl->opcm);
		if (err < 0) {
			DALSA("couldn't stop play stream", err);
			hdl->sio.eof = 1;
			return 0;
		}
		free(hdl->otmpbuf);
	}
	if (hdl->sio.mode & SIO_REC) {
		err = snd_pcm_drop(hdl->ipcm);
		if (err < 0) {
			DALSA("couldn't stop rec stream", err);
			hdl->sio.eof = 1;
			return 0;
		}
		free(hdl->itmpbuf);
	}
	if ((hdl->sio.mode & SIO_PLAY) && (hdl->sio.mode & SIO_REC)) {
		err = snd_pcm_unlink(hdl->ipcm);
		if (err < 0) {
			DALSA("couldn't unlink streams", err);
			hdl->sio.eof = 1;
			return 0;
		}
	}
	DPRINTFN(2, "sio_alsa_flush: stopped\n");
	return 1;
}

static int
sio_alsa_xrun(struct sio_alsa_hdl *hdl)
{
	int clk;
	int wsil, rdrop, cmove;
	int rbpf, rround;
	int wbpf;

	DPRINTFN(2, "sio_alsa_xrun:\n");
#ifdef DEBUG
	if (_sndio_debug >= 2)
		_sio_printpos(&hdl->sio);
#endif

	/*
	 * we assume rused/wused are zero if rec/play modes are not
	 * selected. This allows us to keep the same formula for all
	 * modes, provided we set rbpf/wbpf to 1 to avoid division by
	 * zero.
	 *
	 * to understand the formula, draw a picture :)
	 */
	rbpf = (hdl->sio.mode & SIO_REC) ?
	    hdl->sio.par.bps * hdl->sio.par.rchan : 1;
	wbpf = (hdl->sio.mode & SIO_PLAY) ?
	    hdl->sio.par.bps * hdl->sio.par.pchan : 1;
	rround = hdl->sio.par.round * rbpf;

	clk = hdl->sio.cpos % hdl->sio.par.round;
	rdrop = (clk * rbpf - hdl->sio.rused) % rround;
	if (rdrop < 0)
		rdrop += rround;
	cmove = (rdrop + hdl->sio.rused) / rbpf;
	wsil = cmove * wbpf + hdl->sio.wused;

	DPRINTFN(2, "wsil = %d, cmove = %d, rdrop = %d\n", wsil, cmove, rdrop);

	if (!sio_alsa_flush(&hdl->sio))
		return 0;
	if (!sio_alsa_start(&hdl->sio))
		return 0;
	if (hdl->sio.mode & SIO_PLAY) {
		hdl->odelta -= cmove;
		hdl->sio.wsil = wsil;
	}
	if (hdl->sio.mode & SIO_REC) {
		hdl->idelta -= cmove;
		hdl->sio.rdrop = rdrop;
	}
	DPRINTFN(2, "xrun: corrected\n");
	DPRINTFN(2, "wsil = %d, rdrop = %d, odelta = %d, idelta = %d\n",
	    wsil, rdrop, hdl->odelta, hdl->idelta);
	return 1;
}

static int
sio_alsa_setpar_hw(snd_pcm_t *pcm, snd_pcm_hw_params_t *hwp,
    snd_pcm_format_t *reqfmt, unsigned int *rate, unsigned int *chans,
    snd_pcm_uframes_t *round, unsigned int *periods)
{
	static snd_pcm_format_t fmts[] = {
		SND_PCM_FORMAT_S32_LE,	SND_PCM_FORMAT_S32_BE,
		SND_PCM_FORMAT_U32_LE,	SND_PCM_FORMAT_U32_BE,
		SND_PCM_FORMAT_S24_LE,	SND_PCM_FORMAT_S24_BE,
		SND_PCM_FORMAT_U24_LE,	SND_PCM_FORMAT_U24_BE,
		SND_PCM_FORMAT_S16_LE,	SND_PCM_FORMAT_S16_BE,
		SND_PCM_FORMAT_U16_LE,	SND_PCM_FORMAT_U16_BE,
		SND_PCM_FORMAT_U8,	SND_PCM_FORMAT_S8
	};
	int i, err, dir = 0;
	unsigned req_rate, min_periods = 2;
	unsigned int sample_size, period_mask;

	req_rate = *rate;

	err = snd_pcm_hw_free(pcm);
	if (err < 0) {
		DALSA("couldn't reset hw configuration", err);
		return 0;
	}
	err = snd_pcm_hw_params_any(pcm, hwp);
	if (err < 0) {
		DALSA("couldn't init pars", err);
		return 0;
	}
	err = snd_pcm_hw_params_set_access(pcm, hwp,
	    SND_PCM_ACCESS_RW_INTERLEAVED);
	if (err < 0) {
		DALSA("couldn't set interleaved access", err);
		return 0;
	}
	err = snd_pcm_hw_params_test_format(pcm, hwp, *reqfmt);
	if (err < 0) {
		for (i = 0; ; i++) {
			if (i == sizeof(fmts) / sizeof(snd_pcm_format_t)) {
				DPRINTF("no known format found\n");
				return 0;
			}
			err = snd_pcm_hw_params_test_format(pcm, hwp, fmts[i]);
			if (err)
				continue;
			*reqfmt = fmts[i];
			break;
		}
	}
	err = snd_pcm_hw_params_set_format(pcm, hwp, *reqfmt);
	if (err < 0) {
		DALSA("couldn't set fmt", err);
		return 0;
	}
	err = snd_pcm_hw_params_set_rate_resample(pcm, hwp, 0);
	if (err < 0) {
		DALSA("couldn't turn resampling off", err);
		return 0;
	}
	err = snd_pcm_hw_params_set_rate_near(pcm, hwp, rate, 0);
	if (err < 0) {
		DALSA("couldn't set rate", err);
		return 0;
	}
	err = snd_pcm_hw_params_set_channels_near(pcm, hwp, chans);
	if (err < 0) {
		DALSA("couldn't set channel count", err);
		return 0;
	}
	err = snd_pcm_hw_params_set_periods_integer(pcm, hwp);
	if (err < 0) {
		DALSA("couldn't set periods to integer", err);
		return 0;
	}
	err = snd_pcm_hw_params_set_periods_min(pcm, hwp, &min_periods, NULL);
	if (err < 0) {
		DALSA("couldn't set minimum periods", err);
		return 0;
	}
	err = snd_pcm_hw_params_set_period_size_integer(pcm, hwp);
	if (err < 0) {
		DALSA("couldn't set period to integer", err);
		return 0;
	}

	/*
	 * Make sure that the period size is multiple of 64B
	 * to workaround possible driver bugs. Try to allow small
	 * period sizes if the sample size is allows to do so.
	 */
	sample_size = snd_pcm_format_size(*reqfmt, 1);
	switch (sample_size) {
	case 4:
		period_mask = 15;
		break;
	case 2:
		period_mask = 31;
		break;
	default:
		period_mask = 63;
	}

	*round = *round * *rate / req_rate;
	*round = (*round + period_mask) & ~period_mask;

	err = snd_pcm_hw_params_set_period_size_near(pcm, hwp, round, &dir);
	if (err < 0) {
		DALSA("couldn't set period size failed", err);
		return 0;
	}
	err = snd_pcm_hw_params_set_periods_near(pcm, hwp, periods, &dir);
	if (err < 0) {
		DALSA("couldn't set period count", err);
		return 0;
	}
	err = snd_pcm_hw_params(pcm, hwp);
	if (err < 0) {
		DALSA("couldn't commit params", err);
		return 0;
	}
	return 1;
}

static int
sio_alsa_getcap_hw(snd_pcm_t *pcm, int *rates, int *fmts, int *chans)
{
	int i, err;
	snd_pcm_hw_params_t *hwp;

	snd_pcm_hw_params_alloca(&hwp);

	err = snd_pcm_hw_params_any(pcm, hwp);
	if (err < 0) {
		DALSA("sio_alsa_trypar: couldn't init pars", err);
		return 0;
	}

	*fmts = 0;
	for (i = 0; i < CAP_NFMTS; i++) {
		err = snd_pcm_hw_params_test_format(pcm, hwp, cap_fmts[i]);
		if (err == 0) {
			*fmts |= 1 << i;
		}
	}
	*rates = 0;
	for (i = 0; i < CAP_NRATES; i++) {
		err = snd_pcm_hw_params_test_rate(pcm, hwp, cap_rates[i], 0);
		if (err == 0) {
			*rates |= 1 << i;
		}
	}
	*chans = 0;
	for (i = 0; i < CAP_NCHANS; i++) {
		err = snd_pcm_hw_params_test_channels(pcm, hwp, cap_chans[i]);
		if (err == 0) {
			*chans |= 1 << i;
		}
	}
	return 1;
}

/*
 * guess device capabilities
 */
static int
sio_alsa_getcap(struct sio_hdl *sh, struct sio_cap *cap)
{
	struct sio_alsa_hdl *hdl = (struct sio_alsa_hdl *)sh;
	int irates, orates, ifmts, ofmts, ichans, ochans;
	int i;

	irates = orates = ifmts = ofmts = ichans = ochans = 0;

	if (hdl->sio.mode & SIO_PLAY) {
		if (!sio_alsa_getcap_hw(hdl->opcm,
			&orates, &ofmts, &ochans)) {
			return 0;
		}
	}
	if (hdl->sio.mode & SIO_REC) {
		if (!sio_alsa_getcap_hw(hdl->ipcm,
			&irates, &ifmts, &ichans)) {
			return 0;
		}
	}

	for (i = 0; i < CAP_NFMTS; i++) {
		sio_alsa_fmttopar(hdl, cap_fmts[i],
		    &cap->enc[i].bits,
		    &cap->enc[i].sig,
		    &cap->enc[i].le);
		cap->enc[i].bps = SIO_BPS(cap->enc[0].bits);
		cap->enc[i].msb = 1;
	}
	for (i = 0; i < CAP_NRATES; i++) {
		cap->rate[i] = cap_rates[i];
	}
	for (i = 0; i < CAP_NCHANS; i++) {
		cap->pchan[i] = cap_chans[i];
		cap->rchan[i] = cap_chans[i];
	}
	cap->confs[0].enc = ~0U;
	cap->confs[0].rate = ~0U;
	cap->confs[0].pchan = ~0U;
	cap->confs[0].rchan = ~0U;
	if (hdl->sio.mode & SIO_PLAY) {
		cap->confs[0].pchan &= ochans;
		cap->confs[0].enc &= ofmts;
		cap->confs[0].rate &= orates;
	}
	if (hdl->sio.mode & SIO_REC) {
		cap->confs[0].rchan &= ichans;
		cap->confs[0].enc &= ifmts;
		cap->confs[0].rate &= irates;
	}
	cap->nconf = 1;
	return 1;
#undef NCHANS
#undef NRATES
}

static int
sio_alsa_setpar(struct sio_hdl *sh, struct sio_par *par)
{
	struct sio_alsa_hdl *hdl = (struct sio_alsa_hdl *)sh;
	snd_pcm_hw_params_t *ohwp, *ihwp;
	snd_pcm_sw_params_t *oswp, *iswp;
	snd_pcm_uframes_t iround, oround;
	snd_pcm_format_t ifmt, ofmt;
	unsigned int iperiods, operiods;
	unsigned irate, orate;
	int err;

	snd_pcm_hw_params_alloca(&ohwp);
	snd_pcm_sw_params_alloca(&oswp);
	snd_pcm_hw_params_alloca(&ihwp);
	snd_pcm_sw_params_alloca(&iswp);

	sio_alsa_enctofmt(hdl, &ifmt, par->bits, par->sig, par->le);
	irate = (par->rate == ~0U) ? 48000 : par->rate;
	if (par->appbufsz != ~0U) {
		iround = (par->round != ~0U) ?
		    par->round : (par->appbufsz + 1) / 2;
		iperiods = par->appbufsz / iround;
		if (iperiods < 2)
			iperiods = 2;
	} else if (par->round != ~0U) {
		iround = par->round;
		iperiods = 2;
	} else {
		iperiods = 2;
		iround = irate / 100;
	}

	if (hdl->sio.mode & SIO_REC) {
		hdl->par.rchan = par->rchan;
		if (!sio_alsa_setpar_hw(hdl->ipcm, ihwp,
			&ifmt, &irate, &hdl->par.rchan,
			&iround, &iperiods)) {
			hdl->sio.eof = 1;
			return 0;
		}
	}
	ofmt = ifmt;
	orate = irate;
	oround = iround;
	operiods = iperiods;
	if (hdl->sio.mode & SIO_PLAY) {
		hdl->par.pchan = par->pchan;
		if (!sio_alsa_setpar_hw(hdl->opcm, ohwp,
			&ofmt, &orate, &hdl->par.pchan,
			&oround, &operiods)) {
			hdl->sio.eof = 1;
			return 0;
		}
		if (!(hdl->sio.mode & SIO_REC)) {
			ifmt = ofmt;
			irate = orate;
			iround = oround;
			iperiods = operiods;
		}
	}

	DPRINTFN(2, "ofmt = %u, orate = %u, oround = %u, operiods = %u\n",
	    ofmt, orate, (unsigned int)oround, operiods);
	DPRINTFN(2, "ifmt = %u, irate = %u, iround = %u, iperiods = %u\n",
	    ifmt, irate, (unsigned int)iround, iperiods);

	if (ifmt != ofmt) {
		DPRINTF("play and rec formats differ\n");
		hdl->sio.eof = 1;
		return 0;
	}
	if (irate != orate) {
		DPRINTF("play and rec rates differ\n");
		hdl->sio.eof = 1;
		return 0;
	}
	if (iround != oround) {
		DPRINTF("play and rec block sizes differ\n");
		hdl->sio.eof = 1;
		return 0;
	}
	if (!sio_alsa_fmttopar(hdl, ifmt,
		&hdl->par.bits, &hdl->par.sig, &hdl->par.le))
		return 0;
	hdl->par.msb = 1;
	hdl->par.bps = SIO_BPS(hdl->par.bits);
	hdl->par.rate = orate;
	hdl->par.round = oround;
	hdl->par.bufsz = oround * operiods;
	hdl->par.appbufsz = hdl->par.bufsz;

	/* software params */

	if (hdl->sio.mode & SIO_REC) {
		err = snd_pcm_sw_params_current(hdl->ipcm, iswp);
		if (err < 0) {
			DALSA("couldn't get current rec params", err);
			hdl->sio.eof = 1;
			return 0;
		}
		err = snd_pcm_sw_params_set_start_threshold(hdl->ipcm,
		    iswp, 0);
		if (err < 0) {
			DALSA("couldn't set rec start threshold", err);
			hdl->sio.eof = 1;
			return 0;
		}
		err = snd_pcm_sw_params_set_stop_threshold(hdl->ipcm,
		    iswp, hdl->par.bufsz);
		if (err < 0) {
			DALSA("couldn't set rec stop threshold", err);
			hdl->sio.eof = 1;
			return 0;
		}
		err = snd_pcm_sw_params_set_avail_min(hdl->ipcm,
		    iswp, 1);
		if (err < 0) {
			DALSA("couldn't set rec avail min", err);
			hdl->sio.eof = 1;
			return 0;
		}
		err = snd_pcm_sw_params_set_period_event(hdl->ipcm, iswp, 1);
		if (err < 0) {
			DALSA("couldn't set rec period event", err);
			hdl->sio.eof = 1;
			return 0;
		}
		err = snd_pcm_sw_params(hdl->ipcm, iswp);
		if (err < 0) {
			DALSA("couldn't commit rec sw params", err);
			hdl->sio.eof = 1;
			return 0;
		}
	}
	if (hdl->sio.mode & SIO_PLAY) {
		err = snd_pcm_sw_params_current(hdl->opcm, oswp);
		if (err < 0) {
			DALSA("couldn't get current play params", err);
			hdl->sio.eof = 1;
			return 0;
		}
		err = snd_pcm_sw_params_set_start_threshold(hdl->opcm,
		    oswp, hdl->par.bufsz - hdl->par.round);
		if (err < 0) {
			DALSA("couldn't set play start threshold", err);
			hdl->sio.eof = 1;
			return 0;
		}
		err = snd_pcm_sw_params_set_stop_threshold(hdl->opcm,
		    oswp, hdl->par.bufsz);
		if (err < 0) {
			DALSA("couldn't set play stop threshold", err);
			hdl->sio.eof = 1;
			return 0;
		}
		err = snd_pcm_sw_params_set_avail_min(hdl->opcm,
		    oswp, 1);
		if (err < 0) {
			DALSA("couldn't set play avail min", err);
			hdl->sio.eof = 1;
			return 0;
		}
		err = snd_pcm_sw_params_set_period_event(hdl->opcm, oswp, 1);
		if (err < 0) {
			DALSA("couldn't set play period event", err);
			hdl->sio.eof = 1;
			return 0;
		}
		err = snd_pcm_sw_params(hdl->opcm, oswp);
		if (err < 0) {
			DALSA("couldn't commit play sw params", err);
			hdl->sio.eof = 1;
			return 0;
		}
	}
#ifdef DEBUG
	if (_sndio_debug >= 2) {
		if (hdl->sio.mode & SIO_REC)
			snd_pcm_dump(hdl->ipcm, output);
		if (hdl->sio.mode & SIO_PLAY)
			snd_pcm_dump(hdl->opcm, output);
	}
#endif
	hdl->initialized = 1;
	return 1;
}

static int
sio_alsa_getpar(struct sio_hdl *sh, struct sio_par *par)
{
	struct sio_alsa_hdl *hdl = (struct sio_alsa_hdl *)sh;

	if (!sio_alsa_init(hdl))
		return 0;

	*par = hdl->par;
	return 1;
}

static size_t
sio_alsa_read(struct sio_hdl *sh, void *buf, size_t len)
{
	struct sio_alsa_hdl *hdl = (struct sio_alsa_hdl *)sh;
	snd_pcm_sframes_t n;
	size_t todo;

	if (hdl->ipartial > 0) {
		todo = hdl->ipartial;
		if (todo > len)
			todo = len;
		memcpy(buf, hdl->itmpbuf + hdl->ibpf - hdl->ipartial, todo);
		hdl->ipartial -= todo;
		return todo;
	} else {
		if (len < hdl->ibpf) {
			buf = hdl->itmpbuf;
			len = hdl->ibpf;
		}
	}
	todo = len / hdl->ibpf;
	if (todo == 0)
		return 0;
	while ((n = snd_pcm_readi(hdl->ipcm, buf, todo)) < 0) {
		if (n == -EINTR)
			continue;
		if (n == -EPIPE || n == -ESTRPIPE) {
			sio_alsa_xrun(hdl);
			return 0;
		}
		if (n != -EAGAIN) {
			DALSA("couldn't read data", n);
			hdl->sio.eof = 1;
		}
		return 0;
	}
	if (n == 0) {
		DPRINTF("sio_alsa_read: eof\n");
		hdl->sio.eof = 1;
		return 0;
	}
	hdl->idelta += n;
	if (buf == hdl->itmpbuf) {
		hdl->ipartial = hdl->ibpf;
		return 0;
	}
	return n * hdl->ibpf;
}

static size_t
sio_alsa_write(struct sio_hdl *sh, const void *buf, size_t len)
{
	struct sio_alsa_hdl *hdl = (struct sio_alsa_hdl *)sh;
	snd_pcm_sframes_t n;
	size_t todo;

	if (len < hdl->obpf || hdl->opartial > 0) {
		todo = hdl->obpf - hdl->opartial;
		if (todo > 0) {
			if (todo > len)
				todo = len;
			memcpy(hdl->otmpbuf + hdl->opartial, buf, todo);
			hdl->opartial += todo;
			return todo;
		}
		len = hdl->obpf;
		buf = hdl->otmpbuf;
	}
	todo = len / hdl->obpf;
	if (todo == 0)
		return 0;
	while ((n = snd_pcm_writei(hdl->opcm, buf, todo)) < 0) {
		if (n == -EINTR)
			continue;
		if (n == -ESTRPIPE || n == -EPIPE) {
			sio_alsa_xrun(hdl);
			return 0;
		}
		if (n != -EAGAIN) {
			DALSA("couldn't write data", n);
			hdl->sio.eof = 1;
		}
		return 0;
	}
	hdl->odelta += n;
	if (buf == hdl->otmpbuf) {
		if (n > 0)
			hdl->opartial = 0;
		return 0;
	}
	return n * hdl->obpf;
}

void
sio_alsa_onmove(struct sio_alsa_hdl *hdl)
{
	int delta;

	if (hdl->running) {
		switch (hdl->sio.mode & (SIO_PLAY | SIO_REC)) {
		case SIO_PLAY:
			delta = hdl->odelta;
			break;
		case SIO_REC:
			delta = hdl->idelta;
			break;
		default: /* SIO_PLAY | SIO_REC */
			delta = hdl->odelta > hdl->idelta ?
				hdl->odelta : hdl->idelta;
		}
		if (delta <= 0)
			return;
	} else {
		delta = 0;
		hdl->running = 1;
	}
	_sio_onmove_cb(&hdl->sio, delta);
	if (hdl->sio.mode & SIO_PLAY)
		hdl->odelta -= delta;
	if (hdl->sio.mode & SIO_REC)
		hdl->idelta -= delta;
}

static int
sio_alsa_nfds(struct sio_hdl *sh)
{
	struct sio_alsa_hdl *hdl = (struct sio_alsa_hdl *)sh;

	return hdl->nfds;
}

static int
sio_alsa_pollfd(struct sio_hdl *sh, struct pollfd *pfd, int events)
{
	struct sio_alsa_hdl *hdl = (struct sio_alsa_hdl *)sh;
	int i;

	if (hdl->sio.eof)
		return 0;

	hdl->events = events & (POLLIN | POLLOUT);
	if (!(hdl->sio.mode & SIO_PLAY))
		hdl->events &= ~POLLOUT;
	if (!(hdl->sio.mode & SIO_REC))
		hdl->events &= ~POLLIN;
	if (!hdl->sio.started)
		hdl->events = 0;
	memset(pfd, 0, sizeof(struct pollfd) * hdl->nfds);
	hdl->onfds = hdl->infds = 0;
	if (hdl->events & POLLOUT) {
		if (!hdl->running &&
		    snd_pcm_state(hdl->opcm) == SND_PCM_STATE_RUNNING)
			sio_alsa_onmove(hdl);
		hdl->onfds = snd_pcm_poll_descriptors(hdl->opcm,
		    pfd, hdl->nfds);
		if (hdl->onfds < 0) {
			DALSA("couldn't poll play descriptors",
			    hdl->onfds);
			hdl->sio.eof = 1;
			return 0;
		}
	}
	if (hdl->events & POLLIN) {
		if (!hdl->running &&
		    snd_pcm_state(hdl->ipcm) == SND_PCM_STATE_RUNNING)
			sio_alsa_onmove(hdl);
		hdl->infds = snd_pcm_poll_descriptors(hdl->ipcm,
		    pfd + hdl->onfds, hdl->nfds - hdl->onfds);
		if (hdl->infds < 0) {
			DALSA("couldn't poll rec descriptors",
			    hdl->infds);
			hdl->sio.eof = 1;
			return 0;
		}
	}
	DPRINTFN(4, "sio_alsa_pollfd: events = %x, nfds = %d + %d\n",
	    events, hdl->onfds, hdl->infds);

	for (i = 0; i < hdl->onfds + hdl->infds; i++) {
		DPRINTFN(4, "sio_alsa_pollfd: pfds[%d].events = %x\n",
		    i, pfd[i].events);
	}
	return hdl->onfds + hdl->infds;
}

int
sio_alsa_revents(struct sio_hdl *sh, struct pollfd *pfd)
{
	struct sio_alsa_hdl *hdl = (struct sio_alsa_hdl *)sh;
	snd_pcm_sframes_t iused, oavail, oused;
	snd_pcm_state_t istate, ostate;
	unsigned short revents, r;
	int nfds, err, i;

	if (hdl->sio.eof)
		return POLLHUP;

	for (i = 0; i < hdl->onfds + hdl->infds; i++) {
		DPRINTFN(4, "sio_alsa_revents: pfds[%d].revents = %x\n",
		    i, pfd[i].revents);
	}
	revents = nfds = 0;
	if (hdl->events & POLLOUT) {
		err = snd_pcm_poll_descriptors_revents(hdl->opcm,
		    pfd, hdl->onfds, &r);
		if (err < 0) {
			DALSA("couldn't get play events", err);
			hdl->sio.eof = 1;
			return POLLHUP;
		}
		revents |= r;
		nfds += hdl->onfds;
	}
	if (hdl->events & POLLIN) {
		err = snd_pcm_poll_descriptors_revents(hdl->ipcm,
		    pfd + nfds, hdl->infds, &r);
		if (err < 0) {
			DALSA("couldn't get rec events", err);
			hdl->sio.eof = 1;
			return POLLHUP;
		}
		revents |= r;
		nfds += hdl->infds;
	}
	if (hdl->sio.mode & SIO_PLAY) {
		ostate = snd_pcm_state(hdl->opcm);
		if (ostate == SND_PCM_STATE_XRUN) {
			if (!sio_alsa_xrun(hdl))
				return POLLHUP;
			return 0;
		}
		if (ostate == SND_PCM_STATE_RUNNING ||
		    ostate == SND_PCM_STATE_PREPARED) {
			oavail = snd_pcm_avail_update(hdl->opcm);
			if (oavail < 0) {
				if (oavail == -EPIPE || oavail == -ESTRPIPE) {
					if (!sio_alsa_xrun(hdl))
						return POLLHUP;
					return 0;
				}
				DALSA("couldn't get play buffer ptr", oavail);
				hdl->sio.eof = 1;
				return POLLHUP;
			}
			oused = hdl->par.bufsz - oavail;
			hdl->odelta -= oused - hdl->oused;
			hdl->oused = oused;
		}
	}
	if (hdl->sio.mode & SIO_REC) {
		istate = snd_pcm_state(hdl->ipcm);
		if (istate == SND_PCM_STATE_XRUN) {
			if (!sio_alsa_xrun(hdl))
				return POLLHUP;
			return 0;
		}
		if (istate == SND_PCM_STATE_RUNNING ||
		    istate == SND_PCM_STATE_PREPARED) {
			iused = snd_pcm_avail_update(hdl->ipcm);
			if (iused < 0) {
				if (iused == -EPIPE || iused == -ESTRPIPE) {
					if (!sio_alsa_xrun(hdl))
						return POLLHUP;
					return 0;
				}
				DALSA("couldn't get rec buffer ptr", iused);
				hdl->sio.eof = 1;
				return POLLHUP;
			}
			hdl->idelta += iused - hdl->iused;
			hdl->iused = iused;
		}
	}
	if ((revents & (POLLIN | POLLOUT)) && hdl->running)
		sio_alsa_onmove(hdl);
	return revents;
}
#endif /* defined USE_ALSA */
