/*
 * Copyright (c) 2022 Cypress Semiconductor Corporation (an Infineon company) or
 * an affiliate of Cypress Semiconductor Corporation
 *
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @brief UART driver for Infineon CAT1 MCU family.
 *
 * Note:
 * - Uart ASYNC functionality is not implemented in current
 *   version of Uart CAT1 driver.
 */

#define DT_DRV_COMPAT infineon_cat1_uart

#include <zephyr/drivers/uart.h>
#include <zephyr/drivers/pinctrl.h>
#include <cyhal_uart.h>
#include <cyhal_utils_impl.h>
#include <cyhal_scb_common.h>

/* Data structure */
struct ifx_cat1_uart_data {
	cyhal_uart_t obj;                               /* UART CYHAL object */
	struct uart_config cfg;
	cyhal_resource_inst_t hw_resource;
	cyhal_clock_t clock;

#if CONFIG_UART_INTERRUPT_DRIVEN
	uart_irq_callback_user_data_t irq_cb;           /* Interrupt Callback */
	void *irq_cb_data;                              /* Interrupt Callback Arg */
#endif /* CONFIG_UART_INTERRUPT_DRIVEN */
};

/* Device config structure */
struct ifx_cat1_uart_config {
	const struct pinctrl_dev_config *pcfg;
	CySCB_Type *reg_addr;
	struct uart_config dt_cfg;
	uint8_t irq_priority;
};

/* Default Counter configuration structure */
static const cy_stc_scb_uart_config_t _cyhal_uart_default_config = {
	.uartMode = CY_SCB_UART_STANDARD,
	.enableMutliProcessorMode = false,
	.smartCardRetryOnNack = false,
	.irdaInvertRx = false,
	.irdaEnableLowPowerReceiver = false,
	.oversample = 12,
	.enableMsbFirst = false,
	.dataWidth = 8UL,
	.parity = CY_SCB_UART_PARITY_NONE,
	.stopBits = CY_SCB_UART_STOP_BITS_1,
	.enableInputFilter = false,
	.breakWidth = 11UL,
	.dropOnFrameError = false,
	.dropOnParityError = false,

	.receiverAddress = 0x0UL,
	.receiverAddressMask = 0x0UL,
	.acceptAddrInFifo = false,

	.enableCts = false,
	.ctsPolarity = CY_SCB_UART_ACTIVE_LOW,
#if defined(COMPONENT_CAT1A) || defined(COMPONENT_CAT1B)
	.rtsRxFifoLevel = 20UL,
#elif defined(COMPONENT_CAT2)
	.rtsRxFifoLevel = 3UL,
#endif
	.rtsPolarity = CY_SCB_UART_ACTIVE_LOW,

	/* Level triggers when at least one element is in FIFO */
	.rxFifoTriggerLevel = 0UL,
	.rxFifoIntEnableMask = 0x0UL,

	/* Level triggers when half-fifo is half empty */
	.txFifoTriggerLevel = (CY_SCB_FIFO_SIZE / 2 - 1),
	.txFifoIntEnableMask = 0x0UL
};

/* Helper API */
static cyhal_uart_parity_t _convert_uart_parity_z_to_cyhal(enum uart_config_parity parity)
{
	cyhal_uart_parity_t cyhal_parity;

	switch (parity) {
	case UART_CFG_PARITY_NONE:
		cyhal_parity = CYHAL_UART_PARITY_NONE;
		break;
	case UART_CFG_PARITY_ODD:
		cyhal_parity = CYHAL_UART_PARITY_ODD;
		break;
	case UART_CFG_PARITY_EVEN:
		cyhal_parity = CYHAL_UART_PARITY_EVEN;
		break;
	default:
		cyhal_parity = CYHAL_UART_PARITY_NONE;
	}
	return cyhal_parity;
}

static uint32_t _convert_uart_stop_bits_z_to_cyhal(enum uart_config_stop_bits stop_bits)
{
	uint32_t cyhal_stop_bits;

	switch (stop_bits) {
	case UART_CFG_STOP_BITS_1:
		cyhal_stop_bits = 1u;
		break;

	case UART_CFG_STOP_BITS_2:
		cyhal_stop_bits = 2u;
		break;
	default:
		cyhal_stop_bits = 1u;
	}
	return cyhal_stop_bits;
}

