/* Copyright 2023 NXP
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#ifndef ZEPHYR_DRIVERS_DAI_NXP_SAI_H_
#define ZEPHYR_DRIVERS_DAI_NXP_SAI_H_

#include <zephyr/drivers/clock_control.h>
#include <zephyr/drivers/pinctrl.h>
#include <zephyr/logging/log.h>
#include <fsl_sai.h>

LOG_MODULE_REGISTER(nxp_dai_sai);

#ifdef CONFIG_SAI_HAS_MCLK_CONFIG_OPTION
#define SAI_MCLK_MCR_MSEL_SHIFT 24
#define SAI_MCLK_MCR_MSEL_MASK GENMASK(24, 25)
#endif /* CONFIG_SAI_HAS_MCLK_CONFIG_OPTION */
/* workaround the fact that device_map() doesn't exist for SoCs with no MMU */
#ifndef DEVICE_MMIO_IS_IN_RAM
#define device_map(virt, phys, size, flags) *(virt) = (phys)
#endif /* DEVICE_MMIO_IS_IN_RAM */

/* used to convert an uint to I2S_Type * */
#define UINT_TO_I2S(x) ((I2S_Type *)(uintptr_t)(x))

/* macros used for parsing DTS data */

/* used instead of IDENTITY because LISTIFY expects the used macro function
 * to also take a variable number of arguments.
 */
#define IDENTITY_VARGS(V, ...) IDENTITY(V)

/* used to generate the list of clock indexes */
#define _SAI_CLOCK_INDEX_ARRAY(inst)\
	LISTIFY(DT_INST_PROP_LEN_OR(inst, clocks, 0), IDENTITY_VARGS, (,))

/* used to retrieve a clock's ID using its index generated via _SAI_CLOCK_INDEX_ARRAY */
#define _SAI_GET_CLOCK_ID(clock_idx, inst)\
	DT_INST_CLOCKS_CELL_BY_IDX(inst, clock_idx, name)

/* used to retrieve a clock's name using its index generated via _SAI_CLOCK_INDEX_ARRAY */
#define _SAI_GET_CLOCK_NAME(clock_idx, inst)\
	DT_INST_PROP_BY_IDX(inst, clock_names, clock_idx)

/* used to convert the clocks property into an array of clock IDs */
#define _SAI_CLOCK_ID_ARRAY(inst)\
	FOR_EACH_FIXED_ARG(_SAI_GET_CLOCK_ID, (,), inst, _SAI_CLOCK_INDEX_ARRAY(inst))

/* used to convert the clock-names property into an array of clock names */
#define _SAI_CLOCK_NAME_ARRAY(inst)\
	FOR_EACH_FIXED_ARG(_SAI_GET_CLOCK_NAME, (,), inst, _SAI_CLOCK_INDEX_ARRAY(inst))

/* used to convert a clocks property into an array of clock IDs. If the property
 * is not specified then this macro will return {}.
 */
#define _SAI_GET_CLOCK_ARRAY(inst)\
	COND_CODE_1(DT_NODE_HAS_PROP(DT_INST(inst, nxp_dai_sai), clocks),\
		    ({ _SAI_CLOCK_ID_ARRAY(inst) }),\
		    ({ }))

/* used to retrieve a const struct device *dev pointing to the clock controller.
 * It is assumed that all SAI clocks come from a single clock provider.
 * This macro returns a NULL if the clocks property doesn't exist.
 */
#define _SAI_GET_CLOCK_CONTROLLER(inst)\
	COND_CODE_1(DT_NODE_HAS_PROP(DT_INST(inst, nxp_dai_sai), clocks),\
		    (DEVICE_DT_GET(DT_INST_CLOCKS_CTLR(inst))),\
		    (NULL))

/* used to convert a clock-names property into an array of clock names. If the
 * property is not specified then this macro will return {}.
 */
#define _SAI_GET_CLOCK_NAMES(inst)\
	COND_CODE_1(DT_NODE_HAS_PROP(DT_INST(inst, nxp_dai_sai), clocks),\
		    ({ _SAI_CLOCK_NAME_ARRAY(inst) }),\
		    ({ }))

