/*
 * Copyright (c) 2026 Abdelkader Boudih <freebsd@seuros.com>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

/*
 * fwisound(4) - Apple FireWire audio driver
 *
 * Protocol reverse-engineered by Clemens Ladisch (Linux isight_audio).
 */

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/module.h>
#include <sys/bus.h>
#include <sys/kernel.h>
#include <sys/malloc.h>
#include <sys/lock.h>
#include <sys/mutex.h>
#include <sys/sysctl.h>
#include <sys/taskqueue.h>
#include <sys/mbuf.h>

#include <dev/firewire/firewire.h>
#include <dev/firewire/firewirereg.h>
#include <dev/firewire/iec13213.h>
#include <dev/firewire/fwisound.h>
#include <dev/firewire/fw_helpers.h>

static MALLOC_DEFINE(M_FWISOUND, "fwisound", "Apple FireWire Audio");

static int debug = 0;
static int iso_channel = FWISOUND_ISO_CHANNEL;
SYSCTL_DECL(_hw_firewire);
static SYSCTL_NODE(_hw_firewire, OID_AUTO, fwisound,
    CTLFLAG_RD | CTLFLAG_MPSAFE, 0, "Apple FireWire Audio");
SYSCTL_INT(_hw_firewire_fwisound, OID_AUTO, debug, CTLFLAG_RWTUN, &debug, 0,
    "fwisound debug level");
SYSCTL_INT(_hw_firewire_fwisound, OID_AUTO, iso_channel, CTLFLAG_RWTUN,
    &iso_channel, 0, "ISO channel for isochronous receive (default 1)");

