/*-
 * SPDX-License-Identifier: BSD-2-Clause
 *
 * Copyright (c) 2003 Mathew Kanner
 * Copyright (c) 1998 The NetBSD Foundation, Inc.
 * All rights reserved.
 * Copyright (c) 2025 The FreeBSD Foundation
 *
 * Portions of this software were developed by Christos Margiolis
 * <christos@FreeBSD.org> under sponsorship from the FreeBSD Foundation.
 *
 * This code is derived from software contributed to The NetBSD Foundation
 * by Lennart Augustsson (augustss@netbsd.org).
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/conf.h>
#include <sys/fcntl.h>
#include <sys/kernel.h>
#include <sys/kobj.h>
#include <sys/limits.h>
#include <sys/lock.h>
#include <sys/mutex.h>
#include <sys/poll.h>
#include <sys/selinfo.h>
#include <sys/uio.h>

#ifdef HAVE_KERNEL_OPTION_HEADERS
#include "opt_snd.h"
#endif

#include <dev/sound/midi/midi.h>
#include <dev/sound/midi/midiq.h>

#include "mpu_if.h"

MALLOC_DEFINE(M_MIDI, "midi buffers", "Midi data allocation area");

#define MIDI_NAMELEN   16
struct snd_midi {
	KOBJ_FIELDS;
	struct mtx lock;
	void   *cookie;

	int	unit;
	int	channel;

	int	flags;			/* File flags */
	MIDIQ_HEAD(, char) inq, outq;
	int	rchan, wchan;
	struct selinfo rsel, wsel;
	int	hiwat;			/* QLEN(outq)>High-water -> disable
					 * writes from userland */
	struct cdev *dev;
};

static d_open_t midi_open;
static d_close_t midi_close;
static d_ioctl_t midi_ioctl;
static d_read_t midi_read;
static d_write_t midi_write;
static d_poll_t midi_poll;

static struct cdevsw midi_cdevsw = {
	.d_version = D_VERSION,
	.d_open = midi_open,
	.d_close = midi_close,
	.d_read = midi_read,
	.d_write = midi_write,
	.d_ioctl = midi_ioctl,
	.d_poll = midi_poll,
	.d_name = "midi",
};

struct unrhdr *dev_unr = NULL;
struct unrhdr *chn_unr = NULL;

/*
 * Register a new midi device.
 *
 * "cookie" is passed to the MPU calls, and is normally set to the driver's
 * softc.
 */
struct snd_midi *
midi_init(kobj_class_t cls, void *cookie)
{
	struct snd_midi *m;
	int inqsize, outqsize;
	uint8_t *ibuf = NULL;
	uint8_t *obuf = NULL;

	m = malloc(sizeof(*m), M_MIDI, M_WAITOK | M_ZERO);
	kobj_init((kobj_t)m, cls);
	inqsize = MPU_INQSIZE(m, cookie);
	outqsize = MPU_OUTQSIZE(m, cookie);

	if (!inqsize && !outqsize)
		goto err1;

	mtx_init(&m->lock, "raw midi", NULL, 0);

	if (inqsize)
		ibuf = malloc(inqsize, M_MIDI, M_WAITOK);
	if (outqsize)
		obuf = malloc(outqsize, M_MIDI, M_WAITOK);

	mtx_lock(&m->lock);

	m->hiwat = outqsize / 2;

	MIDIQ_INIT(m->inq, ibuf, inqsize);
	MIDIQ_INIT(m->outq, obuf, outqsize);

	m->flags = 0;
	m->unit = alloc_unr(dev_unr);
	m->channel = alloc_unr(chn_unr);
	m->cookie = cookie;

	if (MPU_INIT(m, cookie))
		goto err2;

	mtx_unlock(&m->lock);

	m->dev = make_dev(&midi_cdevsw, m->unit, UID_ROOT, GID_WHEEL, 0666,
	    "midi%d.%d", m->unit, m->channel);
	m->dev->si_drv1 = m;

	return m;

err2:
	mtx_destroy(&m->lock);

	free(MIDIQ_BUF(m->inq), M_MIDI);
	free(MIDIQ_BUF(m->outq), M_MIDI);
err1:
	free(m, M_MIDI);
	return NULL;
}

int
midi_uninit(struct snd_midi *m)
{
	mtx_lock(&m->lock);
	if (m->rchan) {
		wakeup(&m->rchan);
		m->rchan = 0;
	}
	if (m->wchan) {
		wakeup(&m->wchan);
		m->wchan = 0;
	}
	mtx_unlock(&m->lock);
	MPU_UNINIT(m, m->cookie);
	destroy_dev(m->dev);
	free_unr(dev_unr, m->unit);
	free_unr(chn_unr, m->channel);
	free(MIDIQ_BUF(m->inq), M_MIDI);
	free(MIDIQ_BUF(m->outq), M_MIDI);
	mtx_destroy(&m->lock);
	free(m, M_MIDI);

	return (0);
}