/* used to declare a struct clock_data */
#define SAI_CLOCK_DATA_DECLARE(inst)					\
{									\
	.clocks = (uint32_t [])_SAI_GET_CLOCK_ARRAY(inst),		\
	.clock_num = DT_INST_PROP_LEN_OR(inst, clocks, 0),		\
	.dev = _SAI_GET_CLOCK_CONTROLLER(inst),				\
	.clock_names = (const char *[])_SAI_GET_CLOCK_NAMES(inst),	\
}

/* used to parse the tx-fifo-watermark property. If said property is not
 * specified then this macro will return half of the number of words in the
 * FIFO.
 */
#define SAI_TX_FIFO_WATERMARK(inst)\
	DT_INST_PROP_OR(inst, tx_fifo_watermark,\
			FSL_FEATURE_SAI_FIFO_COUNTn(UINT_TO_I2S(DT_INST_REG_ADDR(inst))) / 2)

/* used to parse the rx-fifo-watermark property. If said property is not
 * specified then this macro will return half of the number of words in the
 * FIFO.
 */
#define SAI_RX_FIFO_WATERMARK(inst)\
	DT_INST_PROP_OR(inst, rx_fifo_watermark,\
			FSL_FEATURE_SAI_FIFO_COUNTn(UINT_TO_I2S(DT_INST_REG_ADDR(inst))) / 2)

/* used to retrieve TDR0's address based on SAI's physical address */
#define SAI_TX_FIFO_BASE(inst, idx)\
	POINTER_TO_UINT(&(UINT_TO_I2S(DT_INST_REG_ADDR(inst))->TDR[idx]))

/* used to retrieve RDR0's address based on SAI's physical address */
#define SAI_RX_FIFO_BASE(inst, idx)\
	POINTER_TO_UINT(&(UINT_TO_I2S(DT_INST_REG_ADDR(inst))->RDR[idx]))

/* internal macro used to retrieve the default TX/RX FIFO's size (in FIFO words) */
#define _SAI_FIFO_DEPTH(inst)\
	FSL_FEATURE_SAI_FIFO_COUNTn(UINT_TO_I2S(DT_INST_REG_ADDR(inst)))

/* used to retrieve the TX/RX FIFO's size (in FIFO words) */
#define SAI_FIFO_DEPTH(inst)\
	DT_INST_PROP_OR(inst, fifo_depth, _SAI_FIFO_DEPTH(inst))

/* used to retrieve the DMA MUX for transmitter */
#define SAI_TX_DMA_MUX(inst)\
	FSL_FEATURE_SAI_TX_DMA_MUXn(UINT_TO_I2S(DT_INST_REG_ADDR(inst)))

/* used to retrieve the DMA MUX for receiver */
#define SAI_RX_DMA_MUX(inst)\
	FSL_FEATURE_SAI_RX_DMA_MUXn(UINT_TO_I2S(DT_INST_REG_ADDR(inst)))

/* used to retrieve the synchronization mode of the transmitter. If this
 * property is not specified, ASYNC mode will be used.
 */
#define SAI_TX_SYNC_MODE(inst)\
	DT_INST_PROP_OR(inst, tx_sync_mode, kSAI_ModeAsync)

/* used to retrieve the synchronization mode of the receiver. If this property
 * is not specified, ASYNC mode will be used.
 */
#define SAI_RX_SYNC_MODE(inst)\
	DT_INST_PROP_OR(inst, rx_sync_mode, kSAI_ModeAsync)

/* used to retrieve the handshake value for given direction. The handshake
 * is computed as follows:
 *	handshake = CHANNEL_ID | (MUX_VALUE << 8)
 * The channel ID and MUX value are each encoded in 8 bits.
 */
#define SAI_TX_RX_DMA_HANDSHAKE(inst, dir)\
	((DT_INST_DMAS_CELL_BY_NAME(inst, dir, channel) & GENMASK(7, 0)) |\
	 ((DT_INST_DMAS_CELL_BY_NAME(inst, dir, mux) << 8) & GENMASK(15, 8)))

