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

/**
 * @brief SDIO driver for Infineon CAT1 MCU family.
 *
 * This driver support only SDIO protocol of the SD interface for general
 * I/O functions.
 *
 * Refer to the SD Specifications Part 1 SDIO Specifications Version 4.10 for more
 * information on the SDIO protocol and specifications.
 *
 * Features
 * - Supports 4-bit interface
 * - Supports Ultra High Speed (UHS-I) mode
 * - Supports Default Speed (DS), High Speed (HS), SDR12, SDR25 and SDR50 speed modes
 * - Supports SDIO card interrupts in both 1-bit SD and 4-bit SD modes
 * - Supports Standard capacity (SDSC), High capacity (SDHC) and
 *   Extended capacity (SDXC) memory
 *
 * Note (limitations):
 * - current version of ifx_cat1_sdio supports only following set of commands:
 *   > GO_IDLE_STATE      (CMD0)
 *   > SEND_RELATIVE_ADDR (CMD3)
 *   > IO_SEND_OP_COND    (CMD5)
 *   > SELECT_CARD        (CMD7)
 *   > VOLTAGE_SWITCH     (CMD11)
 *   > GO_INACTIVE_STATE  (CMD15)
 *   > IO_RW_DIRECT       (CMD52)
 *   > IO_RW_EXTENDED     (CMD53)
 */

#define DT_DRV_COMPAT infineon_cat1_sdhc_sdio

#include <zephyr/kernel.h>
#include <zephyr/devicetree.h>
#include <zephyr/drivers/sdhc.h>
#include <zephyr/sd/sd_spec.h>
#include <zephyr/drivers/clock_control.h>
#include <zephyr/drivers/gpio.h>
#include <zephyr/logging/log.h>
#include <soc.h>
#include <zephyr/drivers/pinctrl.h>

#include <cyhal_hw_resources.h>
#include <cyhal_sdhc.h>
#include <cyhal_sdio.h>
#include <cyhal_gpio.h>

LOG_MODULE_REGISTER(ifx_cat1_sdio, CONFIG_SDHC_LOG_LEVEL);

#include <zephyr/irq.h>

#define IFX_CAT1_SDIO_F_MIN (SDMMC_CLOCK_400KHZ)
#define IFX_CAT1_SDIO_F_MAX (SD_CLOCK_50MHZ)

struct ifx_cat1_sdio_config {
	const struct pinctrl_dev_config *pincfg;
	SDHC_Type *reg_addr;
	uint8_t irq_priority;
};

struct ifx_cat1_sdio_data {
	cyhal_sdio_t sdio_obj;
	cyhal_resource_inst_t hw_resource;
	cyhal_sdio_configurator_t cyhal_sdio_config;
	enum sdhc_clock_speed clock_speed;
	enum sdhc_bus_width bus_width;

	void *sdio_cb_user_data;
	sdhc_interrupt_cb_t sdio_cb;
};

static uint32_t sdio_rca;
static const cy_stc_sd_host_init_config_t host_config = {false, CY_SD_HOST_DMA_ADMA2, false};
static cy_en_sd_host_card_capacity_t sd_host_card_capacity = CY_SD_HOST_SDSC;
static cy_en_sd_host_card_type_t sd_host_card_type = CY_SD_HOST_NOT_EMMC;
static cy_stc_sd_host_sd_card_config_t sd_host_sd_card_config = {
	.lowVoltageSignaling = false,
	.busWidth = CY_SD_HOST_BUS_WIDTH_4_BIT,
	.cardType = &sd_host_card_type,
	.rca = &sdio_rca,
	.cardCapacity = &sd_host_card_capacity,
};

/* List of available SDHC instances */
static SDHC_Type *const IFX_CAT1_SDHC_BASE_ADDRESSES[CY_IP_MXSDHC_INSTANCES] = {
#ifdef SDHC0
	SDHC0,
#endif /* ifdef SDHC0 */

#ifdef SDHC1
	SDHC1,
#endif /* ifdef SDHC1 */
};

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

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

	return -EINVAL;
}

static int ifx_cat1_sdio_reset(const struct device *dev)
{
	struct ifx_cat1_sdio_data *dev_data = dev->data;

	cyhal_sdhc_software_reset((cyhal_sdhc_t *)&dev_data->sdio_obj);

	return 0;
}

