/*
 * SPDX-FileCopyrightText: 2015-2025 Espressif Systems (Shanghai) CO LTD
 *
 * SPDX-License-Identifier: Apache-2.0
 */

/*
Architecture:

The whole SDIO slave peripheral consists of three parts: the registers (including the control registers of
interrupts and shared registers), the sending FIFO and the receiving FIFO. A document ``esp_slave_protocol.rst``
describes the functionality of the peripheral detailedly.

The host can access only one of those parts at once, and the hardware functions of these parts are totally
independent. Hence this driver is designed into these three independent parts. The shared registers are quite
simple. As well as the interrupts: when a slave interrupt is written by the host, the slave gets an interrupt;
when one of the host interrupt bits is active, slave hardware output interrupt signals on the DAT1 line.

For the FIFOs, the peripheral provides counters as registers so that the host can always know whether the slave
is ready to send/receive data. The driver resets the counters during initialization, and the host should somehow
inform the slave to reset the counters again if it should reboot (or lose the counter value for some reasons).
Then the host can read/write the FIFOs by CMD53 commands according to the counters.

Since we don't want to copy all the data from the buffer each time we use sending/receiving buffer,
the buffers are directly loaded onto the sending/receiving linked-list and taken off only after use.
Hence the driver takes ownership of the buffer when the buffer is fed to the driver.

The driver returns the ownership of buffers when a "finish" function is called. When the hardware finishes
the sending/receiving of a buffer, the ISR is invoked and it goes through the linked-list to see how many buffers
are freed after last interrupt, and send corresponding signals to the app.

The driver of FIFOs works as below:

1.  The receive driver requires application to "register" a buffer before it's used. The driver
    dynamically allocate a linked-list descriptor for the buffer, and return the descriptor as a handle
    to the app.

    Each time the app asks to receive by a buffer, the descriptor of the buffer is loaded onto the linked-list,
    and the counter of receiving buffers is increased so that the host will know this by the receiving interrupt.
    The hardware will automatically go through the linked list and write data into the buffers loaded on the
    list.

    The receiving driver sends a counting semaphore to the app for each buffer finished receiving. A task can only
    check the linked list and fetch one finished buffer for a received semaphore.

2.  The sending driver is slightly different due to different hardware working styles. (See README.md in the same folder
    for the diagram how task and ISR work concurrently)
    (TODO: re-write this part if the stitch mode is released)
    The hardware has a cache, so that once a descriptor is loaded onto the linked-list, it cannot be modified
    until returned (used) by the hardware. This forbids us from loading descriptors onto the linked list during
    the transfer (or the time waiting for host to start a transfer). However, we use a "ringbuffer" (different from
    the one in ``freertos/`` folder) holding descriptors to solve this:

    1.  The driver allocates continuous memory for several buffer descriptors (the maximum buffer number) during
        initialization. Then the driver points the STAILQ_NEXT pointer of all the descriptors except the last one
        to the next descriptor of each of them. Then the pointer of the last descriptor points back to the first one:
        now the descriptor is in a ring.

    2.  The "ringbuffer" has a write pointer points to where app can write new descriptor. The app writes the new descriptor
        indicated by the write pointer without touching the STAILQ_NEXT pointer so that the descriptors are always in a
        ring-like linked-list. The app never touches the part of linked-list being used by the hardware.

    3.  When the hardware needs some data to send, it automatically pick a part of connected descriptors. According to the mode:
            - Buffer mode: only pick the next one of the last sent one;
            - Stream mode: pick the one above to the latest one.

        The driver removes the STAILQ_NEXT pointer of the last descriptor and put the head of the part to the DMA controller so
        that it looks like just a linear linked-list rather than a ring to the hardware.

    4.  The counter of sending FIFO can increase when app load new buffers (in STREAM_MODE) or when new transfer should
        start (in PACKET_MODE).

    5.  When the sending transfer is finished, the driver goes through the descriptors just send in the ISR and push all
        the ``arg`` member of descriptors to the queue back to the app, so that the app can handle finished buffers. The
        driver also fix the STAILQ_NEXT pointer of the last descriptor so that the descriptors are now in a ring again.
*/

