/*
   forward-alsa-core.c - ALSA driver for SoftLab-NSK Forward boards

   Copyright (C) 2017 - 2024 SoftLab-NSK <forward@softlab.tv>

   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License
   as published by the Free Software Foundation; either version 2
   of the License, or (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
*/

#include "forward.h"
#include "forward-alsa.h"
#include "forward-vdma.h"

#include "forward-alsa-io.h"

#include <linux/module.h>
#include <sound/core.h>
#include <sound/initval.h>
#include <linux/kthread.h>
#include <linux/vmalloc.h>

#define FORWARD_ALSA_MODE_NAME "alsa"

extern struct forward_alsa_dev_ops fd788_alsa_ops;
extern struct forward_alsa_dev_ops fd722_alsa_ops;
extern struct forward_alsa_dev_ops fd720_alsa_ops;
extern struct forward_alsa_dev_ops fd922_alsa_ops;
extern struct forward_alsa_dev_ops fd940_alsa_ops;
extern struct forward_alsa_dev_ops fd2110_alsa_ops;

int snd_pcm_alloc_vmalloc_buffer(struct snd_pcm_substream *subs, size_t size)
{
	struct snd_pcm_runtime *runtime = subs->runtime;

	if (runtime->dma_area)
		vfree(runtime->dma_area);

	runtime->dma_area = vzalloc(size);
	if (!runtime->dma_area)
		return -ENOMEM;

	runtime->dma_bytes = size;

	return 0;
}

void snd_pcm_free_vmalloc_buffer(struct snd_pcm_substream *subs)
{
	struct snd_pcm_runtime *runtime = subs->runtime;

	if (runtime->dma_area)
		vfree(runtime->dma_area);

	runtime->dma_area = 0;
	runtime->dma_bytes = 0;
}

struct page *snd_pcm_get_vmalloc_page(struct snd_pcm_substream *subs, unsigned long offset)
{
	void *pageptr = subs->runtime->dma_area + offset;
	return vmalloc_to_page(pageptr);
}

static void forward_alsa_remove(struct forward_dev *dev, void **private)
{
	struct forward_alsa *alsa = *private;
	int i;

	if (!alsa)
		return;

	if (alsa->card)
		snd_card_disconnect(alsa->card);

	if (alsa->io) {
		for (i = 0; i < alsa->dev->cfg.io_count; i++) {
			forward_alsa_io_deinit(&alsa->io[i]);
			dev->io[i].alsa = NULL;
		}

		devm_kfree(dev->dev, alsa->io);
	}
	alsa->io = NULL;

	if (alsa->card)
		snd_card_free(alsa->card);
	alsa->card = NULL;

	alsa->ops = NULL;

	*private = NULL;
	devm_kfree(dev->dev, alsa);
}

static int forward_alsa_probe(struct forward_dev *dev, void **private)
{
	int result = 0;
	struct forward_alsa *alsa;
	int i;

	if (!forward_has_mode(dev, FORWARD_ALSA_MODE_NAME))
		return -EOPNOTSUPP;

	alsa = devm_kzalloc(dev->dev, sizeof(struct forward_alsa), GFP_KERNEL);
	if (!alsa)
		return -ENOMEM;

	alsa->dev = dev;
	*private = alsa;

	switch (dev->type) {
	case FORWARD_FD788:
		alsa->ops = &fd788_alsa_ops;
		break;
	case FORWARD_FD722:
	case FORWARD_FD722M2:
		alsa->ops = &fd722_alsa_ops;
		break;
	case FORWARD_FD720:
		alsa->ops = &fd720_alsa_ops;
		break;
	case FORWARD_FD922:
		alsa->ops = &fd922_alsa_ops;
		break;
	case FORWARD_FD940:
		alsa->ops = &fd940_alsa_ops;
		break;
	case FORWARD_FD2110:
		alsa->ops = &fd2110_alsa_ops;
		break;
	default:
		result = -EOPNOTSUPP;
		goto fail;
		break;
	};

	result = snd_card_new(dev->dev, SNDRV_DEFAULT_IDX1, SNDRV_DEFAULT_STR1, THIS_MODULE, 0,
			      &alsa->card);

	if (result)
		goto fail;

	alsa->io = devm_kzalloc(dev->dev, dev->cfg.io_count * sizeof(struct forward_alsa_io),
				GFP_KERNEL);

	if (!alsa->io) {
		result = -ENOMEM;
		goto fail;
	}

	for (i = 0; i < dev->cfg.io_count; i++) {
		alsa->io[i].alsa = alsa;
		alsa->io[i].index = i;
		alsa->io[i].number = dev->io[i].number;
		alsa->io[i].state = dev->io[i].state;
		dev->io[i].alsa = &alsa->io[i];
		mutex_init(&alsa->io[i].lock);

		result = forward_alsa_io_init(&alsa->io[i]);
		if (result)
			goto fail;
	}

	strncpy(alsa->card->driver, FORWARD_ALSA_MODULE_NAME, sizeof(alsa->card->driver));
	strncpy(alsa->card->shortname, alsa->dev->name, sizeof(alsa->card->shortname));

	result = snd_card_register(alsa->card);
	if (result < 0)
		goto fail;

	forward_info(dev, "alsa: %d devices created", dev->cfg.io_count);

	return 0;

fail:
	forward_alsa_remove(dev, private);
	return result;
}

static bool forward_alsa_available(struct forward_dev *dev)
{
	switch (dev->type) {
	case FORWARD_FD722:
	case FORWARD_FD788:
	case FORWARD_FD720:
	case FORWARD_FD922:
	case FORWARD_FD940:
	case FORWARD_FD722M2:
	case FORWARD_FD2110:
		return true;
	default:
		return false;
	}
}

static int forward_alsa_reprobe(struct forward_dev *dev, void **private)
{
	if (!forward_has_mode(dev, FORWARD_ALSA_MODE_NAME))
		return -EOPNOTSUPP;

	forward_alsa_remove(dev, private);
	return forward_alsa_probe(dev, private);
}

static void forward_alsa_io_changed(struct forward_dev *dev, struct forward_io *io,
				    enum forward_io_state state, void **private)
{
	struct forward_alsa *alsa = *private;

	if (!alsa || !alsa->io)
		return;

	forward_alsa_io_deinit(&alsa->io[io->index]);
	alsa->io[io->index].number = io->number;
	alsa->io[io->index].state = state;
	forward_alsa_io_init(&alsa->io[io->index]);
	// Re-register card for device update
	snd_card_register(alsa->card);
}

static struct forward_extension forward_alsa_ext = {
	.name = FORWARD_ALSA_MODE_NAME,
	.version = FORWARD_VERSION_CODE,
	.available = forward_alsa_available,
	.probe = forward_alsa_probe,
	.reprobe = forward_alsa_reprobe,
	.remove = forward_alsa_remove,
	.io_changed = forward_alsa_io_changed,
};

static int forward_alsa_init(void)
{
	return forward_register_extension(&forward_alsa_ext);
}

static void forward_alsa_exit(void)
{
	forward_unregister_extension(&forward_alsa_ext);
}

module_init(forward_alsa_init);
module_exit(forward_alsa_exit);

MODULE_LICENSE("GPL");