static int ifx_cat1_sdio_set_io(const struct device *dev, struct sdhc_io *ios)
{
	cy_rslt_t ret;
	struct ifx_cat1_sdio_data *dev_data = dev->data;
	cyhal_sdio_cfg_t config = {.frequencyhal_hz = ios->clock};

	/* NOTE: Set bus width, set card power, set host signal voltage,
	 * set I/O timing does not support in current version of driver
	 */

	/* Set host clock */
	if ((dev_data->clock_speed != ios->clock) && (ios->clock != 0)) {

		if ((ios->clock > IFX_CAT1_SDIO_F_MAX) || (ios->clock < IFX_CAT1_SDIO_F_MIN)) {
			return -EINVAL;
		}

		ret = cyhal_sdio_configure(&dev_data->sdio_obj, &config);
		if (ret != CY_RSLT_SUCCESS) {
			return -ENOTSUP;
		}

		dev_data->clock_speed = ios->clock;
	}

	return 0;
}

static int ifx_cat1_sdio_card_busy(const struct device *dev)
{
	struct ifx_cat1_sdio_data *dev_data = dev->data;

	return cyhal_sdio_is_busy(&dev_data->sdio_obj) ? 1 : 0;
}

static int ifx_cat1_sdio_request(const struct device *dev, struct sdhc_command *cmd,
				 struct sdhc_data *data)
{
	struct ifx_cat1_sdio_data *dev_data = dev->data;
	int ret;

	switch (cmd->opcode) {
	case CYHAL_SDIO_CMD_GO_IDLE_STATE:
	case CYHAL_SDIO_CMD_SEND_RELATIVE_ADDR:
	case CYHAL_SDIO_CMD_IO_SEND_OP_COND:
	case CYHAL_SDIO_CMD_SELECT_CARD:
	case CYHAL_SDIO_CMD_VOLTAGE_SWITCH:
	case CYHAL_SDIO_CMD_GO_INACTIVE_STATE:
	case CYHAL_SDIO_CMD_IO_RW_DIRECT:
		ret = cyhal_sdio_send_cmd(&dev_data->sdio_obj, CYHAL_SDIO_XFER_TYPE_READ,
					  cmd->opcode, cmd->arg, cmd->response);
		if (ret != CY_RSLT_SUCCESS) {
			LOG_ERR("cyhal_sdio_send_cmd failed ret = %d \r\n", ret);
		}
		break;

	case CYHAL_SDIO_CMD_IO_RW_EXTENDED:
		cyhal_sdio_transfer_type_t direction;

		direction = (cmd->arg & BIT(SDIO_CMD_ARG_RW_SHIFT)) ? CYHAL_SDIO_XFER_TYPE_WRITE
								    : CYHAL_SDIO_XFER_TYPE_READ;

		ret = cyhal_sdio_bulk_transfer(&dev_data->sdio_obj, direction, cmd->arg, data->data,
					       data->blocks * data->block_size, cmd->response);

		if (ret != CY_RSLT_SUCCESS) {
			LOG_ERR("cyhal_sdio_bulk_transfer failed ret = %d \r\n", ret);
		}
		break;

	default:
		ret = -ENOTSUP;
	}

	return ret;
}

static int ifx_cat1_sdio_get_card_present(const struct device *dev)
{
	return 1;
}

static int ifx_cat1_sdio_get_host_props(const struct device *dev, struct sdhc_host_props *props)
{
	memset(props, 0, sizeof(*props));
	props->f_max = IFX_CAT1_SDIO_F_MAX;
	props->f_min = IFX_CAT1_SDIO_F_MIN;
	props->host_caps.bus_4_bit_support = true;
	props->host_caps.high_spd_support = true;
	props->host_caps.sdr50_support = true;
	props->host_caps.sdio_async_interrupt_support = true;
	props->host_caps.vol_330_support = true;

	return 0;
}

static int ifx_cat1_sdio_enable_interrupt(const struct device *dev, sdhc_interrupt_cb_t callback,
					  int sources, void *user_data)
{
	struct ifx_cat1_sdio_data *data = dev->data;
	const struct ifx_cat1_sdio_config *cfg = dev->config;

	if (sources != SDHC_INT_SDIO) {
		return -ENOTSUP;
	}

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

	/* Record SDIO callback parameters */
	data->sdio_cb = callback;
	data->sdio_cb_user_data = user_data;

	/* Enable CARD INTERRUPT event */
	cyhal_sdio_enable_event(&data->sdio_obj, CYHAL_SDIO_CARD_INTERRUPT,
				cfg->irq_priority, true);

	return 0;
}