#include <string.h>

#include "soc/soc_memory_layout.h"
#include "soc/gpio_periph.h"
#include "soc/soc_caps.h"
#include "soc/sdio_slave_periph.h"
#include "esp_cpu.h"
#include "esp_intr_alloc.h"
#include "esp_log.h"
#include "hal/sdio_slave_hal.h"
#include "hal/gpio_hal.h"
#include "freertos/FreeRTOS.h"
#include "freertos/semphr.h"
#include "esp_private/periph_ctrl.h"
#include "esp_private/gpio.h"
#if CONFIG_PM_POWER_DOWN_PERIPHERAL_IN_LIGHT_SLEEP
#include "esp_private/sleep_retention.h"
#endif
#include "driver/gpio.h"
#include "driver/sdio_slave.h"

#define SDIO_SLAVE_CHECK(res, str, ret_val) do { if(!(res)){\
    SDIO_SLAVE_LOGE("%s", str);\
    return ret_val;\
} }while (0)

static const char TAG[] = "sdio_slave";

#define SDIO_SLAVE_LOGE(s, ...) ESP_LOGE(TAG, "%s(%d): "s, __FUNCTION__,__LINE__,##__VA_ARGS__)
#define SDIO_SLAVE_LOGW(s, ...) ESP_LOGW(TAG, "%s: "s, __FUNCTION__,##__VA_ARGS__)

#if !SOC_RCC_IS_INDEPENDENT
#define SDIO_SLAVE_RCC_ATOMIC() PERIPH_RCC_ATOMIC()
#else
#define SDIO_SLAVE_RCC_ATOMIC()
#endif

// sdio_slave_buf_handle_t is of type recv_desc_t*;
typedef struct recv_desc_s {
    union {
        struct {
            // the third word, pointer to next desc, is shared with the tailq entry.
            sdio_slave_hal_recv_desc_t hal_desc;
            // when the forth word is used (not NULL), means the tailq is used, not in the receiving state.
            uint32_t not_receiving;
        };
        struct {
            // first 3 WORDs of this struct is defined by and compatible to the DMA link list format.
            uint32_t _reserved0;
            uint32_t _reserved1;
            TAILQ_ENTRY(recv_desc_s) tail_entry; // tailq used to store the registered descriptors.
        };
    };
} recv_desc_t;

typedef TAILQ_HEAD(recv_tailq_head_s, recv_desc_s) recv_tailq_t;

typedef struct {
    sdio_slave_config_t     config;
    sdio_slave_context_t    *hal;
    intr_handle_t           intr_handle; //allocated interrupt handle
    /*------- events ---------------*/
    union {
        SemaphoreHandle_t       events[9]; // 0-7 for gp intr
        struct {
            SemaphoreHandle_t   _events[8];
            SemaphoreHandle_t   recv_event; // 8 for recv
        };
    };
    portMUX_TYPE            reg_spinlock;
    /*------- sending ---------------*/
    //desc in the send_link_list are temporary, taken information and space from the ringbuf, return to ringbuf after use.
    SemaphoreHandle_t   remain_cnt;
    portMUX_TYPE        write_spinlock;
    QueueHandle_t       ret_queue;
    /*------- receiving ---------------*/
    recv_tailq_t        recv_reg_list;  // removed from the link list, registered but not used now
    portMUX_TYPE        recv_spinlock;
} sdio_context_t;

#define CONTEXT_INIT_VAL { \
    .intr_handle = NULL, \
    .hal = NULL, \
    /*------- events ---------------*/ \
    .events     =   {}, \
    .reg_spinlock = portMUX_INITIALIZER_UNLOCKED, \
    /*------- sending ---------------*/ \
    .ret_queue      =   NULL, \
    .write_spinlock =   portMUX_INITIALIZER_UNLOCKED, \
    /*------- receiving ---------------*/ \
    .recv_reg_list  =   TAILQ_HEAD_INITIALIZER(context.recv_reg_list), \
    .recv_spinlock  =   portMUX_INITIALIZER_UNLOCKED, \
}