/* used to retrieve the number of supported transmission/receive lines */
#define SAI_DLINE_COUNT(inst)\
	FSL_FEATURE_SAI_CHANNEL_COUNTn(UINT_TO_I2S(DT_INST_REG_ADDR(inst)))

/* used to retrieve the index of the transmission line */
#define SAI_TX_DLINE_INDEX(inst) DT_INST_PROP_OR(inst, tx_dataline, 0)

/* used to retrieve the index of the receive line */
#define SAI_RX_DLINE_INDEX(inst) DT_INST_PROP_OR(inst, rx_dataline, 0)

/* utility macros */

/* invert a clock's polarity. This works because a clock's polarity is expressed
 * as a 0 or as a 1.
 */
#define SAI_INVERT_POLARITY(polarity) (polarity) = !(polarity)

/* used to issue a software reset of the transmitter/receiver */
#define SAI_TX_RX_SW_RESET(dir, regmap)\
	((dir) == DAI_DIR_RX ? SAI_RxSoftwareReset(UINT_TO_I2S(regmap), kSAI_ResetTypeSoftware) :\
	 SAI_TxSoftwareReset(UINT_TO_I2S(regmap), kSAI_ResetTypeSoftware))

/* used to enable/disable the transmitter/receiver.
 * When enabling the SYNC component, the ASYNC component will also be enabled.
 * Attempting to disable the SYNC component will fail unless the SYNC bit is
 * cleared. It is recommended to use sai_tx_rx_force_disable() instead of this
 * macro when disabling transmitter/receiver.
 */
#define SAI_TX_RX_ENABLE_DISABLE(dir, regmap, enable)\
	((dir) == DAI_DIR_RX ? SAI_RxEnable(UINT_TO_I2S(regmap), enable) :\
	 SAI_TxEnable(UINT_TO_I2S(regmap), enable))

/* used to enable/disable the DMA requests for transmitter/receiver */
#define SAI_TX_RX_DMA_ENABLE_DISABLE(dir, regmap, enable)\
	((dir) == DAI_DIR_RX ? SAI_RxEnableDMA(UINT_TO_I2S(regmap),\
					       kSAI_FIFORequestDMAEnable, enable) :\
	SAI_TxEnableDMA(UINT_TO_I2S(regmap), kSAI_FIFORequestDMAEnable, enable))

/* used to check if the hardware transmitter/receiver is enabled */
#define SAI_TX_RX_IS_HW_ENABLED(dir, regmap)\
	((dir) == DAI_DIR_RX ? (UINT_TO_I2S(regmap)->RCSR & I2S_RCSR_RE_MASK) : \
	 (UINT_TO_I2S(regmap)->TCSR & I2S_TCSR_TE_MASK))

/* used to enable various transmitter/receiver interrupts */
#define _SAI_TX_RX_ENABLE_IRQ(dir, regmap, which)\
	((dir) == DAI_DIR_RX ? SAI_RxEnableInterrupts(UINT_TO_I2S(regmap), which) : \
	 SAI_TxEnableInterrupts(UINT_TO_I2S(regmap), which))

/* used to disable various transmitter/receiver interrupts */
#define _SAI_TX_RX_DISABLE_IRQ(dir, regmap, which)\
	((dir) == DAI_DIR_RX ? SAI_RxDisableInterrupts(UINT_TO_I2S(regmap), which) : \
	 SAI_TxDisableInterrupts(UINT_TO_I2S(regmap), which))

/* used to enable/disable various transmitter/receiver interrupts */
#define SAI_TX_RX_ENABLE_DISABLE_IRQ(dir, regmap, which, enable)\
	((enable == true) ? _SAI_TX_RX_ENABLE_IRQ(dir, regmap, which) :\
	 _SAI_TX_RX_DISABLE_IRQ(dir, regmap, which))

/* used to check if a status flag is set */
#define SAI_TX_RX_STATUS_IS_SET(dir, regmap, which)\
	((dir) == DAI_DIR_RX ? ((UINT_TO_I2S(regmap))->RCSR & (which)) : \
	 ((UINT_TO_I2S(regmap))->TCSR & (which)))