static int ifx_cat1_sdio_disable_interrupt(const struct device *dev, int sources)
{
	struct ifx_cat1_sdio_data *data = dev->data;
	const struct ifx_cat1_sdio_config *cfg = dev->config;

	if (sources != SDHC_INT_SDIO) {
		return -ENOTSUP;
	}

	data->sdio_cb = NULL;
	data->sdio_cb_user_data = NULL;

	/* Disable CARD INTERRUPT event */
	cyhal_sdio_enable_event(&data->sdio_obj, CYHAL_SDIO_CARD_INTERRUPT,
				cfg->irq_priority, false);

	return 0;
}

static void ifx_cat1_sdio_event_callback(void *callback_arg, cyhal_sdio_event_t event)
{
	const struct device *dev = callback_arg;
	struct ifx_cat1_sdio_data *data = dev->data;

	if ((event == CYHAL_SDIO_CARD_INTERRUPT) && (data->sdio_cb != NULL)) {
		data->sdio_cb(dev, SDHC_INT_SDIO, data->sdio_cb_user_data);
	}
}

static int ifx_cat1_sdio_init(const struct device *dev)
{
	cy_rslt_t ret;
	struct ifx_cat1_sdio_data *data = dev->data;
	const struct ifx_cat1_sdio_config *config = dev->config;

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

	/* Dedicate SDIO HW resource */
	data->hw_resource.type = CYHAL_RSC_SDHC;
	data->hw_resource.block_num = _get_hw_block_num(config->reg_addr);
	data->hw_resource.channel_num = 0;

	/* Initialize the SDIO peripheral */
	data->cyhal_sdio_config.resource = &data->hw_resource;
	data->cyhal_sdio_config.host_config = &host_config,
	data->cyhal_sdio_config.card_config = &sd_host_sd_card_config,
	data->cyhal_sdio_config.gpios.cmd = NC;
	data->cyhal_sdio_config.gpios.clk = NC;
	data->cyhal_sdio_config.gpios.data[0] = NC;
	data->cyhal_sdio_config.gpios.data[1] = NC;
	data->cyhal_sdio_config.gpios.data[2] = NC;
	data->cyhal_sdio_config.gpios.data[3] = NC;
	data->cyhal_sdio_config.clock = NULL;

	ret = cyhal_sdio_init_cfg(&data->sdio_obj, &data->cyhal_sdio_config);
	if (ret != CY_RSLT_SUCCESS) {
		LOG_ERR("cyhal_sdio_init_cfg failed ret = %d \r\n", ret);
		return ret;
	}

	/* Register callback for SDIO events */
	cyhal_sdio_register_callback(&data->sdio_obj, ifx_cat1_sdio_event_callback, (void *)dev);

	return 0;
}

static DEVICE_API(sdhc, ifx_cat1_sdio_api) = {
	.reset = ifx_cat1_sdio_reset,
	.request = ifx_cat1_sdio_request,
	.set_io = ifx_cat1_sdio_set_io,
	.get_card_present = ifx_cat1_sdio_get_card_present,
	.card_busy = ifx_cat1_sdio_card_busy,
	.get_host_props = ifx_cat1_sdio_get_host_props,
	.enable_interrupt = ifx_cat1_sdio_enable_interrupt,
	.disable_interrupt = ifx_cat1_sdio_disable_interrupt,
};

#define IFX_CAT1_SDHC_INIT(n)                                                                      \
                                                                                                   \
	PINCTRL_DT_INST_DEFINE(n);                                                                 \
                                                                                                   \
	static const struct ifx_cat1_sdio_config ifx_cat1_sdio_##n##_config = {                    \
		.pincfg = PINCTRL_DT_INST_DEV_CONFIG_GET(n),                                       \
		.reg_addr = (SDHC_Type *)DT_INST_REG_ADDR(n),                                      \
		.irq_priority = DT_INST_IRQ(n, priority)};                                         \
                                                                                                   \
	static struct ifx_cat1_sdio_data ifx_cat1_sdio_##n##_data;                                 \
                                                                                                   \
	DEVICE_DT_INST_DEFINE(n, &ifx_cat1_sdio_init, NULL, &ifx_cat1_sdio_##n##_data,             \
			      &ifx_cat1_sdio_##n##_config, POST_KERNEL, CONFIG_SDHC_INIT_PRIORITY, \
			      &ifx_cat1_sdio_api);

DT_INST_FOREACH_STATUS_OKAY(IFX_CAT1_SDHC_INIT)