static sdio_context_t context = CONTEXT_INIT_VAL;

static void sdio_intr(void *);
static void sdio_intr_host(void *);
static void sdio_intr_send(void *);
static void sdio_intr_recv(void *);

static esp_err_t send_flush_data(void);
static esp_err_t recv_flush_data(void);

static inline void critical_enter_recv(void);
static inline void critical_exit_recv(void);

static void deinit_context(void);

static inline void show_ll(sdio_slave_ll_desc_t *item)
{
    ESP_EARLY_LOGI(TAG, "=> %p: size: %d(%d), eof: %d, owner: %d", item, item->size, item->length, item->eof, item->owner);
    ESP_EARLY_LOGI(TAG, "   buf: %p, stqe_next: %p", item->buf, item->qe.stqe_next);
}

static void __attribute((unused)) dump_ll(sdio_slave_ll_desc_t *queue)
{
    int cnt = 0;
    sdio_slave_ll_desc_t *item = queue;
    while (item != NULL) {
        cnt++;
        show_ll(item);
        item = STAILQ_NEXT(item, qe);
    }
    ESP_EARLY_LOGI(TAG, "total: %d", cnt);
}

static inline void deinit_context(void)
{
    context.config = (sdio_slave_config_t) {};
    for (int i = 0; i < 9; i++) {
        if (context.events[i] != NULL) {
            vSemaphoreDelete(context.events[i]);
            context.events[i] = NULL;
        }
    }
    if (context.ret_queue != NULL) {
        vQueueDelete(context.ret_queue);
        context.ret_queue = NULL;
    }
    if (context.remain_cnt != NULL) {
        vSemaphoreDelete(context.remain_cnt);
    }
    free(context.hal->send_desc_queue.data);
    context.hal->send_desc_queue.data = NULL;
    free(context.hal);
    context.hal = NULL;
}

static esp_err_t init_context(const sdio_slave_config_t *config)
{
    SDIO_SLAVE_CHECK(*(uint32_t *)&context.config == 0, "sdio slave already initialized", ESP_ERR_INVALID_STATE);
    context = (sdio_context_t)CONTEXT_INIT_VAL;
    context.config = *config;

    //initialize and configure the HAL
    context.hal = (sdio_slave_context_t *)heap_caps_calloc(sizeof(sdio_slave_context_t), 1, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT);
    if (context.hal == NULL) {
        goto no_mem;
    }

    context.hal->sending_mode = config->sending_mode;
    context.hal->timing = config->timing;
    context.hal->no_highspeed = (config->flags & SDIO_SLAVE_FLAG_DEFAULT_SPEED) == SDIO_SLAVE_FLAG_DEFAULT_SPEED;
    context.hal->send_queue_size = config->send_queue_size;
    context.hal->recv_buffer_size = config->recv_buffer_size;
    //initialize ringbuffer resources
    sdio_ringbuf_t *buf = &(context.hal->send_desc_queue);
    //one item is not used.
    buf->size = SDIO_SLAVE_SEND_DESC_SIZE * (config->send_queue_size + 1);
    buf->data = (uint8_t *)heap_caps_malloc(buf->size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT | MALLOC_CAP_DMA);
    if (buf->data == NULL) {
        goto no_mem;
    }

    sdio_slave_hal_init(context.hal);

    // in theory we can queue infinite buffers in the linked list, but for multi-core reason we have to use a queue to
    // count the finished buffers.
    context.recv_event = xSemaphoreCreateCounting(UINT32_MAX, 0);
    for (int i = 0; i < 9; i++) {
        if (i < 8) {
            context.events[i] = xSemaphoreCreateBinary();
        }   //for 8, already created.
        if (context.events[i] == NULL) {
            SDIO_SLAVE_LOGE("event initialize failed");
            goto no_mem;
        }
    }

    context.remain_cnt = xSemaphoreCreateCounting(context.config.send_queue_size, context.config.send_queue_size);
    if (context.remain_cnt == NULL) {
        goto no_mem;
    }

    context.ret_queue = xQueueCreate(config->send_queue_size, sizeof(void *));
    if (context.ret_queue == NULL) {
        goto no_mem;
    }

    return ESP_OK;

no_mem:
    deinit_context();
    return ESP_ERR_NO_MEM;
}

