/*-
 * Copyright (c) 2019 Oleksandr Tymoshenko <gonzo@FreeBSD.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 AUTHOR ``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 AUTHOR 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/cdefs.h>
__FBSDID("$FreeBSD$");

#include "opt_platform.h"

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/bus.h>
#include <sys/clock.h>
#include <sys/kernel.h>
#include <sys/lock.h>
#include <sys/module.h>
#include <sys/endian.h>

#include <dev/ofw/ofw_bus.h>
#include <dev/ofw/ofw_bus_subr.h>

#include <dev/sound/fdt/audio_dai.h>
#include <dev/sound/pcm/sound.h>
#include "audio_dai_if.h"

#define	AUDIO_BUFFER_SIZE	48000 * 4

struct audio_soc_aux_node {
	SLIST_ENTRY(audio_soc_aux_node)	link;
	device_t			dev;
};

struct audio_soc_channel {
	struct audio_soc_softc	*sc;	/* parent device's softc */
	struct pcm_channel 	*pcm;	/* PCM channel */
	struct snd_dbuf		*buf;	/* PCM buffer */
	int			dir;	/* direction */
};

struct audio_soc_softc {
	/*
	 * pcm_register assumes that sc is snddev_info,
	 * so this has to be first structure member for "compatibility"
	 */
	struct snddev_info	info;
	device_t		dev;
	char			*name;
	struct intr_config_hook init_hook;
	device_t		cpu_dev;
	device_t		codec_dev;
	SLIST_HEAD(, audio_soc_aux_node)	aux_devs;
	unsigned int		mclk_fs;
	struct audio_soc_channel 	play_channel;
	struct audio_soc_channel 	rec_channel;
	/*
	 * The format is from the CPU node, for CODEC node clock roles
	 * need to be reversed.
	 */
	uint32_t		format;
	uint32_t		link_mclk_fs;
};

static struct ofw_compat_data compat_data[] = {
	{"simple-audio-card",	1},
	{NULL,			0},
};

static struct {
	const char *name;
	unsigned int fmt;
} ausoc_dai_formats[] = {
	{ "i2s",	AUDIO_DAI_FORMAT_I2S },
	{ "right_j",	AUDIO_DAI_FORMAT_RJ },
	{ "left_j",	AUDIO_DAI_FORMAT_LJ },
	{ "dsp_a",	AUDIO_DAI_FORMAT_DSPA },
	{ "dsp_b",	AUDIO_DAI_FORMAT_DSPB },
	{ "ac97",	AUDIO_DAI_FORMAT_AC97 },
	{ "pdm",	AUDIO_DAI_FORMAT_PDM },
};

static int	audio_soc_probe(device_t dev);
static int	audio_soc_attach(device_t dev);
static int	audio_soc_detach(device_t dev);

/*
 * Invert master/slave roles for CODEC side of the node
 */
static uint32_t
audio_soc_reverse_clocks(uint32_t format)
{
	int fmt, pol, clk;

	fmt = AUDIO_DAI_FORMAT_FORMAT(format);
	pol = AUDIO_DAI_FORMAT_POLARITY(format);
	clk = AUDIO_DAI_FORMAT_CLOCK(format);

	switch (clk) {
	case AUDIO_DAI_CLOCK_CBM_CFM:
		clk = AUDIO_DAI_CLOCK_CBS_CFS;
		break;
	case AUDIO_DAI_CLOCK_CBS_CFM:
		clk = AUDIO_DAI_CLOCK_CBM_CFS;
		break;
	case AUDIO_DAI_CLOCK_CBM_CFS:
		clk = AUDIO_DAI_CLOCK_CBS_CFM;
		break;
	case AUDIO_DAI_CLOCK_CBS_CFS:
		clk = AUDIO_DAI_CLOCK_CBM_CFM;
		break;
	}

	return AUDIO_DAI_FORMAT(fmt, pol, clk);
}

static uint32_t
audio_soc_chan_setblocksize(kobj_t obj, void *data, uint32_t blocksz)
{

	return (blocksz);
}

static int
audio_soc_chan_setformat(kobj_t obj, void *data, uint32_t format)
{

	struct audio_soc_softc *sc;
	struct audio_soc_channel *ausoc_chan;

	ausoc_chan = data;
	sc = ausoc_chan->sc;

	return AUDIO_DAI_SET_CHANFORMAT(sc->cpu_dev, format);
}

