// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
/*
 * Copyright (c) 2019 Amlogic, Inc. All rights reserved.
 */

#include <linux/module.h>
#include <sound/core.h>
#include <sound/pcm.h>
#include <sound/pcm_params.h>
#include <sound/initval.h>
#include <sound/control.h>
#include <sound/soc.h>
#include "bridge_dsp_card.h"

#define BUFF_SIZE_MAX	(PAGE_SIZE * 64)
#define SOUND_CARD_ID 2
struct aml_aprocess_card *aprocess_card;
unsigned int dsp_pcm_num;
static struct snd_pcm_hardware aml_aprocess_hardware = {
	.info = SNDRV_PCM_INFO_INTERLEAVED | SNDRV_PCM_INFO_BLOCK_TRANSFER
		 | SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_MMAP_VALID
		 | SNDRV_PCM_INFO_PAUSE | SNDRV_PCM_INFO_RESUME,
	.formats = SNDRV_PCM_FMTBIT_S16_LE,
	.rate_min = 16000,
	.rate_max = 16000,
	.channels_min = 2,
	.channels_max = 2,
	.buffer_bytes_max = BUFF_SIZE_MAX,
	.period_bytes_max	=	1024,
	.period_bytes_min	=	1024,
	.periods_min		=	4,
	.periods_max		=	4,
	.fifo_size		=	0,
};

void aml_aprocess_set_hw(struct aml_aprocess *p_aprocess, int channels,
					int format, int rate, int period)
{
	if (!p_aprocess)
		return;
	p_aprocess->g_hw.rate_min = rate;
	p_aprocess->g_hw.rate_max = rate;
	p_aprocess->g_hw.channels_min = channels;
	p_aprocess->g_hw.channels_min = channels;
	p_aprocess->g_hw.formats = format;
	if (format == SNDRV_PCM_FMTBIT_S8)
		p_aprocess->g_hw.period_bytes_min = channels * period;
	else if (format == SNDRV_PCM_FMTBIT_S16)
		p_aprocess->g_hw.period_bytes_min = 2 * channels * period;
	else if (format == SNDRV_PCM_FMTBIT_S32)
		p_aprocess->g_hw.period_bytes_min = 4 * channels * period;
	else
		p_aprocess->g_hw.period_bytes_min = 2 * channels * period;

	p_aprocess->g_hw.period_bytes_max = p_aprocess->g_hw.period_bytes_min;
	p_aprocess->g_hw.period_bytes_max = p_aprocess->g_hw.period_bytes_min;

	p_aprocess->g_hw.buffer_bytes_max = 16 * p_aprocess->g_hw.period_bytes_max;
}

int aml_aprocess_complete(struct aml_aprocess *p_aprocess, char *out, int size)
{
	struct snd_pcm_substream *substream;
	struct snd_pcm_runtime *runtime;
	unsigned int pending;
	unsigned long flags, flags2;
	unsigned int hw_ptr;

	if (!p_aprocess)
		return 0;

	spin_lock_irqsave(&p_aprocess->lock, flags);
	if (!p_aprocess->status)
		goto fail;

	substream = p_aprocess->g_substream;
	if (!substream)
		goto fail;

	snd_pcm_stream_lock_irqsave(substream, flags2);

	runtime = substream->runtime;
	if (!runtime || !snd_pcm_running(substream)) {
		snd_pcm_stream_unlock_irqrestore(substream, flags2);
		goto fail;
	}
	hw_ptr = p_aprocess->g_hwptr;

	/* Pack DSP load in ALSA ring buffer */
	pending = runtime->dma_bytes - hw_ptr;

	if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK) {
		if (!snd_pcm_playback_hw_avail(runtime)) {
			snd_pcm_stream_unlock_irqrestore(substream, flags2);
			goto fail;
		}
		if (unlikely(pending < size)) {
			memcpy(out, runtime->dma_area + hw_ptr, pending);
			memcpy(out + pending, runtime->dma_area,
			       size - pending);
		} else {
			memcpy(out, runtime->dma_area + hw_ptr,
			       size);
		}
	} else {
		if (!snd_pcm_capture_hw_avail(runtime)) {
			snd_pcm_stream_unlock_irqrestore(substream, flags2);
			goto fail;
		}
		if (unlikely(pending < size)) {
			memcpy(runtime->dma_area + hw_ptr, out, pending);
			memcpy(runtime->dma_area, out + pending,
			       size - pending);
		} else {
			memcpy(runtime->dma_area + hw_ptr, out,
			       size);
		}
	}
	/* update hw_ptr after data is copied to memory */
	p_aprocess->g_hwptr = (hw_ptr + size) % runtime->dma_bytes;
	hw_ptr = p_aprocess->g_hwptr;
	snd_pcm_stream_unlock_irqrestore(substream, flags2);
	spin_unlock_irqrestore(&p_aprocess->lock, flags);
	if ((hw_ptr % snd_pcm_lib_period_bytes(substream)) < size)
		snd_pcm_period_elapsed(substream);

	return size;