static void configure_pin(int pin, uint32_t func, bool pullup)
{
    const int drive_strength = 3;
    assert(pin != -1);

    gpio_input_enable(pin);
    gpio_func_sel(pin, func);
    gpio_set_drive_capability(pin, drive_strength);
    gpio_pulldown_dis(pin);
    if (pullup) {
        gpio_pullup_en(pin);
    }
}

static inline esp_err_t sdio_slave_hw_init(sdio_slave_config_t *config)
{
    //initialize pin
    const sdio_slave_slot_info_t *slot = &sdio_slave_slot_info[0];

    bool pullup = config->flags & SDIO_SLAVE_FLAG_INTERNAL_PULLUP;
    configure_pin(slot->clk_gpio, slot->func, false);   //clk doesn't need a pullup
    configure_pin(slot->cmd_gpio, slot->func, pullup);
    configure_pin(slot->d0_gpio, slot->func, pullup);
    if ((config->flags & SDIO_SLAVE_FLAG_HOST_INTR_DISABLED) == 0) {
        configure_pin(slot->d1_gpio, slot->func, pullup);
    }
    if ((config->flags & SDIO_SLAVE_FLAG_DAT2_DISABLED) == 0) {
        configure_pin(slot->d2_gpio, slot->func, pullup);
    }
    configure_pin(slot->d3_gpio, slot->func, pullup);

    //enable register clock
    SDIO_SLAVE_RCC_ATOMIC() {
        sdio_slave_ll_enable_bus_clock(true);
        sdio_slave_ll_reset_register();
    }

    sdio_slave_hal_hw_init(context.hal);
    return ESP_OK;
}

static void recover_pin(int pin, int sdio_func)
{
    gpio_io_config_t io_cfg = {};
    esp_err_t ret = gpio_get_io_config(pin, &io_cfg);
    assert(ret == ESP_OK);

    if (io_cfg.fun_sel == sdio_func) {
        gpio_set_direction(pin, GPIO_MODE_INPUT);
        gpio_func_sel(pin, PIN_FUNC_GPIO);
    }
}

static void sdio_slave_hw_deinit(void)
{
    const sdio_slave_slot_info_t *slot = &sdio_slave_slot_info[0];
    recover_pin(slot->clk_gpio, slot->func);
    recover_pin(slot->cmd_gpio, slot->func);
    recover_pin(slot->d0_gpio, slot->func);
    recover_pin(slot->d1_gpio, slot->func);
    recover_pin(slot->d2_gpio, slot->func);
    recover_pin(slot->d3_gpio, slot->func);

    //disable register clock
    SDIO_SLAVE_RCC_ATOMIC() {
        sdio_slave_ll_enable_bus_clock(false);
    }
}

esp_err_t sdio_slave_initialize(sdio_slave_config_t *config)
{
    esp_err_t r;
    intr_handle_t intr_handle = NULL;
    const int flags = 0;

    r = init_context(config);
    SDIO_SLAVE_CHECK(r == ESP_OK, "context initialization failed", r);

    r = esp_intr_alloc_intrstatus(
            ETS_SLC0_INTR_SOURCE,
            flags,
            (uint32_t)sdio_slave_hal_get_intr_status_reg(context.hal),
            sdio_slave_ll_intr_status_mask,
            sdio_intr,
            NULL,
            &intr_handle
        );
    SDIO_SLAVE_CHECK(r == ESP_OK, "interrupt allocation failed", r);
    context.intr_handle = intr_handle;

#if CONFIG_PM_POWER_DOWN_PERIPHERAL_IN_LIGHT_SLEEP
    r = sleep_retention_power_lock_acquire();
    if (r != ESP_OK) {
        return r;
    }
#endif

    r = sdio_slave_hw_init(config);
    if (r != ESP_OK) {
        return r;
    }

    sdio_slave_reset();
    return ESP_OK;
}