/* used to retrieve the SYNC direction. Use this macro when you know for sure
 * you have 1 SYNC direction with 1 ASYNC direction.
 */
#define SAI_TX_RX_GET_SYNC_DIR(cfg)\
	((cfg)->tx_sync_mode == kSAI_ModeSync ? DAI_DIR_TX : DAI_DIR_RX)

/* used to retrieve the ASYNC direction. Use this macro when you know for sure
 * you have 1 SYNC direction with 1 ASYNC direction.
 */
#define SAI_TX_RX_GET_ASYNC_DIR(cfg)\
	((cfg)->tx_sync_mode == kSAI_ModeAsync ? DAI_DIR_TX : DAI_DIR_RX)

/* used to check if transmitter/receiver is SW enabled */
#define SAI_TX_RX_DIR_IS_SW_ENABLED(dir, data)\
	((dir) == DAI_DIR_TX ? data->tx_enabled : data->rx_enabled)

/* used to compute the mask for the transmission/receive lines based on
 * the index passed from the DTS.
 */
#define SAI_TX_RX_DLINE_MASK(dir, cfg)\
	((dir) == DAI_DIR_TX ? BIT((cfg)->tx_dline) : BIT((cfg)->rx_dline))

struct sai_clock_data {
	uint32_t *clocks;
	uint32_t clock_num;
	/* assumption: all clocks belong to the same producer */
	const struct device *dev;
	const char **clock_names;
};

struct sai_data {
	mm_reg_t regmap;
	sai_transceiver_t rx_config;
	sai_transceiver_t tx_config;
	bool tx_enabled;
	bool rx_enabled;
	enum dai_state tx_state;
	enum dai_state rx_state;
	struct dai_config cfg;
};

struct sai_config {
	uint32_t regmap_phys;
	uint32_t regmap_size;
	struct sai_clock_data clk_data;
	bool mclk_is_output;
	/* if the tx/rx-fifo-watermark properties are not specified, it's going
	 * to be assumed that the watermark should be set to half of the FIFO
	 * size.
	 */
	uint32_t rx_fifo_watermark;
	uint32_t tx_fifo_watermark;
	const struct dai_properties *tx_props;
	const struct dai_properties *rx_props;
	uint32_t dai_index;
	/* RX synchronization mode - may be SYNC or ASYNC */
	sai_sync_mode_t rx_sync_mode;
	/* TX synchronization mode - may be SYNC or ASYNC */
	sai_sync_mode_t tx_sync_mode;
	void (*irq_config)(void);
	uint32_t tx_dline;
	uint32_t rx_dline;
	const struct pinctrl_dev_config *pincfg;
};

/* this needs to perfectly match SOF's struct sof_ipc_dai_sai_params */
struct sai_bespoke_config {
	uint32_t reserved0;

	uint16_t reserved1;
	uint16_t mclk_id;
	uint32_t mclk_direction;

	/* CLOCK-related data */
	uint32_t mclk_rate;
	uint32_t fsync_rate;
	uint32_t bclk_rate;

	/* TDM-related data */
	uint32_t tdm_slots;
	uint32_t rx_slots;
	uint32_t tx_slots;
	uint16_t tdm_slot_width;
	uint16_t reserved2;
};

#ifdef CONFIG_SAI_HAS_MCLK_CONFIG_OPTION
static int get_msel(sai_bclk_source_t bclk_source, uint32_t *msel)
{
	switch (bclk_source) {
	case kSAI_BclkSourceMclkOption1:
		*msel = 0;
		break;
	case kSAI_BclkSourceMclkOption2:
		*msel = (0x2 << SAI_MCLK_MCR_MSEL_SHIFT);
		break;
	case kSAI_BclkSourceMclkOption3:
		*msel = (0x3 << SAI_MCLK_MCR_MSEL_SHIFT);
		break;
	default:
		return -EINVAL;
	}

	return 0;
}

static void set_msel(uint32_t regmap, int msel)
{
	UINT_TO_I2S(regmap)->MCR &= ~SAI_MCLK_MCR_MSEL_MASK;
	UINT_TO_I2S(regmap)->MCR |= msel;
}