fail:
	spin_unlock_irqrestore(&p_aprocess->lock, flags);
	return 0;
}

static int aml_aprocess_open(struct snd_pcm_substream *substream)
{
	struct aml_aprocess *p_aprocess;
	struct aml_aprocess_card *apro_card = snd_pcm_substream_chip(substream);
	struct snd_pcm_runtime *runtime = substream->runtime;

	if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
		p_aprocess = (struct aml_aprocess *)apro_card->p_prm;
	else
		p_aprocess = (struct aml_aprocess *)apro_card->c_prm;

	runtime->hw = p_aprocess->g_hw;
	snd_pcm_hw_constraint_integer(runtime, SNDRV_PCM_HW_PARAM_PERIODS);
	return 0;
}

static int aml_aprocess_close(struct snd_pcm_substream *substream)
{
	return 0;
}

static int aml_aprocess_hw_params(struct snd_pcm_substream *substream,
			     struct snd_pcm_hw_params *hw_params)
{
	return snd_pcm_lib_malloc_pages(substream,
					params_buffer_bytes(hw_params));
}

static int aml_aprocess_hw_free(struct snd_pcm_substream *substream)
{
	return snd_pcm_lib_free_pages(substream);
}

static int aml_aprocess_prepare(struct snd_pcm_substream *substream)
{
	return 0;
}

static int aml_aprocess_trigger(struct snd_pcm_substream *substream, int cmd)
{
	struct aml_aprocess *p_aprocess;
	struct aml_aprocess_card *apro_card = snd_pcm_substream_chip(substream);
	unsigned long flags;
	int err = 0;

	if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
		p_aprocess = (struct aml_aprocess *)apro_card->p_prm;
	else
		p_aprocess = (struct aml_aprocess *)apro_card->c_prm;

	spin_lock_irqsave(&p_aprocess->lock, flags);
	p_aprocess->g_hwptr = 0;
	switch (cmd) {
	case SNDRV_PCM_TRIGGER_START:
	case SNDRV_PCM_TRIGGER_RESUME:
		p_aprocess->status = 1;
		p_aprocess->g_substream = substream;
		break;
	case SNDRV_PCM_TRIGGER_STOP:
	case SNDRV_PCM_TRIGGER_SUSPEND:
		p_aprocess->status = 0;
		p_aprocess->g_substream = NULL;
		break;
	default:
		err = -EINVAL;
	}
	spin_unlock_irqrestore(&p_aprocess->lock, flags);
	return err;
}

static snd_pcm_uframes_t aml_aprocess_pointer(struct snd_pcm_substream *substream)
{
	struct aml_aprocess *p_aprocess;
	struct aml_aprocess_card *apro_card = snd_pcm_substream_chip(substream);
	struct snd_pcm_runtime *runtime = substream->runtime;

	if (substream->stream == SNDRV_PCM_STREAM_PLAYBACK)
		p_aprocess = (struct aml_aprocess *)apro_card->p_prm;
	else
		p_aprocess = (struct aml_aprocess *)apro_card->c_prm;

	return bytes_to_frames(runtime, p_aprocess->g_hwptr);
}