void sdio_slave_deinit(void)
{
    sdio_slave_hw_deinit();

#if CONFIG_PM_POWER_DOWN_PERIPHERAL_IN_LIGHT_SLEEP
    esp_err_t r = sleep_retention_power_lock_release();
    assert(r == ESP_OK);
#endif

    //unregister all buffers registered but returned (not loaded)
    recv_desc_t *temp_desc;
    recv_desc_t *desc;
    TAILQ_FOREACH_SAFE(desc, &context.recv_reg_list, tail_entry, temp_desc) {
        TAILQ_REMOVE(&context.recv_reg_list, desc, tail_entry);
        free(desc);
    }
    //unregister all buffers that is loaded and not returned
    while (1) {
        desc = (recv_desc_t *)sdio_slave_hal_recv_unload_desc(context.hal);
        if (desc == NULL) {
            break;
        }
        free(desc);
    }
    esp_err_t ret = esp_intr_free(context.intr_handle);
    assert(ret == ESP_OK);
    (void)ret;
    context.intr_handle = NULL;
    deinit_context();
}

esp_err_t sdio_slave_start(void)
{
    esp_err_t ret;
    sdio_slave_hostint_t intr = (sdio_slave_hostint_t)UINT32_MAX;
    sdio_slave_hal_hostint_clear(context.hal, &intr);
    ret = sdio_slave_hal_send_start(context.hal);
    if (ret != ESP_OK) {
        return ret;
    }

    critical_enter_recv();
    sdio_slave_hal_recv_start(context.hal);
    critical_exit_recv();

    sdio_slave_hal_set_ioready(context.hal, true);
    return ESP_OK;
}

esp_err_t sdio_slave_reset(void)
{
    esp_err_t err;
    err = send_flush_data();
    if (err != ESP_OK) {
        return err;
    }

    err = sdio_slave_hal_send_reset_counter(context.hal);
    if (err != ESP_OK) {
        return err;
    }

    err = recv_flush_data();
    if (err != ESP_OK) {
        return err;
    }

    critical_enter_recv();
    sdio_slave_hal_recv_reset_counter(context.hal);
    critical_exit_recv();
    err = ESP_OK;
    return err;
}

void sdio_slave_stop(void)
{
    sdio_slave_hal_set_ioready(context.hal, false);
    sdio_slave_hal_send_stop(context.hal);
    sdio_slave_hal_recv_stop(context.hal);
}

static void sdio_intr(void *arg)
{
    sdio_intr_send(arg);
    sdio_intr_recv(arg);
    sdio_intr_host(arg);
}

/*---------------------------------------------------------------------------
 *                  Host
 *--------------------------------------------------------------------------*/
static void sdio_intr_host(void *arg)
{
    sdio_slave_ll_slvint_t int_val;
    sdio_slave_hal_slvint_fetch_clear(context.hal, &int_val);
    BaseType_t yield = pdFALSE;
    for (int i = 0; i < 8; i++) {
        if (BIT(i) & int_val) {
            if (context.config.event_cb != NULL) {
                (*context.config.event_cb)(i);
            }
            xSemaphoreGiveFromISR(context.events[i], &yield);
        }
    }
    if (yield) {
        portYIELD_FROM_ISR();
    }
}

esp_err_t sdio_slave_wait_int(int pos, TickType_t wait)
{
    SDIO_SLAVE_CHECK(pos >= 0 && pos < 8, "interrupt num invalid", ESP_ERR_INVALID_ARG);
    return xSemaphoreTake(context.events[pos], wait);
}

uint8_t sdio_slave_read_reg(int pos)
{
    if (pos >= 28 && pos <= 31) {
        SDIO_SLAVE_LOGW("%s: interrupt reg, for reference", __FUNCTION__);
    }
    if (pos < 0 || pos >= 64) {
        SDIO_SLAVE_LOGE("read register address wrong");
    }
    return sdio_slave_hal_host_get_reg(context.hal, pos);
}