#define FWISOUND_DEBUG(lev, fmt, ...)					\
	do {								\
		if (debug >= (lev))					\
			printf("fwisound: " fmt, ## __VA_ARGS__);	\
	} while (0)

static void	fwisound_identify(driver_t *, device_t);
static int	fwisound_probe(device_t);
static int	fwisound_attach(device_t);
static int	fwisound_detach(device_t);
static void	fwisound_post_busreset(void *);
static void	fwisound_post_explore(void *);
static void	fwisound_probe_task(void *, int);
static void	fwisound_start_task(void *, int);
static void	fwisound_stop_task(void *, int);
static void	fwisound_iso_input(struct fw_xferq *);

static int
fwisound_read_quadlet(struct fwisound_softc *sc, uint32_t offset, uint32_t *val)
{
	uint16_t dst;
	uint8_t spd;
	int err;

	FWISOUND_LOCK(sc);
	if (sc->fwdev == NULL) {
		FWISOUND_UNLOCK(sc);
		return (ENXIO);
	}
	dst = FWLOCALBUS | sc->fwdev->dst;
	spd = min(sc->fwdev->speed, FWSPD_S400);
	FWISOUND_UNLOCK(sc);

	err = fw_read_quadlet(sc->fd.fc, M_FWISOUND, dst, spd,
	    sc->cmd_hi, sc->cmd_lo + offset, val);
	if (err)
		FWISOUND_DEBUG(1, "read_quadlet: offset=0x%x err=%d\n",
		    offset, err);
	return (err);
}

static int
fwisound_write_quadlet(struct fwisound_softc *sc, uint32_t offset, uint32_t val)
{
	uint16_t dst;
	uint8_t spd;
	int err;

	FWISOUND_LOCK(sc);
	if (sc->fwdev == NULL) {
		FWISOUND_UNLOCK(sc);
		return (ENXIO);
	}
	dst = FWLOCALBUS | sc->fwdev->dst;
	spd = min(sc->fwdev->speed, FWSPD_S400);
	FWISOUND_UNLOCK(sc);

	err = fw_write_quadlet(sc->fd.fc, M_FWISOUND, dst, spd,
	    sc->cmd_hi, sc->cmd_lo + offset, val);
	if (err)
		FWISOUND_DEBUG(1, "write_quadlet: offset=0x%x err=%d\n",
		    offset, err);
	return (err);
}

static uint32_t
fwisound_find_audio_unit(struct fw_device *fwdev)
{
	struct csrhdr *hdr;
	struct csrdirectory *root, *udir;
	struct csrreg *reg, *u;
	int i, j, spec_ok, ver_ok;
	uint32_t audio_base;

	hdr = (struct csrhdr *)fwdev->csrrom;
	if (hdr->info_len <= 1)
		return (0);

	root = (struct csrdirectory *)(fwdev->csrrom + 1 + hdr->info_len);

	for (i = 0; i < root->crc_len; i++) {
		reg = &root->entry[i];

		if (reg->key != CROM_UDIR)
			continue;

		udir = (struct csrdirectory *)(reg + reg->val);

		spec_ok = 0;
		ver_ok = 0;
		audio_base = 0;

		for (j = 0; j < udir->crc_len; j++) {
			u = &udir->entry[j];

			if (u->key == CSRKEY_SPEC && u->val == FWISOUND_SPEC_ID)
				spec_ok = 1;
			else if (u->key == CSRKEY_VER &&
			    u->val == FWISOUND_VERSION)
				ver_ok = 1;
			else if (u->key == IIDC_CROM_CMD_BASE)
				audio_base = u->val;
		}

		if (spec_ok && ver_ok && audio_base != 0)
			return (audio_base);
	}

	return (0);
}

static void
fwisound_probe_task(void *arg, int pending __unused)
{
	struct fwisound_softc *sc = (struct fwisound_softc *)arg;
	uint32_t val;
	int err;

	if (sc->state == FWISOUND_STATE_DETACHING || sc->fwdev == NULL)
		return;

	err = fwisound_write_quadlet(sc, FWISOUND_REG_SAMPLE_RATE,
	    FWISOUND_RATE_48000);
	if (err) {
		device_printf(sc->dev,
		    "failed to set sample rate: %d\n", err);
		return;
	}

	if (fwisound_read_quadlet(sc, FWISOUND_REG_GAIN_RAW_START, &val) == 0)
		sc->gain_raw_min = (int32_t)val;
	if (fwisound_read_quadlet(sc, FWISOUND_REG_GAIN_RAW_END, &val) == 0)
		sc->gain_raw_max = (int32_t)val;

	FWISOUND_LOCK(sc);
	if (sc->state == FWISOUND_STATE_DETACHING) {
		FWISOUND_UNLOCK(sc);
		return;
	}
	sc->state = FWISOUND_STATE_PROBED;
	FWISOUND_UNLOCK(sc);
}

static void
fwisound_start_task(void *arg, int pending __unused)
{
	struct fwisound_softc *sc = (struct fwisound_softc *)arg;

	if (sc->state == FWISOUND_STATE_DETACHING)
		return;

	fwisound_iso_start(sc);
}

static void
fwisound_stop_task(void *arg, int pending __unused)
{
	struct fwisound_softc *sc = (struct fwisound_softc *)arg;

	fwisound_iso_stop(sc);

	FWISOUND_LOCK(sc);
	if (sc->state != FWISOUND_STATE_DETACHING)
		sc->state = FWISOUND_STATE_PROBED;
	FWISOUND_UNLOCK(sc);
}

int
fwisound_iso_start(struct fwisound_softc *sc)
{
	struct firewire_comm *fc = sc->fd.fc;
	struct fw_xferq *xferq;
	uint32_t val;
	uint8_t speed;
	int dma_ch, err;

	mtx_assert(&sc->mtx, MA_NOTOWNED);

	FWISOUND_LOCK(sc);
	if (sc->dma_ch >= 0 || sc->state == FWISOUND_STATE_STREAMING) {
		FWISOUND_UNLOCK(sc);
		return (0);
	}
	if (sc->state == FWISOUND_STATE_DETACHING || sc->fwdev == NULL) {
		FWISOUND_UNLOCK(sc);
		return (ENXIO);
	}
	speed = min(sc->fwdev->speed, FWSPD_S400);
	FWISOUND_UNLOCK(sc);

	dma_ch = fw_open_isodma(fc, 0);
	if (dma_ch < 0) {
		device_printf(sc->dev, "no IR DMA channel available\n");
		return (EBUSY);
	}

	FWISOUND_LOCK(sc);
	if (sc->dma_ch >= 0) {
		FWISOUND_UNLOCK(sc);
		fc->ir[dma_ch]->flag &= ~FWXFERQ_OPEN;
		return (0);
	}
	FWISOUND_UNLOCK(sc);

	xferq = fc->ir[dma_ch];
	xferq->flag |= FWXFERQ_EXTBUF | FWXFERQ_HANDLER | FWXFERQ_STREAM;

	xferq->flag &= ~FWXFERQ_CHTAGMASK;
	xferq->flag |= sc->iso_channel & FWXFERQ_CHTAGMASK;

	xferq->sc = (caddr_t)sc;
	xferq->hand = fwisound_iso_input;
	xferq->bnchunk = FWISOUND_ISO_NCHUNK;
	xferq->bnpacket = 1;
	xferq->psize = FWISOUND_ISO_PKTSIZE;
	xferq->queued = 0;
	xferq->buf = NULL;

	xferq->bulkxfer = malloc(
	    sizeof(struct fw_bulkxfer) * xferq->bnchunk,
	    M_FWISOUND, M_WAITOK | M_ZERO);

	fw_iso_init_chunks(xferq);

	sc->sample_total = 0;

	val = sc->iso_channel | ((uint32_t)speed << 16);
	err = fwisound_write_quadlet(sc, FWISOUND_REG_ISO_TX_CONFIG, val);
	if (err) {
		device_printf(sc->dev,
		    "failed to set ISO_TX_CONFIG: %d\n", err);
		goto fail;
	}

	err = fwisound_write_quadlet(sc, FWISOUND_REG_AUDIO_ENABLE,
	    FWISOUND_AUDIO_ENABLE);
	if (err) {
		device_printf(sc->dev,
		    "failed to enable audio: %d\n", err);
		goto fail;
	}

	err = fc->irx_enable(fc, dma_ch);
	if (err) {
		device_printf(sc->dev, "failed to start IR DMA: %d\n", err);
		fwisound_write_quadlet(sc, FWISOUND_REG_AUDIO_ENABLE, 0);
		goto fail;
	}

	FWISOUND_LOCK(sc);
	if (sc->state == FWISOUND_STATE_DETACHING) {
		FWISOUND_UNLOCK(sc);
		fc->irx_disable(fc, dma_ch);
		fwisound_write_quadlet(sc, FWISOUND_REG_AUDIO_ENABLE, 0);
		err = ENXIO;
		goto fail;
	}
	sc->dma_ch = dma_ch;
	sc->state = FWISOUND_STATE_STREAMING;
	FWISOUND_UNLOCK(sc);

	return (0);

fail:
	fw_iso_free_chunks(xferq, M_FWISOUND);
	xferq->flag &= ~(FWXFERQ_MODEMASK | FWXFERQ_OPEN | FWXFERQ_STREAM |
	    FWXFERQ_EXTBUF | FWXFERQ_HANDLER | FWXFERQ_CHTAGMASK);
	xferq->hand = NULL;
	sc->dma_ch = -1;
	return (err);
}

void
fwisound_iso_stop(struct fwisound_softc *sc)
{
	struct firewire_comm *fc = sc->fd.fc;
	struct fw_xferq *xferq;
	int dma_ch;

	FWISOUND_LOCK(sc);
	dma_ch = sc->dma_ch;
	if (dma_ch < 0) {
		FWISOUND_UNLOCK(sc);
		return;
	}
	sc->dma_ch = -1;
	FWISOUND_UNLOCK(sc);

	xferq = fc->ir[dma_ch];

	xferq->flag &= ~FWXFERQ_OPEN;
	xferq->hand = NULL;

	if (xferq->flag & FWXFERQ_RUNNING)
		fc->irx_disable(fc, dma_ch);

	if (sc->fwdev != NULL)
		fwisound_write_quadlet(sc, FWISOUND_REG_AUDIO_ENABLE, 0);

	FWISOUND_LOCK(sc);
	fw_iso_wait_inactive_locked(&sc->mtx, &sc->iso_active, "fwisis");
	FWISOUND_UNLOCK(sc);

	xferq->flag &= ~(FWXFERQ_MODEMASK | FWXFERQ_STREAM |
	    FWXFERQ_EXTBUF | FWXFERQ_HANDLER | FWXFERQ_CHTAGMASK);

	fw_iso_free_chunks(xferq, M_FWISOUND);
}

static void
fwisound_iso_input(struct fw_xferq *xferq)
{
	struct fwisound_softc *sc = (struct fwisound_softc *)xferq->sc;
	struct fwisound_chan *chan;
	struct fw_bulkxfer *sxfer;
	struct fw_pkt *fp;
	struct fwisound_payload *pay;
	struct mbuf *m;
	uint8_t *ring;
	uint32_t plen, nbytes, sample_count, ring_size, pos, chunk, old_ptr;
	int dma_ch, need_intr;

	FWISOUND_LOCK(sc);
	dma_ch = sc->dma_ch;
	if (dma_ch < 0) {
		FWISOUND_UNLOCK(sc);
		return;
	}
	sc->iso_active = 1;
	chan = sc->chan;
	FWISOUND_UNLOCK(sc);

	while ((sxfer = STAILQ_FIRST(&xferq->stvalid)) != NULL) {
		STAILQ_REMOVE_HEAD(&xferq->stvalid, link);

		m = fw_iso_dequeue(xferq, sxfer, sc->fd.fc);
		if (m == NULL)
			continue;

		fp = mtod(m, struct fw_pkt *);
		plen = fp->mode.stream.len;
		if (plen < 20 || (uint32_t)m->m_len < 4 + plen) {
			m_freem(m);
			continue;
		}

		pay = (struct fwisound_payload *)(mtod(m, uint8_t *) + 4);
		if (ntohl(pay->signature) != FWISOUND_SIGNATURE) {
			m_freem(m);
			continue;
		}

		sample_count = ntohl(pay->sample_count);
		if (sample_count == 0 || sample_count > 475) {
			m_freem(m);
			continue;
		}

		nbytes = sample_count * 4;
		if (plen < 16 + nbytes) {
			m_freem(m);
			continue;
		}

		if (sc->sample_total != 0 &&
		    ntohl(pay->sample_total) != sc->sample_total)
			sc->dropped++;
		sc->sample_total = ntohl(pay->sample_total) + sample_count;

		need_intr = 0;
		if (chan != NULL && chan->running &&
		    chan->buf != NULL && chan->pcm_chan != NULL) {
			ring = sndbuf_getbufofs(chan->buf, 0);
			ring_size = chan->buf->bufsize;
			old_ptr = chan->hw_ptr;

			uint8_t *src = (uint8_t *)pay->samples;
			uint32_t rem = nbytes;

			while (rem > 0) {
				pos = chan->hw_ptr % ring_size;
				chunk = MIN(rem, ring_size - pos);
				memcpy(ring + pos, src, chunk);
				chan->hw_ptr += chunk;
				src += chunk;
				rem -= chunk;
			}

			if (chan->buf->blksz == 0 ||
			    (old_ptr / chan->buf->blksz) !=
			    (chan->hw_ptr / chan->buf->blksz))
				need_intr = 1;
		}

		m_freem(m);

		if (need_intr) {
			chn_intr(chan->pcm_chan);
		}
	}

	fw_iso_rearm_done(xferq, sc->fd.fc, &sc->mtx, &sc->iso_active,
	    &sc->dma_ch, dma_ch);
}

static void
fwisound_post_explore(void *arg)
{
	struct fwisound_softc *sc = (struct fwisound_softc *)arg;
	struct fw_device *fwdev;
	uint32_t audio_base;
	int err;

	FWISOUND_LOCK(sc);
	if (sc->state == FWISOUND_STATE_DETACHING) {
		FWISOUND_UNLOCK(sc);
		return;
	}

	if (sc->fwdev != NULL) {
		STAILQ_FOREACH(fwdev, &sc->fd.fc->devices, link) {
			if (fwdev == sc->fwdev &&
			    fwdev->status == FWDEVATTACHED)
				break;
		}
		if (fwdev == NULL) {
			device_printf(sc->dev, "device disconnected\n");
			sc->fwdev = NULL;
			sc->state = FWISOUND_STATE_IDLE;
			FWISOUND_UNLOCK(sc);
			fwisound_iso_stop(sc);
			FWISOUND_LOCK(sc);
		}
	}

	if (sc->fwdev == NULL) {
		STAILQ_FOREACH(fwdev, &sc->fd.fc->devices, link) {
			if (fwdev->status != FWDEVATTACHED)
				continue;

			audio_base = fwisound_find_audio_unit(fwdev);
			if (audio_base == 0)
				continue;

			sc->fwdev = fwdev;
			sc->cmd_hi = 0xffff;
			sc->cmd_lo = 0xf0000000 | (audio_base << 2);

			FWISOUND_UNLOCK(sc);
			err = taskqueue_enqueue(taskqueue_thread,
			    &sc->probe_task);
			if (err != 0)
				device_printf(sc->dev,
				    "taskqueue_enqueue failed: %d\n", err);
			return;
		}
	}

	FWISOUND_UNLOCK(sc);
}

static void
fwisound_post_busreset(void *arg __unused)
{

}

static void
fwisound_identify(driver_t *driver, device_t parent)
{

	if (device_find_child(parent, "fwisound", DEVICE_UNIT_ANY) == NULL)
		BUS_ADD_CHILD(parent, 0, "fwisound", DEVICE_UNIT_ANY);
}

static int
fwisound_probe(device_t dev)
{

	device_set_desc(dev, "Apple FireWire Audio");
	return (0);
}

static int
fwisound_attach(device_t dev)
{
	struct fwisound_softc *sc;

	sc = device_get_softc(dev);
	sc->fd.dev = dev;
	sc->fd.fc = device_get_ivars(dev);
	sc->dev = dev;
	mtx_init(&sc->mtx, "fwisound", NULL, MTX_DEF);

	sc->fwdev = NULL;
	sc->state = FWISOUND_STATE_IDLE;
	sc->dma_ch = -1;
	sc->iso_active = 0;
	sc->iso_channel = (uint8_t)(iso_channel & FWXFERQ_CHTAGMASK);
	sc->chan = NULL;
	TASK_INIT(&sc->probe_task, 0, fwisound_probe_task, sc);
	TASK_INIT(&sc->start_task, 0, fwisound_start_task, sc);
	TASK_INIT(&sc->stop_task, 0, fwisound_stop_task, sc);

	sc->fd.post_busreset = fwisound_post_busreset;
	sc->fd.post_explore = fwisound_post_explore;

	sc->pcm_dev = device_add_child(dev, "pcm", DEVICE_UNIT_ANY);
	if (sc->pcm_dev == NULL) {
		device_printf(dev, "failed to add pcm child\n");
		mtx_destroy(&sc->mtx);
		return (ENXIO);
	}

	bus_attach_children(dev);

	fwisound_post_explore(sc);

	return (0);
}

static int
fwisound_detach(device_t dev)
{
	struct fwisound_softc *sc;

	sc = device_get_softc(dev);

	FWISOUND_LOCK(sc);
	sc->state = FWISOUND_STATE_DETACHING;
	FWISOUND_UNLOCK(sc);

	taskqueue_drain(taskqueue_thread, &sc->probe_task);
	taskqueue_drain(taskqueue_thread, &sc->start_task);
	taskqueue_drain(taskqueue_thread, &sc->stop_task);
	fwisound_iso_stop(sc);

	if (sc->pcm_dev != NULL)
		device_delete_children(dev);

	FWISOUND_LOCK(sc);
	sc->fwdev = NULL;
	FWISOUND_UNLOCK(sc);

	mtx_destroy(&sc->mtx);
	return (0);
}

static device_method_t fwisound_methods[] = {
	DEVMETHOD(device_identify,	fwisound_identify),
	DEVMETHOD(device_probe,		fwisound_probe),
	DEVMETHOD(device_attach,	fwisound_attach),
	DEVMETHOD(device_detach,	fwisound_detach),

	DEVMETHOD_END
};

static driver_t fwisound_driver = {
	"fwisound",
	fwisound_methods,
	sizeof(struct fwisound_softc),
};

DRIVER_MODULE(fwisound, firewire, fwisound_driver, 0, 0);
MODULE_VERSION(fwisound, 1);
MODULE_DEPEND(fwisound, firewire, 1, 1, 1);
MODULE_DEPEND(fwisound, sound, SOUND_MINVER, SOUND_PREFVER, SOUND_MAXVER);