static struct snd_pcm_ops aml_aprocess_ops = {
	.open = aml_aprocess_open,
	.close = aml_aprocess_close,
	.ioctl = snd_pcm_lib_ioctl,
	.hw_params = aml_aprocess_hw_params,
	.hw_free = aml_aprocess_hw_free,
	.prepare = aml_aprocess_prepare,
	.trigger = aml_aprocess_trigger,
	.pointer = aml_aprocess_pointer,
};

struct aml_aprocess *aml_aprocess_init(struct device *dev, const char *pcm_name,
					const char *card_name, enum PCM_MODE modeid)
{
	struct aml_aprocess *p_aprocess;
	struct snd_card *card;
	struct snd_pcm *pcm;
	int i, err;

	p_aprocess = kzalloc(sizeof(*p_aprocess), GFP_KERNEL);
	if (!p_aprocess)
		return NULL;
	dsp_pcm_num++;
	if (!aprocess_card) {
		aprocess_card = kzalloc(sizeof(*aprocess_card), GFP_KERNEL);
		if (!aprocess_card)
			return NULL;

		aprocess_card->dev = dev;
		/* Choose any slot, with no id */
		for (i = 0; i < 3; i++) {
			err = snd_card_new(dev,
					SOUND_CARD_ID + i, NULL, THIS_MODULE, 0, &card);
			if (!err)
				break;
		}
		if (err < 0)
			goto fail;

		aprocess_card->card = card;
		/*
		 * Create first PCM device
		 * Create a substream only for non-zero channel streams
		 */
		err = snd_pcm_new(card, pcm_name, 0, 1, 1, &pcm);
		if (err < 0)
			goto snd_fail;

		strlcpy(pcm->name, pcm_name, sizeof(pcm->name));
		pcm->private_data = aprocess_card;
		aprocess_card->pcm = pcm;

		snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_CAPTURE, &aml_aprocess_ops);
		snd_pcm_set_ops(pcm, SNDRV_PCM_STREAM_PLAYBACK, &aml_aprocess_ops);

		strlcpy(card->driver, card_name, sizeof(card->driver));
		strlcpy(card->shortname, card_name, sizeof(card->shortname));
		sprintf(card->longname, "%s %i", card_name, card->dev->id);

		snd_pcm_lib_preallocate_pages_for_all(pcm, SNDRV_DMA_TYPE_CONTINUOUS,
			snd_dma_continuous_data(GFP_KERNEL), 0, BUFF_SIZE_MAX);

		err = snd_card_register(card);
		if (err < 0)
			goto snd_fail;
	}
	if (modeid == PCM_CAPTURE)
		aprocess_card->c_prm = p_aprocess;
	else
		aprocess_card->p_prm = p_aprocess;

	p_aprocess->a_card = aprocess_card;
	p_aprocess->status = 0;
	spin_lock_init(&p_aprocess->lock);
	memcpy(&p_aprocess->g_hw, &aml_aprocess_hardware, sizeof(p_aprocess->g_hw));
	return p_aprocess;
snd_fail:
	dsp_pcm_num--;
	if (!dsp_pcm_num && aprocess_card) {
		snd_card_free(card);
		kfree(aprocess_card);
		aprocess_card = NULL;
	}
fail:
	kfree(p_aprocess);
	return NULL;
}

void aml_aprocess_destroy(struct aml_aprocess *p_aprocess)
{
	if (!p_aprocess)
		return;
	dsp_pcm_num--;
	if (!dsp_pcm_num && aprocess_card) {
		snd_card_free(aprocess_card->card);
		kfree(aprocess_card);
		aprocess_card = NULL;
	}
	kfree(p_aprocess);
}