esp_err_t sdio_slave_write_reg(int pos, uint8_t reg)
{
    if (pos >= 28 && pos <= 31) {
        SDIO_SLAVE_LOGE("interrupt reg, please use sdio_slave_clear_int");
        return ESP_ERR_INVALID_ARG;
    }
    if (pos < 0 || pos >= 64) {
        SDIO_SLAVE_LOGE("write register address wrong");
        return ESP_ERR_INVALID_ARG;
    }

    portENTER_CRITICAL(&context.reg_spinlock);
    sdio_slave_hal_host_set_reg(context.hal, pos, reg);
    portEXIT_CRITICAL(&context.reg_spinlock);
    return ESP_OK;
}

sdio_slave_hostint_t sdio_slave_get_host_intena(void)
{
    sdio_slave_hostint_t host_int;
    sdio_slave_hal_hostint_get_ena(context.hal, &host_int);
    return host_int;
}

void sdio_slave_set_host_intena(sdio_slave_hostint_t mask)
{
    sdio_slave_hal_hostint_set_ena(context.hal, &mask);
}

void sdio_slave_clear_host_int(sdio_slave_hostint_t mask)
{
    sdio_slave_hal_hostint_clear(context.hal, &mask);
}

static inline sdio_slave_hostint_t get_hostint_by_pos(int pos)
{
    return (sdio_slave_hostint_t)BIT(pos);
}

esp_err_t sdio_slave_send_host_int(uint8_t pos)
{
    SDIO_SLAVE_CHECK(pos < 8, "interrupt num invalid", ESP_ERR_INVALID_ARG);
    sdio_slave_hostint_t intr = get_hostint_by_pos(pos);
    sdio_slave_hal_hostint_send(context.hal, &intr);
    return ESP_OK;
}

/*---------------------------------------------------------------------------
 *                  Send
 *--------------------------------------------------------------------------*/

/* The link list is handled in the app, while counter and pointer processed in ISR.
 * Driver abuse rx_done bit to invoke ISR.
 * If driver is stopped, the link list is stopped as well as the ISR invoker.
 */

static void sdio_intr_send(void *arg)
{
    ESP_EARLY_LOGV(TAG, "intr_send");
    BaseType_t yield = pdFALSE;

    // this interrupt is abused to get ISR invoked by app
    sdio_slave_hal_send_handle_isr_invoke(context.hal);

    uint32_t returned_cnt;
    if (sdio_slave_hal_send_eof_happened(context.hal)) {
        BaseType_t ret __attribute__((unused));

        esp_err_t err;
        while (1) {
            void *finished_arg;
            err = sdio_slave_hal_send_get_next_finished_arg(context.hal, &finished_arg, &returned_cnt);
            if (err != ESP_OK) {
                break;
            }

            assert(returned_cnt == 0);
            ESP_EARLY_LOGV(TAG, "end: %p", finished_arg);
            ret = xQueueSendFromISR(context.ret_queue, &finished_arg, &yield);
            assert(ret == pdTRUE);
        }
        //get_next_finished_arg returns the total amount of returned descs.
        for (size_t i = 0; i < returned_cnt; i++) {
            ret = xSemaphoreGiveFromISR(context.remain_cnt, &yield);
            assert(ret == pdTRUE);
        }
    }

    sdio_slave_hal_send_new_packet_if_exist(context.hal);

    if (yield) {
        portYIELD_FROM_ISR();
    }
}

esp_err_t sdio_slave_send_queue(uint8_t *addr, size_t len, void *arg, TickType_t wait)
{
    SDIO_SLAVE_CHECK(len > 0 && len <= 4092, "length out of range: (0, 4092]", ESP_ERR_INVALID_ARG);
    SDIO_SLAVE_CHECK(esp_ptr_dma_capable(addr) && (uint32_t)addr % 4 == 0, "buffer to send should be DMA capable and 32-bit aligned",
                     ESP_ERR_INVALID_ARG);

    BaseType_t cnt_ret = xSemaphoreTake(context.remain_cnt, wait);
    if (cnt_ret != pdTRUE) {
        return ESP_ERR_TIMEOUT;
    }

    portENTER_CRITICAL(&context.write_spinlock);
    esp_err_t ret = sdio_slave_hal_send_queue(context.hal, addr, len, arg);
    portEXIT_CRITICAL(&context.write_spinlock);
    if (ret != ESP_OK) {
        return ret;
    }

    return ESP_OK;
}