static uint32_t
audio_soc_chan_setspeed(kobj_t obj, void *data, uint32_t speed)
{

	struct audio_soc_softc *sc;
	struct audio_soc_channel *ausoc_chan;
	uint32_t rate;
	struct audio_soc_aux_node *aux_node;

	ausoc_chan = data;
	sc = ausoc_chan->sc;

	if (sc->link_mclk_fs) {
		rate = speed * sc->link_mclk_fs;
		if (AUDIO_DAI_SET_SYSCLK(sc->cpu_dev, rate, AUDIO_DAI_CLOCK_IN))
			device_printf(sc->dev, "failed to set sysclk for CPU node\n");

		if (AUDIO_DAI_SET_SYSCLK(sc->codec_dev, rate, AUDIO_DAI_CLOCK_OUT))
			device_printf(sc->dev, "failed to set sysclk for codec node\n");

		SLIST_FOREACH(aux_node, &sc->aux_devs, link) {
			if (AUDIO_DAI_SET_SYSCLK(aux_node->dev, rate, AUDIO_DAI_CLOCK_OUT))
				device_printf(sc->dev, "failed to set sysclk for aux node\n");
		}
	}

	/*
	 * Let CPU node determine speed
	 */
	speed = AUDIO_DAI_SET_CHANSPEED(sc->cpu_dev, speed);
	AUDIO_DAI_SET_CHANSPEED(sc->codec_dev, speed);
	SLIST_FOREACH(aux_node, &sc->aux_devs, link) {
		AUDIO_DAI_SET_CHANSPEED(aux_node->dev, speed);
	}

	return (speed);
}

static uint32_t
audio_soc_chan_getptr(kobj_t obj, void *data)
{
	struct audio_soc_softc *sc;
	struct audio_soc_channel *ausoc_chan;

	ausoc_chan = data;
	sc = ausoc_chan->sc;

	return AUDIO_DAI_GET_PTR(sc->cpu_dev, ausoc_chan->dir);
}

static void *
audio_soc_chan_init(kobj_t obj, void *devinfo, struct snd_dbuf *b,
	struct pcm_channel *c, int dir)
{
	struct audio_soc_channel *ausoc_chan;
	void *buffer;

	ausoc_chan = devinfo;
	buffer = malloc(AUDIO_BUFFER_SIZE, M_DEVBUF, M_WAITOK | M_ZERO);

	if (sndbuf_setup(b, buffer, AUDIO_BUFFER_SIZE) != 0) {
		free(buffer, M_DEVBUF);
		return NULL;
	}

	ausoc_chan->dir = dir;
	ausoc_chan->buf = b;
	ausoc_chan->pcm = c;

	return (devinfo);
}

static int
audio_soc_chan_trigger(kobj_t obj, void *data, int go)
{
	struct audio_soc_softc *sc;
	struct audio_soc_channel *ausoc_chan;
	struct audio_soc_aux_node *aux_node;

	ausoc_chan = (struct audio_soc_channel *)data;
	sc = ausoc_chan->sc;
	AUDIO_DAI_TRIGGER(sc->codec_dev, go, ausoc_chan->dir);
	SLIST_FOREACH(aux_node, &sc->aux_devs, link) {
		AUDIO_DAI_TRIGGER(aux_node->dev, go, ausoc_chan->dir);
	}

	return AUDIO_DAI_TRIGGER(sc->cpu_dev, go, ausoc_chan->dir);
}

static int
audio_soc_chan_free(kobj_t obj, void *data)
{

	struct audio_soc_channel *ausoc_chan;
	void *buffer;

	ausoc_chan = (struct audio_soc_channel *)data;

	buffer = sndbuf_getbuf(ausoc_chan->buf);
	if (buffer)
		free(buffer, M_DEVBUF);

	return (0);
}

static struct pcmchan_caps *
audio_soc_chan_getcaps(kobj_t obj, void *data)
{
	struct audio_soc_softc *sc;
	struct audio_soc_channel *ausoc_chan;

	ausoc_chan = data;
	sc = ausoc_chan->sc;

	return AUDIO_DAI_GET_CAPS(sc->cpu_dev);
}

static kobj_method_t audio_soc_chan_methods[] = {
	KOBJMETHOD(channel_init, 	audio_soc_chan_init),
	KOBJMETHOD(channel_free, 	audio_soc_chan_free),
	KOBJMETHOD(channel_setformat, 	audio_soc_chan_setformat),
	KOBJMETHOD(channel_setspeed, 	audio_soc_chan_setspeed),
	KOBJMETHOD(channel_setblocksize,audio_soc_chan_setblocksize),
	KOBJMETHOD(channel_trigger,	audio_soc_chan_trigger),
	KOBJMETHOD(channel_getptr,	audio_soc_chan_getptr),
	KOBJMETHOD(channel_getcaps,	audio_soc_chan_getcaps),
	KOBJMETHOD_END
};
CHANNEL_DECLARE(audio_soc_chan);

