// SPDX-License-Identifier: GPL-2.0
/*
 * AVIRT - ALSA Virtual Soundcard
 *
 * Copyright (c) 2010-2018 Fiberdyne Systems Pty Ltd
 *
 * pcm.c - AVIRT PCM interface
 */

#include "core.h"

#define D_LOGNAME "pcm"

#define D_INFOK(fmt, args...) DINFO(D_LOGNAME, fmt, ##args)
#define D_PRINTK(fmt, args...) DDEBUG(D_LOGNAME, fmt, ##args)
#define D_ERRORK(fmt, args...) DERROR(D_LOGNAME, fmt, ##args)

#define AUDIOPATH_PCM_CB(ap, cb, ops, substream, ...)                          \
	(((ap)->ops->cb) ? (ap)->ops->cb((substream), ##__VA_ARGS__) : 0)

#define DO_AUDIOPATH_PCM_ROUTE_CBS(ap, cb, ops, substream, ...)                \
	({                                                                     \
		int __err = 0, __endpoint = substream->stream;                 \
		struct snd_avirt_stream *__stream =                            \
			snd_avirt_stream_find_by_device(                       \
				substream->pcm->device);                       \
		if (!IS_ERR_VALUE(__stream) && __stream) {                     \
			struct snd_avirt_audiopath *__endpoint_ap;             \
			if (__stream->route) {                                 \
				__endpoint_ap =                                \
					__stream->route                        \
						->endpoint_ap[__endpoint];     \
				if ((__endpoint_ap)) {                         \
					__err = AUDIOPATH_PCM_CB(              \
						__endpoint_ap, cb, ops,        \
						(substream), ##__VA_ARGS__);   \
				}                                              \
			}                                                      \
		}                                                              \
		(__err);                                                       \
	})
/**
 * DO_AUDIOPATH_PCM_CBS - Call an Audio Path's ALSA PCM callbacks from AVIRT.
 * If the Audio Path has routing enabled, then we call the appropriate
 * from/to Audio Path's ALSA PCM callbacks
 */
#define DO_AUDIOPATH_PCM_CBS(ap, cb, substream, ...)                           \
	({                                                                     \
		int __err = 0;                                                 \
		if (substream->stream) {                                       \
			__err = AUDIOPATH_PCM_CB((ap), cb, pcm_capture_ops,    \
						 (substream), ##__VA_ARGS__);  \
			if (__err >= 0) {                                      \
				__err = DO_AUDIOPATH_PCM_ROUTE_CBS(            \
					(ap), cb, pcm_playback_ops,            \
					(substream), ##__VA_ARGS__);           \
			}                                                      \
		} else {                                                       \
			__err = AUDIOPATH_PCM_CB((ap), cb, pcm_playback_ops,   \
						 (substream), ##__VA_ARGS__);  \
			if (__err >= 0) {                                      \
				__err = DO_AUDIOPATH_PCM_ROUTE_CBS(            \
					(ap), cb, pcm_capture_ops,             \
					(substream), ##__VA_ARGS__);           \
			}                                                      \
		}                                                              \
		(__err);                                                       \
	})

#define PRIVATE_DATA(substream)                                                \
	((struct snd_avirt_private_data *)substream->private_data)

/*******************************************************************************
 * ALSA PCM Callbacks
 ******************************************************************************/
/**
 * pcm_open - Implements 'open' callback for PCM middle layer
 * @substream: pointer to ALSA PCM substream
 *
 * This is called when an ALSA PCM substream is opened. The substream device
 * is configured here.
 *
 * Returns 0 on success or error code otherwise.
 */
static int pcm_open(struct snd_pcm_substream *substream)
{
	struct snd_avirt_private_data *avirt_private_data;
	struct snd_avirt_audiopath *audiopath;
	struct snd_avirt_stream *stream;
	struct snd_pcm_hardware *hw;
	unsigned int chans = 0;

	if (!snd_avirt_streams_configured()) {
		D_ERRORK("Cannot open PCM. Card is not configured");
		return -EPERM;
	}

	// Find the Audio Path mapped to this device
	stream = snd_avirt_stream_find_by_device(substream->pcm->device);
	if (IS_ERR_VALUE(stream) || !stream)
		return PTR_ERR(stream);
	audiopath = snd_avirt_audiopath_get(stream->map);
	CHK_NULL_V(audiopath, "Cannot find Audio Path uid: '%s'!", stream->map);

	// Set the private data's Audio Path
	avirt_private_data = substream->private_data;
	avirt_private_data->audiopath = audiopath;

	// Copy the hw params from the audiopath to the pcm
	hw = &substream->runtime->hw;
	memcpy(hw, audiopath->hw, sizeof(struct snd_pcm_hardware));

	// Setup remaining hw properties
	chans = stream->channels;
	if (chans == 0) {
		D_ERRORK("Channels cannot be 0");
		return -EINVAL;
	}
	hw->channels_min = chans;
	hw->channels_max = chans;

	// Do additional Audio Path 'open' callbacks
	return DO_AUDIOPATH_PCM_CBS(audiopath, open, substream);
}

/**
 * pcm_close - Implements 'close' callback for PCM middle layer
 * @substream: pointer to ALSA PCM substream
 *
 * This is called when a PCM substream is closed.
 *
 * Returns 0 on success or error code otherwise.
 */
static int pcm_close(struct snd_pcm_substream *substream)
{
	// Do additional Audio Path 'close' callback
	return DO_AUDIOPATH_PCM_CBS(
		(struct snd_avirt_audiopath *)PRIVATE_DATA(substream)->audiopath,
		close, substream);
}

/**
 * pcm_close - Implements 'close' callback for PCM middle layer
 * @substream: pointer to ALSA PCM substream
 *
 * This is called when a PCM substream is closed.
 *
 * Returns 0 on success or error code otherwise.
 */
static int pcm_trigger(struct snd_pcm_substream *substream, int cmd)
{
	// Do additional Audio Path 'trigger' callback
	return DO_AUDIOPATH_PCM_CBS(
		(struct snd_avirt_audiopath *)PRIVATE_DATA(substream)->audiopath,
		trigger, substream, cmd);
}

/**
 * pcm_close - Implements 'close' callback for PCM middle layer
 * @substream: pointer to ALSA PCM substream
 *
 * This is called when a PCM substream is closed.
 *
 * Returns 0 on success or error code otherwise.
 */
static int pcm_prepare(struct snd_pcm_substream *substream)
{
	// Do additional Audio Path 'prepare' callback
	return DO_AUDIOPATH_PCM_CBS(
		(struct snd_avirt_audiopath *)PRIVATE_DATA(substream)->audiopath,
		prepare, substream);
}

/**
 * pcm_hw_params - Implements 'hw_params' callback for PCM middle layer
 * @substream: pointer to ALSA PCM substream
 * @hw_params: contains the hardware parameters for the PCM
 *
 * This is called when the hardware parameters are set, including buffer size,
 * the period size, the format, etc. The buffer allocation should be done here.
 *
 * Returns 0 on success or error code otherwise.
 */
static int pcm_hw_params(struct snd_pcm_substream *substream,
			 struct snd_pcm_hw_params *hw_params)
{
	int retval;

	/* Check supported channels */
	if ((params_channels(hw_params) <
	     substream->runtime->hw.channels_min) ||
	    (params_channels(hw_params) >
	     substream->runtime->hw.channels_max)) {
		D_ERRORK("Requested number of channels: %d not supported",
			 params_channels(hw_params));
		return -EINVAL;
	}

	retval = snd_pcm_lib_alloc_vmalloc_buffer(
		substream, params_buffer_bytes(hw_params));
	if (retval < 0)
		D_ERRORK("pcm: buffer allocation failed: %d", retval);

	return retval;
}

/**
 * pcm_hw_free - Implements 'hw_free' callback for PCM middle layer
 * @substream: pointer to ALSA PCM substream
 *
 * Release the resources allocated via 'hw_params'.
 * This function is always called before the 'close' callback .
 *
 * Returns 0 on success or error code otherwise.
 */
static int pcm_hw_free(struct snd_pcm_substream *substream)
{
	int err;

	// Do additional Audio Path 'hw_free' callback
	err = DO_AUDIOPATH_PCM_CBS(
		(struct snd_avirt_audiopath *)PRIVATE_DATA(substream)->audiopath,
		hw_free, substream);
	if (err < 0)
		return err;

	return snd_pcm_lib_free_vmalloc_buffer(substream);
}

/**
 * Default PCM ops table. Selected PCM ops must pass through AVIRT before
 * calling back to their respective Audio Paths
 */
struct snd_pcm_ops pcm_ops_avirt = {
	.open = pcm_open,
	.close = pcm_close,
	.prepare = pcm_prepare,
	.trigger = pcm_trigger,
	.ioctl = snd_pcm_lib_ioctl,
	.hw_params = pcm_hw_params,
	.hw_free = pcm_hw_free,
	.page = snd_pcm_lib_get_vmalloc_page,
};