/*
 * Copyright (c) 2022 Intel Corporation.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <errno.h>
#include <stdbool.h>
#include <stdint.h>
#include <zephyr/spinlock.h>
#include <zephyr/devicetree.h>
#include <zephyr/logging/log.h>

#ifdef CONFIG_SOC_SERIES_INTEL_CAVS_V25
#include <adsp_shim.h>
#endif

#define DT_DRV_COMPAT intel_alh_dai
#define LOG_DOMAIN dai_intel_alh

LOG_MODULE_REGISTER(LOG_DOMAIN);

#include "alh.h"

/* global data shared between all alh instances */
struct dai_alh_global_shared dai_alh_global;

/* Digital Audio interface formatting */
static int dai_alh_set_config_tplg(struct dai_intel_alh *dp, const void *spec_config)
{
	struct dai_intel_alh_pdata *alh = dai_get_drvdata(dp);
	const struct dai_intel_ipc3_alh_params *config = spec_config;

	if (config->channels && config->rate) {
		alh->params.channels = config->channels;
		alh->params.rate = config->rate;
		LOG_INF("%s channels %d rate %d", __func__, config->channels, config->rate);
	}

	alh->params.stream_id = config->stream_id;

	return 0;
}

static int dai_alh_set_config_blob(struct dai_intel_alh *dp, const struct dai_config *cfg,
				   const void *spec_config)
{
	struct dai_intel_alh_pdata *alh = dai_get_drvdata(dp);
	const struct dai_intel_ipc4_alh_configuration_blob *blob = spec_config;
	const struct ipc4_alh_multi_gtw_cfg *alh_cfg = &blob->alh_cfg;

	alh->params.rate = cfg->rate;

	for (int i = 0; i < alh_cfg->count; i++) {
		/* the LSB 8bits are for stream id */
		int alh_id = alh_cfg->mapping[i].alh_id & 0xff;

		if (IPC4_ALH_DAI_INDEX(alh_id) == dp->index) {
			alh->params.stream_id = alh_id;
			alh->params.channels = POPCOUNT(alh_cfg->mapping[i].channel_mask);
			break;
		}
	}

	return 0;
}

static int dai_alh_trigger(const struct device *dev, enum dai_dir dir,
			   enum dai_trigger_cmd cmd)
{
	LOG_DBG("cmd %d", cmd);

	return 0;
}

static void alh_claim_ownership(void)
{
#if CONFIG_DAI_ALH_HAS_OWNERSHIP
	uint32_t ALHASCTL = DT_INST_PROP_BY_IDX(0, reg, 0);
	uint32_t ALHCSCTL = DT_INST_PROP_BY_IDX(0, reg, 1);

	sys_write32(sys_read32(ALHASCTL) | ALHASCTL_OSEL(0x3), ALHASCTL);
	sys_write32(sys_read32(ALHCSCTL) | ALHASCTL_OSEL(0x3), ALHCSCTL);
#endif
#ifdef CONFIG_SOC_SERIES_INTEL_CAVS_V25
	/* Allow LPGPDMA connection to Audio Link Hub */
	sys_set_bits(ADSP_DSPALHO_ADDRESS, DSPALHO_ASO_FLAG | DSPALHO_CSO_FLAG);
#endif
}

static void alh_release_ownership(void)
{
#if CONFIG_DAI_ALH_HAS_OWNERSHIP
	uint32_t ALHASCTL = DT_INST_PROP_BY_IDX(0, reg, 0);
	uint32_t ALHCSCTL = DT_INST_PROP_BY_IDX(0, reg, 1);

	sys_write32(sys_read32(ALHASCTL) | ALHASCTL_OSEL(0), ALHASCTL);
	sys_write32(sys_read32(ALHCSCTL) | ALHASCTL_OSEL(0), ALHCSCTL);
#endif
#ifdef CONFIG_SOC_SERIES_INTEL_CAVS_V25
	sys_clear_bits(ADSP_DSPALHO_ADDRESS, DSPALHO_ASO_FLAG | DSPALHO_CSO_FLAG);
#endif
}


static int dai_alh_config_get(const struct device *dev, struct dai_config *cfg,
			      enum dai_dir dir)
{
	struct dai_config *params = (struct dai_config *)dev->config;
	struct dai_intel_alh *dp = (struct dai_intel_alh *)dev->data;
	struct dai_intel_alh_pdata *alh = dai_get_drvdata(dp);