static void
audio_soc_intr(void *arg)
{
	struct audio_soc_softc *sc;
	int channel_intr_required;

	sc = (struct audio_soc_softc *)arg;
	channel_intr_required = AUDIO_DAI_INTR(sc->cpu_dev, sc->play_channel.buf, sc->rec_channel.buf);
	if (channel_intr_required & AUDIO_DAI_PLAY_INTR)
		chn_intr(sc->play_channel.pcm);
	if (channel_intr_required & AUDIO_DAI_REC_INTR)
		chn_intr(sc->rec_channel.pcm);
}

static int
audio_soc_probe(device_t dev)
{

	if (!ofw_bus_status_okay(dev))
		return (ENXIO);

	if (ofw_bus_search_compatible(dev, compat_data)->ocd_data != 0) {
		device_set_desc(dev, "simple-audio-card");
		return (BUS_PROBE_DEFAULT);
	}

	return (ENXIO);
}

static void
audio_soc_init(void *arg)
{
	struct audio_soc_softc *sc;
	phandle_t node, child;
	device_t daidev, auxdev;
	uint32_t xref;
	uint32_t *aux_devs;
	int ncells, i;
	struct audio_soc_aux_node *aux_node;

	sc = (struct audio_soc_softc *)arg;
	config_intrhook_disestablish(&sc->init_hook);

	node = ofw_bus_get_node(sc->dev);
	/* TODO: handle multi-link nodes */
	child = ofw_bus_find_child(node, "simple-audio-card,cpu");
	if (child == 0) {
		device_printf(sc->dev, "cpu node is missing\n");
		return;
	}
	if ((OF_getencprop(child, "sound-dai", &xref, sizeof(xref))) <= 0) {
		device_printf(sc->dev, "missing sound-dai property in cpu node\n");
		return;
	}
	daidev = OF_device_from_xref(xref);
	if (daidev == NULL) {
		device_printf(sc->dev, "no driver attached to cpu node\n");
		return;
	}
	sc->cpu_dev = daidev;

	child = ofw_bus_find_child(node, "simple-audio-card,codec");
	if (child == 0) {
		device_printf(sc->dev, "codec node is missing\n");
		return;
	}
	if ((OF_getencprop(child, "sound-dai", &xref, sizeof(xref))) <= 0) {
		device_printf(sc->dev, "missing sound-dai property in codec node\n");
		return;
	}
	daidev = OF_device_from_xref(xref);
	if (daidev == NULL) {
		device_printf(sc->dev, "no driver attached to codec node\n");
		return;
	}
	sc->codec_dev = daidev;

	/* Add AUX devices */
	aux_devs = NULL;
	ncells = OF_getencprop_alloc_multi(node, "simple-audio-card,aux-devs", sizeof(*aux_devs),
	    (void **)&aux_devs);

	for (i = 0; i < ncells; i++) {
		auxdev = OF_device_from_xref(aux_devs[i]);
		if (auxdev == NULL)
			device_printf(sc->dev, "warning: no driver attached to aux node\n");
		aux_node = (struct audio_soc_aux_node *)malloc(sizeof(*aux_node), M_DEVBUF, M_NOWAIT);
		if (aux_node == NULL) {
			device_printf(sc->dev, "failed to allocate aux node struct\n");
			return;
		}
		aux_node->dev = auxdev;
		SLIST_INSERT_HEAD(&sc->aux_devs, aux_node, link);
	}

	if (aux_devs)
		OF_prop_free(aux_devs);

	if (AUDIO_DAI_INIT(sc->cpu_dev, sc->format)) {
		device_printf(sc->dev, "failed to initialize cpu node\n");
		return;
	}

	/* Reverse clock roles for CODEC */
	if (AUDIO_DAI_INIT(sc->codec_dev, audio_soc_reverse_clocks(sc->format))) {
		device_printf(sc->dev, "failed to initialize codec node\n");
		return;
	}

	SLIST_FOREACH(aux_node, &sc->aux_devs, link) {
		if (AUDIO_DAI_INIT(aux_node->dev, audio_soc_reverse_clocks(sc->format))) {
			device_printf(sc->dev, "failed to initialize aux node\n");
			return;
		}
	}

	if (pcm_register(sc->dev, sc, 1, 1)) {
		device_printf(sc->dev, "failed to register PCM\n");
		return;
	}

	sc->play_channel.sc = sc;
	sc->rec_channel.sc = sc;

	pcm_addchan(sc->dev, PCMDIR_PLAY, &audio_soc_chan_class, &sc->play_channel);
	pcm_addchan(sc->dev, PCMDIR_REC, &audio_soc_chan_class, &sc->rec_channel);

	pcm_setstatus(sc->dev, "at EXPERIMENT");

	AUDIO_DAI_SETUP_INTR(sc->cpu_dev, audio_soc_intr, sc);
	AUDIO_DAI_SETUP_MIXER(sc->codec_dev, sc->dev);
	SLIST_FOREACH(aux_node, &sc->aux_devs, link) {
		AUDIO_DAI_SETUP_MIXER(aux_node->dev, sc->dev);
	}
}