esp_err_t sdio_slave_send_get_finished(void **out_arg, TickType_t wait)
{
    void *arg = NULL;
    BaseType_t err = xQueueReceive(context.ret_queue, &arg, wait);
    if (out_arg) {
        *out_arg = arg;
    }
    if (err != pdTRUE) {
        return ESP_ERR_TIMEOUT;
    }
    return ESP_OK;
}

esp_err_t sdio_slave_transmit(uint8_t *addr, size_t len)
{
    uint32_t timestamp = esp_cpu_get_cycle_count();
    uint32_t ret_stamp;

    esp_err_t err = sdio_slave_send_queue(addr, len, (void *)timestamp, portMAX_DELAY);
    if (err != ESP_OK) {
        return err;
    }
    err = sdio_slave_send_get_finished((void **)&ret_stamp, portMAX_DELAY);
    if (err != ESP_OK) {
        return err;
    }
    SDIO_SLAVE_CHECK(ret_stamp == timestamp, "already sent without return before", ESP_ERR_INVALID_STATE);

    return ESP_OK;
}

//clear data but keep counter
static esp_err_t send_flush_data(void)
{
    esp_err_t err;
    BaseType_t ret __attribute__((unused));

    while (1) {
        void *finished_arg;
        uint32_t return_cnt = 0;
        err = sdio_slave_hal_send_flush_next_buffer(context.hal, &finished_arg, &return_cnt);
        if (err == ESP_OK) {
            ret = xQueueSend(context.ret_queue, &finished_arg, portMAX_DELAY);
            assert(ret == pdTRUE);
            for (size_t i = 0; i < return_cnt; i++) {
                ret = xSemaphoreGive(context.remain_cnt);
                assert(ret == pdTRUE);
            }
        } else {
            if (err == ESP_ERR_NOT_FOUND) {
                err = ESP_OK;
            }
            break;
        }
    }

    if (err == ESP_ERR_INVALID_STATE) {
        ESP_LOGE(TAG, "flush data when transmission started");
    }
    return err;
}

/*---------------------------------------------------------------------------
 *                  Recv
 *--------------------------------------------------------------------------*/
#define CHECK_HANDLE_IDLE(desc) do { if (desc == NULL || !desc->not_receiving) {\
    return ESP_ERR_INVALID_ARG; } } while(0)

static inline void critical_enter_recv(void)
{
    portENTER_CRITICAL(&context.recv_spinlock);
}

static inline void critical_exit_recv(void)
{
    portEXIT_CRITICAL(&context.recv_spinlock);
}

// remove data, still increase the counter
static esp_err_t recv_flush_data(void)
{
    while (1) {
        BaseType_t ret = xSemaphoreTake(context.recv_event, 0);
        if (ret == pdFALSE) {
            break;
        }
        critical_enter_recv();
        sdio_slave_hal_recv_flush_one_buffer(context.hal);
        critical_exit_recv();
    }
    return ESP_OK;
}

static void sdio_intr_recv(void *arg)
{
    BaseType_t yield = 0;
    bool triggered = sdio_slave_hal_recv_done(context.hal);
    while (triggered) {
        portENTER_CRITICAL_ISR(&context.recv_spinlock);
        bool has_next_item = sdio_slave_hal_recv_has_next_item(context.hal);
        portEXIT_CRITICAL_ISR(&context.recv_spinlock);
        if (has_next_item) {
            ESP_EARLY_LOGV(TAG, "intr_recv: Give");
            xSemaphoreGiveFromISR(context.recv_event, &yield);
            continue;   //check the linked list again skip the interrupt checking
        }
        // if no more items on the list, check the interrupt again,
        // will loop until the interrupt bit is kept cleared.
        triggered = sdio_slave_hal_recv_done(context.hal);
    }
    if (yield) {
        portYIELD_FROM_ISR();
    }
}