/*
 * midi_in: process all data until the queue is full, then discards the rest.
 * Since midi_in is a state machine, data discards can cause it to get out of
 * whack.  Process as much as possible.  It calls, wakeup, selnotify and
 * psignal at most once.
 */
int
midi_in(struct snd_midi *m, uint8_t *buf, int size)
{
	int used;

	mtx_lock(&m->lock);

	if (!(m->flags & M_RX)) {
		/* We should return 0 but this may stop receiving/sending. */
		mtx_unlock(&m->lock);
		return (size);
	}

	used = 0;

	if (MIDIQ_AVAIL(m->inq) > size) {
		used = size;
		MIDIQ_ENQ(m->inq, buf, size);
	} else {
		mtx_unlock(&m->lock);
		return 0;
	}
	if (m->rchan) {
		wakeup(&m->rchan);
		m->rchan = 0;
	}
	selwakeup(&m->rsel);
	mtx_unlock(&m->lock);
	return used;
}

/*
 * midi_out: The only clearer of the M_TXEN flag.
 */
int
midi_out(struct snd_midi *m, uint8_t *buf, int size)
{
	int used;

	mtx_lock(&m->lock);

	if (!(m->flags & M_TXEN)) {
		mtx_unlock(&m->lock);
		return (0);
	}

	used = min(size, MIDIQ_LEN(m->outq));
	if (used)
		MIDIQ_DEQ(m->outq, buf, used);
	if (MIDIQ_EMPTY(m->outq)) {
		m->flags &= ~M_TXEN;
		MPU_CALLBACK(m, m->cookie, m->flags);
	}
	if (used && MIDIQ_AVAIL(m->outq) > m->hiwat) {
		if (m->wchan) {
			wakeup(&m->wchan);
			m->wchan = 0;
		}
		selwakeup(&m->wsel);
	}
	mtx_unlock(&m->lock);
	return used;
}

int
midi_open(struct cdev *i_dev, int flags, int mode, struct thread *td)
{
	struct snd_midi *m = i_dev->si_drv1;
	int retval;

	if (m == NULL)
		return ENXIO;

	mtx_lock(&m->lock);

	retval = 0;

	if (flags & FREAD) {
		if (MIDIQ_SIZE(m->inq) == 0)
			retval = ENXIO;
		else if (m->flags & M_RX)
			retval = EBUSY;
		if (retval)
			goto err;
	}
	if (flags & FWRITE) {
		if (MIDIQ_SIZE(m->outq) == 0)
			retval = ENXIO;
		else if (m->flags & M_TX)
			retval = EBUSY;
		if (retval)
			goto err;
	}

	m->rchan = 0;
	m->wchan = 0;

	if (flags & FREAD) {
		m->flags |= M_RX | M_RXEN;
		/*
	         * Only clear the inq, the outq might still have data to drain
	         * from a previous session
	         */
		MIDIQ_CLEAR(m->inq);
	}

	if (flags & FWRITE)
		m->flags |= M_TX;

	MPU_CALLBACK(m, m->cookie, m->flags);

err:
	mtx_unlock(&m->lock);
	return retval;
}

int
midi_close(struct cdev *i_dev, int flags, int mode, struct thread *td)
{
	struct snd_midi *m = i_dev->si_drv1;
	int retval;
	int oldflags;

	if (m == NULL)
		return ENXIO;

	mtx_lock(&m->lock);

	if ((flags & FREAD && !(m->flags & M_RX)) ||
	    (flags & FWRITE && !(m->flags & M_TX))) {
		retval = ENXIO;
		goto err;
	}

	oldflags = m->flags;

	if (flags & FREAD)
		m->flags &= ~(M_RX | M_RXEN);
	if (flags & FWRITE)
		m->flags &= ~M_TX;

	if ((m->flags & (M_TXEN | M_RXEN)) != (oldflags & (M_RXEN | M_TXEN)))
		MPU_CALLBACK(m, m->cookie, m->flags);

	mtx_unlock(&m->lock);
	retval = 0;
err:	return retval;
}

/*
 * TODO: midi_read, per oss programmer's guide pg. 42 should return as soon
 * as data is available.
 */