	if (!cfg) {
		return -EINVAL;
	}

	params->rate = alh->params.rate;
	params->channels = alh->params.channels;
	params->word_size = ALH_WORD_SIZE_DEFAULT;

	*cfg = *params;

	return 0;
}

static int dai_alh_config_set(const struct device *dev, const struct dai_config *cfg,
				  const void *bespoke_cfg, size_t size)
{
	struct dai_intel_alh *dp = (struct dai_intel_alh *)dev->data;

	LOG_DBG("%s", __func__);

	if (cfg->type == DAI_INTEL_ALH) {
		return dai_alh_set_config_tplg(dp, bespoke_cfg);
	} else {
		return dai_alh_set_config_blob(dp, cfg, bespoke_cfg);
	}
}

static const struct dai_properties *dai_alh_get_properties(const struct device *dev,
							   enum dai_dir dir, int stream_id)
{
	struct dai_intel_alh *dp = (struct dai_intel_alh *)dev->data;
	struct dai_intel_alh_pdata *alh = dai_get_drvdata(dp);
	struct dai_properties *prop = &alh->props;
	uint32_t offset = dir == DAI_DIR_PLAYBACK ?
		ALH_TXDA_OFFSET : ALH_RXDA_OFFSET;

	prop->fifo_address = dai_base(dp) + offset + ALH_STREAM_OFFSET * stream_id;
	prop->fifo_depth = ALH_GPDMA_BURST_LENGTH;
	prop->dma_hs_id = alh_handshake_map[stream_id];
	prop->stream_id = alh->params.stream_id;

	LOG_DBG("dai_index %u", dp->index);
	LOG_DBG("fifo %u", prop->fifo_address);
	LOG_DBG("handshake %u", prop->dma_hs_id);

	return prop;
}

static int dai_alh_probe(const struct device *dev)
{
	k_spinlock_key_t key;

	LOG_DBG("%s", __func__);

	key = k_spin_lock(&dai_alh_global.lock);

	if (dai_alh_global.sref == 0) {
		alh_claim_ownership();
	}

	dai_alh_global.sref++;

	k_spin_unlock(&dai_alh_global.lock, key);

	return 0;
}

static int dai_alh_remove(const struct device *dev)
{
	k_spinlock_key_t key;

	LOG_DBG("%s", __func__);

	key = k_spin_lock(&dai_alh_global.lock);

	if (--dai_alh_global.sref == 0) {
		alh_release_ownership();
	}

	k_spin_unlock(&dai_alh_global.lock, key);

	return 0;
}

static DEVICE_API(dai, dai_intel_alh_api_funcs) = {
	.probe			= dai_alh_probe,
	.remove			= dai_alh_remove,
	.config_set		= dai_alh_config_set,
	.config_get		= dai_alh_config_get,
	.trigger		= dai_alh_trigger,
	.get_properties		= dai_alh_get_properties,
};

#define DAI_INTEL_ALH_DEVICE_INIT(n)						\
	static struct dai_config dai_intel_alh_config_##n = {			\
		.type = DAI_INTEL_ALH,						\
		.dai_index = (n / DAI_NUM_ALH_BI_DIR_LINKS_GROUP) << 8 |	\
			(n % DAI_NUM_ALH_BI_DIR_LINKS_GROUP),			\
	};									\
	static struct dai_intel_alh dai_intel_alh_data_##n = {			\
		.index = (n / DAI_NUM_ALH_BI_DIR_LINKS_GROUP) << 8 |		\
			(n % DAI_NUM_ALH_BI_DIR_LINKS_GROUP),			\
		.plat_data = {							\
			.base =	DT_INST_PROP_BY_IDX(n, reg, 0),			\
			.fifo_depth[DAI_DIR_PLAYBACK] =	ALH_GPDMA_BURST_LENGTH,		\
			.fifo_depth[DAI_DIR_CAPTURE] = ALH_GPDMA_BURST_LENGTH,		\
		},								\
	};								\
									\
	DEVICE_DT_INST_DEFINE(n,					\
			NULL, NULL,					\
			&dai_intel_alh_data_##n,			\
			&dai_intel_alh_config_##n,			\
			POST_KERNEL, 32,				\
			&dai_intel_alh_api_funcs);

DT_INST_FOREACH_STATUS_OKAY(DAI_INTEL_ALH_DEVICE_INIT)