static int clk_lookup_by_name(const struct sai_clock_data *clk_data, char *name)
{
	int i;

	for (i = 0; i < clk_data->clock_num; i++) {
		if (!strcmp(name, clk_data->clock_names[i])) {
			return i;
		}
	}

	return -EINVAL;
}

static int get_mclk_rate(const struct sai_clock_data *clk_data,
			 sai_bclk_source_t bclk_source,
			 uint32_t *rate)
{
	int clk_idx;
	char *clk_name;

	switch (bclk_source) {
	case kSAI_BclkSourceMclkOption1:
		clk_name = "mclk1";
		break;
	case kSAI_BclkSourceMclkOption2:
		clk_name = "mclk2";
		break;
	case kSAI_BclkSourceMclkOption3:
		clk_name = "mclk3";
		break;
	default:
		LOG_ERR("invalid bitclock source: %d", bclk_source);
		return -EINVAL;
	}

	clk_idx = clk_lookup_by_name(clk_data, clk_name);
	if (clk_idx < 0) {
		LOG_ERR("failed to get clock index for %s", clk_name);
		return clk_idx;
	}

	return clock_control_get_rate(clk_data->dev,
				      UINT_TO_POINTER(clk_data->clocks[clk_idx]),
				      rate);
}
#endif /* CONFIG_SAI_HAS_MCLK_CONFIG_OPTION */

static inline void get_bclk_default_config(sai_bit_clock_t *cfg)
{
	memset(cfg, 0, sizeof(sai_bit_clock_t));

	/* by default, BCLK has the following properties:
	 *
	 *	1) BCLK is active HIGH.
	 *	2) BCLK uses MCLK1 source. (only applicable to master mode)
	 *	3) No source swap.
	 *	4) No input delay.
	 */
	cfg->bclkPolarity = kSAI_PolarityActiveHigh;
	cfg->bclkSource = kSAI_BclkSourceMclkOption1;
}

static inline void get_fsync_default_config(sai_frame_sync_t *cfg)
{
	memset(cfg, 0, sizeof(sai_frame_sync_t));

	/* by default, FSYNC has the following properties:
	 *
	 *	1) FSYNC is asserted one bit early with respect to the next
	 *	frame.
	 *	2) FSYNC is active HIGH.
	 */
	cfg->frameSyncEarly = true;
	cfg->frameSyncPolarity = kSAI_PolarityActiveHigh;
}

static inline void get_serial_default_config(sai_serial_data_t *cfg)
{
	memset(cfg, 0, sizeof(sai_serial_data_t));

	/* by default, the serial configuration has the following quirks:
	 *
	 * 1) Data pin is not tri-stated.
	 * 2) MSB is first.
	 */
	/* note: this is equivalent to checking if the SAI has xCR4's CHMOD bit */
#if FSL_FEATURE_SAI_HAS_CHANNEL_MODE
	cfg->dataMode = kSAI_DataPinStateOutputZero;
#endif /* FSL_FEATURE_SAI_HAS_CHANNEL_MODE */
	cfg->dataOrder = kSAI_DataMSB;
}

static inline void get_fifo_default_config(sai_fifo_t *cfg)
{
	memset(cfg, 0, sizeof(sai_fifo_t));
}

static inline uint32_t sai_get_state(enum dai_dir dir,
				     struct sai_data *data)
{
	if (dir == DAI_DIR_RX) {
		return data->rx_state;
	} else {
		return data->tx_state;
	}
}

static int sai_update_state(enum dai_dir dir,
			    struct sai_data *data,
			    enum dai_state new_state)
{
	enum dai_state old_state = sai_get_state(dir, data);

	LOG_DBG("attempting to transition from %d to %d", old_state, new_state);

