// SPDX-License-Identifier: BSD-3-Clause
//
// Copyright 2019 NXP
//
// Author: Daniel Baluta <daniel.baluta@nxp.com>
// Author: Paul Olaru <paul.olaru@nxp.com>

#include <sof/audio/component.h>
#include <sof/drivers/edma.h>
#include <sof/drivers/esai.h>
#include <rtos/interrupt.h>
#include <rtos/alloc.h>
#include <sof/lib/dai.h>
#include <sof/lib/dma.h>
#include <sof/lib/uuid.h>
#include <ipc/dai.h>
#include <ipc/topology.h>
#include <errno.h>
#include <stdint.h>

LOG_MODULE_REGISTER(esai, CONFIG_SOF_LOG_LEVEL);

SOF_DEFINE_REG_UUID(esai);

DECLARE_TR_CTX(esai_tr, SOF_UUID(esai_uuid), LOG_LEVEL_INFO);

struct esai_pdata {
	struct {
		uint32_t ecr;
		uint32_t tfcr;
		uint32_t rfcr;
		uint32_t saicr;
		uint32_t tcr;
		uint32_t tccr;
		uint32_t rcr;
		uint32_t rccr;
		uint32_t tsma;
		uint32_t tsmb;
		uint32_t rsma;
		uint32_t rsmb;
		uint32_t prrc;
		uint32_t pcrc;
	} regs;
	struct sof_ipc_dai_esai_params params;
};