esp_err_t sdio_slave_recv_load_buf(sdio_slave_buf_handle_t handle)
{
    recv_desc_t *desc = (recv_desc_t *)handle;
    CHECK_HANDLE_IDLE(desc);
    assert(desc->not_receiving);

    critical_enter_recv();
    TAILQ_REMOVE(&context.recv_reg_list, desc, tail_entry);
    desc->not_receiving = 0; //manually remove the prev link (by set not_receiving=0), to indicate this is in the queue
    sdio_slave_hal_load_buf(context.hal, &desc->hal_desc);
    critical_exit_recv();
    return ESP_OK;
}

sdio_slave_buf_handle_t sdio_slave_recv_register_buf(uint8_t *start)
{
    SDIO_SLAVE_CHECK(esp_ptr_dma_capable(start) && (uint32_t)start % 4 == 0,
                     "buffer to register should be DMA capable and 32-bit aligned", NULL);
    recv_desc_t *desc = (recv_desc_t *)heap_caps_malloc(sizeof(recv_desc_t), MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT | MALLOC_CAP_DMA);
    if (desc == NULL) {
        SDIO_SLAVE_LOGE("cannot allocate lldesc for new buffer");
        return NULL;
    }

    //initially in the reg list
    sdio_slave_hal_recv_init_desc(context.hal, &desc->hal_desc, start);
    critical_enter_recv();
    TAILQ_INSERT_TAIL(&context.recv_reg_list, desc, tail_entry);
    critical_exit_recv();
    return desc;
}

esp_err_t sdio_slave_recv(sdio_slave_buf_handle_t *handle_ret, uint8_t **out_addr, size_t *out_len, TickType_t wait)
{
    esp_err_t ret = sdio_slave_recv_packet(handle_ret, wait);
    if (ret == ESP_ERR_NOT_FINISHED) {
        //This API was not aware of the EOF info, return ESP_OK to keep back-compatible.
        ret = ESP_OK;
    }
    if (ret == ESP_OK) {
        recv_desc_t *desc = (recv_desc_t *)(*handle_ret);
        if (out_addr) {
            *out_addr = (uint8_t *)desc->hal_desc.buf;
        }
        if (out_len) {
            *out_len = desc->hal_desc.length;
        }
    }
    return ret;
}

esp_err_t sdio_slave_recv_packet(sdio_slave_buf_handle_t *handle_ret, TickType_t wait)
{
    SDIO_SLAVE_CHECK(handle_ret != NULL, "handle address cannot be 0", ESP_ERR_INVALID_ARG);
    BaseType_t err = xSemaphoreTake(context.recv_event, wait);
    if (err == pdFALSE) {
        return ESP_ERR_TIMEOUT;
    }

    esp_err_t ret = ESP_OK;
    critical_enter_recv();
    //remove from queue, add back to reg list.
    recv_desc_t *desc = (recv_desc_t *)sdio_slave_hal_recv_unload_desc(context.hal);
    assert(desc != NULL && desc->hal_desc.owner == 0);
    TAILQ_INSERT_TAIL(&context.recv_reg_list, desc, tail_entry);
    critical_exit_recv();

    *handle_ret = (sdio_slave_buf_handle_t)desc;

    if (!desc->hal_desc.eof) {
        ret = ESP_ERR_NOT_FINISHED;
    }
    return ret;
}

esp_err_t sdio_slave_recv_unregister_buf(sdio_slave_buf_handle_t handle)
{
    recv_desc_t *desc = (recv_desc_t *)handle;
    CHECK_HANDLE_IDLE(desc); //in the queue, fail.

    critical_enter_recv();
    TAILQ_REMOVE(&context.recv_reg_list, desc, tail_entry);
    critical_exit_recv();
    free(desc);
    return ESP_OK;
}

uint8_t *sdio_slave_recv_get_buf(sdio_slave_buf_handle_t handle, size_t *len_o)
{
    if (handle == NULL) {
        return NULL;
    }
    recv_desc_t *desc = (recv_desc_t *)handle;

    if (len_o != NULL) {
        *len_o = desc->hal_desc.length;
    }
    return (uint8_t *)desc->hal_desc.buf;
}