static uint32_t _convert_uart_data_bits_z_to_cyhal(enum uart_config_data_bits data_bits)
{
	uint32_t cyhal_data_bits;

	switch (data_bits) {
	case UART_CFG_DATA_BITS_5:
		cyhal_data_bits = 1u;
		break;

	case UART_CFG_DATA_BITS_6:
		cyhal_data_bits = 6u;
		break;

	case UART_CFG_DATA_BITS_7:
		cyhal_data_bits = 7u;
		break;

	case UART_CFG_DATA_BITS_8:
		cyhal_data_bits = 8u;
		break;

	case UART_CFG_DATA_BITS_9:
		cyhal_data_bits = 9u;
		break;

	default:
		cyhal_data_bits = 1u;
	}
	return cyhal_data_bits;
}

static int32_t _get_hw_block_num(CySCB_Type *reg_addr)
{
	uint32_t i;

	for (i = 0u; i < _SCB_ARRAY_SIZE; i++) {
		if (_CYHAL_SCB_BASE_ADDRESSES[i] == reg_addr) {
			return i;
		}
	}

	return -1;
}

static int ifx_cat1_uart_poll_in(const struct device *dev, unsigned char *c)
{
	cy_rslt_t rec;
	struct ifx_cat1_uart_data *data = dev->data;

	rec = cyhal_uart_getc(&data->obj, c, 0u);

	return ((rec == CY_SCB_UART_RX_NO_DATA) ? -1 : 0);
}

static void ifx_cat1_uart_poll_out(const struct device *dev, unsigned char c)
{
	struct ifx_cat1_uart_data *data = dev->data;

	(void) cyhal_uart_putc(&data->obj, (uint32_t)c);
}

static int ifx_cat1_uart_err_check(const struct device *dev)
{
	struct ifx_cat1_uart_data *data = dev->data;
	uint32_t status = Cy_SCB_UART_GetRxFifoStatus(data->obj.base);
	int errors = 0;

	if (status & CY_SCB_UART_RX_OVERFLOW) {
		errors |= UART_ERROR_OVERRUN;
	}

	if (status & CY_SCB_UART_RX_ERR_PARITY) {
		errors |= UART_ERROR_PARITY;
	}

	if (status & CY_SCB_UART_RX_ERR_FRAME) {
		errors |= UART_ERROR_FRAMING;
	}

	return errors;
}

static int ifx_cat1_uart_configure(const struct device *dev,
				   const struct uart_config *cfg)
{
	__ASSERT_NO_MSG(cfg != NULL);

	cy_rslt_t result;
	struct ifx_cat1_uart_data *data = dev->data;

	cyhal_uart_cfg_t uart_cfg = {
		.data_bits = _convert_uart_data_bits_z_to_cyhal(cfg->data_bits),
		.stop_bits = _convert_uart_stop_bits_z_to_cyhal(cfg->stop_bits),
		.parity = _convert_uart_parity_z_to_cyhal(cfg->parity)
	};

	/* Store Uart Zephyr configuration (uart config) into data structure */
	data->cfg = *cfg;

	/* Configure parity, data and stop bits */
	result = cyhal_uart_configure(&data->obj, &uart_cfg);

	/* Configure the baud rate */
	if (result == CY_RSLT_SUCCESS) {
		result = cyhal_uart_set_baud(&data->obj, cfg->baudrate, NULL);
	}

	/* Enable RTS/CTS flow control */
	if ((result == CY_RSLT_SUCCESS) && cfg->flow_ctrl) {
		result = cyhal_uart_enable_flow_control(&data->obj, true, true);
	}

	return (result == CY_RSLT_SUCCESS) ? 0 : -ENOTSUP;
};

static int ifx_cat1_uart_config_get(const struct device *dev,
				    struct uart_config *cfg)
{
	ARG_UNUSED(dev);