static inline int esai_set_config(struct dai *dai, struct ipc_config_dai *common_config,
				  const void *spec_config)
{
	const struct sof_ipc_dai_config *config = spec_config;
	uint32_t xcr = 0, xccr = 0, mask;
	struct esai_pdata *esai = dai_get_drvdata(dai);

	dai_dbg(dai, "ESAI: set_config format 0x%04x",
		config->format);

	esai->params = config->esai;

	switch (config->format & SOF_DAI_FMT_FORMAT_MASK) {
	case SOF_DAI_FMT_I2S:
		/* Data on rising edge of bclk, frame low, 1clk before
		 * data
		 */
		xcr |= ESAI_xCR_xFSR;
		xccr |= ESAI_xCCR_xFSP | ESAI_xCCR_xCKP | ESAI_xCCR_xHCKP;
		break;
	case SOF_DAI_FMT_RIGHT_J:
		/* Data on rising edge of bclk, frame high, right
		 * aligned
		 */
		xccr |= ESAI_xCCR_xCKP | ESAI_xCCR_xHCKP;
		xcr  |= ESAI_xCR_xWA;
		break;
	case SOF_DAI_FMT_LEFT_J:
		/* Data on rising edge of bclk, frame high */
		xccr |= ESAI_xCCR_xCKP | ESAI_xCCR_xHCKP;
		break;
	case SOF_DAI_FMT_DSP_A:
		/* Data on rising edge of bclk, frame high, 1clk before
		 * data
		 */
		xcr |= ESAI_xCR_xFSL | ESAI_xCR_xFSR;
		xccr |= ESAI_xCCR_xCKP | ESAI_xCCR_xHCKP;
		break;
	case SOF_DAI_FMT_DSP_B:
		/* Data on rising edge of bclk, frame high */
		xcr |= ESAI_xCR_xFSL;
		xccr |= ESAI_xCCR_xCKP | ESAI_xCCR_xHCKP;
		break;
	case SOF_DAI_FMT_PDM:
		dai_err(dai, "ESAI: Unsupported format (PDM)");
		return -EINVAL;
	default:
		dai_err(dai, "ESAI: invalid format");
		return -EINVAL;
	}

	switch (config->format & SOF_DAI_FMT_INV_MASK) {
	case SOF_DAI_FMT_NB_NF:
		 /* Nothing to do for both normal cases */
		break;
	case SOF_DAI_FMT_NB_IF:
		/* Invert frame clock */
		xccr ^= ESAI_xCCR_xFSP;
		break;
	case SOF_DAI_FMT_IB_NF:
		/* Invert bit clock */
		xccr ^= ESAI_xCCR_xCKP | ESAI_xCCR_xHCKP;
		break;
	case SOF_DAI_FMT_IB_IF:
		/* Invert both clocks */
		xccr ^= ESAI_xCCR_xCKP | ESAI_xCCR_xHCKP | ESAI_xCCR_xFSP;
		break;
	default:
		dai_err(dai, "ESAI: Invalid bit inversion format");
		return -EINVAL;
	}

	switch (config->format & SOF_DAI_FMT_CLOCK_PROVIDER_MASK) {
	case SOF_DAI_FMT_CBP_CFP:
		/* Nothing to do in the registers */
		break;
	case SOF_DAI_FMT_CBP_CFC:
		xccr |= ESAI_xCCR_xFSD;
		break;
	case SOF_DAI_FMT_CBC_CFP:
		xccr |= ESAI_xCCR_xCKD;
		break;
	case SOF_DAI_FMT_CBC_CFC:
		xccr |= ESAI_xCCR_xFSD | ESAI_xCCR_xCKD;
		break;
	default:
		dai_err(dai, "ESAI: Invalid clock provider-consumer configuration");
		return -EINVAL;
	}

	/* Set networked mode; we only support 2 channels now, not 1 */
	xcr |= ESAI_xCR_xMOD_NETWORK;
	xccr |= ESAI_xCCR_xDC(2);

	/* Codec desires 32-bit samples, while the pipeline works with 24-bit
	 * samples. Pad the least significant bits with zeros.
	 * TODO replace 24 with the sample width requested from the topology.
	 */
	xcr |= ESAI_xCR_xSWS(32, 24) | ESAI_xCR_PADC;

	/* Remove "RESET" flag so we can configure the ESAI */
	dai_update_bits(dai, REG_ESAI_ECR, ESAI_ECR_ERST, 0);

	/* EXTAL transmitter in, we should use external EXTAL pin as MCLK */
	dai_update_bits(dai, REG_ESAI_ECR, ESAI_ECR_ETI, ESAI_ECR_ETI);

	mask = ESAI_xCCR_xCKP | ESAI_xCCR_xHCKP | ESAI_xCCR_xFSP |
		ESAI_xCCR_xFSD | ESAI_xCCR_xCKD | ESAI_xCCR_xHCKD |
		ESAI_xCCR_xDC_MASK;

	xccr |= ESAI_xCCR_xHCKD; /* Set the HCKT pin as an output */

	dai_update_bits(dai, REG_ESAI_TCCR, mask, xccr);
	/* There is a hardware limitation which prevents tx and rx to be
	 * simultaneously provider or simultaneously consumer. As a workaround,
	 * we will leave tx as provider and set rx as consumer.
	 */
	xccr &= ~(ESAI_xCCR_xCKD | ESAI_xCCR_xFSD);
	dai_update_bits(dai, REG_ESAI_RCCR, mask, xccr);

	mask = ESAI_xCR_xFSL | ESAI_xCR_xFSR | ESAI_xCR_xWA |
		ESAI_xCR_xMOD_MASK | ESAI_xCR_xSWS_MASK | ESAI_xCR_PADC |
		ESAI_xCR_xPR;
	/* Personal reset, suspend the actual TX/RX for now */
	xcr |= ESAI_xCR_xPR;

	dai_update_bits(dai, REG_ESAI_TCR, mask, xcr);
	/* rx doesn't have any PADC bit, remove it from the mask */
	mask &= ~ESAI_xCR_PADC;
	dai_update_bits(dai, REG_ESAI_RCR, mask, xcr);

	/* Disable transmission by disabling all slots */
	dai_write(dai, REG_ESAI_TSMA, 0);
	dai_write(dai, REG_ESAI_TSMB, 0);
	dai_write(dai, REG_ESAI_RSMA, 0);
	dai_write(dai, REG_ESAI_RSMB, 0);

	/* Program FIFOs */
	dai_update_bits(dai, REG_ESAI_RFCR, ESAI_xFCR_xFR, 0);

	/* Reset transmit FIFO */
	dai_update_bits(dai, REG_ESAI_TFCR,
			ESAI_xFCR_xFR_MASK,
			ESAI_xFCR_xFR);
	/* Reset receive FIFO */
	dai_update_bits(dai, REG_ESAI_RFCR,
			ESAI_xFCR_xFR_MASK,
			ESAI_xFCR_xFR);

	/* Set transmit fifo configuration register
	 * xWA(24): 24-bit samples as input/output. Must agree with xSWS above.
	 *	    TODO get sample width from topology.
	 * xFWM(96): Trigger next DMA transfer when at least 96 (of the 128)
	 *	     slots are empty (or full for capture).
	 * TE(1): Enable 1 transmitter.
	 * RE(1): Enable 1 receiver.
	 * TIEN: Transmitter initialization enable. This will pull the initial
	 *       samples from the FIFO in the transmit registers. The
	 *       alternative would have been to manually initialize the
	 *       transmit registers manually, which would have been more
	 *       complex to implement.
	 */
	dai_update_bits(dai, REG_ESAI_TFCR,
			ESAI_xFCR_xFR_MASK | ESAI_xFCR_xWA_MASK |
			ESAI_xFCR_xFWM_MASK | ESAI_xFCR_TE_MASK |
			ESAI_xFCR_TIEN,
			ESAI_xFCR_xWA(24) | ESAI_xFCR_xFWM(96) |
			ESAI_xFCR_TE(1) | ESAI_xFCR_TIEN);

	dai_update_bits(dai, REG_ESAI_RFCR,
			ESAI_xFCR_xFR_MASK | ESAI_xFCR_xWA_MASK |
			ESAI_xFCR_xFWM_MASK | ESAI_xFCR_RE_MASK,
			ESAI_xFCR_xWA(24) | ESAI_xFCR_xFWM(96) |
			ESAI_xFCR_RE(1));

	/* Set the clock divider to divide EXTAL by 16 (DIV8 from PSR,
	 * plus a divide by 2 which is mandatory overall)
	 * This configuration supports hardcoded MCLK at 49152000 Hz and
	 * obtains frame clock of 96000 Hz (2 48000Hz channels) and bit clock
	 * of 3072000 Hz (32-bit samples).
	 * xFP(1): No division from this divider (can do 1-16)
	 * xPSR_DIV8: Divide by 8
	 * There is also an additional divide by 2 which is forced by the
	 * hardware design of the ESAI.
	 *
	 * TODO use ESAI params instead of hardcode to compute the clock divider
	 * settings.
	 */
	dai_update_bits(dai, REG_ESAI_TCCR, ESAI_xCCR_xFP_MASK,
			ESAI_xCCR_xFP(1));
	dai_update_bits(dai, REG_ESAI_RCCR, ESAI_xCCR_xFP_MASK,
			ESAI_xCCR_xFP(1));
	dai_update_bits(dai, REG_ESAI_TCCR, ESAI_xCCR_xPSR_MASK,
			ESAI_xCCR_xPSR_DIV8);
	dai_update_bits(dai, REG_ESAI_RCCR, ESAI_xCCR_xPSR_MASK,
			ESAI_xCCR_xPSR_DIV8);

	/* Remove ESAI personal reset */
	dai_update_bits(dai, REG_ESAI_xCR(0), ESAI_xCR_xPR, 0);
	dai_update_bits(dai, REG_ESAI_xCR(1), ESAI_xCR_xPR, 0);

	/* Configure ESAI pins, enable them all */
	dai_update_bits(dai, REG_ESAI_PRRC, ESAI_PRRC_PDC_MASK,
			ESAI_PRRC_PDC(ESAI_GPIO));
	dai_update_bits(dai, REG_ESAI_PCRC, ESAI_PCRC_PC_MASK,
			ESAI_PCRC_PC(ESAI_GPIO));
	return 0;
}