static int
audio_soc_attach(device_t dev)
{
	struct audio_soc_softc *sc;
	char *name;
	phandle_t node, cpu_child;
	uint32_t xref;
	int i, ret;
	char tmp[32];
	unsigned int fmt, pol, clk;
	bool frame_master, bitclock_master;

	sc = device_get_softc(dev);
	sc->dev = dev;
	node = ofw_bus_get_node(dev);

	ret = OF_getprop_alloc(node, "name", (void **)&name);
	if (ret == -1)
		name = "SoC audio";

	sc->name = strdup(name, M_DEVBUF);
	device_set_desc(dev, sc->name);

	if (ret != -1)
		OF_prop_free(name);

	SLIST_INIT(&sc->aux_devs);

	ret = OF_getprop(node, "simple-audio-card,format", tmp, sizeof(tmp));
	if (ret == 0) {
		for (i = 0; i < nitems(ausoc_dai_formats); i++) {
			if (strcmp(tmp, ausoc_dai_formats[i].name) == 0) {
				fmt = ausoc_dai_formats[i].fmt;
				break;
			}
		}
		if (i == nitems(ausoc_dai_formats))
			return (EINVAL);
	} else
		fmt = AUDIO_DAI_FORMAT_I2S;

	if (OF_getencprop(node, "simple-audio-card,mclk-fs",
	    &sc->link_mclk_fs, sizeof(sc->link_mclk_fs)) <= 0)
		sc->link_mclk_fs = 0;

	/* Unless specified otherwise, CPU node is the master */
	frame_master = bitclock_master = true;

	cpu_child = ofw_bus_find_child(node, "simple-audio-card,cpu");

	if ((OF_getencprop(node, "simple-audio-card,frame-master", &xref, sizeof(xref))) > 0)
		frame_master = cpu_child == OF_node_from_xref(xref);

	if ((OF_getencprop(node, "simple-audio-card,bitclock-master", &xref, sizeof(xref))) > 0)
		bitclock_master = cpu_child == OF_node_from_xref(xref);

	if (frame_master) {
		clk = bitclock_master ?
		    AUDIO_DAI_CLOCK_CBM_CFM : AUDIO_DAI_CLOCK_CBS_CFM;
	} else {
		clk = bitclock_master ?
		    AUDIO_DAI_CLOCK_CBM_CFS : AUDIO_DAI_CLOCK_CBS_CFS;
	}

	bool bitclock_inversion = OF_hasprop(node, "simple-audio-card,bitclock-inversion");
	bool frame_inversion = OF_hasprop(node, "simple-audio-card,frame-inversion");
	if (bitclock_inversion) {
		pol = frame_inversion ?
		    AUDIO_DAI_POLARITY_IB_IF : AUDIO_DAI_POLARITY_IB_NF;
	} else {
		pol = frame_inversion ?
		    AUDIO_DAI_POLARITY_NB_IF : AUDIO_DAI_POLARITY_NB_NF;
	}

	sc->format = AUDIO_DAI_FORMAT(fmt, pol, clk);

	sc->init_hook.ich_func = audio_soc_init;
	sc->init_hook.ich_arg = sc;
	if (config_intrhook_establish(&sc->init_hook) != 0)
		return (ENOMEM);

	return (0);
}

static int
audio_soc_detach(device_t dev)
{
	struct audio_soc_softc *sc;
	struct audio_soc_aux_node *aux;

	sc = device_get_softc(dev);
	if (sc->name)
		free(sc->name, M_DEVBUF);

	while ((aux = SLIST_FIRST(&sc->aux_devs)) != NULL) {
		SLIST_REMOVE_HEAD(&sc->aux_devs, link);
		free(aux, M_DEVBUF);
	}

	return (0);
}

static device_method_t audio_soc_methods[] = {
        /* device_if methods */
	DEVMETHOD(device_probe,		audio_soc_probe),
	DEVMETHOD(device_attach,	audio_soc_attach),
	DEVMETHOD(device_detach,	audio_soc_detach),

	DEVMETHOD_END,
};

static driver_t audio_soc_driver = {
	"pcm",
	audio_soc_methods,
	sizeof(struct audio_soc_softc),
};

DRIVER_MODULE(audio_soc, simplebus, audio_soc_driver, NULL, NULL);
MODULE_VERSION(audio_soc, 1);