	struct ifx_cat1_uart_data *const data = dev->data;

	if (cfg == NULL) {
		return -EINVAL;
	}

	*cfg = data->cfg;
	return 0;
}

#ifdef CONFIG_UART_INTERRUPT_DRIVEN

/* Uart event callback for Interrupt driven mode */
static void _uart_event_callback_irq_mode(void *arg, cyhal_uart_event_t event)
{
	ARG_UNUSED(event);

	const struct device *dev = (const struct device *) arg;
	struct ifx_cat1_uart_data *const data = dev->data;

	if (data->irq_cb != NULL) {
		data->irq_cb(dev, data->irq_cb_data);
	}
}

/* Fill FIFO with data */
static int ifx_cat1_uart_fifo_fill(const struct device *dev,
				   const uint8_t *tx_data, int size)
{
	struct ifx_cat1_uart_data *const data = dev->data;
	size_t _size = (size_t) size;

	(void)cyhal_uart_write(&data->obj, (uint8_t *) tx_data,  &_size);
	return (int) _size;
}

/* Read data from FIFO */
static int ifx_cat1_uart_fifo_read(const struct device *dev,
				   uint8_t *rx_data, const int size)
{
	struct ifx_cat1_uart_data *const data = dev->data;
	size_t _size = (size_t) size;

	(void)cyhal_uart_read(&data->obj, rx_data, &_size);
	return (int) _size;
}

/* Enable TX interrupt */
static void ifx_cat1_uart_irq_tx_enable(const struct device *dev)
{
	struct ifx_cat1_uart_data *const data = dev->data;
	const struct ifx_cat1_uart_config *const config = dev->config;

	cyhal_uart_enable_event(&data->obj,
				(cyhal_uart_event_t) CYHAL_UART_IRQ_TX_EMPTY,
				config->irq_priority, 1);
}

/* Disable TX interrupt */
static void ifx_cat1_uart_irq_tx_disable(const struct device *dev)
{
	struct ifx_cat1_uart_data *const data = dev->data;
	const struct ifx_cat1_uart_config *const config = dev->config;

	cyhal_uart_enable_event(&data->obj,
				(cyhal_uart_event_t) CYHAL_UART_IRQ_TX_EMPTY,
				config->irq_priority, 0);
}

/* Check if UART TX buffer can accept a new char */
static int ifx_cat1_uart_irq_tx_ready(const struct device *dev)
{
	struct ifx_cat1_uart_data *const data = dev->data;
	uint32_t mask = Cy_SCB_GetTxInterruptStatusMasked(data->obj.base);

	return (((mask & (CY_SCB_UART_TX_NOT_FULL | SCB_INTR_TX_EMPTY_Msk)) != 0u) ? 1 : 0);
}

/* Check if UART TX block finished transmission */
static int ifx_cat1_uart_irq_tx_complete(const struct device *dev)
{
	struct ifx_cat1_uart_data *const data = dev->data;

	return (int) !(cyhal_uart_is_tx_active(&data->obj));
}

/* Enable RX interrupt */
static void ifx_cat1_uart_irq_rx_enable(const struct device *dev)
{
	struct ifx_cat1_uart_data *const data = dev->data;
	const struct ifx_cat1_uart_config *const config = dev->config;

	cyhal_uart_enable_event(&data->obj,
				(cyhal_uart_event_t) CYHAL_UART_IRQ_RX_NOT_EMPTY,
				config->irq_priority, 1);
}

/* Disable TX interrupt */
static void ifx_cat1_uart_irq_rx_disable(const struct device *dev)
{
	struct ifx_cat1_uart_data *const data = dev->data;
	const struct ifx_cat1_uart_config *const config = dev->config;

	cyhal_uart_enable_event(&data->obj,
				(cyhal_uart_event_t) CYHAL_UART_IRQ_RX_NOT_EMPTY,
				config->irq_priority, 0);
}

/* Check if UART RX buffer has a received char */
static int ifx_cat1_uart_irq_rx_ready(const struct device *dev)
{
	struct ifx_cat1_uart_data *const data = dev->data;

	return cyhal_uart_readable(&data->obj) ? 1 : 0;
}