static void esai_start(struct dai *dai, int direction)
{
	/* FIFO enable */
	dai_update_bits(dai, REG_ESAI_xFCR(direction), ESAI_xFCR_xFEN_MASK,
			ESAI_xFCR_xFEN);

	/* To prevent channel swap issues, write a sample per channel. The ESAI
	 * can begin transmitting these samples while it is requesting for a
	 * DMA transfer, before said DMA transfer actually puts more samples
	 * into the ESAI FIFO. Only needed on playback/transmit.
	 */
	if (!direction) {
		/* TODO adjust this when we have more than 2 channels;
		 * we need to write a word per active channel.
		 */
		dai_write(dai, REG_ESAI_ETDR, 0);
		dai_write(dai, REG_ESAI_ETDR, 0);
	}

	/* Enable one transmitter or receiver */
	dai_update_bits(dai, REG_ESAI_xCR(direction),
			ESAI_xCR_xE_MASK(direction), ESAI_xCR_xE(direction, 1));

	/* Configure time slot registers, enable two time slots for two
	 * channels.
	 *
	 * This actually begins playback/capture.
	 *
	 * The order of setting xSMB first and xSMA second is required to
	 * correctly start playback/capture; setting them in reverse order may
	 * cause significant channel swap issues when using more than 16
	 * channels.
	 */
	dai_update_bits(dai, REG_ESAI_xSMB(direction), ESAI_xSMB_xS_MASK,
			ESAI_xSMB_xS_CHANS(2));

	dai_update_bits(dai, REG_ESAI_xSMA(direction), ESAI_xSMA_xS_MASK,
			ESAI_xSMA_xS_CHANS(2));
}

static void esai_stop(struct dai *dai, int direction)
{
	/* Disable transmitters/receivers */
	dai_update_bits(dai, REG_ESAI_xCR(direction),
			direction ? ESAI_xCR_TE_MASK : ESAI_xCR_RE_MASK, 0);

	/* Turn off pins */
	dai_update_bits(dai, REG_ESAI_xSMA(direction), ESAI_xSMA_xS_MASK, 0);
	dai_update_bits(dai, REG_ESAI_xSMB(direction), ESAI_xSMB_xS_MASK, 0);

	/* disable and reset FIFO */
	dai_update_bits(dai, REG_ESAI_xFCR(direction),
			ESAI_xFCR_xFR | ESAI_xFCR_xFEN, ESAI_xFCR_xFR);
	dai_update_bits(dai, REG_ESAI_xFCR(direction), ESAI_xFCR_xFR, 0);
}

