/*
   forward-alsa-io.c - ALSA driver for SoftLab-NSK Forward video 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-alsa-io.h"
#include "forward-vdma.h"

#include "forward-alsa-utils.h"

#include <sound/core.h>
#include <linux/delay.h>
#include <linux/kthread.h>
#include <sound/control.h>
#include <linux/vmalloc.h>

#include "../forward-v4l2/forward-v4l2-io.h"

#define forward_alsa_substream_io(substream) \
	((struct forward_alsa_io *)snd_pcm_substream_chip(substream))

#define FORWARD_HW_FIFO_DELAY_BYTES (2048 * 16)

static void forward_alsa_remove_hw_buffers(struct forward_alsa_io *io)
{
	struct forward_dev *dev = io->alsa->dev;

	if (io->hw_buffer_size) {
		forward_vdma_unmap_buf(dev->vdma, io, io->hw_buffer_address,
				       ((io->hw_buffer_size - 1) & PAGE_MASK) + PAGE_SIZE);
		forward_vdma_put_region(dev->vdma, io, io->hw_buffer_address);

		if (dev->io[io->index].v4l2) {
			struct forward_v4l2_io *v4l2 =
				(struct forward_v4l2_io *)dev->io[io->index].v4l2;
			mutex_lock(&v4l2->lock);
			v4l2->hanc_buffer = NULL;
			v4l2->hanc_buffer_size = 0;
			mutex_unlock(&v4l2->lock);
		}

		io->hw_buffer_address = 0;
		io->hw_buffer_size = 0;
	}

	if (io->hw_buffer) {
		vfree(io->hw_buffer);
		io->hw_buffer = NULL;
	}
}

static void forward_alsa_check_hw_buffers(struct forward_alsa_io *io)
{
	struct forward_alsa *alsa = io->alsa;
	struct forward_dev *dev = alsa->dev;
	u32 address;
	size_t size;

	alsa->ops->buffer_info(io, &address, &size);

	if (!io->hw_buffer || io->hw_buffer_address != address || io->hw_buffer_size != size) {
		int sizep = ((size - 1) & PAGE_MASK) + PAGE_SIZE;

		forward_alsa_remove_hw_buffers(io);

		io->hw_buffer = vzalloc(sizep);
		forward_vdma_get_region(dev->vdma, io, address, sizep);
		forward_vdma_map_kernel_buf(dev->vdma, io, io->hw_buffer, address, sizep,
					    (io->state == FORWARD_IO_RX) ? true : false);
		io->hw_buffer_address = address;
		io->hw_buffer_size = size;

		if (dev->io[io->index].v4l2) {
			struct forward_v4l2_io *v4l2 =
				(struct forward_v4l2_io *)dev->io[io->index].v4l2;
			mutex_lock(&v4l2->lock);
			v4l2->hanc_buffer = io->hw_buffer;
			v4l2->hanc_buffer_size = size;
			mutex_unlock(&v4l2->lock);
		}
	}
}

static void forward_alsa_in_generate_silence(struct forward_alsa_io *io, int group,
					     snd_pcm_uframes_t count)
{
	struct snd_pcm_runtime *runtime = io->stream->runtime;
	snd_pcm_uframes_t pos = io->position[group] % runtime->buffer_size;
	int k, ch, endc = min(group * 4 + 4, (int)runtime->channels);

	count = min(count, runtime->buffer_size);

	for (k = 0; k < count; k++) {
		u32 *p = (u32 *)(runtime->dma_area + frames_to_bytes(runtime, pos));
		for (ch = group * 4; ch < endc; ch++)
			p[ch] = 0;

		forward_alsa_ptr_inc(pos, runtime);
	}
}

static void forward_alsa_io_process(struct forward_alsa_io *io, int offset, size_t size, bool field,
				    u64 timestamp)
{
	struct snd_pcm_runtime *runtime;
	snd_pcm_sframes_t frames;
	bool period = false;
	int j;
	struct forward_v4l2_io *v4l2;
	bool need_sync = false;

	if (io->alsa->dev->io[io->index].v4l2)
		v4l2 = (struct forward_v4l2_io *)io->alsa->dev->io[io->index].v4l2;

	if (io->streaming)
		forward_alsa_check_hw_buffers(io);

	if (io->streaming && !io->hw_streaming && (!io->interlaced || !field)) {
		io->seq_n = 0;
		io->seq_d = 0;
		io->alsa->ops->update_format(io);
		io->alsa->ops->start(io);
		io->hw_streaming = true;
		if (v4l2) {
			v4l2->stats.alsa_last_hw_samples_p = 0;
			v4l2->stats.alsa_last_sw_samples_p = 0;
			v4l2->stats.alsa_last_hw_samples = 0;
			v4l2->stats.alsa_last_sw_samples = 0;
			v4l2->stats.alsa_hw_samples = 0;
			v4l2->stats.alsa_sw_samples = 0;
			v4l2->stats.alsa_expected_samples = 0;
			v4l2->stats.alsa_resyncs = 0;
		}
	} else if (!io->streaming && io->hw_streaming) {
		if (io->stats.was_resync_count > 0) {
			if (io->state == FORWARD_IO_RX) {
				forward_info(io->alsa->dev,
					     "ALSA  IN%d resync #%d: repeated %d times", io->number,
					     io->stats.resyncs, io->stats.was_resync_count + 1);
			} else {
				forward_info(io->alsa->dev,
					     "ALSA OUT%d resync #%d: repeated %d times", io->number,
					     io->stats.resyncs, io->stats.was_resync_count + 1);
			}
		}
		io->alsa->ops->stop(io);
		io->hw_streaming = false;
		forward_alsa_remove_hw_buffers(io);
	}

	if (!io->streaming || !io->hw_streaming)
		return;

	if (!io->stream || !io->stream->runtime)
		return;

	runtime = io->stream->runtime;

	if (v4l2 && v4l2->streaming) {
		io->interlaced = (v4l2->hw_timings.type == FORWARD_TIMING_SDI) ?
					 v4l2->hw_timings.sdi.interlaced :
					 v4l2->hw_timings.hdmi.bt.interlaced;
		need_sync = true;
	} else {
		io->interlaced = false;
		need_sync = false;
	}

	if (io->fps_n) {
		if (io->seq_d != io->fps_n) {
			if (io->seq_d)
				io->seq_n = (io->seq_n / io->seq_d) * io->fps_n;
			io->seq_d = io->fps_n;
		}
		if (v4l2 && v4l2->streaming) {
			int frame_seq = v4l2->buf_sequence + ((io->interlaced && field) ? 0 : 1);
			io->seq_n = frame_seq * io->fps_d * (s64)runtime->rate;
		} else {
			if (!io->interlaced || field)
				io->seq_n += (s64)runtime->rate * io->fps_d;
		}
	}

	if (need_sync && io->seq_d)
		io->ex_position = io->seq_n / io->seq_d;
	else
		io->ex_position = io->position[0];

	if (io->state == FORWARD_IO_RX) {
		int leftsize = min((int)size, (int)(io->hw_buffer_size - offset));
		bool last = false;

		io->alsa->ops->update_format(io);

		io->hw_rd_position = offset;
		io->hw_wr_position = io->hw_rd_position + size;
		if (io->hw_wr_position >= io->hw_buffer_size)
			io->hw_wr_position -= io->hw_buffer_size;

		switch (io->type) {
		case FORWARD_ALSA_SMPTE_272:
			frames = forward_alsa_input_decode_smpte272(io, io->hw_buffer + offset,
								    size);
			break;
		case FORWARD_ALSA_SMPTE_299:
			frames = forward_alsa_input_decode_smpte299(io, io->hw_buffer + offset,
								    size);
			break;
		case FORWARD_ALSA_HDMI:
			frames = forward_alsa_input_decode_hdmi(io, io->hw_buffer + offset, size);
			break;
		case FORWARD_ALSA_RFC3190:
			frames = forward_alsa_input_decode_rfc3190(
				io, io->hw_buffer + offset, leftsize, (u8)io->subtype, &last);
			if (last && (leftsize == size))
				io->hw_wr_position = 0;
			if (leftsize < size)
				frames += forward_alsa_input_decode_rfc3190(
					io, io->hw_buffer, size - leftsize, (u8)io->subtype, &last);
			break;
		default:
			frames = 0;
			break;
		}
		if (!io->interlaced || field) {
			for (j = 0; j < (runtime->channels - 1) / 4 + 1; j++) {
				s64 delta = io->ex_position - io->position[j];
				if (need_sync && (abs(delta) > FORWARD_ALSA_MAX_RESYNC_FRAMES)) {
					s64 last_delta =
						io->position[j] - io->last_period_position[j];

					if ((j == 0) && !io->stats.was_resync) {
						forward_info(
							io->alsa->dev,
							"ALSA  IN%d resync #%d: expected %lld, got %llu, last_period = %lld, hw_ptr = %lu, delta = %lld",
							io->number, io->stats.resyncs,
							io->ex_position, io->position[j],
							io->last_period_position[j],
							runtime->status->hw_ptr,
							(s64)runtime->status->hw_ptr -
								io->last_period_position[0]);
					}

					if (j == 0) {
						if (v4l2)
							v4l2->stats.alsa_resyncs++;
						io->stats.resyncs++;

						if (io->stats.was_resync)
							io->stats.was_resync_count++;
						io->stats.was_resync = true;
					}

					if (delta > 0)
						forward_alsa_in_generate_silence(io, j, delta);

					if ((delta < 0) && (last_delta < -delta))
						io->position[j] = io->last_period_position[j];
					else
						io->position[j] = io->ex_position;
				} else {
					if (io->stats.was_resync_count > 0) {
						forward_info(
							io->alsa->dev,
							"ALSA  IN%d resync #%d: repeated %d times",
							io->number, io->stats.resyncs,
							io->stats.was_resync_count + 1);
					}
					io->stats.was_resync = false;
					io->stats.was_resync_count = 0;
				}
			}
		}
	} else {
		snd_pcm_sframes_t delta = 0;
		int groups = (runtime->channels - 1) / 4 + 1;
		int hw_size;
		snd_pcm_sframes_t queued, wr_count;

		switch (io->type) {
		case FORWARD_ALSA_RAW_16BE:
			offset = offset / io->hw_channels / 2;
			hw_size = io->hw_buffer_size / io->hw_channels / 2;
			break;
		case FORWARD_ALSA_RAW_20BE:
			offset = offset / io->hw_channels * 8 / 20;
			hw_size = io->hw_buffer_size / io->hw_channels * 8 / 20;
			break;
		case FORWARD_ALSA_RAW_24BE:
			offset = offset / io->hw_channels / 3;
			hw_size = io->hw_buffer_size / io->hw_channels / 3;
			break;
		default:
			offset = offset / groups / 4 / 4;
			hw_size = io->hw_buffer_size / groups / 4 / 4;
			break;
		}

		if (io->hw_rd_position >= 0) {
			delta = offset - io->hw_rd_position;
			if (delta < 0)
				delta += hw_size;
		}
		io->hw_rd_position = offset;

		io->position[0] += delta;

		if (!io->interlaced || field) {
			s64 ex_delta = io->ex_position - io->position[0];

			if (need_sync && (abs(ex_delta) > FORWARD_ALSA_MAX_RESYNC_FRAMES) &&
			    (io->sw_rd_position >= 0) && (io->hw_wr_position >= 0)) {
				s64 last_delta = io->position[0] - io->last_period_position[0];

				if (!io->stats.was_resync) {
					forward_info(
						io->alsa->dev,
						"ALSA OUT%d resync #%d: expected %lld, got %llu, last_period = %lld, hw_ptr = %lu",
						io->number, io->stats.resyncs, io->ex_position,
						io->position[0], io->last_period_position[0],
						runtime->status->hw_ptr);
				}

				if (v4l2)
					v4l2->stats.alsa_resyncs++;
				io->stats.resyncs++;

				if (io->stats.was_resync)
					io->stats.was_resync_count++;
				io->stats.was_resync = true;

				if ((ex_delta < 0) && (last_delta < -ex_delta))
					io->position[0] = io->last_period_position[0];
				else
					io->position[0] = io->ex_position;
				io->sw_rd_position = io->position[0];
				io->hw_wr_position = io->hw_rd_position;

			} else {
				if (io->stats.was_resync_count > 0) {
					forward_info(io->alsa->dev,
						     "ALSA OUT%d resync #%d: repeated %d times",
						     io->number, io->stats.resyncs,
						     io->stats.was_resync_count + 1);
				}
				io->stats.was_resync = false;
				io->stats.was_resync_count = 0;
			}
		}

		for (j = 1; j < FORWARD_ALSA_MAX_SDI_GROUPS; j++)
			io->position[j] = io->position[0];

		if ((io->sw_rd_position < 0) || (io->hw_wr_position < 0)) {
			io->sw_rd_position = io->position[0];
			io->hw_wr_position = io->hw_rd_position;
		}

		// How many data available in sw buffer
		// Tricky because snd_pcm_playback_hw_avail uses last period pointer
		queued = (io->sw_rd_position - io->last_period_position[0]);
		queued = snd_pcm_playback_hw_avail(runtime) - queued;

		// How many data available in hw buffer
		wr_count = hw_size - io->hw_wr_position + io->hw_rd_position;
		if (wr_count > hw_size)
			wr_count -= hw_size;

		if (wr_count > queued)
			wr_count = queued;

		switch (io->type) {
		case FORWARD_ALSA_RAW:
			frames = forward_alsa_output_encode_raw(io, wr_count);
			break;
		case FORWARD_ALSA_RAW_24BE:
			frames = forward_alsa_output_encode_raw24be(io, wr_count);
			break;
		default:
			frames = wr_count;
		}

		io->sw_rd_position += frames;
		io->hw_wr_position += frames;
		if (io->hw_wr_position >= hw_size)
			io->hw_wr_position %= hw_size;
	}

	io->position_time = timestamp;

	if (!io->interlaced || field) {
		for (j = 0; j < FORWARD_ALSA_MAX_SDI_GROUPS; j++) {
			s64 delta = io->position[j] - io->last_period_position[j];
			if (delta >= runtime->period_size)
				period = true;
		}
	}

	if (v4l2) {
		s64 delta = io->position[0] - io->last_period_position[0];

		if (!io->interlaced || field) {
			v4l2->stats.alsa_last_hw_samples = v4l2->stats.alsa_last_hw_samples_p;
			v4l2->stats.alsa_last_sw_samples = v4l2->stats.alsa_last_sw_samples_p;
			v4l2->stats.alsa_last_hw_samples_p = 0;
			v4l2->stats.alsa_last_sw_samples_p = 0;
		}
		if (period) {
			v4l2->stats.alsa_last_sw_samples_p += delta;
			v4l2->stats.alsa_sw_samples += delta;
		}
		v4l2->stats.alsa_last_hw_samples_p += frames;
		v4l2->stats.alsa_hw_samples += frames;
		if (io->seq_d)
			v4l2->stats.alsa_expected_samples = io->seq_n / io->seq_d;
		else
			v4l2->stats.alsa_expected_samples = v4l2->stats.alsa_sw_samples;
	}

	if (period) {
		mutex_unlock(&io->lock);

		for (j = 0; j < FORWARD_ALSA_MAX_SDI_GROUPS; j++)
			io->last_period_position[j] = io->position[j];

		snd_pcm_period_elapsed(io->stream);

		mutex_lock(&io->lock);
	}
}

static void forward_alsa_io_irq_wq_func(void *p, const forward_irq_t *irq, u64 timestamp)
{
	struct forward_alsa_io *io = p;
	int offset;
	size_t size;
	bool field;

	mutex_lock(&io->lock);
	if (!io->ready) {
		mutex_unlock(&io->lock);
		return;
	}
	if (!io->alsa->ops->irq_info(io, irq, &offset, &size, &field)) {
		mutex_unlock(&io->lock);
		return;
	}
	forward_alsa_io_process(io, offset, size, field, timestamp);
	mutex_unlock(&io->lock);
}

static struct snd_pcm_hardware forward_alsa_capture_hw = {
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 1, 0)
	.info = (SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_INTERLEAVED | SNDRV_PCM_INFO_BLOCK_TRANSFER |
		 SNDRV_PCM_INFO_MMAP_VALID | SNDRV_PCM_INFO_HAS_LINK_ABSOLUTE_ATIME),
#else
	.info = (SNDRV_PCM_INFO_MMAP | SNDRV_PCM_INFO_INTERLEAVED | SNDRV_PCM_INFO_BLOCK_TRANSFER |
		 SNDRV_PCM_INFO_MMAP_VALID),
#endif
	.formats = SNDRV_PCM_FMTBIT_S32_LE,
	.rates = SNDRV_PCM_RATE_48000,
	.rate_min = 48000,
	.rate_max = 48000,
	.channels_min = 1,
	.channels_max = 16,
	.buffer_bytes_max = 4 * 1024 * 1024,
	.period_bytes_min = 640,
	.period_bytes_max = 128000,
	.periods_min = 1,
	.periods_max = 1024,
};

static int forward_alsa_io_open(struct snd_pcm_substream *substream)
{
	struct snd_pcm_runtime *runtime = substream->runtime;
	struct forward_alsa_io *io = forward_alsa_substream_io(substream);

	io->stream = substream;
	runtime->hw = forward_alsa_capture_hw;

	io->alsa->ops->fill_hw(io, &runtime->hw);

	if (io->forced_channels != 0) {
		runtime->hw.channels_min = io->forced_channels;
		runtime->hw.channels_max = io->forced_channels;
	}

	return 0;
}

static int forward_alsa_io_close(struct snd_pcm_substream *substream)
{
	struct forward_alsa_io *io = forward_alsa_substream_io(substream);

	io->stream = NULL;

	return 0;
}

static int forward_alsa_io_hw_params(struct snd_pcm_substream *substream,
				     struct snd_pcm_hw_params *hw_params)
{
	struct forward_alsa_io *io = forward_alsa_substream_io(substream);
	int result;
	int min_size = 2 * 2000 * params_channels(hw_params) * 4 + FORWARD_HW_FIFO_DELAY_BYTES;

	// Check buffer too small (< two frames @ 24Hz + fifo delay)
	if (params_buffer_bytes(hw_params) < min_size) {
		forward_err(io->alsa->dev, "ALSA buffer is too small, minimum %d bytes required.",
			    min_size);
		return -EINVAL;
	}

	mutex_lock(&io->lock);
	result = snd_pcm_alloc_vmalloc_buffer(substream, params_buffer_bytes(hw_params));
	mutex_unlock(&io->lock);
	return result;
}

static int forward_alsa_io_hw_free(struct snd_pcm_substream *substream)
{
	struct forward_alsa_io *io = forward_alsa_substream_io(substream);

	mutex_lock(&io->lock);
	snd_pcm_free_vmalloc_buffer(substream);
	mutex_unlock(&io->lock);
	return 0;
}

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

static int forward_alsa_io_trigger(struct snd_pcm_substream *substream, int cmd)
{
	struct forward_alsa_io *io = forward_alsa_substream_io(substream);
	int i;

	switch (cmd) {
	case SNDRV_PCM_TRIGGER_START:
		for (i = 0; i < FORWARD_ALSA_MAX_SDI_GROUPS; i++) {
			io->position[i] = 0;
			io->last_period_position[i] = 0;
		}
		io->sw_rd_position = -1;
		io->hw_rd_position = -1;
		io->hw_wr_position = -1;
		io->stats.resyncs = 0;
		io->stats.was_resync = false;
		io->stats.was_resync_count = 0;
		io->streaming = true;
		break;
	case SNDRV_PCM_TRIGGER_STOP:
		io->streaming = false;
		break;
	default:
		return -EINVAL;
	}
	return 0;
}

static snd_pcm_uframes_t forward_alsa_io_pointer(struct snd_pcm_substream *substream)
{
	// FIXME: Lock here?
	struct forward_alsa_io *io = forward_alsa_substream_io(substream);

	return io->last_period_position[0] % substream->runtime->buffer_size;
}

#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 1, 0)
static int forward_alsa_io_get_time_info(struct snd_pcm_substream *substream,
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 6, 0)
					 struct timespec64 *system_ts, struct timespec64 *audio_ts,
#else
					 struct timespec *system_ts, struct timespec *audio_ts,
#endif
					 struct snd_pcm_audio_tstamp_config *audio_tstamp_config,
					 struct snd_pcm_audio_tstamp_report *audio_tstamp_report)
{
	// FIXME: Lock here?
	struct forward_alsa_io *io = forward_alsa_substream_io(substream);

	if ((substream->runtime->hw.info & SNDRV_PCM_INFO_HAS_LINK_ABSOLUTE_ATIME) &&
	    (audio_tstamp_config->type_requested == SNDRV_PCM_AUDIO_TSTAMP_TYPE_LINK_ABSOLUTE)) {
		snd_pcm_gettime(substream->runtime, system_ts);

#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 6, 0)
		*audio_ts = ns_to_timespec64(io->position_time);
#else
		*audio_ts = ns_to_timespec(io->position_time);
#endif

		audio_tstamp_report->actual_type = SNDRV_PCM_AUDIO_TSTAMP_TYPE_LINK_ABSOLUTE;
		audio_tstamp_report->accuracy_report = 0;
	} else {
		audio_tstamp_report->actual_type = SNDRV_PCM_AUDIO_TSTAMP_TYPE_DEFAULT;
	}

	return 0;
}
#endif

static struct snd_pcm_ops forward_alsa_io_ops = {
	.open = forward_alsa_io_open,
	.close = forward_alsa_io_close,
	.ioctl = snd_pcm_lib_ioctl,
	.hw_params = forward_alsa_io_hw_params,
	.hw_free = forward_alsa_io_hw_free,
	.prepare = forward_alsa_io_prepare,
	.trigger = forward_alsa_io_trigger,
	.pointer = forward_alsa_io_pointer,
	.page = snd_pcm_get_vmalloc_page,
#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 1, 0)
	.get_time_info = forward_alsa_io_get_time_info,
#endif
};

static int forward_alsa_ctl_info(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_info *uinfo)
{
	struct forward_alsa_io *io = snd_kcontrol_chip(kcontrol);
	if (kcontrol == io->ctl_force_ch) {
		uinfo->type = SNDRV_CTL_ELEM_TYPE_INTEGER;
		uinfo->count = 1;
		uinfo->value.integer.min = 0;
		uinfo->value.integer.max = 16;
		uinfo->value.integer.step = 2;
	} else
		return -EINVAL;
	return 0;
}

static int forward_alsa_ctl_get(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol)
{
	struct forward_alsa_io *io = snd_kcontrol_chip(kcontrol);
	if (kcontrol == io->ctl_force_ch)
		ucontrol->value.integer.value[0] = io->forced_channels;
	else
		return -EINVAL;
	return 0;
}

static int forward_alsa_ctl_put(struct snd_kcontrol *kcontrol, struct snd_ctl_elem_value *ucontrol)
{
	struct forward_alsa_io *io = snd_kcontrol_chip(kcontrol);
	if (kcontrol == io->ctl_force_ch)
		io->forced_channels = ucontrol->value.integer.value[0] & 0xE;
	else
		return -EINVAL;
	return 0;
}

static struct snd_kcontrol_new forward_alsa_input_ch_force_ctl = {
	.iface = SNDRV_CTL_ELEM_IFACE_PCM,
	.name = "PCM Capture Channels",
	.access = SNDRV_CTL_ELEM_ACCESS_READWRITE,
	.info = forward_alsa_ctl_info,
	.get = forward_alsa_ctl_get,
	.put = forward_alsa_ctl_put,
};

void forward_alsa_io_deinit(struct forward_alsa_io *io)
{
	io->ready = false;

	if (io->irq.private) {
		forward_irq_listener_remove(io->alsa->dev, &io->irq);
		io->irq.private = NULL;
	}

	if (io->alsa->ops->fini_io)
		io->alsa->ops->fini_io(io);

	forward_alsa_remove_hw_buffers(io);

	if (io->ctl_force_ch)
		snd_ctl_remove(io->alsa->card, io->ctl_force_ch);
	io->ctl_force_ch = NULL;

	if (io->pcm)
		snd_device_free(io->alsa->card, io->pcm);
	io->pcm = NULL;
}

int forward_alsa_io_init(struct forward_alsa_io *io)
{
	struct forward_alsa *alsa = io->alsa;
	int status = 0;
	struct snd_kcontrol_new ctl;

	if (io->state == FORWARD_IO_DISABLED)
		return 0;

	if (io->state == FORWARD_IO_RX)
		status = snd_pcm_new(alsa->card, "IN", io->index, 0, 1, &io->pcm);
	else if (io->state == FORWARD_IO_TX)
		status = snd_pcm_new(alsa->card, "OUT", io->index, 1, 0, &io->pcm);
	else
		status = -EINVAL;

	if (status)
		goto fail;

	io->pcm->private_data = io;

	if (io->state == FORWARD_IO_RX) {
		snprintf(io->pcm->name, sizeof(io->pcm->name), "IN %d", io->number);
		snd_pcm_set_ops(io->pcm, SNDRV_PCM_STREAM_CAPTURE, &forward_alsa_io_ops);
	} else {
		snprintf(io->pcm->name, sizeof(io->pcm->name), "OUT %d", io->number);
		snd_pcm_set_ops(io->pcm, SNDRV_PCM_STREAM_PLAYBACK, &forward_alsa_io_ops);
	}

	ctl = forward_alsa_input_ch_force_ctl;
	ctl.device = io->pcm->device;
	ctl.subdevice = (io->state == FORWARD_IO_RX) ? SNDRV_PCM_STREAM_CAPTURE :
						       SNDRV_PCM_STREAM_PLAYBACK;
	io->ctl_force_ch = snd_ctl_new1(&ctl, io);
	snd_ctl_add(alsa->card, io->ctl_force_ch);

	if (alsa->ops->init_io)
		alsa->ops->init_io(io);

	io->hw_streaming = false;
	io->ready = true;

	forward_irq_listener_init(&io->irq);
	io->irq.type = FORWARD_IRQ_LISTENER_WORKQUEUE;
	io->irq.mask = io->alsa->ops->irq_mask(io);
	io->irq.func = NULL;
	io->irq.wq_func = &forward_alsa_io_irq_wq_func;
	io->irq.private = io;
	io->irq.work_queue = io->index;
	io->irq.priority = FORWARD_ALSA_IRQ_PRIORITY;
	forward_irq_listener_add(io->alsa->dev, &io->irq);

	return 0;

fail:
	forward_alsa_io_deinit(io);
	return status;
}