int
midi_read(struct cdev *i_dev, struct uio *uio, int ioflag)
{
#define MIDI_RSIZE 32
	struct snd_midi *m = i_dev->si_drv1;
	int retval;
	int used;
	char buf[MIDI_RSIZE];

	retval = EIO;

	if (m == NULL)
		goto err0;

	mtx_lock(&m->lock);

	if (!(m->flags & M_RX))
		goto err1;

	while (uio->uio_resid > 0) {
		while (MIDIQ_EMPTY(m->inq)) {
			retval = EWOULDBLOCK;
			if (ioflag & O_NONBLOCK)
				goto err1;
			m->rchan = 1;
			retval = msleep(&m->rchan, &m->lock,
			    PCATCH | PDROP, "midi RX", 0);
			/*
			 * We slept, maybe things have changed since last
			 * dying check
			 */
			if (retval == EINTR)
				goto err0;
			if (m != i_dev->si_drv1)
				retval = ENXIO;
			if (retval)
				goto err0;
			mtx_lock(&m->lock);
			m->rchan = 0;
		}
		/*
	         * At this point, it is certain that m->inq has data
	         */

		used = min(MIDIQ_LEN(m->inq), uio->uio_resid);
		used = min(used, MIDI_RSIZE);

		MIDIQ_DEQ(m->inq, buf, used);
		mtx_unlock(&m->lock);
		retval = uiomove(buf, used, uio);
		if (retval)
			goto err0;
		mtx_lock(&m->lock);
	}

	/*
	 * If we Made it here then transfer is good
	 */
	retval = 0;
err1:
	mtx_unlock(&m->lock);
err0:
	return retval;
}

/*
 * midi_write: The only setter of M_TXEN
 */

int
midi_write(struct cdev *i_dev, struct uio *uio, int ioflag)
{
#define MIDI_WSIZE 32
	struct snd_midi *m = i_dev->si_drv1;
	int retval;
	int used;
	char buf[MIDI_WSIZE];

	retval = EIO;
	if (m == NULL)
		goto err0;

	mtx_lock(&m->lock);

	if (!(m->flags & M_TX))
		goto err1;

	while (uio->uio_resid > 0) {
		while (MIDIQ_AVAIL(m->outq) == 0) {
			retval = EWOULDBLOCK;
			if (ioflag & O_NONBLOCK)
				goto err1;
			m->wchan = 1;
			retval = msleep(&m->wchan, &m->lock,
			    PCATCH | PDROP, "midi TX", 0);
			/*
			 * We slept, maybe things have changed since last
			 * dying check
			 */
			if (retval == EINTR)
				goto err0;
			if (m != i_dev->si_drv1)
				retval = ENXIO;
			if (retval)
				goto err0;
			mtx_lock(&m->lock);
			m->wchan = 0;
		}

		/*
	         * We are certain than data can be placed on the queue
	         */

		used = min(MIDIQ_AVAIL(m->outq), uio->uio_resid);
		used = min(used, MIDI_WSIZE);

		mtx_unlock(&m->lock);
		retval = uiomove(buf, used, uio);
		if (retval)
			goto err0;
		mtx_lock(&m->lock);
		MIDIQ_ENQ(m->outq, buf, used);
		/*
	         * Inform the bottom half that data can be written
	         */
		if (!(m->flags & M_TXEN)) {
			m->flags |= M_TXEN;
			MPU_CALLBACK(m, m->cookie, m->flags);
		}
	}
	/*
	 * If we Made it here then transfer is good
	 */
	retval = 0;
err1:
	mtx_unlock(&m->lock);
err0:	return retval;
}

int
midi_ioctl(struct cdev *i_dev, u_long cmd, caddr_t arg, int mode,
    struct thread *td)
{
	return ENXIO;
}

int
midi_poll(struct cdev *i_dev, int events, struct thread *td)
{
	struct snd_midi *m = i_dev->si_drv1;
	int revents;

	if (m == NULL)
		return 0;

	revents = 0;

	mtx_lock(&m->lock);

	if (events & (POLLIN | POLLRDNORM)) {
		if (!MIDIQ_EMPTY(m->inq))
			revents |= events & (POLLIN | POLLRDNORM);
		else
			selrecord(td, &m->rsel);
	}
	if (events & (POLLOUT | POLLWRNORM)) {
		if (MIDIQ_AVAIL(m->outq) < m->hiwat)
			revents |= events & (POLLOUT | POLLWRNORM);
		else
			selrecord(td, &m->wsel);
	}

	mtx_unlock(&m->lock);

	return (revents);
}

static void
midi_sysinit(void *data __unused)
{
	dev_unr = new_unrhdr(0, INT_MAX, NULL);
	chn_unr = new_unrhdr(0, INT_MAX, NULL);
}
SYSINIT(midi_sysinit, SI_SUB_DRIVERS, SI_ORDER_FIRST, midi_sysinit, NULL);

static void
midi_sysuninit(void *data __unused)
{
	if (dev_unr != NULL)
		delete_unrhdr(dev_unr);
	if (chn_unr != NULL)
		delete_unrhdr(chn_unr);
}
SYSUNINIT(midi_sysuninit, SI_SUB_DRIVERS, SI_ORDER_ANY, midi_sysuninit, NULL);