/* Enable Error interrupts */
static void ifx_cat1_uart_irq_err_enable(const struct device *dev)
{
	struct ifx_cat1_uart_data *const data = dev->data;
	const struct ifx_cat1_uart_config *const config = dev->config;

	cyhal_uart_enable_event(&data->obj, (cyhal_uart_event_t)
				(CYHAL_UART_IRQ_TX_ERROR | CYHAL_UART_IRQ_RX_ERROR),
				config->irq_priority, 1);
}

/* Disable Error interrupts */
static void ifx_cat1_uart_irq_err_disable(const struct device *dev)
{
	struct ifx_cat1_uart_data *const data = dev->data;
	const struct ifx_cat1_uart_config *const config = dev->config;

	cyhal_uart_enable_event(&data->obj, (cyhal_uart_event_t)
				(CYHAL_UART_IRQ_TX_ERROR | CYHAL_UART_IRQ_RX_ERROR),
				config->irq_priority, 0);
}

/* Check if any IRQs is pending */
static int ifx_cat1_uart_irq_is_pending(const struct device *dev)
{
	struct ifx_cat1_uart_data *const data = dev->data;
	uint32_t intcause = Cy_SCB_GetInterruptCause(data->obj.base);

	return (int) (intcause & (CY_SCB_TX_INTR | CY_SCB_RX_INTR));
}

/* Start processing interrupts in ISR.
 * This function should be called the first thing in the ISR. Calling
 * uart_irq_rx_ready(), uart_irq_tx_ready(), uart_irq_tx_complete()
 * allowed only after this.
 */
static int ifx_cat1_uart_irq_update(const struct device *dev)
{
	struct ifx_cat1_uart_data *const data = dev->data;
	int status = 1;

	if (((ifx_cat1_uart_irq_is_pending(dev) & CY_SCB_RX_INTR) != 0u) &&
	    (Cy_SCB_UART_GetNumInRxFifo(data->obj.base) == 0u)) {
		status = 0;
	}

	return status;
}

static void ifx_cat1_uart_irq_callback_set(const struct device *dev,
					   uart_irq_callback_user_data_t cb,
					   void *cb_data)
{
	struct ifx_cat1_uart_data *data = dev->data;
	cyhal_uart_t *uart_obj = &data->obj;

	/* Store user callback info */
	data->irq_cb = cb;
	data->irq_cb_data = cb_data;

	/* Register a uart general callback handler  */
	cyhal_uart_register_callback(uart_obj, _uart_event_callback_irq_mode, (void *) dev);
}
#endif /* CONFIG_UART_INTERRUPT_DRIVEN */


static int ifx_cat1_uart_init(const struct device *dev)
{
	struct ifx_cat1_uart_data *const data = dev->data;
	const struct ifx_cat1_uart_config *const config = dev->config;
	cy_rslt_t result;
	int ret;

	cyhal_uart_configurator_t uart_init_cfg = {
		.resource = &data->hw_resource,
		.config = &_cyhal_uart_default_config,
		.clock = &data->clock,
	};

	/* Dedicate SCB HW resource */
	data->hw_resource.type = CYHAL_RSC_SCB;
	data->hw_resource.block_num = _get_hw_block_num(config->reg_addr);

	/* Configure dt provided device signals when available */
	ret = pinctrl_apply_state(config->pcfg, PINCTRL_STATE_DEFAULT);
	if (ret < 0) {
		return ret;
	}

	/* Allocates clock for selected IP block */
	result = _cyhal_utils_allocate_clock(&data->clock, &data->hw_resource,
					     CYHAL_CLOCK_BLOCK_PERIPHERAL_16BIT, true);
	if (result != CY_RSLT_SUCCESS) {
		return -ENOTSUP;
	}

	/* Assigns a programmable divider to a selected IP block */
	en_clk_dst_t clk_idx = _cyhal_scb_get_clock_index(uart_init_cfg.resource->block_num);

	result = _cyhal_utils_peri_pclk_assign_divider(clk_idx, uart_init_cfg.clock);
	if (result != CY_RSLT_SUCCESS) {
		return -ENOTSUP;
	}

	/* Initialize the UART peripheral */
	result = cyhal_uart_init_cfg(&data->obj, &uart_init_cfg);
	if (result != CY_RSLT_SUCCESS) {
		return -ENOTSUP;
	}

	/* Perform initial Uart configuration */
	data->obj.is_clock_owned = true;
	ret = ifx_cat1_uart_configure(dev, &config->dt_cfg);

	return ret;
}