static int esai_trigger(struct dai *dai, int cmd, int direction)
{
	dai_dbg(dai, "ESAI: trigger");

	switch (cmd) {
	case COMP_TRIGGER_START:
	case COMP_TRIGGER_RELEASE:
		esai_start(dai, direction);
		break;
	case COMP_TRIGGER_STOP:
	case COMP_TRIGGER_PAUSE:
		esai_stop(dai, direction);
		break;
	default:
		dai_err(dai, "ESAI: invalid trigger cmd %d", cmd);
		return -EINVAL;
	}
	return 0;
}

static int esai_probe(struct dai *dai)
{
	struct esai_pdata *pdata;

	dai_dbg(dai, "ESAI: probe");
	if (dai_get_drvdata(dai)) {
		dai_err(dai, "ESAI: Repeated probe, skipping");
		return -EEXIST;
	}
	pdata = rzalloc(SOF_MEM_FLAG_KERNEL | SOF_MEM_FLAG_COHERENT, sizeof(*pdata));
	if (!pdata) {
		dai_err(dai, "ESAI probe failure, out of memory");
		return -ENOMEM;
	}
	dai_set_drvdata(dai, pdata);

	/* ESAI core reset */
	dai_write(dai, REG_ESAI_ECR, ESAI_ECR_ERST | ESAI_ECR_ESAIEN);
	dai_write(dai, REG_ESAI_ECR, ESAI_ECR_ESAIEN);
	/* ESAI personal reset (should be by default but just to be sure) */
	dai_write(dai, REG_ESAI_PRRC, 0);
	dai_write(dai, REG_ESAI_PCRC, 0);
	/* ESAI FIFO reset */
	dai_write(dai, REG_ESAI_TFCR, ESAI_xFCR_xFR);
	dai_write(dai, REG_ESAI_RFCR, ESAI_xFCR_xFR);
	/* Clear TSMA, TSMB (disable all transmitters and receivers) */
	dai_write(dai, REG_ESAI_TSMA, 0);
	dai_write(dai, REG_ESAI_TSMB, 0);
	dai_write(dai, REG_ESAI_RSMA, 0);
	dai_write(dai, REG_ESAI_RSMB, 0);

	return 0;
}

static int esai_remove(struct dai *dai)
{
	struct esai_pdata *pdata = dai_get_drvdata(dai);

	dai_info(dai, "esai_remove()");

	rfree(pdata);
	dai_set_drvdata(dai, NULL);

	return 0;
}

static int esai_get_handshake(struct dai *dai, int direction, int stream_id)
{
	int handshake = dai->plat_data.fifo[direction].handshake;
	int channel = EDMA_HS_GET_CHAN(handshake);
	int irq = irqstr_get_sof_int(EDMA_HS_GET_IRQ(handshake));

	return EDMA_HANDSHAKE(irq, channel, 0);
}

static int esai_get_fifo(struct dai *dai, int direction, int stream_id)
{
	switch (direction) {
	case DAI_DIR_PLAYBACK:
	case DAI_DIR_CAPTURE:
		return dai_fifo(dai, direction); /* stream_id is unused */
	default:
		dai_err(dai, "esai_get_fifo(): Invalid direction");
		return -EINVAL;
	}
}

static int esai_get_fifo_depth(struct dai *dai, int direction)
{
	switch (direction) {
	case DAI_DIR_PLAYBACK:
	case DAI_DIR_CAPTURE:
		return dai->plat_data.fifo[direction].depth;
	default:
		dai_err(dai, "esai_get_fifo_depth(): Invalid direction");
		return -EINVAL;
	}
}

static int esai_get_hw_params(struct dai *dai,
			      struct sof_ipc_stream_params *params,
			      int dir)
{
	struct esai_pdata *esai = dai_get_drvdata(dai);

	/* ESAI only currently supports these parameters */
	params->rate = esai->params.fsync_rate;
	params->channels = 2;
	params->buffer_fmt = 0;
	params->frame_fmt = SOF_IPC_FRAME_S24_4LE;

	dai_info(dai, "esai_get_hw_params(): params->rate = %d, fsync_rate = %d",
		 params->rate, esai->params.fsync_rate);

	return 0;
}

const struct dai_driver esai_driver = {
	.type = SOF_DAI_IMX_ESAI,
	.uid = SOF_UUID(esai_uuid),
	.tctx = &esai_tr,
	.dma_dev = DMA_DEV_ESAI,
	.ops = {
		.trigger		= esai_trigger,
		.set_config		= esai_set_config,
		.probe			= esai_probe,
		.remove			= esai_remove,
		.get_handshake		= esai_get_handshake,
		.get_fifo		= esai_get_fifo,
		.get_fifo_depth		= esai_get_fifo_depth,
		.get_hw_params		= esai_get_hw_params,
	},
};