	/* check if transition is possible */
	switch (new_state) {
	case DAI_STATE_NOT_READY:
		/* this shouldn't be possible */
		return -EPERM;
	case DAI_STATE_READY:
		if (old_state != DAI_STATE_NOT_READY &&
		    old_state != DAI_STATE_READY &&
		    old_state != DAI_STATE_STOPPING) {
			return -EPERM;
		}
		break;
	case DAI_STATE_RUNNING:
		if (old_state != DAI_STATE_PAUSED &&
		    old_state != DAI_STATE_STOPPING &&
		    old_state != DAI_STATE_READY) {
			return -EPERM;
		}
		break;
	case DAI_STATE_PAUSED:
		if (old_state != DAI_STATE_RUNNING) {
			return -EPERM;
		}
		break;
	case DAI_STATE_STOPPING:
		if (old_state != DAI_STATE_READY &&
		    old_state != DAI_STATE_RUNNING &&
		    old_state != DAI_STATE_PAUSED) {
			return -EPERM;
		}
		break;
	case DAI_STATE_ERROR:
	case DAI_STATE_PRE_RUNNING:
		/* these states are not used so transitioning to them
		 * is considered invalid.
		 */
	default:
		return -EINVAL;
	}

	if (dir == DAI_DIR_RX) {
		data->rx_state = new_state;
	} else {
		data->tx_state = new_state;
	}

	return 0;
}

static inline void sai_tx_rx_force_disable(enum dai_dir dir,
					   uint32_t regmap)
{
	I2S_Type *base = UINT_TO_I2S(regmap);

	if (dir == DAI_DIR_RX) {
		base->RCSR = ((base->RCSR & 0xFFE3FFFFU) & (~I2S_RCSR_RE_MASK));
	} else {
		base->TCSR = ((base->TCSR & 0xFFE3FFFFU) & (~I2S_TCSR_TE_MASK));
	}
}

static inline void sai_tx_rx_sw_enable_disable(enum dai_dir dir,
					       struct sai_data *data,
					       bool enable)
{
	if (dir == DAI_DIR_RX) {
		data->rx_enabled = enable;
	} else {
		data->tx_enabled = enable;
	}
}

static inline int count_leading_zeros(uint32_t word)
{
	int num = 0;

	while (word) {
		if (!(word & 0x1)) {
			num++;
		} else {
			break;
		}

		word = word >> 1;
	}

	return num;
}

static inline void sai_tx_rx_set_dline_mask(enum dai_dir dir, uint32_t regmap, uint32_t mask)
{
	I2S_Type *base = UINT_TO_I2S(regmap);

	if (dir == DAI_DIR_RX) {
		base->RCR3 &= ~I2S_RCR3_RCE_MASK;
		base->RCR3 |= I2S_RCR3_RCE(mask);
	} else {
		base->TCR3 &= ~I2S_TCR3_TCE_MASK;
		base->TCR3 |= I2S_TCR3_TCE(mask);
	}
}

static inline void sai_dump_register_data(uint32_t regmap)
{
	I2S_Type *base = UINT_TO_I2S(regmap);

	LOG_DBG("TCSR: 0x%x", base->TCSR);
	LOG_DBG("RCSR: 0x%x", base->RCSR);

	LOG_DBG("TCR1: 0x%x", base->TCR1);
	LOG_DBG("RCR1: 0x%x", base->RCR1);

	LOG_DBG("TCR2: 0x%x", base->TCR2);
	LOG_DBG("RCR2: 0x%x", base->RCR2);

	LOG_DBG("TCR3: 0x%x", base->TCR3);
	LOG_DBG("RCR3: 0x%x", base->RCR3);

	LOG_DBG("TCR4: 0x%x", base->TCR4);
	LOG_DBG("RCR4: 0x%x", base->RCR4);

	LOG_DBG("TCR5: 0x%x", base->TCR5);
	LOG_DBG("RCR5: 0x%x", base->RCR5);

	LOG_DBG("TMR: 0x%x", base->TMR);
	LOG_DBG("RMR: 0x%x", base->RMR);

#ifdef CONFIG_SAI_HAS_MCLK_CONFIG_OPTION
	LOG_DBG("MCR: 0x%x", base->MCR);
#endif /* CONFIG_SAI_HAS_MCLK_CONFIG_OPTION */
}

#endif /* ZEPHYR_DRIVERS_DAI_NXP_SAI_H_ */