static const struct uart_driver_api ifx_cat1_uart_driver_api = {
	.poll_in = ifx_cat1_uart_poll_in,
	.poll_out = ifx_cat1_uart_poll_out,
	.err_check = ifx_cat1_uart_err_check,

#ifdef CONFIG_UART_USE_RUNTIME_CONFIGURE
	.configure = ifx_cat1_uart_configure,
	.config_get = ifx_cat1_uart_config_get,
#endif /* CONFIG_UART_USE_RUNTIME_CONFIGURE */

#ifdef CONFIG_UART_INTERRUPT_DRIVEN
	.fifo_fill = ifx_cat1_uart_fifo_fill,
	.fifo_read = ifx_cat1_uart_fifo_read,
	.irq_tx_enable = ifx_cat1_uart_irq_tx_enable,
	.irq_tx_disable = ifx_cat1_uart_irq_tx_disable,
	.irq_tx_ready = ifx_cat1_uart_irq_tx_ready,
	.irq_rx_enable = ifx_cat1_uart_irq_rx_enable,
	.irq_rx_disable = ifx_cat1_uart_irq_rx_disable,
	.irq_tx_complete = ifx_cat1_uart_irq_tx_complete,
	.irq_rx_ready = ifx_cat1_uart_irq_rx_ready,
	.irq_err_enable = ifx_cat1_uart_irq_err_enable,
	.irq_err_disable = ifx_cat1_uart_irq_err_disable,
	.irq_is_pending = ifx_cat1_uart_irq_is_pending,
	.irq_update = ifx_cat1_uart_irq_update,
	.irq_callback_set = ifx_cat1_uart_irq_callback_set,
#endif    /* CONFIG_UART_INTERRUPT_DRIVEN */
};

#define INFINEON_CAT1_UART_INIT(n)							     \
	PINCTRL_DT_INST_DEFINE(n);							     \
	static struct ifx_cat1_uart_data ifx_cat1_uart##n##_data;			     \
											     \
	static struct ifx_cat1_uart_config ifx_cat1_uart##n##_cfg = {			     \
		.dt_cfg.baudrate = DT_INST_PROP(n, current_speed),			     \
		.dt_cfg.parity = DT_INST_ENUM_IDX_OR(n, parity, UART_CFG_PARITY_NONE),	     \
		.dt_cfg.stop_bits = DT_INST_ENUM_IDX_OR(n, stop_bits, UART_CFG_STOP_BITS_1), \
		.dt_cfg.data_bits = DT_INST_ENUM_IDX_OR(n, data_bits, UART_CFG_DATA_BITS_8), \
		.dt_cfg.flow_ctrl = DT_INST_PROP(n, hw_flow_control),			     \
		.pcfg = PINCTRL_DT_INST_DEV_CONFIG_GET(n),				     \
		.reg_addr = (CySCB_Type *)DT_INST_REG_ADDR(n),				     \
		.irq_priority = DT_INST_IRQ(n, priority)				     \
	};										     \
											     \
	DEVICE_DT_INST_DEFINE(n,							     \
			      &ifx_cat1_uart_init, NULL,				     \
			      &ifx_cat1_uart##n##_data,					     \
			      &ifx_cat1_uart##n##_cfg, PRE_KERNEL_1,			     \
			      CONFIG_SERIAL_INIT_PRIORITY,				     \
			      &ifx_cat1_uart_driver_api);

DT_INST_FOREACH_STATUS_OKAY(INFINEON_CAT1_UART_INIT)
